fix: mixing clientId & socketId in UserList (#7461)

This commit is contained in:
David Luzar 2023-12-18 18:21:57 +01:00 committed by GitHub
parent 0808532b49
commit 57ea4e61d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 46 additions and 39 deletions

View File

@ -123,7 +123,7 @@ class Collab extends PureComponent<Props, CollabState> {
private socketInitializationTimer?: number; private socketInitializationTimer?: number;
private lastBroadcastedOrReceivedSceneVersion: number = -1; private lastBroadcastedOrReceivedSceneVersion: number = -1;
private collaborators = new Map<string, Collaborator>(); private collaborators = new Map<SocketId, Collaborator>();
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -618,7 +618,7 @@ class Collab extends PureComponent<Props, CollabState> {
this.portal.socket.on( this.portal.socket.on(
WS_EVENTS.USER_FOLLOW_ROOM_CHANGE, WS_EVENTS.USER_FOLLOW_ROOM_CHANGE,
(followedBy: string[]) => { (followedBy: SocketId[]) => {
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
appState: { followedBy: new Set(followedBy) }, appState: { followedBy: new Set(followedBy) },
}); });
@ -795,7 +795,7 @@ class Collab extends PureComponent<Props, CollabState> {
document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
}; };
setCollaborators(sockets: string[]) { setCollaborators(sockets: SocketId[]) {
const collaborators: InstanceType<typeof Collab>["collaborators"] = const collaborators: InstanceType<typeof Collab>["collaborators"] =
new Map(); new Map();
for (const socketId of sockets) { for (const socketId of sockets) {

View File

@ -10,6 +10,7 @@ import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import { import {
OnUserFollowedPayload, OnUserFollowedPayload,
SocketId,
UserIdleState, UserIdleState,
} from "../../packages/excalidraw/types"; } from "../../packages/excalidraw/types";
import { trackEvent } from "../../packages/excalidraw/analytics"; import { trackEvent } from "../../packages/excalidraw/analytics";
@ -51,7 +52,7 @@ class Portal {
/* syncAll */ true, /* syncAll */ true,
); );
}); });
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: SocketId[]) => {
this.collab.setCollaborators(clients); this.collab.setCollaborators(clients);
}); });
@ -186,7 +187,7 @@ class Portal {
const data: SocketUpdateDataSource["IDLE_STATUS"] = { const data: SocketUpdateDataSource["IDLE_STATUS"] = {
type: WS_SUBTYPES.IDLE_STATUS, type: WS_SUBTYPES.IDLE_STATUS,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id as SocketId,
userState, userState,
username: this.collab.state.username, username: this.collab.state.username,
}, },
@ -206,7 +207,7 @@ class Portal {
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
type: WS_SUBTYPES.MOUSE_LOCATION, type: WS_SUBTYPES.MOUSE_LOCATION,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id as SocketId,
pointer: payload.pointer, pointer: payload.pointer,
button: payload.button || "up", button: payload.button || "up",
selectedElementIds: selectedElementIds:
@ -232,7 +233,7 @@ class Portal {
const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = { const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = {
type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS, type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id as SocketId,
username: this.collab.state.username, username: this.collab.state.username,
sceneBounds: payload.sceneBounds, sceneBounds: payload.sceneBounds,
}, },

View File

@ -22,6 +22,7 @@ import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
SocketId,
UserIdleState, UserIdleState,
} from "../../packages/excalidraw/types"; } from "../../packages/excalidraw/types";
import { bytesToHexString } from "../../packages/excalidraw/utils"; import { bytesToHexString } from "../../packages/excalidraw/utils";
@ -117,7 +118,7 @@ export type SocketUpdateDataSource = {
MOUSE_LOCATION: { MOUSE_LOCATION: {
type: WS_SUBTYPES.MOUSE_LOCATION; type: WS_SUBTYPES.MOUSE_LOCATION;
payload: { payload: {
socketId: string; socketId: SocketId;
pointer: { x: number; y: number; tool: "pointer" | "laser" }; pointer: { x: number; y: number; tool: "pointer" | "laser" };
button: "down" | "up"; button: "down" | "up";
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
@ -127,7 +128,7 @@ export type SocketUpdateDataSource = {
USER_VISIBLE_SCENE_BOUNDS: { USER_VISIBLE_SCENE_BOUNDS: {
type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS; type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
payload: { payload: {
socketId: string; socketId: SocketId;
username: string; username: string;
sceneBounds: SceneBounds; sceneBounds: SceneBounds;
}; };
@ -135,7 +136,7 @@ export type SocketUpdateDataSource = {
IDLE_STATUS: { IDLE_STATUS: {
type: WS_SUBTYPES.IDLE_STATUS; type: WS_SUBTYPES.IDLE_STATUS;
payload: { payload: {
socketId: string; socketId: SocketId;
userState: UserIdleState; userState: UserIdleState;
username: string; username: string;
}; };

View File

@ -12,6 +12,7 @@ export const actionGoToCollaborator = register({
trackEvent: { category: "collab" }, trackEvent: { category: "collab" },
perform: (_elements, appState, collaborator: Collaborator) => { perform: (_elements, appState, collaborator: Collaborator) => {
if ( if (
!collaborator.socketId ||
appState.userToFollow?.socketId === collaborator.socketId || appState.userToFollow?.socketId === collaborator.socketId ||
collaborator.isCurrentUser collaborator.isCurrentUser
) { ) {
@ -28,7 +29,7 @@ export const actionGoToCollaborator = register({
appState: { appState: {
...appState, ...appState,
userToFollow: { userToFollow: {
socketId: collaborator.socketId!, socketId: collaborator.socketId,
username: collaborator.username || "", username: collaborator.username || "",
}, },
// Close mobile menu // Close mobile menu

View File

@ -3472,7 +3472,6 @@ class App extends React.Component<AppProps, AppState> {
}; };
private maybeUnfollowRemoteUser = () => { private maybeUnfollowRemoteUser = () => {
console.warn("maybeUnfollowRemoteUser");
if (this.state.userToFollow) { if (this.state.userToFollow) {
this.setState({ userToFollow: null }); this.setState({ userToFollow: null });
} }

View File

@ -3,6 +3,7 @@ import { LaserPointer } from "@excalidraw/laser-pointer";
import { sceneCoordsToViewportCoords } from "../../utils"; import { sceneCoordsToViewportCoords } from "../../utils";
import App from "../App"; import App from "../App";
import { getClientColor } from "../../clients"; import { getClientColor } from "../../clients";
import { SocketId } from "../../types";
// decay time in milliseconds // decay time in milliseconds
const DECAY_TIME = 1000; const DECAY_TIME = 1000;
@ -88,7 +89,7 @@ type CollabolatorState = {
export class LaserPathManager { export class LaserPathManager {
private ownState: CollabolatorState; private ownState: CollabolatorState;
private collaboratorsState: Map<string, CollabolatorState> = new Map(); private collaboratorsState: Map<SocketId, CollabolatorState> = new Map();
private rafId: number | undefined; private rafId: number | undefined;
private isDrawing = false; private isDrawing = false;

View File

@ -14,51 +14,54 @@ import { t } from "../i18n";
import { isShallowEqual } from "../utils"; import { isShallowEqual } from "../utils";
export type GoToCollaboratorComponentProps = [ export type GoToCollaboratorComponentProps = [
SocketId, ClientId,
Collaborator, Collaborator,
boolean, boolean,
boolean, boolean,
]; ];
/** collaborator user id or socket id (fallback) */
type ClientId = string & { _brand: "UserId" };
const FIRST_N_AVATARS = 3; const FIRST_N_AVATARS = 3;
const SHOW_COLLABORATORS_FILTER_AT = 8; const SHOW_COLLABORATORS_FILTER_AT = 8;
const ConditionalTooltipWrapper = ({ const ConditionalTooltipWrapper = ({
shouldWrap, shouldWrap,
children, children,
socketId, clientId,
username, username,
}: { }: {
shouldWrap: boolean; shouldWrap: boolean;
children: React.ReactNode; children: React.ReactNode;
username?: string | null; username?: string | null;
socketId: string; clientId: ClientId;
}) => }) =>
shouldWrap ? ( shouldWrap ? (
<Tooltip label={username || "Unknown user"} key={socketId}> <Tooltip label={username || "Unknown user"} key={clientId}>
{children} {children}
</Tooltip> </Tooltip>
) : ( ) : (
<React.Fragment key={socketId}>{children}</React.Fragment> <React.Fragment key={clientId}>{children}</React.Fragment>
); );
const renderCollaborator = ({ const renderCollaborator = ({
actionManager, actionManager,
collaborator, collaborator,
socketId, clientId,
withName = false, withName = false,
shouldWrapWithTooltip = false, shouldWrapWithTooltip = false,
isBeingFollowed, isBeingFollowed,
}: { }: {
actionManager: ActionManager; actionManager: ActionManager;
collaborator: Collaborator; collaborator: Collaborator;
socketId: string; clientId: ClientId;
withName?: boolean; withName?: boolean;
shouldWrapWithTooltip?: boolean; shouldWrapWithTooltip?: boolean;
isBeingFollowed: boolean; isBeingFollowed: boolean;
}) => { }) => {
const data: GoToCollaboratorComponentProps = [ const data: GoToCollaboratorComponentProps = [
socketId, clientId,
collaborator, collaborator,
withName, withName,
isBeingFollowed, isBeingFollowed,
@ -67,8 +70,8 @@ const renderCollaborator = ({
return ( return (
<ConditionalTooltipWrapper <ConditionalTooltipWrapper
key={socketId} key={clientId}
socketId={socketId} clientId={clientId}
username={collaborator.username} username={collaborator.username}
shouldWrap={shouldWrapWithTooltip} shouldWrap={shouldWrapWithTooltip}
> >
@ -100,12 +103,13 @@ export const UserList = React.memo(
({ className, mobile, collaborators, userToFollow }: UserListProps) => { ({ className, mobile, collaborators, userToFollow }: UserListProps) => {
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
const uniqueCollaboratorsMap = new Map<string, Collaborator>(); const uniqueCollaboratorsMap = new Map<ClientId, Collaborator>();
collaborators.forEach((collaborator, socketId) => { collaborators.forEach((collaborator, socketId) => {
const userId = (collaborator.id || socketId) as ClientId;
uniqueCollaboratorsMap.set( uniqueCollaboratorsMap.set(
// filter on user id, else fall back on unique socketId // filter on user id, else fall back on unique socketId
collaborator.id || socketId, userId,
{ ...collaborator, socketId }, { ...collaborator, socketId },
); );
}); });
@ -134,25 +138,25 @@ export const UserList = React.memo(
); );
const firstNAvatarsJSX = firstNCollaborators.map( const firstNAvatarsJSX = firstNCollaborators.map(
([socketId, collaborator]) => ([clientId, collaborator]) =>
renderCollaborator({ renderCollaborator({
actionManager, actionManager,
collaborator, collaborator,
socketId, clientId,
shouldWrapWithTooltip: true, shouldWrapWithTooltip: true,
isBeingFollowed: socketId === userToFollow, isBeingFollowed: collaborator.socketId === userToFollow,
}), }),
); );
return mobile ? ( return mobile ? (
<div className={clsx("UserList UserList_mobile", className)}> <div className={clsx("UserList UserList_mobile", className)}>
{uniqueCollaboratorsArray.map(([socketId, collaborator]) => {uniqueCollaboratorsArray.map(([clientId, collaborator]) =>
renderCollaborator({ renderCollaborator({
actionManager, actionManager,
collaborator, collaborator,
socketId, clientId,
shouldWrapWithTooltip: true, shouldWrapWithTooltip: true,
isBeingFollowed: socketId === userToFollow, isBeingFollowed: collaborator.socketId === userToFollow,
}), }),
)} )}
</div> </div>
@ -205,13 +209,13 @@ export const UserList = React.memo(
<div className="UserList__hint"> <div className="UserList__hint">
{t("userList.hint.text")} {t("userList.hint.text")}
</div> </div>
{filteredCollaborators.map(([socketId, collaborator]) => {filteredCollaborators.map(([clientId, collaborator]) =>
renderCollaborator({ renderCollaborator({
actionManager, actionManager,
collaborator, collaborator,
socketId, clientId,
withName: true, withName: true,
isBeingFollowed: socketId === userToFollow, isBeingFollowed: collaborator.socketId === userToFollow,
}), }),
)} )}
</div> </div>

View File

@ -41,7 +41,7 @@ import { Merge, ValueOf } from "./utility-types";
export type Point = Readonly<RoughPoint>; export type Point = Readonly<RoughPoint>;
export type SocketId = string; export type SocketId = string & { _brand: "SocketId" };
export type Collaborator = Readonly<{ export type Collaborator = Readonly<{
pointer?: CollaboratorPointer; pointer?: CollaboratorPointer;
@ -128,7 +128,7 @@ export type SidebarName = string;
export type SidebarTabName = string; export type SidebarTabName = string;
export type UserToFollow = { export type UserToFollow = {
socketId: string; socketId: SocketId;
username: string; username: string;
}; };
@ -296,7 +296,7 @@ export interface AppState {
offsetLeft: number; offsetLeft: number;
fileHandle: FileSystemHandle | null; fileHandle: FileSystemHandle | null;
collaborators: Map<string, Collaborator>; collaborators: Map<SocketId, Collaborator>;
showStats: boolean; showStats: boolean;
currentChartType: ChartType; currentChartType: ChartType;
pasteDialog: pasteDialog:
@ -321,7 +321,7 @@ export interface AppState {
/** the user's clientId & username who is being followed on the canvas */ /** the user's clientId & username who is being followed on the canvas */
userToFollow: UserToFollow | null; userToFollow: UserToFollow | null;
/** the clientIds of the users following the current user */ /** the clientIds of the users following the current user */
followedBy: Set<string>; followedBy: Set<SocketId>;
} }
export type UIAppState = Omit< export type UIAppState = Omit<
@ -474,7 +474,7 @@ export interface ExcalidrawProps {
export type SceneData = { export type SceneData = {
elements?: ImportedDataState["elements"]; elements?: ImportedDataState["elements"];
appState?: ImportedDataState["appState"]; appState?: ImportedDataState["appState"];
collaborators?: Map<string, Collaborator>; collaborators?: Map<SocketId, Collaborator>;
commitToHistory?: boolean; commitToHistory?: boolean;
}; };