From 7954ef26bf3a88ceacf52f054b5330d54abba679 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Mon, 13 Jan 2025 10:25:16 +0100 Subject: [PATCH] feat: Add bidirectional communication to exchange texts Signed-off-by: nidhal-labidi --- flottform/forms/src/default-component.ts | 5 +- .../forms/src/flottform-channel-client.ts | 44 ++++- flottform/forms/src/flottform-channel-host.ts | 42 +++- .../forms/src/flottform-text-input-client.ts | 22 ++- .../forms/src/flottform-text-input-host.ts | 13 +- flottform/forms/src/internal.ts | 1 + servers/demo/src/api.ts | 4 + servers/demo/src/routes/+page.svelte | 5 + .../[endpointId]/+page.svelte | 138 ++++++++++++++ .../routes/flottform-messaging/+page.svelte | 179 ++++++++++++++++++ .../[endpointId]/+page.svelte | 11 +- .../where-are-you-at/src/routes/+page.svelte | 2 +- .../src/routes/now/[endpointId]/+page.svelte | 5 +- 13 files changed, 437 insertions(+), 34 deletions(-) create mode 100644 servers/demo/src/routes/flottform-messaging-client/[endpointId]/+page.svelte create mode 100644 servers/demo/src/routes/flottform-messaging/+page.svelte diff --git a/flottform/forms/src/default-component.ts b/flottform/forms/src/default-component.ts index 025cf36..0bfe8a4 100644 --- a/flottform/forms/src/default-component.ts +++ b/flottform/forms/src/default-component.ts @@ -305,8 +305,9 @@ const handleTextInputStates = ({ if (id) { flottformItem.setAttribute('id', id); } - flottformTextInputHost.on('done', (message: string) => { - statusInformation.innerHTML = onSuccessText ?? `✨ You have succesfully submitted your message`; + flottformTextInputHost.on('text-received', (message: string) => { + statusInformation.innerHTML = + onSuccessText ?? `✨ You have succesfully submitted your message: ${message}`; statusInformation.appendChild(refreshChannelButton); flottformItem.replaceChildren(statusInformation); }); diff --git a/flottform/forms/src/flottform-channel-client.ts b/flottform/forms/src/flottform-channel-client.ts index ab9bb93..91c88c8 100644 --- a/flottform/forms/src/flottform-channel-client.ts +++ b/flottform/forms/src/flottform-channel-client.ts @@ -16,6 +16,8 @@ type Listeners = { 'connecting-to-host': []; connected: []; 'connection-impossible': []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 'receiving-data': [e: any]; done: []; disconnected: []; error: [e: string]; @@ -96,15 +98,8 @@ export class FlottformChannelClient extends EventEmitter { this.logger.info(`ondatachannel: ${e.channel}`); this.changeState('connected'); this.dataChannel = e.channel; - // Set the maximum amount of data waiting inside the datachannel's buffer - this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD; - // Set the listener to listen then emit an event when the buffer has more space available and can be used to send more data - this.dataChannel.onbufferedamountlow = () => { - this.emit('bufferedamountlow'); - }; - this.dataChannel.onopen = (e) => { - this.logger.info(`ondatachannel - onopen: ${e.type}`); - }; + this.configureDataChannel(); + this.setupDataChannelListener(); }; this.changeState('sending-client-info'); @@ -122,6 +117,37 @@ export class FlottformChannelClient extends EventEmitter { this.changeState('disconnected'); }; + private setupDataChannelListener = () => { + if (this.dataChannel == null) { + this.changeState( + 'error', + 'dataChannel is null. Unable to setup the listeners for the data channel' + ); + return; + } + + this.dataChannel.onmessage = (e) => { + // Handling the incoming data from the Host depends on the use case. + this.emit('receiving-data', e); + }; + }; + + private configureDataChannel = () => { + if (this.dataChannel == null) { + this.changeState('error', 'dataChannel is null. Unable to setup the configure it!'); + return; + } + // Set the maximum amount of data waiting inside the datachannel's buffer + this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD; + // Set the listener to listen then emit an event when the buffer has more space available and can be used to send more data + this.dataChannel.onbufferedamountlow = () => { + this.emit('bufferedamountlow'); + }; + this.dataChannel.onopen = (e) => { + this.logger.info(`ondatachannel - onopen: ${e.type}`); + }; + }; + // sendData = (data: string | Blob | ArrayBuffer | ArrayBufferView) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any sendData = (data: any) => { diff --git a/flottform/forms/src/flottform-channel-host.ts b/flottform/forms/src/flottform-channel-host.ts index ca75481..2b2286f 100644 --- a/flottform/forms/src/flottform-channel-host.ts +++ b/flottform/forms/src/flottform-channel-host.ts @@ -21,6 +21,7 @@ export class FlottformChannelHost extends EventEmitter { private openPeerConnection: RTCPeerConnection | null = null; private dataChannel: RTCDataChannel | null = null; private pollForIceTimer: NodeJS.Timeout | number | null = null; + private BUFFER_THRESHOLD = 128 * 1024; // 128KB buffer threshold (maximum of 4 chunks in the buffer waiting to be sent over the network) constructor({ flottformApi, @@ -70,6 +71,11 @@ export class FlottformChannelHost extends EventEmitter { this.openPeerConnection = new RTCPeerConnection(this.rtcConfiguration); this.dataChannel = this.createDataChannel(); + if (this.dataChannel) { + //this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD; + this.configureDataChannel(); + this.setupDataChannelListener(); + } const session = await this.openPeerConnection.createOffer(); await this.openPeerConnection.setLocalDescription(session); @@ -120,6 +126,40 @@ export class FlottformChannelHost extends EventEmitter { }; }; + private configureDataChannel = () => { + if (this.dataChannel == null) { + this.changeState('error', 'dataChannel is null. Unable to setup the configure it!'); + return; + } + // Set the maximum amount of data waiting inside the datachannel's buffer + this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD; + // Set the listener to listen then emit an event when the buffer has more space available and can be used to send more data + this.dataChannel.onbufferedamountlow = () => { + this.emit('bufferedamountlow'); + }; + this.dataChannel.onopen = (e) => { + this.logger.info(`ondatachannel - onopen: ${e.type}`); + }; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendData = (data: any) => { + if (this.dataChannel == null) { + this.changeState('error', 'dataChannel is null. Unable to send the data to the Client!'); + return; + } else if (!this.canSendMoreData()) { + this.logger.warn('Data channel is full! Cannot send data at the moment'); + return; + } + this.dataChannel.send(data); + }; + + canSendMoreData = () => { + return ( + this.dataChannel && + this.dataChannel.bufferedAmount < this.dataChannel.bufferedAmountLowThreshold + ); + }; + private setupHostIceGathering = ( putHostInfoUrl: string, hostKey: string, @@ -279,7 +319,7 @@ export class FlottformChannelHost extends EventEmitter { this.dataChannel.onerror = (e) => { this.logger.log('channel.onerror', e); - this.changeState('error', { message: 'file-transfer' }); + this.changeState('error', { message: e.error.message }); }; }; diff --git a/flottform/forms/src/flottform-text-input-client.ts b/flottform/forms/src/flottform-text-input-client.ts index aea740d..6e0ea44 100644 --- a/flottform/forms/src/flottform-text-input-client.ts +++ b/flottform/forms/src/flottform-text-input-client.ts @@ -2,10 +2,11 @@ import { FlottformChannelClient } from './flottform-channel-client'; import { EventEmitter, Logger, POLL_TIME_IN_MS } from './internal'; type Listeners = { + init: []; connected: []; 'webrtc:connection-impossible': []; - sending: []; // Emitted to signal the start of sending the file(s) - done: []; + 'text-transfered': [text: string]; // Emitted to signal the transfer of one text TO the Host. + 'text-received': [text: string]; // Emitted to signal the reception of one text FROM the Host. disconnected: []; error: [e: string]; }; @@ -46,13 +47,19 @@ export class FlottformTextInputClient extends EventEmitter { sendText = (text: string) => { // For now, I didn't handle very large texts since for most use cases the text won't exceed the size of 1 chunk ( 16KB ) - this.emit('sending'); this.channel?.sendData(text); - this.emit('done'); + this.emit('text-transfered', text); + }; + + private handleIncomingData = (e: MessageEvent) => { + // We suppose that the data received is small enough to be all included in 1 message + this.emit('text-received', e.data); }; private registerListeners = () => { - this.channel?.on('init', () => {}); + this.channel?.on('init', () => { + this.emit('init'); + }); this.channel?.on('retrieving-info-from-endpoint', () => {}); this.channel?.on('sending-client-info', () => {}); this.channel?.on('connecting-to-host', () => {}); @@ -62,8 +69,11 @@ export class FlottformTextInputClient extends EventEmitter { this.channel?.on('connection-impossible', () => { this.emit('webrtc:connection-impossible'); }); + this.channel?.on('receiving-data', (e) => { + this.handleIncomingData(e); + }); this.channel?.on('done', () => { - this.emit('done'); + //this.emit('done'); }); this.channel?.on('disconnected', () => { this.emit('disconnected'); diff --git a/flottform/forms/src/flottform-text-input-host.ts b/flottform/forms/src/flottform-text-input-host.ts index 93cb739..67f890e 100644 --- a/flottform/forms/src/flottform-text-input-host.ts +++ b/flottform/forms/src/flottform-text-input-host.ts @@ -2,9 +2,9 @@ import { FlottformChannelHost } from './flottform-channel-host'; import { BaseInputHost, BaseListeners, Logger, POLL_TIME_IN_MS } from './internal'; type Listeners = BaseListeners & { - done: [data: string]; + 'text-transfered': [text: string]; // Emitted to signal the transfer of one text TO the Client. + 'text-received': [text: string]; // Emitted to signal the reception of one text FROM the Client. 'webrtc:waiting-for-text': []; - receive: []; 'webrtc:waiting-for-data': []; }; @@ -67,10 +67,15 @@ export class FlottformTextInputHost extends BaseInputHost { return this.qrCode; }; + sendText = (text: string) => { + // For now, I didn't handle very large texts since for most use cases the text won't exceed the size of 1 chunk ( 16KB ) + this.channel?.sendData(text); + this.emit('text-transfered', text); + }; + private handleIncomingData = (e: MessageEvent) => { - this.emit('receive'); // We suppose that the data received is small enough to be all included in 1 message - this.emit('done', e.data); + this.emit('text-received', e.data); if (this.inputField) { this.inputField.value = e.data; const event = new Event('change'); diff --git a/flottform/forms/src/internal.ts b/flottform/forms/src/internal.ts index 2658b9c..4c51cff 100644 --- a/flottform/forms/src/internal.ts +++ b/flottform/forms/src/internal.ts @@ -122,6 +122,7 @@ export type FlottformEventMap = { error: [error: Error]; connected: []; disconnected: []; + bufferedamountlow: []; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/servers/demo/src/api.ts b/servers/demo/src/api.ts index 526a36b..d444d50 100644 --- a/servers/demo/src/api.ts +++ b/servers/demo/src/api.ts @@ -21,3 +21,7 @@ export const createExpenseReportClientUrl = async ({ endpointId }: { endpointId: export const createDeExpenseReportClientUrl = async ({ endpointId }: { endpointId: string }) => { return `${window.location.origin}${base}/belegeinreichung-client/${endpointId}`; }; + +export const createFlottformMessagingClientUrl = async ({ endpointId }: { endpointId: string }) => { + return `${window.location.origin}${base}/flottform-messaging-client/${endpointId}`; +}; diff --git a/servers/demo/src/routes/+page.svelte b/servers/demo/src/routes/+page.svelte index 0ba96a6..cd05e6b 100644 --- a/servers/demo/src/routes/+page.svelte +++ b/servers/demo/src/routes/+page.svelte @@ -64,5 +64,10 @@ title="Customized default UI" description="See how you can tailor Flottform's default UI to better match your design, while still retaining all the powerful features of the original interface. This demo lets you explore how easy it is to adapt the default elements to fit seamlessly with your brand's style." /> + diff --git a/servers/demo/src/routes/flottform-messaging-client/[endpointId]/+page.svelte b/servers/demo/src/routes/flottform-messaging-client/[endpointId]/+page.svelte new file mode 100644 index 0000000..8f31369 --- /dev/null +++ b/servers/demo/src/routes/flottform-messaging-client/[endpointId]/+page.svelte @@ -0,0 +1,138 @@ + + + + Flottform DEMO + + +
+
+

Flottform Messaging - Client

+
+ {#if connectionStatus === 'init'} +
+

Trying to connect to the host...

+
+ {:else if connectionStatus === 'connected'} +
+
+ {#if messages.length === 0} +

+ You're connected to Host! You can start exchanging messages! +

+ {/if} + {#each messages as message} +
+

{message.text}

+
+ {/each} +
+
+ e.key === 'Enter' && handleSend()} + /> +
+ + +
+
+
+ {/if} + {#if connectionStatus === 'disconnected'} +
+

Connection Channel Disconnected!

+

+ Do want to connect one more time? Scan the QR code from the other peer or paste the link + to the browser! +

+
+ {/if} + {#if connectionStatus === 'error'} +
+

+ Connection Failed with the following error: {error} +

+ +
+ {/if} +
+
+
+ + diff --git a/servers/demo/src/routes/flottform-messaging/+page.svelte b/servers/demo/src/routes/flottform-messaging/+page.svelte new file mode 100644 index 0000000..2d80b79 --- /dev/null +++ b/servers/demo/src/routes/flottform-messaging/+page.svelte @@ -0,0 +1,179 @@ + + + + Flottform DEMO + + +
+
+

Flottform Messaging - Host

+
+ {#if connectionStatus === 'new'} +
+

Start a new connection to chat with someone else!

+ +
+ {:else if connectionStatus === 'endpoint-created'} +
+
+ QR Code +
+

+ Scan the QR code or use the link below to connect your device +

+
+ {connectionInfo.link} + +
+ +
+ {:else if connectionStatus === 'connected'} +
+
+ {#if messages.length === 0} +

+ You're connected to Client! You can start exchanging messages! +

+ {/if} + {#each messages as message} +
+

{message.text}

+
+ {/each} +
+
+ e.key === 'Enter' && handleSend()} + /> +
+ + +
+
+
+ {/if} + {#if connectionStatus === 'disconnected'} +
+

Channel is disconnected!

+

Do want to connect one more time? Click the button below!

+ +
+ {/if} + {#if connectionStatus === 'error'} +
+

+ Connection Channel Failed with the following error: {error} +

+ +
+ {/if} +
+
+
+ + diff --git a/servers/demo/src/routes/flottform-text-client/[endpointId]/+page.svelte b/servers/demo/src/routes/flottform-text-client/[endpointId]/+page.svelte index 9257b4d..bb8be83 100644 --- a/servers/demo/src/routes/flottform-text-client/[endpointId]/+page.svelte +++ b/servers/demo/src/routes/flottform-text-client/[endpointId]/+page.svelte @@ -5,7 +5,7 @@ import { sdpExchangeServerBase } from '../../../api'; let currentState = $state< - 'init' | 'connected' | 'sending' | 'done' | 'error-user-denied' | 'error' + 'init' | 'connected' | 'text-transfered' | 'sending' | 'error-user-denied' | 'error' >('init'); let textInput: HTMLInputElement; let sendTextToForm = $state<() => void>(); @@ -23,11 +23,8 @@ flottformTextInputClient.on('connected', () => { currentState = 'connected'; }); - flottformTextInputClient.on('sending', () => { - currentState = 'sending'; - }); - flottformTextInputClient.on('done', () => { - currentState = 'done'; + flottformTextInputClient.on('text-transfered', () => { + currentState = 'text-transfered'; }); flottformTextInputClient.on('error', () => { currentState = 'error'; @@ -67,7 +64,7 @@ - {:else if currentState === 'done'} + {:else if currentState === 'text-transfered'}
{ $currentState = 'connected'; }); - flottformTextInputHost.on('done', (message: string) => { + flottformTextInputHost.on('text-received', (message: string) => { $currentState = 'done'; const coords: Coordinates = JSON.parse(message); $latitude = coords.latitude; diff --git a/servers/where-are-you-at/src/routes/now/[endpointId]/+page.svelte b/servers/where-are-you-at/src/routes/now/[endpointId]/+page.svelte index e82df48..f3f340c 100644 --- a/servers/where-are-you-at/src/routes/now/[endpointId]/+page.svelte +++ b/servers/where-are-you-at/src/routes/now/[endpointId]/+page.svelte @@ -21,10 +21,7 @@ flottformTextInputClient.on('connected', () => { currentState = 'connected'; }); - flottformTextInputClient.on('sending', () => { - currentState = 'sending'; - }); - flottformTextInputClient.on('done', () => { + flottformTextInputClient.on('text-transfered', () => { currentState = 'done'; }); flottformTextInputClient.on('error', () => {