Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add bidirectional communication to exchange texts #108

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions flottform/forms/src/default-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
44 changes: 35 additions & 9 deletions flottform/forms/src/flottform-channel-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -96,15 +98,8 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
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');
Expand All @@ -122,6 +117,37 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
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;
}
Comment on lines +121 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either === null or something like this:

Suggested change
if (this.dataChannel == null) {
this.changeState(
'error',
'dataChannel is null. Unable to setup the listeners for the data channel'
);
return;
}
if (!this.dataChannel) {
this.changeState(
'error',
'dataChannel is not defined. 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this.dataChannel == null) {
if (this.dataChannel === null) {

this.changeState('error', 'dataChannel is null. Unable to setup the configure it!');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.changeState('error', 'dataChannel is null. Unable to setup the configure it!');
this.changeState('error', 'dataChannel is null. Unable to 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) => {
Expand Down
42 changes: 41 additions & 1 deletion flottform/forms/src/flottform-channel-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
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,
Expand Down Expand Up @@ -70,6 +71,11 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
this.openPeerConnection = new RTCPeerConnection(this.rtcConfiguration);

this.dataChannel = this.createDataChannel();
if (this.dataChannel) {
//this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code

Suggested change
//this.dataChannel.bufferedAmountLowThreshold = this.BUFFER_THRESHOLD;

this.configureDataChannel();
this.setupDataChannelListener();
}

const session = await this.openPeerConnection.createOffer();
await this.openPeerConnection.setLocalDescription(session);
Expand Down Expand Up @@ -120,6 +126,40 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
};
};

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,
Expand Down Expand Up @@ -279,7 +319,7 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {

this.dataChannel.onerror = (e) => {
this.logger.log('channel.onerror', e);
this.changeState('error', { message: 'file-transfer' });
this.changeState('error', { message: e.error.message });
};
};

Expand Down
22 changes: 16 additions & 6 deletions flottform/forms/src/flottform-text-input-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};
Expand Down Expand Up @@ -46,13 +47,19 @@ export class FlottformTextInputClient extends EventEmitter<Listeners> {

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', () => {});
Expand All @@ -62,8 +69,11 @@ export class FlottformTextInputClient extends EventEmitter<Listeners> {
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');
Expand Down
13 changes: 9 additions & 4 deletions flottform/forms/src/flottform-text-input-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': [];
};

Expand Down Expand Up @@ -67,10 +67,15 @@ export class FlottformTextInputHost extends BaseInputHost<Listeners> {
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');
Expand Down
1 change: 1 addition & 0 deletions flottform/forms/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type FlottformEventMap = {
error: [error: Error];
connected: [];
disconnected: [];
bufferedamountlow: [];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
4 changes: 4 additions & 0 deletions servers/demo/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
};
5 changes: 5 additions & 0 deletions servers/demo/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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."
/>
<DemoLink
href="{base}/flottform-messaging"
title="Flottform Messaging"
description="See how you can use Flottform to build a simple messaging application!"
/>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script lang="ts">
import { FlottformTextInputClient } from '@flottform/forms';
import { sdpExchangeServerBase } from '../../../api';
import { page } from '$app/stores';
import { onMount } from 'svelte';

let connectionStatus = $state<'init' | 'connected' | 'done' | 'disconnected' | 'error'>('init');
let error = $state<string>('');
let createWebRtcChannel: () => void;
let sendMessage: (text: string) => void;
let endConversation: () => void;

let messageInput = $state<string>('');
let messages = $state<{ sender: string; text: string }[]>([]);

function handleSend() {
if (messageInput.trim()) {
sendMessage(messageInput);
messages = [...messages, { text: messageInput, sender: 'client' }];
messageInput = '';
}
}
onMount(async () => {
const flottformTextInputClient = new FlottformTextInputClient({
endpointId: $page.params.endpointId,
flottformApi: sdpExchangeServerBase
});

flottformTextInputClient.start();
sendMessage = flottformTextInputClient.sendText;
endConversation = flottformTextInputClient.close;

flottformTextInputClient.on('connected', () => {
connectionStatus = 'connected';
});
flottformTextInputClient.on('text-transfered', (textTransfered) => {
// Nothing for now, just add the message to the list 'messages' which is done by the method 'handleSend'
});
flottformTextInputClient.on('text-received', (textReceived) => {
messages = [...messages, { text: textReceived, sender: 'host' }];
});
flottformTextInputClient.on('disconnected', () => {
connectionStatus = 'disconnected';
});
flottformTextInputClient.on('error', (e) => {
connectionStatus = 'error';
error = e;
});
});
</script>

<svelte:head>
<title>Flottform DEMO</title>
</svelte:head>

<div class="min-h-screen flex items-center justify-center overflow-hidden bg-gray-50">
<div class="max-w-screen-xl w-full p-4 box-border flex flex-col items-center">
<h1 class="text-2xl font-bold text-gray-800 mb-8">Flottform Messaging - Client</h1>
<div class="w-full max-w-md bg-white shadow-lg rounded-lg">
{#if connectionStatus === 'init'}
<div class="flex flex-col items-center w-full p-8">
<p class="p-8 text-center">Trying to connect to the host...</p>
</div>
{:else if connectionStatus === 'connected'}
<div class="flex flex-col rounded-lg border-[#ddd] h-[60vh]">
<div class="flex flex-col gap-3 flex-grow overflow-y-auto p-5">
{#if messages.length === 0}
<p class="text-center italic">
You're connected to Host! You can start exchanging messages!
</p>
{/if}
{#each messages as message}
<div class="p-3 rounded-lg max-w-[70%] break-words {message.sender}">
<p>{message.text}</p>
</div>
{/each}
</div>
<div class="p-5 border-t border-[#ddd]">
<input
type="text"
class="border border-[#ddd] p-3 mb-3 w-full rounded"
bind:value={messageInput}
placeholder="Type your message..."
onkeypress={(e) => e.key === 'Enter' && handleSend()}
/>
<div class="flex gap-3 flex-col sm:flex-row">
<button
class="text-white px-5 py-2.5 rounded-md border-none cursor-pointer bg-[#007bff] hover:bg-[#0056b3]"
onclick={handleSend}>Send</button
>
<button
class="text-white px-5 py-2.5 rounded-md border-none cursor-pointer bg-[#dc3545] hover:bg-[#c82333]"
onclick={endConversation}
>
End Conversation
</button>
</div>
</div>
</div>
{/if}
{#if connectionStatus === 'disconnected'}
<div class="flex flex-col items-center w-full gap-4 p-8">
<p class="text-center">Connection Channel Disconnected!</p>
<p class="text-center">
Do want to connect one more time? Scan the QR code from the other peer or paste the link
to the browser!
</p>
</div>
{/if}
{#if connectionStatus === 'error'}
<div class="flex flex-col items-center w-full gap-4 p-8">
<p class="text-center text-red-500">
Connection Failed with the following error: {error}
</p>
<button
onclick={createWebRtcChannel}
class="bg-[#0079b2] hover:bg-[#007bff] text-white px-4 py-2 rounded-lg text-base"
>Try to Connect Again</button
>
</div>
{/if}
</div>
</div>
</div>

<style>
.client {
align-self: flex-end;
background-color: #007bff;
color: white;
}

.host {
align-self: flex-start;
background-color: #e9ecef;
color: black;
}
</style>
Loading