Jelajahi Sumber

feat: deduplicate collab avatars based on `id` (#5309)

David Luzar 3 tahun lalu
induk
melakukan
5feacd9a3b

+ 1 - 10
src/actions/actionNavigate.tsx

@@ -31,16 +31,7 @@ export const actionGoToCollaborator = register({
     };
   },
   PanelComponent: ({ appState, updateData, data }) => {
-    const clientId: string | undefined = data?.id;
-    if (!clientId) {
-      return null;
-    }
-
-    const collaborator = appState.collaborators.get(clientId);
-
-    if (!collaborator) {
-      return null;
-    }
+    const [clientId, collaborator] = data as [string, Collaborator];
 
     const { background, stroke } = getClientColors(clientId, appState);
 

+ 1 - 2
src/actions/types.ts

@@ -6,7 +6,6 @@ import {
   ExcalidrawProps,
   BinaryFiles,
 } from "../types";
-import { ToolButtonSize } from "../components/ToolButton";
 
 export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
 
@@ -119,7 +118,7 @@ export type PanelComponentProps = {
   appState: AppState;
   updateData: (formData?: any) => void;
   appProps: ExcalidrawProps;
-  data?: Partial<{ id: string; size: ToolButtonSize }>;
+  data?: Record<string, any>;
 };
 
 export interface Action {

+ 4 - 17
src/components/LayerUI.tsx

@@ -25,7 +25,6 @@ import { PasteChartDialog } from "./PasteChartDialog";
 import { Section } from "./Section";
 import { HelpDialog } from "./HelpDialog";
 import Stack from "./Stack";
-import { Tooltip } from "./Tooltip";
 import { UserList } from "./UserList";
 import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
 import { JSONExportDialog } from "./JSONExportDialog";
@@ -380,22 +379,10 @@ const LayerUI = ({
               },
             )}
           >
-            <UserList>
-              {appState.collaborators.size > 0 &&
-                Array.from(appState.collaborators)
-                  // Collaborator is either not initialized or is actually the current user.
-                  .filter(([_, client]) => Object.keys(client).length !== 0)
-                  .map(([clientId, client]) => (
-                    <Tooltip
-                      label={client.username || "Unknown user"}
-                      key={clientId}
-                    >
-                      {actionManager.renderAction("goToCollaborator", {
-                        id: clientId,
-                      })}
-                    </Tooltip>
-                  ))}
-            </UserList>
+            <UserList
+              collaborators={appState.collaborators}
+              actionManager={actionManager}
+            />
             {renderTopRightUI?.(deviceType.isMobile, appState)}
           </div>
         </div>

+ 5 - 14
src/components/MobileMenu.tsx

@@ -202,20 +202,11 @@ export const MobileMenu = ({
                   {appState.collaborators.size > 0 && (
                     <fieldset>
                       <legend>{t("labels.collaborators")}</legend>
-                      <UserList mobile>
-                        {Array.from(appState.collaborators)
-                          // Collaborator is either not initialized or is actually the current user.
-                          .filter(
-                            ([_, client]) => Object.keys(client).length !== 0,
-                          )
-                          .map(([clientId, client]) => (
-                            <React.Fragment key={clientId}>
-                              {actionManager.renderAction("goToCollaborator", {
-                                id: clientId,
-                              })}
-                            </React.Fragment>
-                          ))}
-                      </UserList>
+                      <UserList
+                        mobile
+                        collaborators={appState.collaborators}
+                        actionManager={actionManager}
+                      />
                     </fieldset>
                   )}
                 </Stack.Col>

+ 39 - 5
src/components/UserList.tsx

@@ -2,17 +2,51 @@ import "./UserList.scss";
 
 import React from "react";
 import clsx from "clsx";
+import { AppState, Collaborator } from "../types";
+import { Tooltip } from "./Tooltip";
+import { ActionManager } from "../actions/manager";
 
-type UserListProps = {
-  children: React.ReactNode;
+export const UserList: React.FC<{
   className?: string;
   mobile?: boolean;
-};
+  collaborators: AppState["collaborators"];
+  actionManager: ActionManager;
+}> = ({ className, mobile, collaborators, actionManager }) => {
+  const uniqueCollaborators = new Map<string, Collaborator>();
+
+  collaborators.forEach((collaborator, socketId) => {
+    uniqueCollaborators.set(
+      // filter on user id, else fall back on unique socketId
+      collaborator.id || socketId,
+      collaborator,
+    );
+  });
+
+  const avatars =
+    uniqueCollaborators.size > 0 &&
+    Array.from(uniqueCollaborators)
+      .filter(([_, client]) => Object.keys(client).length !== 0)
+      .map(([clientId, collaborator]) => {
+        const avatarJSX = actionManager.renderAction("goToCollaborator", [
+          clientId,
+          collaborator,
+        ]);
+
+        return mobile ? (
+          <Tooltip
+            label={collaborator.username || "Unknown user"}
+            key={clientId}
+          >
+            {avatarJSX}
+          </Tooltip>
+        ) : (
+          <React.Fragment key={clientId}>{avatarJSX}</React.Fragment>
+        );
+      });
 
-export const UserList = ({ children, className, mobile }: UserListProps) => {
   return (
     <div className={clsx("UserList", className, { UserList_mobile: mobile })}>
-      {children}
+      {avatars}
     </div>
   );
 };

+ 2 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -17,6 +17,8 @@ Please add the latest change on the top under the correct section.
 
 #### Features
 
+- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309)
+
 - Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215).
 
 - Export [`sceneCoordsToViewportCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) and [`viewportCoordsToSceneCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) utilities [#5187](https://github.com/excalidraw/excalidraw/pull/5187).

+ 2 - 0
src/types.ts

@@ -48,6 +48,8 @@ export type Collaborator = {
   // The url of the collaborator's avatar, defaults to username intials
   // if not present
   avatarUrl?: string;
+  // user id. If supplied, we'll filter out duplicates when rendering user avatars.
+  id?: string;
 };
 
 export type DataURL = string & { _brand: "DataURL" };