From 570e2d4496756c40136efdcae2b4c91209e422ee Mon Sep 17 00:00:00 2001 From: Eduard-Constantin Ibinceanu Date: Fri, 4 Oct 2024 16:56:53 +0000 Subject: [PATCH 1/4] Create debounce function --- apps/web/lib/utils.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts index bd0c391d..01262daf 100644 --- a/apps/web/lib/utils.ts +++ b/apps/web/lib/utils.ts @@ -1,6 +1,17 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function debounce void>( + func: T, + delay: number +): T { + let timeoutId: ReturnType + return function (this: any, ...args: Parameters) { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => func.apply(this, args), delay) + } as T +} From 2f91b75247b3ecd303345fe1dea70714d1ea7387 Mon Sep 17 00:00:00 2001 From: Eduard-Constantin Ibinceanu Date: Fri, 4 Oct 2024 16:57:07 +0000 Subject: [PATCH 2/4] Add debounce to mouse event --- .../Objects/Editor/EditorWrapper.tsx | 165 +++++++++--------- 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/apps/web/components/Objects/Editor/EditorWrapper.tsx b/apps/web/components/Objects/Editor/EditorWrapper.tsx index 860beeae..b3221771 100644 --- a/apps/web/components/Objects/Editor/EditorWrapper.tsx +++ b/apps/web/components/Objects/Editor/EditorWrapper.tsx @@ -14,7 +14,8 @@ import { IndexeddbPersistence } from 'y-indexeddb' import { getCollaborationServerUrl } from '@services/config/config' import randomColor from 'randomcolor' import MouseMovements from './MouseMovements' -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from 'uuid' +import { debounce } from '@/lib/utils' interface EditorWrapperProps { content: string @@ -25,73 +26,75 @@ interface EditorWrapperProps { function EditorWrapper(props: EditorWrapperProps): JSX.Element { const session = useLHSession() as any - const access_token = session?.data?.tokens?.access_token; + const access_token = session?.data?.tokens?.access_token // Define provider in the state - const [provider, setProvider] = React.useState(null); - const [thisPageColor, setThisPageColor] = useState(randomColor({ luminosity: 'light' }) as string) - let uuid = uuidv4(); - const [onlinePageInstanceID, setOnlinePageInstanceID] = useState(uuid as string) - + const [provider, setProvider] = React.useState( + null + ) + const [thisPageColor, setThisPageColor] = useState( + randomColor({ luminosity: 'light' }) as string + ) + let uuid = uuidv4() + const [onlinePageInstanceID, setOnlinePageInstanceID] = useState( + uuid as string + ) /* Collaboration Features */ const collab = getCollaborationServerUrl() - const isCollabEnabledOnThisOrg = props.org.config.config.features.collaboration.enabled && collab + const isCollabEnabledOnThisOrg = + props.org.config.config.features.collaboration.enabled && collab const doc = new Y.Doc() // mouse movement - const [mouseMovements, setMouseMovements] = useState({} as any); - const timeoutRef = useRef(null); + const [mouseMovements, setMouseMovements] = useState({} as any) + const timeoutRef = useRef(null) const debouncedSetMouseMovements = (newMovements: any) => { if (timeoutRef.current) { - clearTimeout(timeoutRef.current); + clearTimeout(timeoutRef.current) } timeoutRef.current = setTimeout(() => { - setMouseMovements(newMovements); - }, 10); - }; - + setMouseMovements(newMovements) + }, 10) + } // Store the Y document in the browser new IndexeddbPersistence(props.activity.activity_uuid, doc) - document.addEventListener("mousemove", (event) => { - // Share any information you like - provider?.setAwarenessField("userMouseMovement", { - user: session.data.user, + document.addEventListener('mousemove', debounce((event: MouseEvent) => { + provider?.setAwarenessField('userMouseMovement', { + user: session.user, mouseX: event.clientX, mouseY: event.clientY, color: thisPageColor, - onlineInstanceID: onlinePageInstanceID - }); - }); - + onlineInstanceID: onlinePageInstanceID, + }) + }, 300)) async function setContent(content: any) { let activity = props.activity activity.content = content - - provider?.setAwarenessField("savings_states", { + provider?.setAwarenessField('savings_states', { [session.data.user.user_uuid]: { status: 'action_save', timestamp: new Date().toISOString(), - user: session.data.user - } - }); - - toast.promise(updateActivity(activity, activity.activity_uuid, access_token), { - loading: 'Saving...', - success: Activity saved!, - error: Could not save., + user: session.data.user, + }, }) + + toast.promise( + updateActivity(activity, activity.activity_uuid, access_token), + { + loading: 'Saving...', + success: Activity saved!, + error: Could not save., + } + ) } - - - // Create a ref to store the last save timestamp of each user - const lastSaveTimestampRef = useRef({}) as any; + const lastSaveTimestampRef = useRef({}) as any useEffect(() => { // Check if provider is not already set @@ -104,18 +107,18 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { // TODO(alpha code): This whole block of code should be improved to something more efficient and less hacky onConnect: () => { // Set the online page instance ID - setOnlinePageInstanceID(uuidv4()); + setOnlinePageInstanceID(uuidv4()) // Set the user color - setThisPageColor(randomColor({ luminosity: 'light' }) as string); + setThisPageColor(randomColor({ luminosity: 'light' }) as string) }, onAwarenessUpdate: ({ states }) => { - const usersStates = states; + const usersStates = states /* Showing user mouse movement */ usersStates.forEach((userState: any) => { if (userState.userMouseMovement) { - const userMouseMovement = userState.userMouseMovement; + const userMouseMovement = userState.userMouseMovement // Update the mouse movements state debouncedSetMouseMovements((prevMovements: any) => { @@ -126,15 +129,12 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { mouseX: userMouseMovement.mouseX, mouseY: userMouseMovement.mouseY, color: userMouseMovement.color, - onlinePageInstanceID: userMouseMovement.onlineInstanceID + onlinePageInstanceID: userMouseMovement.onlineInstanceID, }, - }; - } - ); - + } + }) } - }); - + }) /* Notifiying if a user has saved course content */ usersStates.forEach((userState: any) => { @@ -142,59 +142,68 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { const savingsState = userState.savings_states // Check if a user has saved the document - Object.keys(savingsState).forEach(user => { - const userObj = savingsState[user].user; - const status = savingsState[user].status; - const timestamp = savingsState[user].timestamp; + Object.keys(savingsState).forEach((user) => { + const userObj = savingsState[user].user + const status = savingsState[user].status + const timestamp = savingsState[user].timestamp // Get the current timestamp - const currentTimestamp = new Date().getTime(); + const currentTimestamp = new Date().getTime() // If the user has saved the document and the timestamp is close to the current timestamp, show the toast - if (status === 'action_save' && Math.abs(currentTimestamp - new Date(timestamp).getTime()) < 10) { // 5000 milliseconds = 5 seconds + if ( + status === 'action_save' && + Math.abs(currentTimestamp - new Date(timestamp).getTime()) < + 10 + ) { + // 5000 milliseconds = 5 seconds // Update the last save timestamp for this user - lastSaveTimestampRef.current[user] = timestamp; + lastSaveTimestampRef.current[user] = timestamp - toast.success(`${userObj.first_name} ${userObj.last_name} has saved the document`); + toast.success( + `${userObj.first_name} ${userObj.last_name} has saved the document` + ) } - }); + }) } }) }, - }); + }) // Set the new provider - setProvider(newProvider); + setProvider(newProvider) } - }, []); - + }, []) { return ( <> - + - {!session.isLoading && ()} + {!session.isLoading && ( + + )} ) } } - - export default EditorWrapper - From d9c31e4d07216fcd06029657a592998610976cec Mon Sep 17 00:00:00 2001 From: Eduard-Constantin Ibinceanu Date: Sun, 13 Oct 2024 14:33:40 +0000 Subject: [PATCH 3/4] Fix format --- .../Objects/Editor/EditorWrapper.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/web/components/Objects/Editor/EditorWrapper.tsx b/apps/web/components/Objects/Editor/EditorWrapper.tsx index b3221771..c7094fa8 100644 --- a/apps/web/components/Objects/Editor/EditorWrapper.tsx +++ b/apps/web/components/Objects/Editor/EditorWrapper.tsx @@ -61,15 +61,18 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { // Store the Y document in the browser new IndexeddbPersistence(props.activity.activity_uuid, doc) - document.addEventListener('mousemove', debounce((event: MouseEvent) => { - provider?.setAwarenessField('userMouseMovement', { - user: session.user, - mouseX: event.clientX, - mouseY: event.clientY, - color: thisPageColor, - onlineInstanceID: onlinePageInstanceID, - }) - }, 300)) + document.addEventListener( + 'mousemove', + debounce((event: MouseEvent) => { + provider?.setAwarenessField('userMouseMovement', { + user: session.data.user, + mouseX: event.clientX, + mouseY: event.clientY, + color: thisPageColor, + onlineInstanceID: onlinePageInstanceID, + }) + }, 300) + ) async function setContent(content: any) { let activity = props.activity From d90af6947ef07e49fdb056adb5e36cdd6f59171a Mon Sep 17 00:00:00 2001 From: Eduard-Constantin Ibinceanu Date: Sun, 13 Oct 2024 14:36:45 +0000 Subject: [PATCH 4/4] Format for real --- .../Objects/Editor/EditorWrapper.tsx | 153 ++++++++---------- 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/apps/web/components/Objects/Editor/EditorWrapper.tsx b/apps/web/components/Objects/Editor/EditorWrapper.tsx index c7094fa8..e51180a3 100644 --- a/apps/web/components/Objects/Editor/EditorWrapper.tsx +++ b/apps/web/components/Objects/Editor/EditorWrapper.tsx @@ -14,8 +14,8 @@ import { IndexeddbPersistence } from 'y-indexeddb' import { getCollaborationServerUrl } from '@services/config/config' import randomColor from 'randomcolor' import MouseMovements from './MouseMovements' -import { v4 as uuidv4 } from 'uuid' -import { debounce } from '@/lib/utils' +import { v4 as uuidv4 } from 'uuid'; +import { debounce } from '@/lib/utils'; interface EditorWrapperProps { content: string @@ -26,37 +26,32 @@ interface EditorWrapperProps { function EditorWrapper(props: EditorWrapperProps): JSX.Element { const session = useLHSession() as any - const access_token = session?.data?.tokens?.access_token + const access_token = session?.data?.tokens?.access_token; // Define provider in the state - const [provider, setProvider] = React.useState( - null - ) - const [thisPageColor, setThisPageColor] = useState( - randomColor({ luminosity: 'light' }) as string - ) - let uuid = uuidv4() - const [onlinePageInstanceID, setOnlinePageInstanceID] = useState( - uuid as string - ) + const [provider, setProvider] = React.useState(null); + const [thisPageColor, setThisPageColor] = useState(randomColor({ luminosity: 'light' }) as string) + let uuid = uuidv4(); + const [onlinePageInstanceID, setOnlinePageInstanceID] = useState(uuid as string) + /* Collaboration Features */ const collab = getCollaborationServerUrl() - const isCollabEnabledOnThisOrg = - props.org.config.config.features.collaboration.enabled && collab + const isCollabEnabledOnThisOrg = props.org.config.config.features.collaboration.enabled && collab const doc = new Y.Doc() // mouse movement - const [mouseMovements, setMouseMovements] = useState({} as any) - const timeoutRef = useRef(null) + const [mouseMovements, setMouseMovements] = useState({} as any); + const timeoutRef = useRef(null); const debouncedSetMouseMovements = (newMovements: any) => { if (timeoutRef.current) { - clearTimeout(timeoutRef.current) + clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { - setMouseMovements(newMovements) - }, 10) - } + setMouseMovements(newMovements); + }, 10); + }; + // Store the Y document in the browser new IndexeddbPersistence(props.activity.activity_uuid, doc) @@ -78,26 +73,27 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { let activity = props.activity activity.content = content - provider?.setAwarenessField('savings_states', { + + provider?.setAwarenessField("savings_states", { [session.data.user.user_uuid]: { status: 'action_save', timestamp: new Date().toISOString(), - user: session.data.user, - }, - }) - - toast.promise( - updateActivity(activity, activity.activity_uuid, access_token), - { - loading: 'Saving...', - success: Activity saved!, - error: Could not save., + user: session.data.user } - ) + }); + + toast.promise(updateActivity(activity, activity.activity_uuid, access_token), { + loading: 'Saving...', + success: Activity saved!, + error: Could not save., + }) } + + + // Create a ref to store the last save timestamp of each user - const lastSaveTimestampRef = useRef({}) as any + const lastSaveTimestampRef = useRef({}) as any; useEffect(() => { // Check if provider is not already set @@ -110,18 +106,18 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { // TODO(alpha code): This whole block of code should be improved to something more efficient and less hacky onConnect: () => { // Set the online page instance ID - setOnlinePageInstanceID(uuidv4()) + setOnlinePageInstanceID(uuidv4()); // Set the user color - setThisPageColor(randomColor({ luminosity: 'light' }) as string) + setThisPageColor(randomColor({ luminosity: 'light' }) as string); }, onAwarenessUpdate: ({ states }) => { - const usersStates = states + const usersStates = states; /* Showing user mouse movement */ usersStates.forEach((userState: any) => { if (userState.userMouseMovement) { - const userMouseMovement = userState.userMouseMovement + const userMouseMovement = userState.userMouseMovement; // Update the mouse movements state debouncedSetMouseMovements((prevMovements: any) => { @@ -132,12 +128,15 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { mouseX: userMouseMovement.mouseX, mouseY: userMouseMovement.mouseY, color: userMouseMovement.color, - onlinePageInstanceID: userMouseMovement.onlineInstanceID, + onlinePageInstanceID: userMouseMovement.onlineInstanceID }, - } - }) + }; + } + ); + } - }) + }); + /* Notifiying if a user has saved course content */ usersStates.forEach((userState: any) => { @@ -145,68 +144,58 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { const savingsState = userState.savings_states // Check if a user has saved the document - Object.keys(savingsState).forEach((user) => { - const userObj = savingsState[user].user - const status = savingsState[user].status - const timestamp = savingsState[user].timestamp + Object.keys(savingsState).forEach(user => { + const userObj = savingsState[user].user; + const status = savingsState[user].status; + const timestamp = savingsState[user].timestamp; // Get the current timestamp - const currentTimestamp = new Date().getTime() + const currentTimestamp = new Date().getTime(); // If the user has saved the document and the timestamp is close to the current timestamp, show the toast - if ( - status === 'action_save' && - Math.abs(currentTimestamp - new Date(timestamp).getTime()) < - 10 - ) { - // 5000 milliseconds = 5 seconds + if (status === 'action_save' && Math.abs(currentTimestamp - new Date(timestamp).getTime()) < 10) { // 5000 milliseconds = 5 seconds // Update the last save timestamp for this user - lastSaveTimestampRef.current[user] = timestamp + lastSaveTimestampRef.current[user] = timestamp; - toast.success( - `${userObj.first_name} ${userObj.last_name} has saved the document` - ) + toast.success(`${userObj.first_name} ${userObj.last_name} has saved the document`); } - }) + }); } }) }, - }) + }); // Set the new provider - setProvider(newProvider) + setProvider(newProvider); } - }, []) + }, []); + { return ( <> - + - {!session.isLoading && ( - - )} + {!session.isLoading && ()} ) } } -export default EditorWrapper + + +export default EditorWrapper \ No newline at end of file