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>
This commit is contained in:
Kostas Bariotis 2020-04-07 14:02:42 +01:00 committed by GitHub
parent 0c3d34261e
commit 67805bc7a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 154 additions and 2 deletions

View File

@ -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,
};
},

View File

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

View File

@ -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(

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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;
}

View File

@ -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}
/>

View File

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

View File

@ -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;

View File

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

View File

@ -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 = {

View File

@ -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,
}

View File

@ -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;