Skip to content

Commit

Permalink
Following logic
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <[email protected]>
  • Loading branch information
hweihwang committed Dec 11, 2024
1 parent ac790eb commit 9dd824d
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 31 deletions.
22 changes: 22 additions & 0 deletions src/collaboration/Portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { generateUrl } from '@nextcloud/router'
enum BroadcastType {
SceneInit = 'SCENE_INIT',
MouseLocation = 'MOUSE_LOCATION',
ViewportUpdate = 'VIEWPORT_UPDATE',
}

export class Portal {
Expand Down Expand Up @@ -151,6 +152,14 @@ export class Portal {
case BroadcastType.MouseLocation:
this.collab.updateCursor(decoded.payload)
break
case BroadcastType.ViewportUpdate:
this.collab.updateCollaboratorViewport(
decoded.payload.userId,
decoded.payload.scrollX,
decoded.payload.scrollY,
decoded.payload.zoom,
)
break
}
}

Expand Down Expand Up @@ -247,4 +256,17 @@ export class Portal {
})
}

async broadcastViewport(scrollX: number, scrollY: number, zoom: number) {
const data = {
type: BroadcastType.ViewportUpdate,
payload: {
userId: this.socket?.id,
scrollX,
scrollY,
zoom,
},
}
await this._broadcastSocketData(data, true)
}

}
167 changes: 137 additions & 30 deletions src/collaboration/collab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
*/

import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import type { AppState, BinaryFileData, BinaryFiles, Collaborator, ExcalidrawImperativeAPI, Gesture } from '@excalidraw/excalidraw/types/types'
import type {
AppState,
BinaryFileData,
BinaryFiles,
Collaborator,
ExcalidrawImperativeAPI,
Gesture,
} from '@excalidraw/excalidraw/types/types'
import { Portal } from './Portal'
import { restoreElements } from '@excalidraw/excalidraw'
import { throttle } from 'lodash'
Expand All @@ -20,8 +27,19 @@ export class Collab {
lastBroadcastedOrReceivedSceneVersion: number = -1
private collaborators = new Map<string, Collaborator>()
private files = new Map<string, BinaryFileData>()
private followedUserId: string | null = null
private lastBroadcastedViewport = {
scrollX: 0,
scrollY: 0,
zoom: 1,
}

constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>) {
constructor(
excalidrawAPI: ExcalidrawImperativeAPI,
fileId: number,
publicSharingToken: string | null,
setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>,
) {
this.excalidrawAPI = excalidrawAPI
this.fileId = fileId
this.publicSharingToken = publicSharingToken
Expand All @@ -36,6 +54,8 @@ export class Collab {
this.portal.connectSocket()

this.excalidrawAPI.onChange(this.onChange)

window.collab = this
}

getSceneElementsIncludingDeleted = () => {
Expand All @@ -47,59 +67,92 @@ export class Collab {
const localElements = this.getSceneElementsIncludingDeleted()
const appState = this.excalidrawAPI.getAppState()

return reconcileElements(localElements, restoredRemoteElements, appState)
return reconcileElements(
localElements,
restoredRemoteElements,
appState,
)
}

handleRemoteSceneUpdate = (elements: ExcalidrawElement[]) => {
this.excalidrawAPI.updateScene({
elements,
},
)
})
}

private getLastBroadcastedOrReceivedSceneVersion = () => {
return this.lastBroadcastedOrReceivedSceneVersion
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onChange = (elements: readonly ExcalidrawElement[], _state: AppState, files: BinaryFiles) => {
if (hashElementsVersion(elements)
private onChange = (
elements: readonly ExcalidrawElement[],
state: AppState,
files: BinaryFiles,
) => {
if (
hashElementsVersion(elements)
> this.getLastBroadcastedOrReceivedSceneVersion()
) {
this.lastBroadcastedOrReceivedSceneVersion = hashElementsVersion(elements)
this.lastBroadcastedOrReceivedSceneVersion
= hashElementsVersion(elements)
throttle(() => {
this.portal.broadcastScene('SCENE_INIT', elements)

const syncedFiles = Array.from(this.files.keys())
const newFiles = Object.keys(files).filter((id) => !syncedFiles.includes(id)).reduce((acc, id) => {
acc[id] = files[id]
return acc
}, {} as BinaryFiles)
const newFiles = Object.keys(files)
.filter((id) => !syncedFiles.includes(id))
.reduce((acc, id) => {
acc[id] = files[id]
return acc
}, {} as BinaryFiles)
if (Object.keys(newFiles).length > 0) {
this.portal.sendImageFiles(newFiles)
}
})()
}

this.broadcastViewportIfChanged(state)
}

private broadcastViewportIfChanged(state: AppState) {
const { scrollX, scrollY, zoom } = state
if (
scrollX !== this.lastBroadcastedViewport.scrollX
|| scrollY !== this.lastBroadcastedViewport.scrollY
|| zoom.value !== this.lastBroadcastedViewport.zoom
) {
this.lastBroadcastedViewport = {
scrollX,
scrollY,
zoom: zoom.value,
}
this.portal.broadcastViewport(scrollX, scrollY, zoom.value)
}
}

onPointerUpdate = (payload: {
pointersMap: Gesture['pointers'],
pointer: { x: number; y: number; tool: 'laser' | 'pointer' },
pointersMap: Gesture['pointers']
pointer: { x: number; y: number; tool: 'laser' | 'pointer' }
button: 'down' | 'up'
}) => {
payload.pointersMap.size < 2 && this.portal.socket && this.portal.broadcastMouseLocation(payload)
payload.pointersMap.size < 2
&& this.portal.socket
&& this.portal.broadcastMouseLocation(payload)
}

updateCollaborators = (users: {
user: {
id: string,
name: string
},
socketId: string,
pointer: { x: number, y: number, tool: 'pointer' | 'laser' },
button: 'down' | 'up',
selectedElementIds: AppState['selectedElementIds']
}[]) => {
updateCollaborators = (
users: {
user: {
id: string
name: string
}
socketId: string
pointer: { x: number; y: number; tool: 'pointer' | 'laser' }
button: 'down' | 'up'
selectedElementIds: AppState['selectedElementIds']
}[],
) => {
const collaborators = new Map<string, Collaborator>()

users.forEach((payload) => {
Expand All @@ -115,12 +168,12 @@ export class Collab {
}

updateCursor = (payload: {
socketId: string,
pointer: { x: number, y: number, tool: 'pointer' | 'laser' },
button: 'down' | 'up',
selectedElementIds: AppState['selectedElementIds'],
socketId: string
pointer: { x: number; y: number; tool: 'pointer' | 'laser' }
button: 'down' | 'up'
selectedElementIds: AppState['selectedElementIds']
user: {
id: string,
id: string
name: string
}
}) => {
Expand Down Expand Up @@ -152,4 +205,58 @@ export class Collab {
this.excalidrawAPI.addFiles([file])
}

followUser(userId: string) {
this.followedUserId = userId
const collabInfo = this.collaborators.get(userId)
if (collabInfo?.viewport) {
this.applyViewport(collabInfo.viewport)
}
}

unfollowUser() {
this.followedUserId = null
}

updateCollaboratorViewport = (
userId: string,
scrollX: number,
scrollY: number,
zoom: number,
) => {
const collaborator = this.collaborators.get(userId)
if (!collaborator) return

const updated = {
...collaborator,
viewport: { scrollX, scrollY, zoom },
}
this.collaborators.set(userId, updated)

if (this.followedUserId === userId) {
this.applyViewport({ scrollX, scrollY, zoom })
}

this.excalidrawAPI.updateScene({ collaborators: this.collaborators })
}

private applyViewport({
scrollX,
scrollY,
zoom,
}: {
scrollX: number
scrollY: number
zoom: number
}) {
const appState = this.excalidrawAPI.getAppState()
this.excalidrawAPI.updateScene({
appState: {
...appState,
scrollX,
scrollY,
zoom: { value: zoom },
},
})
}

}
25 changes: 24 additions & 1 deletion websocket_server/SocketManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ export default class SocketManager {
Utils.convertArrayBufferToString(encryptedData),
)

if (payload.type === 'MOUSE_LOCATION') {
switch (payload.type) {
case 'MOUSE_LOCATION': {
const socketData = await this.socketDataManager.getSocketData(
socket.id,
)
Expand All @@ -252,6 +253,28 @@ export default class SocketManager {
'client-broadcast',
Utils.convertStringToArrayBuffer(JSON.stringify(eventData)),
)
break
}

case 'VIEWPORT_UPDATE': {
const socketData = await this.socketDataManager.getSocketData(socket.id)
if (!socketData) return
const eventData = {
type: 'VIEWPORT_UPDATE',
payload: {
...payload.payload,
userId: socketData.user.id,
},
}

socket.volatile.broadcast
.to(roomID)
.emit(
'client-broadcast',
Utils.convertStringToArrayBuffer(JSON.stringify(eventData)),
)
break
}
}
}

Expand Down

0 comments on commit 9dd824d

Please sign in to comment.