From 9320e6fffb04724d5a50109eb7136eafedcbec1c Mon Sep 17 00:00:00 2001 From: Nathan Wang Date: Fri, 20 Dec 2024 21:23:35 -0500 Subject: [PATCH] add saving / saved indicator (#167) --- .../RealtimeEditor/RealtimeEditor.tsx | 59 ++++++++++++++++++- .../EditorConnectionStatusIndicator.tsx | 9 ++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/components/RealtimeEditor/RealtimeEditor.tsx b/src/components/RealtimeEditor/RealtimeEditor.tsx index a5a90f2a..38d0c4fc 100644 --- a/src/components/RealtimeEditor/RealtimeEditor.tsx +++ b/src/components/RealtimeEditor/RealtimeEditor.tsx @@ -26,6 +26,38 @@ const WEBSOCKET_SERVER = SHOULD_USE_DEV_YJS_SERVER ? 'ws://localhost:1234' : 'wss://yjs.usaco.guide:443'; +const createWebsocketInterceptorClass = ( + onMessageSyncHandler: () => void, + onSaveHandler: () => void +) => { + return class WebsocketInterceptor extends WebSocket { + send(data: string | ArrayBuffer | Blob | ArrayBufferView) { + const messageSync = 0; // defined in y-websocket + if ( + data instanceof Uint8Array && + data.length > 0 && + data[0] === messageSync + ) { + console.log(data); + onMessageSyncHandler(); + } + super.send(data); + } + + set onmessage(f: any) { + const messageSaved = 100; // defined in ide-yjs + super.onmessage = m => { + const decoder = new Uint8Array(m.data); + if (decoder.length > 0 && decoder[0] === messageSaved) { + onSaveHandler(); + } else { + f(m); + } + }; + } + }; +}; + const RealtimeEditor = ({ defaultValue, yjsDocumentId, @@ -43,7 +75,7 @@ const RealtimeEditor = ({ } | null>(null); const [connectionStatus, setConnectionStatus] = useState< - 'disconnected' | 'connecting' | 'connected' + 'disconnected' | 'connecting' | 'saved' | 'saving' >('disconnected'); const [isSynced, setIsSynced] = useState(false); @@ -58,10 +90,31 @@ const RealtimeEditor = ({ const documentId = yjsDocumentId; const ydocument = new Y.Doc(); + + let lastUpdate = 0; + const CustomWebsocketInterceptor = createWebsocketInterceptorClass( + () => { + if (!provider._synced) { + return; + } + + // As a sketchy hack, we'll assume every sync message sent + // corresponds to an edit the user made. + lastUpdate = Date.now(); + setConnectionStatus('saving'); + }, + () => { + if (Date.now() - lastUpdate > 1000) { + setConnectionStatus('saved'); + } + } + ); + const provider = new WebsocketProvider( WEBSOCKET_SERVER, documentId, - ydocument + ydocument, + { WebSocketPolyfill: CustomWebsocketInterceptor } ); // Set the cursor color @@ -118,7 +171,7 @@ const RealtimeEditor = ({ provider.on( 'status', ({ status }: { status: 'disconnected' | 'connecting' | 'connected' }) => { - setConnectionStatus(status); + setConnectionStatus(status === 'connected' ? 'saved' : status); } ); provider.on('sync', (isSynced: boolean) => { diff --git a/src/components/editor/EditorConnectionStatusIndicator.tsx b/src/components/editor/EditorConnectionStatusIndicator.tsx index 2e4a1d2a..89f40c5c 100644 --- a/src/components/editor/EditorConnectionStatusIndicator.tsx +++ b/src/components/editor/EditorConnectionStatusIndicator.tsx @@ -10,16 +10,19 @@ export default function EditorConnectionStatusIndicator({ connectionStatus, isSynced, }: { - connectionStatus: 'disconnected' | 'connecting' | 'connected'; + connectionStatus: 'disconnected' | 'connecting' | 'saving' | 'saved'; isSynced: boolean; }) { let connectionText; let statusIndicatorClass; // for now, our editors are either connecting or connected. - if (connectionStatus === 'connected' && isSynced) { - connectionText = 'Connected'; + if (connectionStatus === 'saved' && isSynced) { + connectionText = 'Saved'; statusIndicatorClass = 'bg-green-500'; + } else if (connectionStatus == 'saving') { + connectionText = 'Saving...'; + statusIndicatorClass = 'bg-yellow-500'; } else { connectionText = 'Connecting...'; statusIndicatorClass = 'bg-yellow-500';