Procházet zdrojové kódy

Add collaborators names (#1223)

* add random usernames

* add username state

* add username input

* ability to set names

* fix tests

* set username oon mobile

* remove auto generated names

* remove commented code

* always string

* updaate snapshots

* maintain username when clearing canvas

* Update src/renderer/renderScene.ts

Co-Authored-By: Lipis <lipiridis@gmail.com>

* add border

* fix styles

Co-authored-by: Pete Hunt <petehunt@users.noreply.github.com>
Co-authored-by: Faustino Kialungila <faustino.kialungila@gmail.com>
Co-authored-by: Lipis <lipiridis@gmail.com>
Kostas Bariotis před 5 roky
rodič
revize
67805bc7a7

+ 6 - 2
src/actions/actionCanvas.tsx

@@ -10,6 +10,7 @@ import { getShortcutKey } from "../utils";
 import useIsMobile from "../is-mobile";
 import { register } from "./register";
 import { newElementWith } from "../element/mutateElement";
+import { AppState } from "../types";
 
 export const actionChangeViewBackgroundColor = register({
   name: "changeViewBackgroundColor",
@@ -35,12 +36,15 @@ export const actionChangeViewBackgroundColor = register({
 
 export const actionClearCanvas = register({
   name: "clearCanvas",
-  perform: (elements) => {
+  perform: (elements, appState: AppState) => {
     return {
       elements: elements.map((element) =>
         newElementWith(element, { isDeleted: true }),
       ),
-      appState: getDefaultAppState(),
+      appState: {
+        ...getDefaultAppState(),
+        username: appState.username,
+      },
       commitToHistory: true,
     };
   },

+ 1 - 0
src/appState.ts

@@ -29,6 +29,7 @@ export function getDefaultAppState(): AppState {
     cursorButton: "up",
     scrolledOutside: false,
     name: `excalidraw-${getDateTime()}`,
+    username: "",
     isCollaborating: false,
     isResizing: false,
     isRotating: false,

+ 8 - 0
src/components/App.tsx

@@ -437,6 +437,7 @@ export class App extends React.Component<any, AppState> {
     } = {};
     const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
     const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
+    const pointerUsernames: { [id: string]: string } = {};
     this.state.collaborators.forEach((user, socketID) => {
       if (user.selectedElementIds) {
         for (const id of Object.keys(user.selectedElementIds)) {
@@ -449,6 +450,9 @@ export class App extends React.Component<any, AppState> {
       if (!user.pointer) {
         return;
       }
+      if (user.username) {
+        pointerUsernames[socketID] = user.username;
+      }
       pointerViewportCoords[socketID] = sceneCoordsToViewportCoords(
         {
           sceneX: user.pointer.x,
@@ -483,6 +487,7 @@ export class App extends React.Component<any, AppState> {
         remotePointerViewportCoords: pointerViewportCoords,
         remotePointerButton: cursorButton,
         remoteSelectedElementIds: remoteSelectedElementIds,
+        remotePointerUsernames: pointerUsernames,
         shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
       },
       {
@@ -884,6 +889,7 @@ export class App extends React.Component<any, AppState> {
                 socketID,
                 pointerCoords,
                 button,
+                username,
                 selectedElementIds,
               } = decryptedData.payload;
               this.setState((state) => {
@@ -894,6 +900,7 @@ export class App extends React.Component<any, AppState> {
                 user.pointer = pointerCoords;
                 user.button = button;
                 user.selectedElementIds = selectedElementIds;
+                user.username = username;
                 state.collaborators.set(socketID, user);
                 return state;
               });
@@ -947,6 +954,7 @@ export class App extends React.Component<any, AppState> {
           pointerCoords: payload.pointerCoords,
           button: payload.button || "up",
           selectedElementIds: this.state.selectedElementIds,
+          username: this.state.username,
         },
       };
       return this._broadcastSocketData(

+ 6 - 0
src/components/LayerUI.tsx

@@ -133,6 +133,12 @@ export const LayerUI = React.memo(
                       <RoomDialog
                         isCollaborating={appState.isCollaborating}
                         collaboratorCount={appState.collaborators.size}
+                        username={appState.username}
+                        onUsernameChange={(username) => {
+                          setAppState({
+                            username,
+                          });
+                        }}
                         onRoomCreate={onRoomCreate}
                         onRoomDestroy={onRoomDestroy}
                       />

+ 2 - 0
src/components/MobileMenu.tsx

@@ -90,6 +90,8 @@ export function MobileMenu({
                   <RoomDialog
                     isCollaborating={appState.isCollaborating}
                     collaboratorCount={appState.collaborators.size}
+                    username={appState.username}
+                    onUsernameChange={(username) => setAppState({ username })}
                     onRoomCreate={onRoomCreate}
                     onRoomDestroy={onRoomDestroy}
                   />

+ 27 - 0
src/components/RoomDialog.scss

@@ -36,6 +36,33 @@
   background-color: #eee;
 }
 
+.RoomDialog-usernameContainer {
+  display: flex;
+  margin: 1.5em 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.RoomDialog-usernameLabel {
+}
+
+.RoomDialog-username {
+  min-width: 0;
+  flex: 1 1 auto;
+  margin-left: 1em;
+  display: inline-block;
+  cursor: pointer;
+  border: none;
+  height: 2.5rem;
+  line-height: 2.5rem;
+  padding: 0 0.5rem;
+  white-space: nowrap;
+  border-radius: var(--space-factor);
+  background-color: #fff;
+  border: 1px solid #eee;
+}
+
 .RoomDialog-link:hover {
   background-color: #eee;
 }

+ 22 - 0
src/components/RoomDialog.tsx

@@ -11,14 +11,19 @@ import { AppState } from "../types";
 
 function RoomModal({
   activeRoomLink,
+  username,
+  onUsernameChange,
   onRoomCreate,
   onRoomDestroy,
 }: {
   activeRoomLink: string;
+  username: string;
+  onUsernameChange: (username: string) => void;
   onRoomCreate: () => void;
   onRoomDestroy: () => void;
 }) {
   const roomLinkInput = useRef<HTMLInputElement>(null);
+
   function copyRoomLink() {
     copyTextToSystemClipboard(activeRoomLink);
     if (roomLinkInput.current) {
@@ -71,6 +76,17 @@ function RoomModal({
               onPointerDown={selectInput}
             />
           </div>
+          <div className="RoomDialog-usernameContainer">
+            <label className="RoomDialog-usernameLabel" htmlFor="username">
+              Username:
+            </label>
+            <input
+              id="username"
+              value={username || ""}
+              className="RoomDialog-username"
+              onChange={(event) => onUsernameChange(event.target.value)}
+            />
+          </div>
           <p>{`🔒 ${t("roomDialog.desc_privacy")}`}</p>
           <p>
             <span role="img" aria-hidden="true">
@@ -99,11 +115,15 @@ function RoomModal({
 export function RoomDialog({
   isCollaborating,
   collaboratorCount,
+  username,
+  onUsernameChange,
   onRoomCreate,
   onRoomDestroy,
 }: {
   isCollaborating: AppState["isCollaborating"];
   collaboratorCount: number;
+  username: string;
+  onUsernameChange: (username: string) => void;
   onRoomCreate: () => void;
   onRoomDestroy: () => void;
 }) {
@@ -149,6 +169,8 @@ export function RoomDialog({
         >
           <RoomModal
             activeRoomLink={activeRoomLink}
+            username={username}
+            onUsernameChange={onUsernameChange}
             onRoomCreate={onRoomCreate}
             onRoomDestroy={onRoomDestroy}
           />

+ 1 - 0
src/data/index.ts

@@ -54,6 +54,7 @@ export type SocketUpdateDataSource = {
       pointerCoords: { x: number; y: number };
       button: "down" | "up";
       selectedElementIds: AppState["selectedElementIds"];
+      username: string;
     };
   };
 };

+ 36 - 0
src/renderer/renderScene.ts

@@ -330,6 +330,7 @@ export function renderScene(
   // Paint remote pointers
   for (const clientId in sceneState.remotePointerViewportCoords) {
     let { x, y } = sceneState.remotePointerViewportCoords[clientId];
+    const username = sceneState.remotePointerUsernames[clientId];
 
     const width = 9;
     const height = 14;
@@ -383,6 +384,41 @@ export function renderScene(
     context.lineTo(x, y);
     context.fill();
     context.stroke();
+
+    if (!isOutOfBounds && username) {
+      const offsetX = x + width;
+      const offsetY = y + height;
+      const paddingHorizontal = 4;
+      const paddingVertical = 4;
+      const measure = context.measureText(username);
+      const measureHeight =
+        measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
+
+      // Border
+      context.fillStyle = stroke;
+      context.globalAlpha = globalAlpha;
+      context.fillRect(
+        offsetX - 1,
+        offsetY - 1,
+        measure.width + 2 * paddingHorizontal + 2,
+        measureHeight + 2 * paddingVertical + 2,
+      );
+      // Background
+      context.fillStyle = background;
+      context.fillRect(
+        offsetX,
+        offsetY,
+        measure.width + 2 * paddingHorizontal,
+        measureHeight + 2 * paddingVertical,
+      );
+      context.fillStyle = "#ffffff";
+      context.fillText(
+        username,
+        offsetX + paddingHorizontal,
+        offsetY + paddingVertical + measure.actualBoundingBoxAscent,
+      );
+    }
+
     context.strokeStyle = strokeStyle;
     context.fillStyle = fillStyle;
     context.globalAlpha = globalAlpha;

+ 1 - 0
src/scene/export.ts

@@ -54,6 +54,7 @@ export function exportToCanvas(
       remotePointerViewportCoords: {},
       remoteSelectedElementIds: {},
       shouldCacheIgnoreZoom: false,
+      remotePointerUsernames: {},
     },
     {
       renderScrollbars: false,

+ 1 - 0
src/scene/types.ts

@@ -11,6 +11,7 @@ export type SceneState = {
   remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
   remotePointerButton?: { [id: string]: string | undefined };
   remoteSelectedElementIds: { [elementId: string]: string[] };
+  remotePointerUsernames: { [id: string]: string };
 };
 
 export type SceneScroll = {

+ 41 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -38,6 +38,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -233,6 +234,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -348,6 +350,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -616,6 +619,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -772,6 +776,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -967,6 +972,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -1221,6 +1227,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -1589,6 +1596,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2205,6 +2213,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2320,6 +2329,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2435,6 +2445,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2550,6 +2561,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2687,6 +2699,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2824,6 +2837,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -2961,6 +2975,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -3076,6 +3091,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -3191,6 +3207,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -3328,6 +3345,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -3443,6 +3461,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": true,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -3513,6 +3532,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -4379,6 +4399,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -4793,6 +4814,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -5116,6 +5138,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -5352,6 +5375,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -5521,6 +5545,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -6339,6 +6364,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -7050,6 +7076,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -7658,6 +7685,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -8168,6 +8196,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -8629,6 +8658,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -8997,6 +9027,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -9276,6 +9307,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -9486,6 +9518,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -10359,6 +10392,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -11123,6 +11157,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -11782,6 +11817,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -12336,6 +12372,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -12707,6 +12744,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -12761,6 +12799,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": true,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -12815,6 +12854,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }
@@ -13106,6 +13146,7 @@ Object {
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "username": "",
   "viewBackgroundColor": "#ffffff",
   "zoom": 1,
 }

+ 2 - 0
src/types.ts

@@ -37,6 +37,7 @@ export type AppState = {
   cursorButton: "up" | "down";
   scrolledOutside: boolean;
   name: string;
+  username: string;
   isCollaborating: boolean;
   isResizing: boolean;
   isRotating: boolean;
@@ -53,6 +54,7 @@ export type AppState = {
       };
       button?: "up" | "down";
       selectedElementIds?: AppState["selectedElementIds"];
+      username?: string | null;
     }
   >;
   shouldCacheIgnoreZoom: boolean;