Add user list component + snap to user functionality (#1749)
This commit is contained in:
parent
8f65e37dac
commit
ca87ca6fe9
52
src/actions/actionNavigate.tsx
Normal file
52
src/actions/actionNavigate.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Avatar } from "../components/Avatar";
|
||||||
|
import { register } from "./register";
|
||||||
|
import { getClientColors, getClientInitials } from "../clients";
|
||||||
|
import { Collaborator } from "../types";
|
||||||
|
import { normalizeScroll } from "../scene";
|
||||||
|
|
||||||
|
export const actionGoToCollaborator = register({
|
||||||
|
name: "goToCollaborator",
|
||||||
|
perform: (_elements, appState, value) => {
|
||||||
|
const point = value as Collaborator["pointer"];
|
||||||
|
if (!point) {
|
||||||
|
return { appState, commitToHistory: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
scrollX: normalizeScroll(window.innerWidth / 2 - point.x),
|
||||||
|
scrollY: normalizeScroll(window.innerHeight / 2 - point.y),
|
||||||
|
// Close mobile menu
|
||||||
|
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData, id }) => {
|
||||||
|
const clientId = id;
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collaborator = appState.collaborators.get(clientId);
|
||||||
|
|
||||||
|
if (!collaborator) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { background } = getClientColors(clientId);
|
||||||
|
const shortName = getClientInitials(collaborator.username);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
color={background}
|
||||||
|
onClick={() => updateData(collaborator.pointer)}
|
||||||
|
>
|
||||||
|
{shortName}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
@ -47,3 +47,5 @@ export {
|
|||||||
} from "./actionMenu";
|
} from "./actionMenu";
|
||||||
|
|
||||||
export { actionGroup, actionUngroup } from "./actionGroup";
|
export { actionGroup, actionUngroup } from "./actionGroup";
|
||||||
|
|
||||||
|
export { actionGoToCollaborator } from "./actionNavigate";
|
||||||
|
@ -101,7 +101,11 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAction = (name: ActionName) => {
|
// Id is an attribute that we can use to pass in data like keys.
|
||||||
|
// This is needed for dynamically generated action components
|
||||||
|
// like the user list. We can use this key to extract more
|
||||||
|
// data from app state. This is an alternative to generic prop hell!
|
||||||
|
renderAction = (name: ActionName, id?: string) => {
|
||||||
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@ -120,6 +124,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,8 @@ export type ActionName =
|
|||||||
| "toggleFullScreen"
|
| "toggleFullScreen"
|
||||||
| "toggleShortcuts"
|
| "toggleShortcuts"
|
||||||
| "group"
|
| "group"
|
||||||
| "ungroup";
|
| "ungroup"
|
||||||
|
| "goToCollaborator";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
@ -66,6 +67,7 @@ export interface Action {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
|
id?: string;
|
||||||
}>;
|
}>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
|
30
src/clients.ts
Normal file
30
src/clients.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import colors from "./colors";
|
||||||
|
|
||||||
|
export const getClientColors = (clientId: string) => {
|
||||||
|
// Naive way of getting an integer out of the clientId
|
||||||
|
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
||||||
|
|
||||||
|
// Skip transparent background.
|
||||||
|
const backgrounds = colors.elementBackground.slice(1);
|
||||||
|
const strokes = colors.elementStroke.slice(1);
|
||||||
|
return {
|
||||||
|
background: backgrounds[sum % backgrounds.length],
|
||||||
|
stroke: strokes[sum % strokes.length],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClientInitials = (username?: string | null) => {
|
||||||
|
if (!username) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
const names = username.trim().split(" ");
|
||||||
|
|
||||||
|
if (names.length < 2) {
|
||||||
|
return names[0].substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstName = names[0];
|
||||||
|
const lastName = names[names.length - 1];
|
||||||
|
|
||||||
|
return (firstName[0] + lastName[0]).toUpperCase();
|
||||||
|
};
|
14
src/components/Avatar.scss
Normal file
14
src/components/Avatar.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@import "../css/_variables";
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: $oc-white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
25
src/components/Avatar.tsx
Normal file
25
src/components/Avatar.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import "./Avatar.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type AvatarProps = {
|
||||||
|
children: string;
|
||||||
|
className?: string;
|
||||||
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Avatar = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color,
|
||||||
|
onClick,
|
||||||
|
}: AvatarProps) => (
|
||||||
|
<div
|
||||||
|
className={`Avatar ${className}`}
|
||||||
|
style={{ background: color }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
@ -10,6 +10,7 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
|
import { UserList } from "./UserList";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockIcon } from "./LockIcon";
|
||||||
import { ExportDialog, ExportCB } from "./ExportDialog";
|
import { ExportDialog, ExportCB } from "./ExportDialog";
|
||||||
import { LanguageList } from "./LanguageList";
|
import { LanguageList } from "./LanguageList";
|
||||||
@ -28,6 +29,7 @@ import { LoadingMessage } from "./LoadingMessage";
|
|||||||
import { CLASSES } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import { shield } from "./icons";
|
import { shield } from "./icons";
|
||||||
import { GitHubCorner } from "./GitHubCorner";
|
import { GitHubCorner } from "./GitHubCorner";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
|
|
||||||
@ -61,6 +63,7 @@ const LayerUI = ({
|
|||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
// TODO: Extend tooltip component and use here.
|
||||||
const renderEncryptedIcon = () => (
|
const renderEncryptedIcon = () => (
|
||||||
<a
|
<a
|
||||||
className={`encrypted-icon tooltip zen-mode-visibility ${
|
className={`encrypted-icon tooltip zen-mode-visibility ${
|
||||||
@ -203,7 +206,23 @@ const LayerUI = ({
|
|||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
<div />
|
<UserList
|
||||||
|
className={`zen-mode-transition ${
|
||||||
|
zenModeEnabled && "transition-right"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{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", clientId)}
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</UserList>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
<div
|
<div
|
||||||
|
@ -16,6 +16,7 @@ import { RoomDialog } from "./RoomDialog";
|
|||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockIcon } from "./LockIcon";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
|
import { UserList } from "./UserList";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -105,6 +106,22 @@ export const MobileMenu = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<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",
|
||||||
|
clientId,
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</UserList>
|
||||||
|
</fieldset>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
48
src/components/Tooltip.scss
Normal file
48
src/components/Tooltip.scss
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
@import "../css/_variables";
|
||||||
|
|
||||||
|
.Tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tooltip__label {
|
||||||
|
--arrow-size: 4px;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 10ch;
|
||||||
|
background: $oc-black;
|
||||||
|
color: $oc-white;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
top: calc(100% + var(--arrow-size) + 3px);
|
||||||
|
// extra pixel offset for unknown reasons
|
||||||
|
left: calc(-50% + var(--arrow-size) / 2 - 1px);
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
border: var(--arrow-size) solid transparent;
|
||||||
|
border-bottom-color: $oc-black;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: calc(50% - var(--arrow-size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following 3 rules ensure that the tooltip doesn't show (nor affect
|
||||||
|
// the cursor) when you drag over when you draw on canvas, but at the same
|
||||||
|
// time it still works when clicking on the link/shield
|
||||||
|
body:active .Tooltip:not(:hover) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(:active) .Tooltip:hover .Tooltip__label {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tooltip__label:hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
15
src/components/Tooltip.tsx
Normal file
15
src/components/Tooltip.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import "./Tooltip.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tooltip = ({ children, label }: TooltipProps) => (
|
||||||
|
<div className="Tooltip">
|
||||||
|
<span className="Tooltip__label">{label}</span>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
22
src/components/UserList.css
Normal file
22
src/components/UserList.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.UserList {
|
||||||
|
pointer-events: none;
|
||||||
|
/*github corner*/
|
||||||
|
padding: var(--space-factor) 40px var(--space-factor) var(--space-factor);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList > * {
|
||||||
|
pointer-events: all;
|
||||||
|
margin: 0 0 var(--space-factor) var(--space-factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList_mobile {
|
||||||
|
padding: 0;
|
||||||
|
justify-content: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList_mobile > * {
|
||||||
|
margin: 0 var(--space-factor) var(--space-factor) 0;
|
||||||
|
}
|
23
src/components/UserList.tsx
Normal file
23
src/components/UserList.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import "./UserList.css";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type UserListProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserList = ({ children, className, mobile }: UserListProps) => {
|
||||||
|
let compClassName = "UserList";
|
||||||
|
|
||||||
|
if (className) {
|
||||||
|
compClassName += ` ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobile) {
|
||||||
|
compClassName += " UserList_mobile";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={compClassName}>{children}</div>;
|
||||||
|
};
|
@ -457,6 +457,7 @@ button,
|
|||||||
.github-corner {
|
.github-corner {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,8 @@
|
|||||||
"yourName": "Your name",
|
"yourName": "Your name",
|
||||||
"madeWithExcalidraw": "Made with Excalidraw",
|
"madeWithExcalidraw": "Made with Excalidraw",
|
||||||
"group": "Group selection",
|
"group": "Group selection",
|
||||||
"ungroup": "Ungroup selection"
|
"ungroup": "Ungroup selection",
|
||||||
|
"collaborators": "Collaborators"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
import { getSelectedElements } from "../scene/selection";
|
import { getSelectedElements } from "../scene/selection";
|
||||||
|
|
||||||
import { renderElement, renderElementToSvg } from "./renderElement";
|
import { renderElement, renderElementToSvg } from "./renderElement";
|
||||||
import colors from "../colors";
|
import { getClientColors } from "../clients";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import {
|
||||||
@ -39,19 +39,6 @@ import {
|
|||||||
|
|
||||||
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
||||||
|
|
||||||
const colorsForClientId = (clientId: string) => {
|
|
||||||
// Naive way of getting an integer out of the clientId
|
|
||||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
|
||||||
|
|
||||||
// Skip transparent background.
|
|
||||||
const backgrounds = colors.elementBackground.slice(1);
|
|
||||||
const strokes = colors.elementStroke.slice(1);
|
|
||||||
return {
|
|
||||||
background: backgrounds[sum % backgrounds.length],
|
|
||||||
stroke: strokes[sum % strokes.length],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const strokeRectWithRotation = (
|
const strokeRectWithRotation = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@ -232,7 +219,7 @@ export const renderScene = (
|
|||||||
if (sceneState.remoteSelectedElementIds[element.id]) {
|
if (sceneState.remoteSelectedElementIds[element.id]) {
|
||||||
selectionColors.push(
|
selectionColors.push(
|
||||||
...sceneState.remoteSelectedElementIds[element.id].map((socketId) => {
|
...sceneState.remoteSelectedElementIds[element.id].map((socketId) => {
|
||||||
const { background } = colorsForClientId(socketId);
|
const { background } = getClientColors(socketId);
|
||||||
return background;
|
return background;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -444,7 +431,7 @@ export const renderScene = (
|
|||||||
y = Math.max(y, 0);
|
y = Math.max(y, 0);
|
||||||
y = Math.min(y, normalizedCanvasHeight - height);
|
y = Math.min(y, normalizedCanvasHeight - height);
|
||||||
|
|
||||||
const { background, stroke } = colorsForClientId(clientId);
|
const { background, stroke } = getClientColors(clientId);
|
||||||
|
|
||||||
const strokeStyle = context.strokeStyle;
|
const strokeStyle = context.strokeStyle;
|
||||||
const fillStyle = context.fillStyle;
|
const fillStyle = context.fillStyle;
|
||||||
|
39
src/tests/clients.test.ts
Normal file
39
src/tests/clients.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { getClientInitials } from "../clients";
|
||||||
|
|
||||||
|
describe("getClientInitials", () => {
|
||||||
|
it("returns substring if one name provided", () => {
|
||||||
|
const result = getClientInitials("Alan");
|
||||||
|
expect(result).toBe("AL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns initials", () => {
|
||||||
|
const result = getClientInitials("John Doe");
|
||||||
|
expect(result).toBe("JD");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct initials if many names provided", () => {
|
||||||
|
const result = getClientInitials("John Alan Doe");
|
||||||
|
expect(result).toBe("JD");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns single initial if 1 letter provided", () => {
|
||||||
|
const result = getClientInitials("z");
|
||||||
|
expect(result).toBe("Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims trailing whitespace", () => {
|
||||||
|
const result = getClientInitials(" q ");
|
||||||
|
expect(result).toBe("Q");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "?" if falsey value provided', () => {
|
||||||
|
let result = getClientInitials("");
|
||||||
|
expect(result).toBe("?");
|
||||||
|
|
||||||
|
result = getClientInitials(undefined);
|
||||||
|
expect(result).toBe("?");
|
||||||
|
|
||||||
|
result = getClientInitials(null);
|
||||||
|
expect(result).toBe("?");
|
||||||
|
});
|
||||||
|
});
|
23
src/types.ts
23
src/types.ts
@ -16,6 +16,16 @@ import { LinearElementEditor } from "./element/linearElementEditor";
|
|||||||
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
|
export type Collaborator = {
|
||||||
|
pointer?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
button?: "up" | "down";
|
||||||
|
selectedElementIds?: AppState["selectedElementIds"];
|
||||||
|
username?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type AppState = {
|
export type AppState = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
@ -58,18 +68,7 @@ export type AppState = {
|
|||||||
lastPointerDownWith: PointerType;
|
lastPointerDownWith: PointerType;
|
||||||
selectedElementIds: { [id: string]: boolean };
|
selectedElementIds: { [id: string]: boolean };
|
||||||
previousSelectedElementIds: { [id: string]: boolean };
|
previousSelectedElementIds: { [id: string]: boolean };
|
||||||
collaborators: Map<
|
collaborators: Map<string, Collaborator>;
|
||||||
string,
|
|
||||||
{
|
|
||||||
pointer?: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
button?: "up" | "down";
|
|
||||||
selectedElementIds?: AppState["selectedElementIds"];
|
|
||||||
username?: string | null;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
shouldCacheIgnoreZoom: boolean;
|
shouldCacheIgnoreZoom: boolean;
|
||||||
showShortcutsDialog: boolean;
|
showShortcutsDialog: boolean;
|
||||||
zenModeEnabled: boolean;
|
zenModeEnabled: boolean;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user