Room dialog (#905)
* support ToolIcon className and fix label padding * factor some ExportDialog classes out to Modal * initial RoomDialog prototype * change label for another-session button * remove unused css * add color comments * Move the collaboration button to the main menu, add support for mobile * remove button for creating another session * add locks * Fix alignment issue * Reorder button * reuse current scene for collab session * keep collaboration state on restore Co-authored-by: Jed Fox <git@twopointzero.us>
This commit is contained in:
parent
aa9a6b0909
commit
b82b0754ac
@ -1,6 +1,5 @@
|
|||||||
import { AppState, FlooredNumber } from "./types";
|
import { AppState, FlooredNumber } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
import { getCollaborationLinkData } from "./data";
|
|
||||||
|
|
||||||
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
||||||
export const DEFAULT_FONT = "20px Virgil";
|
export const DEFAULT_FONT = "20px Virgil";
|
||||||
@ -28,7 +27,7 @@ export function getDefaultAppState(): AppState {
|
|||||||
cursorY: 0,
|
cursorY: 0,
|
||||||
scrolledOutside: false,
|
scrolledOutside: false,
|
||||||
name: DEFAULT_PROJECT_NAME,
|
name: DEFAULT_PROJECT_NAME,
|
||||||
isCollaborating: !!getCollaborationLinkData(window.location.href),
|
isCollaborating: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
@ -190,7 +190,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
if (commitToHistory) {
|
if (commitToHistory) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
}
|
}
|
||||||
this.setState({ ...res.appState });
|
this.setState(state => ({
|
||||||
|
...res.appState,
|
||||||
|
isCollaborating: state.isCollaborating,
|
||||||
|
remotePointers: state.remotePointers,
|
||||||
|
collaboratorCount: state.collaboratorCount,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -226,12 +231,27 @@ export class App extends React.Component<any, AppState> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private destroySocketClient = () => {
|
||||||
|
this.setState({
|
||||||
|
isCollaborating: false,
|
||||||
|
});
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.close();
|
||||||
|
this.socket = null;
|
||||||
|
this.roomID = null;
|
||||||
|
this.roomKey = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private initializeSocketClient = () => {
|
private initializeSocketClient = () => {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
const roomMatch = getCollaborationLinkData(window.location.href);
|
||||||
if (roomMatch) {
|
if (roomMatch) {
|
||||||
|
this.setState({
|
||||||
|
isCollaborating: true,
|
||||||
|
});
|
||||||
this.socket = socketIOClient(SOCKET_SERVER);
|
this.socket = socketIOClient(SOCKET_SERVER);
|
||||||
this.roomID = roomMatch[1];
|
this.roomID = roomMatch[1];
|
||||||
this.roomKey = roomMatch[2];
|
this.roomKey = roomMatch[2];
|
||||||
@ -611,6 +631,20 @@ export class App extends React.Component<any, AppState> {
|
|||||||
gesture.pointers.delete(event.pointerId);
|
gesture.pointers.delete(event.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createRoom = async () => {
|
||||||
|
window.history.pushState(
|
||||||
|
{},
|
||||||
|
"Excalidraw",
|
||||||
|
await generateCollaborationLink(),
|
||||||
|
);
|
||||||
|
this.initializeSocketClient();
|
||||||
|
};
|
||||||
|
|
||||||
|
destroyRoom = () => {
|
||||||
|
window.history.pushState({}, "Excalidraw", window.location.origin);
|
||||||
|
this.destroySocketClient();
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const canvasDOMWidth = window.innerWidth;
|
const canvasDOMWidth = window.innerWidth;
|
||||||
const canvasDOMHeight = window.innerHeight;
|
const canvasDOMHeight = window.innerHeight;
|
||||||
@ -630,6 +664,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
setElements={this.setElements}
|
setElements={this.setElements}
|
||||||
language={getLanguage()}
|
language={getLanguage()}
|
||||||
|
onRoomCreate={this.createRoom}
|
||||||
|
onRoomDestroy={this.destroyRoom}
|
||||||
/>
|
/>
|
||||||
<main>
|
<main>
|
||||||
<canvas
|
<canvas
|
||||||
|
@ -1,28 +1,3 @@
|
|||||||
.ExportDialog__dialog {
|
|
||||||
/* transition: opacity 0.15s ease-in, transform 0.15s ease-in; */
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
animation: ExportDialog__fade-in 0.1s ease-out 0.05s forwards;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ExportDialog__fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExportDialog__close {
|
|
||||||
position: absolute;
|
|
||||||
right: calc(var(--space-factor) * 5);
|
|
||||||
top: calc(var(--space-factor) * 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExportDialog__preview {
|
.ExportDialog__preview {
|
||||||
--preview-padding: calc(var(--space-factor) * 4);
|
--preview-padding: calc(var(--space-factor) * 4);
|
||||||
|
|
||||||
|
@ -112,10 +112,10 @@ function ExportModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ExportDialog__dialog" onKeyDown={handleKeyDown}>
|
<div onKeyDown={handleKeyDown}>
|
||||||
<Island padding={4}>
|
<Island padding={4}>
|
||||||
<button
|
<button
|
||||||
className="ExportDialog__close"
|
className="Modal__close"
|
||||||
onClick={onCloseRequest}
|
onClick={onCloseRequest}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
ref={closeButton}
|
ref={closeButton}
|
||||||
|
@ -21,6 +21,7 @@ import { ExportType } from "../scene/types";
|
|||||||
import { MobileMenu } from "./MobileMenu";
|
import { MobileMenu } from "./MobileMenu";
|
||||||
import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { RoomDialog } from "./RoomDialog";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -30,6 +31,8 @@ interface LayerUIProps {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
language: string;
|
language: string;
|
||||||
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
||||||
|
onRoomCreate: () => void;
|
||||||
|
onRoomDestroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LayerUI = React.memo(
|
export const LayerUI = React.memo(
|
||||||
@ -41,6 +44,8 @@ export const LayerUI = React.memo(
|
|||||||
elements,
|
elements,
|
||||||
language,
|
language,
|
||||||
setElements,
|
setElements,
|
||||||
|
onRoomCreate,
|
||||||
|
onRoomDestroy,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -92,21 +97,28 @@ export const LayerUI = React.memo(
|
|||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
exportButton={renderExportDialog()}
|
exportButton={renderExportDialog()}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
|
onRoomCreate={onRoomCreate}
|
||||||
|
onRoomDestroy={onRoomDestroy}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
<HintViewer appState={appState} elements={elements} />
|
<HintViewer appState={appState} elements={elements} />
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col gap={4} align="end">
|
<Stack.Col gap={4}>
|
||||||
<Section className="App-right-menu" heading="canvasActions">
|
<Section className="App-right-menu" heading="canvasActions">
|
||||||
<Island padding={4}>
|
<Island padding={4}>
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
<Stack.Row justifyContent={"space-between"}>
|
<Stack.Row gap={2.25} justifyContent={"space-between"}>
|
||||||
{actionManager.renderAction("loadScene")}
|
{actionManager.renderAction("loadScene")}
|
||||||
{actionManager.renderAction("saveScene")}
|
{actionManager.renderAction("saveScene")}
|
||||||
{renderExportDialog()}
|
{renderExportDialog()}
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<RoomDialog
|
||||||
|
isCollaborating={appState.isCollaborating}
|
||||||
|
onRoomCreate={onRoomCreate}
|
||||||
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
|
@ -12,6 +12,7 @@ import { HintViewer } from "./HintViewer";
|
|||||||
import { calculateScrollCenter, getTargetElement } from "../scene";
|
import { calculateScrollCenter, getTargetElement } from "../scene";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { RoomDialog } from "./RoomDialog";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -20,6 +21,8 @@ type MobileMenuProps = {
|
|||||||
setAppState: any;
|
setAppState: any;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
setElements: any;
|
setElements: any;
|
||||||
|
onRoomCreate: () => void;
|
||||||
|
onRoomDestroy: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobileMenu({
|
export function MobileMenu({
|
||||||
@ -29,6 +32,8 @@ export function MobileMenu({
|
|||||||
actionManager,
|
actionManager,
|
||||||
exportButton,
|
exportButton,
|
||||||
setAppState,
|
setAppState,
|
||||||
|
onRoomCreate,
|
||||||
|
onRoomDestroy,
|
||||||
}: MobileMenuProps) {
|
}: MobileMenuProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -40,6 +45,11 @@ export function MobileMenu({
|
|||||||
{actionManager.renderAction("saveScene")}
|
{actionManager.renderAction("saveScene")}
|
||||||
{exportButton}
|
{exportButton}
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<RoomDialog
|
||||||
|
isCollaborating={appState.isCollaborating}
|
||||||
|
onRoomCreate={onRoomCreate}
|
||||||
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
/>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.language")}</legend>
|
<legend>{t("labels.language")}</legend>
|
||||||
|
@ -27,4 +27,26 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes Modal__content_fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Modal__close {
|
||||||
|
position: absolute;
|
||||||
|
right: calc(var(--space-factor) * 5);
|
||||||
|
top: calc(var(--space-factor) * 5);
|
||||||
}
|
}
|
||||||
|
43
src/components/RoomDialog.scss
Normal file
43
src/components/RoomDialog.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.RoomDialog-modalButton.is-collaborating {
|
||||||
|
background-color: #ebfbee; // OC GREEN-0
|
||||||
|
color: #2b8a3e; // OC GREEN-9
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-linkContainer {
|
||||||
|
display: flex;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-link {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
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: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-link:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-link:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-sessionStartButtonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDialog-stopSession {
|
||||||
|
background-color: #ffe3e3; // OC RED-1
|
||||||
|
color: #c92a2a; // OC RED-9
|
||||||
|
}
|
165
src/components/RoomDialog.tsx
Normal file
165
src/components/RoomDialog.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import useIsMobile from "../is-mobile";
|
||||||
|
import { users, clipboard, start, stop } from "./icons";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
|
||||||
|
import "./RoomDialog.scss";
|
||||||
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
|
||||||
|
function RoomModal({
|
||||||
|
onCloseRequest,
|
||||||
|
activeRoomLink,
|
||||||
|
onRoomCreate,
|
||||||
|
onRoomDestroy,
|
||||||
|
}: {
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
activeRoomLink: string;
|
||||||
|
onRoomCreate: () => void;
|
||||||
|
onRoomDestroy: () => void;
|
||||||
|
}) {
|
||||||
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
|
function copyRoomLink() {
|
||||||
|
copyTextToSystemClipboard(activeRoomLink);
|
||||||
|
if (roomLinkInput.current) {
|
||||||
|
roomLinkInput.current.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function selectInput(event: React.MouseEvent<HTMLInputElement>) {
|
||||||
|
if (event.target !== document.activeElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
(event.target as HTMLInputElement).select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="RoomDialog-modal">
|
||||||
|
<Island padding={4}>
|
||||||
|
<button
|
||||||
|
className="Modal__close"
|
||||||
|
onClick={onCloseRequest}
|
||||||
|
aria-label={t("buttons.close")}
|
||||||
|
>
|
||||||
|
╳
|
||||||
|
</button>
|
||||||
|
<h2 id="export-title">{t("labels.createRoom")}</h2>
|
||||||
|
{!activeRoomLink && (
|
||||||
|
<>
|
||||||
|
<p>{t("roomDialog.desc_intro")}</p>
|
||||||
|
<p>{`🔒 ${t("roomDialog.desc_privacy")}`}</p>
|
||||||
|
<p>{t("roomDialog.desc_start")}</p>
|
||||||
|
<div className="RoomDialog-sessionStartButtonContainer">
|
||||||
|
<ToolButton
|
||||||
|
className="RoomDialog-startSession"
|
||||||
|
type="button"
|
||||||
|
icon={start}
|
||||||
|
title={t("roomDialog.button_startSession")}
|
||||||
|
aria-label={t("roomDialog.button_startSession")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={onRoomCreate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{activeRoomLink && (
|
||||||
|
<>
|
||||||
|
<p>{t("roomDialog.desc_inProgressIntro")}</p>
|
||||||
|
<p>{t("roomDialog.desc_shareLink")}</p>
|
||||||
|
<div className="RoomDialog-linkContainer">
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
icon={clipboard}
|
||||||
|
title={t("labels.copy")}
|
||||||
|
aria-label={t("labels.copy")}
|
||||||
|
onClick={copyRoomLink}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={activeRoomLink}
|
||||||
|
readOnly={true}
|
||||||
|
className="RoomDialog-link"
|
||||||
|
ref={roomLinkInput}
|
||||||
|
onPointerDown={selectInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>{`🔒 ${t("roomDialog.desc_privacy")}`}</p>
|
||||||
|
<p>
|
||||||
|
<span role="img" aria-hidden="true">
|
||||||
|
⚠️
|
||||||
|
</span>{" "}
|
||||||
|
{t("roomDialog.desc_persistenceWarning")}
|
||||||
|
</p>
|
||||||
|
<p>{t("roomDialog.desc_exitSession")}</p>
|
||||||
|
<div className="RoomDialog-sessionStartButtonContainer">
|
||||||
|
<ToolButton
|
||||||
|
className="RoomDialog-stopSession"
|
||||||
|
type="button"
|
||||||
|
icon={stop}
|
||||||
|
title={t("roomDialog.button_stopSession")}
|
||||||
|
aria-label={t("roomDialog.button_stopSession")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={onRoomDestroy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Island>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomDialog({
|
||||||
|
isCollaborating,
|
||||||
|
onRoomCreate,
|
||||||
|
onRoomDestroy,
|
||||||
|
}: {
|
||||||
|
isCollaborating: boolean;
|
||||||
|
onRoomCreate: () => void;
|
||||||
|
onRoomDestroy: () => void;
|
||||||
|
}) {
|
||||||
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
|
const [activeRoomLink, setActiveRoomLink] = useState("");
|
||||||
|
|
||||||
|
const triggerButton = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
setModalIsShown(false);
|
||||||
|
triggerButton.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveRoomLink(isCollaborating ? window.location.href : "");
|
||||||
|
}, [isCollaborating]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
className={`RoomDialog-modalButton ${
|
||||||
|
isCollaborating ? "is-collaborating" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setModalIsShown(true)}
|
||||||
|
icon={users}
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.roomDialog")}
|
||||||
|
aria-label={t("buttons.roomDialog")}
|
||||||
|
showAriaLabel={useIsMobile()}
|
||||||
|
ref={triggerButton}
|
||||||
|
/>
|
||||||
|
{modalIsShown && (
|
||||||
|
<Modal
|
||||||
|
maxWidth={800}
|
||||||
|
labelledBy="room-title"
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
>
|
||||||
|
<RoomModal
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
activeRoomLink={activeRoomLink}
|
||||||
|
onRoomCreate={onRoomCreate}
|
||||||
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -17,6 +17,7 @@ type ToolButtonBaseProps = {
|
|||||||
showAriaLabel?: boolean;
|
showAriaLabel?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolButtonProps =
|
type ToolButtonProps =
|
||||||
@ -43,7 +44,7 @@ export const ToolButton = React.forwardRef(function(
|
|||||||
<button
|
<button
|
||||||
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
|
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
|
||||||
props.selected ? " ToolIcon--selected" : ""
|
props.selected ? " ToolIcon--selected" : ""
|
||||||
}`}
|
} ${props.className || ""}`}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -29,10 +29,15 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& + .ToolIcon__label {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__label {
|
.ToolIcon__label {
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
|
margin: 0 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon_size_s .ToolIcon__icon {
|
.ToolIcon_size_s .ToolIcon__icon {
|
||||||
|
@ -181,3 +181,26 @@ export const sendToBack = createIcon(
|
|||||||
</>,
|
</>,
|
||||||
24,
|
24,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const users = createIcon(
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32 80 82.1 80 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zM480 256c53 0 96-43 96-96s-43-96-96-96-96 43-96 96 43 96 96 96zm48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4 24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48 0-61.9-50.1-112-112-112z"
|
||||||
|
></path>,
|
||||||
|
640,
|
||||||
|
512,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const start = createIcon(
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm115.7 272l-176 101c-15.8 8.8-35.7-2.5-35.7-21V152c0-18.4 19.8-29.8 35.7-21l176 107c16.4 9.2 16.4 32.9 0 42z"
|
||||||
|
></path>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const stop = createIcon(
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm96 328c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V176c0-8.8 7.2-16 16-16h160c8.8 0 16 7.2 16 16v160z"
|
||||||
|
></path>,
|
||||||
|
);
|
||||||
|
@ -92,7 +92,7 @@ export function getCollaborationLinkData(link: string) {
|
|||||||
export async function generateCollaborationLink() {
|
export async function generateCollaborationLink() {
|
||||||
const id = await generateRandomID();
|
const id = await generateRandomID();
|
||||||
const key = await generateEncryptionKey();
|
const key = await generateEncryptionKey();
|
||||||
return `${window.location.href}#room=${id},${key}`;
|
return `${window.location.origin}#room=${id},${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImportedKey(key: string, usage: string): Promise<CryptoKey> {
|
async function getImportedKey(key: string, usage: string): Promise<CryptoKey> {
|
||||||
|
@ -41,7 +41,8 @@
|
|||||||
"canvasBackground": "Canvas background",
|
"canvasBackground": "Canvas background",
|
||||||
"drawingCanvas": "Drawing Canvas",
|
"drawingCanvas": "Drawing Canvas",
|
||||||
"layers": "Layers",
|
"layers": "Layers",
|
||||||
"language": "Language"
|
"language": "Language",
|
||||||
|
"createRoom": "Share a live-collaboration session"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
@ -62,7 +63,9 @@
|
|||||||
"done": "Done",
|
"done": "Done",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"redo": "Redo"
|
"redo": "Redo",
|
||||||
|
"roomDialog": "Share a live-collaboration session",
|
||||||
|
"createNewRoom": "Create new room"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||||
@ -105,5 +108,16 @@
|
|||||||
"errorStack": "Error stack trace:",
|
"errorStack": "Error stack trace:",
|
||||||
"errorStack_loading": "Loading data. please wait...",
|
"errorStack_loading": "Loading data. please wait...",
|
||||||
"sceneContent": "Scene content:"
|
"sceneContent": "Scene content:"
|
||||||
|
},
|
||||||
|
"roomDialog": {
|
||||||
|
"desc_intro": "You can invite people to your current scene to collaborate with you.",
|
||||||
|
"desc_privacy": "Don't worry, the session uses end-to-end encryption, so whatever you draw will stay private. Not even our server will be able to see what you come up with.",
|
||||||
|
"desc_start": "To begin, click the button below. (Will use the current scene. If you don't want this, you can manually clear it, first)",
|
||||||
|
"button_startSession": "Start session",
|
||||||
|
"button_stopSession": "Stop session",
|
||||||
|
"desc_inProgressIntro": "Live-collaboration session is now in progress.",
|
||||||
|
"desc_persistenceWarning": "Note that the scene data is shared across collaborators in a P2P fashion, and not persisted to our server. Thus, if all of you disconnect, you will loose the data unless you export it to a file or a shareable link.",
|
||||||
|
"desc_shareLink": "Share this link with anyone you want to collaborate with:",
|
||||||
|
"desc_exitSession": "Stopping the session will disconnect your from the room, but you'll be able to continue working with the scene, locally. Note that this won't affect other people, and they'll still be able to collaborate on their version."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,10 +239,6 @@ button,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-right-menu {
|
|
||||||
width: 13.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ErrorSplash {
|
.ErrorSplash {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user