follow scroll location POC

This commit is contained in:
barnabasmolnar 2023-07-21 00:34:29 +02:00
parent a80ac4c748
commit 9152ce24f2
8 changed files with 108 additions and 2 deletions

View File

@ -9,7 +9,9 @@ export const actionGoToCollaborator = register({
viewMode: true, viewMode: true,
trackEvent: { category: "collab" }, trackEvent: { category: "collab" },
perform: (_elements, appState, value) => { perform: (_elements, appState, value) => {
const point = value as Collaborator["pointer"]; const _value = value as Collaborator & { clientId: string };
const point = _value.pointer;
if (!point) { if (!point) {
return { appState, commitToHistory: false }; return { appState, commitToHistory: false };
} }
@ -17,6 +19,8 @@ export const actionGoToCollaborator = register({
return { return {
appState: { appState: {
...appState, ...appState,
// ˇˇ or maybe an atom? 🤔
userToFollow: _value.clientId,
...centerScrollOn({ ...centerScrollOn({
scenePoint: point, scenePoint: point,
viewportDimensions: { viewportDimensions: {
@ -39,7 +43,7 @@ export const actionGoToCollaborator = register({
return ( return (
<Avatar <Avatar
color={background} color={background}
onClick={() => updateData(collaborator.pointer)} onClick={() => updateData({ ...collaborator, clientId })}
name={collaborator.username || ""} name={collaborator.username || ""}
src={collaborator.avatarUrl} src={collaborator.avatarUrl}
/> />

View File

@ -98,6 +98,7 @@ export const getDefaultAppState = (): Omit<
pendingImageElementId: null, pendingImageElementId: null,
showHyperlinkPopup: false, showHyperlinkPopup: false,
selectedLinearElement: null, selectedLinearElement: null,
userToFollow: null,
}; };
}; };
@ -204,6 +205,7 @@ const APP_STATE_STORAGE_CONF = (<
pendingImageElementId: { browser: false, export: false, server: false }, pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false }, showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false }, selectedLinearElement: { browser: true, export: false, server: false },
userToFollow: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <

View File

@ -1500,6 +1500,8 @@ class App extends React.Component<AppProps, AppState> {
this.refreshDeviceState(this.excalidrawContainerRef.current); this.refreshDeviceState(this.excalidrawContainerRef.current);
} }
// TODO follow-participant
// add zoom change
if ( if (
prevState.scrollX !== this.state.scrollX || prevState.scrollX !== this.state.scrollX ||
prevState.scrollY !== this.state.scrollY prevState.scrollY !== this.state.scrollY

View File

@ -89,6 +89,7 @@ export interface CollabAPI {
/** function so that we can access the latest value from stale callbacks */ /** function so that we can access the latest value from stale callbacks */
isCollaborating: () => boolean; isCollaborating: () => boolean;
onPointerUpdate: CollabInstance["onPointerUpdate"]; onPointerUpdate: CollabInstance["onPointerUpdate"];
onScrollChange: CollabInstance["onScrollChange"];
startCollaboration: CollabInstance["startCollaboration"]; startCollaboration: CollabInstance["startCollaboration"];
stopCollaboration: CollabInstance["stopCollaboration"]; stopCollaboration: CollabInstance["stopCollaboration"];
syncElements: CollabInstance["syncElements"]; syncElements: CollabInstance["syncElements"];
@ -162,6 +163,7 @@ class Collab extends PureComponent<Props, CollabState> {
const collabAPI: CollabAPI = { const collabAPI: CollabAPI = {
isCollaborating: this.isCollaborating, isCollaborating: this.isCollaborating,
onPointerUpdate: this.onPointerUpdate, onPointerUpdate: this.onPointerUpdate,
onScrollChange: this.onScrollChange,
startCollaboration: this.startCollaboration, startCollaboration: this.startCollaboration,
syncElements: this.syncElements, syncElements: this.syncElements,
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
@ -506,6 +508,8 @@ class Collab extends PureComponent<Props, CollabState> {
break; break;
} }
case WS_SCENE_EVENT_TYPES.UPDATE: case WS_SCENE_EVENT_TYPES.UPDATE:
console.log("received update", decryptedData);
console.log(this.excalidrawAPI.getAppState());
this.handleRemoteSceneUpdate( this.handleRemoteSceneUpdate(
this.reconcileElements(decryptedData.payload.elements), this.reconcileElements(decryptedData.payload.elements),
); );
@ -513,6 +517,9 @@ class Collab extends PureComponent<Props, CollabState> {
case "MOUSE_LOCATION": { case "MOUSE_LOCATION": {
const { pointer, button, username, selectedElementIds } = const { pointer, button, username, selectedElementIds } =
decryptedData.payload; decryptedData.payload;
// console.log({ decryptedData });
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
decryptedData.payload.socketId || decryptedData.payload.socketId ||
// @ts-ignore legacy, see #2094 (#2097) // @ts-ignore legacy, see #2094 (#2097)
@ -530,6 +537,35 @@ class Collab extends PureComponent<Props, CollabState> {
}); });
break; break;
} }
// TODO follow-participant
// case "SCROLL_LOCATION"
// case "ZOOM_VALUE"
// if following someone, update scroll and zoom
case "SCROLL_LOCATION":
const {
scroll: { x, y },
} = decryptedData.payload;
const socketId: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["socketId"] =
decryptedData.payload.socketId;
console.log({ decryptedData });
const appState = this.excalidrawAPI.getAppState();
console.log({ appState });
if (appState.userToFollow === socketId) {
this.excalidrawAPI.updateScene({
appState: {
scrollX: x,
scrollY: y,
},
});
}
break;
case "IDLE_STATUS": { case "IDLE_STATUS": {
const { userState, socketId, username } = decryptedData.payload; const { userState, socketId, username } = decryptedData.payload;
const collaborators = new Map(this.collaborators); const collaborators = new Map(this.collaborators);
@ -756,6 +792,7 @@ class Collab extends PureComponent<Props, CollabState> {
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
pointersMap: Gesture["pointers"]; pointersMap: Gesture["pointers"];
}) => { }) => {
// console.log({ payload });
payload.pointersMap.size < 2 && payload.pointersMap.size < 2 &&
this.portal.socket && this.portal.socket &&
this.portal.broadcastMouseLocation(payload); this.portal.broadcastMouseLocation(payload);
@ -763,6 +800,21 @@ class Collab extends PureComponent<Props, CollabState> {
CURSOR_SYNC_TIMEOUT, CURSOR_SYNC_TIMEOUT,
); );
// TODO follow-participant
// - onScrollChange
// -- broadCastScrollLocation
// - onZoomChange
// -- broadCastZoomValue
onScrollChange = throttle(
(payload: {
scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"];
scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"];
}) => {
this.portal.socket && this.portal.broadcastScrollLocation(payload);
},
);
onIdleStateChange = (userState: UserIdleState) => { onIdleStateChange = (userState: UserIdleState) => {
this.portal.broadcastIdleChange(userState); this.portal.broadcastIdleChange(userState);
}; };

View File

@ -213,6 +213,36 @@ class Portal {
username: this.collab.state.username, username: this.collab.state.username,
}, },
}; };
// console.log("broadcastMouseLocation data", data);
return this._broadcastSocketData(
data as SocketUpdateData,
true, // volatile
);
}
};
// TODO follow-participant
// - broadCastScrollLocation
// - broadCastZoomValue
broadcastScrollLocation = (payload: {
scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"];
scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"];
}) => {
if (this.socket?.id) {
const data: SocketUpdateDataSource["SCROLL_LOCATION"] = {
type: "SCROLL_LOCATION",
payload: {
socketId: this.socket.id,
scroll: { x: payload.scrollX, y: payload.scrollY },
username: this.collab.state.username,
},
};
console.log("broadcastScrollLocation data", data);
return this._broadcastSocketData( return this._broadcastSocketData(
data as SocketUpdateData, data as SocketUpdateData,
true, // volatile true, // volatile

View File

@ -113,6 +113,14 @@ export type SocketUpdateDataSource = {
username: string; username: string;
}; };
}; };
SCROLL_LOCATION: {
type: "SCROLL_LOCATION";
payload: {
socketId: string;
scroll: { x: number; y: number };
username: string;
};
};
IDLE_STATUS: { IDLE_STATUS: {
type: "IDLE_STATUS"; type: "IDLE_STATUS";
payload: { payload: {

View File

@ -649,6 +649,13 @@ const ExcalidrawWrapper = () => {
})} })}
> >
<Excalidraw <Excalidraw
// TODO follow-participant
// add onZoomChange
onScrollChange={(x, y) => {
// console.log({ x, y });
collabAPI?.onScrollChange({ scrollX: x, scrollY: y });
}}
ref={excalidrawRefCallback} ref={excalidrawRefCallback}
onChange={onChange} onChange={onChange}
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}

View File

@ -223,6 +223,7 @@ export type AppState = {
pendingImageElementId: ExcalidrawImageElement["id"] | null; pendingImageElementId: ExcalidrawImageElement["id"] | null;
showHyperlinkPopup: false | "info" | "editor"; showHyperlinkPopup: false | "info" | "editor";
selectedLinearElement: LinearElementEditor | null; selectedLinearElement: LinearElementEditor | null;
userToFollow: string | null;
}; };
export type UIAppState = Omit< export type UIAppState = Omit<