feat: redesign of Live Collaboration dialog (#6635)

* feat: redesiged Live Collaboration dialog

* fix: address lints

* fix: inactive dialog dark mode improvements

* fix: follow styleguide with event parameter, add FilledButton size prop

* fix: change timer to be imperative

* fix: add spacing after emoji

* fix: remove unused useEffect

* fix: change margin into whitespace

* fix: add share button check back
This commit is contained in:
Are
2023-05-31 18:27:29 +02:00
committed by GitHub
parent 253c5c7866
commit 7bf4de5892
13 changed files with 698 additions and 253 deletions

View File

@ -1,76 +1,149 @@
@import "../../css/variables.module";
.excalidraw {
.RoomDialog__button {
border: 1px solid var(--default-border-color) !important;
}
.RoomDialog-linkContainer {
.RoomDialog {
display: flex;
margin: 1.5em 0;
}
flex-direction: column;
gap: 1.5rem;
input.RoomDialog-link {
color: var(--text-primary-color);
min-width: 0;
flex: 1 1 auto;
margin-inline-start: 1em;
display: inline-block;
cursor: pointer;
border: none;
padding: 0 0.5rem;
white-space: nowrap;
border-radius: var(--space-factor);
background-color: var(--button-gray-1);
}
.RoomDialog-emoji {
font-family: sans-serif;
}
.RoomDialog-usernameContainer {
display: flex;
margin: 1.5em 0;
display: flex;
align-items: center;
justify-content: center;
@include isMobile {
flex-direction: column;
align-items: stretch;
height: calc(100vh - 5rem);
}
}
@include isMobile {
.RoomDialog-usernameLabel {
font-weight: bold;
&__popover {
@keyframes RoomDialog__popover__scaleIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
box-sizing: border-box;
z-index: 100;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
padding: 0.125rem 0.5rem;
gap: 0.125rem;
height: 1.125rem;
border: none;
border-radius: 0.6875rem;
font-family: "Assistant";
font-style: normal;
font-weight: 600;
font-size: 0.75rem;
line-height: 110%;
background: var(--color-success-lighter);
color: var(--color-success);
& > svg {
width: 0.875rem;
height: 0.875rem;
}
transform-origin: var(--radix-popover-content-transform-origin);
animation: RoomDialog__popover__scaleIn 150ms ease-out;
}
}
.RoomDialog-username {
background-color: var(--input-bg-color);
border-color: var(--input-border-color);
appearance: none;
min-width: 0;
flex: 1 1 auto;
margin-inline-start: 1em;
@include isMobile {
margin-top: 0.5em;
margin-inline-start: 0;
&__inactive {
font-family: "Assistant";
&__illustration {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
& svg {
filter: var(--theme-filter);
}
}
&__header {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.3125rem;
line-height: 130%;
color: var(--color-primary);
}
&__description {
font-weight: 400;
font-size: 0.875rem;
line-height: 150%;
text-align: center;
color: var(--text-primary-color);
& strong {
display: block;
font-weight: 700;
}
}
&__start_session {
display: flex;
align-items: center;
justify-content: center;
}
}
font-size: 1em;
}
.RoomDialog-sessionStartButtonContainer {
display: flex;
justify-content: center;
}
&__active {
&__share {
display: none !important;
.Modal .RoomDialog-stopSession {
background-color: var(--button-destructive-bg-color);
@include isMobile {
display: flex !important;
}
}
.ToolIcon__label,
.ToolIcon__icon svg {
color: var(--button-destructive-color);
&__header {
margin: 0;
}
&__linkRow {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 0.75rem;
}
&__description {
border-top: 1px solid var(--color-gray-20);
padding: 0.5rem 0.5rem 0;
font-weight: 400;
font-size: 0.75rem;
line-height: 150%;
& p {
margin: 0;
}
& p + p {
margin-top: 1em;
}
}
&__actions {
display: flex;
justify-content: center;
}
}
}
}

View File

@ -1,24 +1,29 @@
import React, { useRef } from "react";
import { useRef, useState } from "react";
import * as Popover from "@radix-ui/react-popover";
import { copyTextToSystemClipboard } from "../../clipboard";
import { Dialog } from "../../components/Dialog";
import {
clipboard,
start,
stop,
share,
shareIOS,
shareWindows,
} from "../../components/icons";
import { ToolButton } from "../../components/ToolButton";
import "./RoomDialog.scss";
import Stack from "../../components/Stack";
import { AppState } from "../../types";
import { trackEvent } from "../../analytics";
import { getFrame } from "../../utils";
import DialogActionButton from "../../components/DialogActionButton";
import { useI18n } from "../../i18n";
import { KEYS } from "../../keys";
import { Dialog } from "../../components/Dialog";
import {
copyIcon,
playerPlayIcon,
playerStopFilledIcon,
share,
shareIOS,
shareWindows,
tablerCheckIcon,
} from "../../components/icons";
import { TextField } from "../../components/TextField";
import { FilledButton } from "../../components/FilledButton";
import { ReactComponent as CollabImage } from "../../assets/lock.svg";
import "./RoomDialog.scss";
const getShareIcon = () => {
const navigator = window.navigator as any;
const isAppleBrowser = /Apple/.test(navigator.vendor);
@ -33,16 +38,7 @@ const getShareIcon = () => {
return share;
};
const RoomDialog = ({
handleClose,
activeRoomLink,
username,
onUsernameChange,
onRoomCreate,
onRoomDestroy,
setErrorMessage,
theme,
}: {
export type RoomModalProps = {
handleClose: () => void;
activeRoomLink: string;
username: string;
@ -51,19 +47,41 @@ const RoomDialog = ({
onRoomDestroy: () => void;
setErrorMessage: (message: string) => void;
theme: AppState["theme"];
}) => {
};
export const RoomModal = ({
activeRoomLink,
onRoomCreate,
onRoomDestroy,
setErrorMessage,
username,
onUsernameChange,
handleClose,
}: RoomModalProps) => {
const { t } = useI18n();
const roomLinkInput = useRef<HTMLInputElement>(null);
const [justCopied, setJustCopied] = useState(false);
const timerRef = useRef<number>(0);
const ref = useRef<HTMLInputElement>(null);
const isShareSupported = "share" in navigator;
const copyRoomLink = async () => {
try {
await copyTextToSystemClipboard(activeRoomLink);
setJustCopied(true);
if (timerRef.current) {
window.clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => {
setJustCopied(false);
}, 3000);
} catch (error: any) {
setErrorMessage(error.message);
}
if (roomLinkInput.current) {
roomLinkInput.current.select();
}
ref.current?.select();
};
const shareRoomLink = async () => {
@ -78,114 +96,129 @@ const RoomDialog = ({
}
};
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target !== document.activeElement) {
event.preventDefault();
(event.target as HTMLInputElement).select();
}
};
const renderRoomDialog = () => {
if (activeRoomLink) {
return (
<div className="RoomDialog-modal">
{!activeRoomLink && (
<>
<p>{t("roomDialog.desc_intro")}</p>
<p>{`🔒 ${t("roomDialog.desc_privacy")}`}</p>
<div className="RoomDialog-sessionStartButtonContainer">
<DialogActionButton
label={t("roomDialog.button_startSession")}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();
}}
>
{start}
</DialogActionButton>
</div>
</>
)}
{activeRoomLink && (
<>
<p>{t("roomDialog.desc_inProgressIntro")}</p>
<p>{t("roomDialog.desc_shareLink")}</p>
<div className="RoomDialog-linkContainer">
<Stack.Row gap={2}>
{"share" in navigator ? (
<ToolButton
className="RoomDialog__button"
type="button"
icon={getShareIcon()}
title={t("labels.share")}
aria-label={t("labels.share")}
onClick={shareRoomLink}
/>
) : null}
<ToolButton
className="RoomDialog__button"
type="button"
icon={clipboard}
title={t("labels.copy")}
aria-label={t("labels.copy")}
onClick={copyRoomLink}
/>
</Stack.Row>
<input
type="text"
value={activeRoomLink}
readOnly={true}
className="RoomDialog-link"
ref={roomLinkInput}
onPointerDown={selectInput}
<>
<h3 className="RoomDialog__active__header">
{t("labels.liveCollaboration")}
</h3>
<TextField
value={username}
placeholder="Your name"
label="Your name"
onChange={onUsernameChange}
onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
/>
<div className="RoomDialog__active__linkRow">
<TextField
ref={ref}
label="Link"
readonly
fullWidth
value={activeRoomLink}
/>
{isShareSupported && (
<FilledButton
size="large"
variant="icon"
label="Share"
startIcon={getShareIcon()}
className="RoomDialog__active__share"
onClick={shareRoomLink}
/>
)}
<Popover.Root open={justCopied}>
<Popover.Trigger asChild>
<FilledButton
size="large"
label="Copy link"
startIcon={copyIcon}
onClick={copyRoomLink}
/>
</div>
<div className="RoomDialog-usernameContainer">
<label className="RoomDialog-usernameLabel" htmlFor="username">
{t("labels.yourName")}
</label>
<input
type="text"
id="username"
value={username.trim() || ""}
className="RoomDialog-username TextInput"
onChange={(event) => onUsernameChange(event.target.value)}
onKeyPress={(event) =>
event.key === KEYS.ENTER && handleClose()
}
/>
</div>
<p>
<span role="img" aria-hidden="true" className="RoomDialog-emoji">
{"🔒"}
</span>
{t("roomDialog.desc_privacy")}
</p>
<p>{t("roomDialog.desc_exitSession")}</p>
<div className="RoomDialog-sessionStartButtonContainer">
<DialogActionButton
actionType="danger"
label={t("roomDialog.button_stopSession")}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
}}
>
{stop}
</DialogActionButton>
</div>
</>
)}
</div>
</Popover.Trigger>
<Popover.Content
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
className="RoomDialog__popover"
side="top"
align="end"
sideOffset={5.5}
>
{tablerCheckIcon} copied
</Popover.Content>
</Popover.Root>
</div>
<div className="RoomDialog__active__description">
<p>
<span
role="img"
aria-hidden="true"
className="RoomDialog__active__description__emoji"
>
🔒{" "}
</span>
{t("roomDialog.desc_privacy")}
</p>
<p>{t("roomDialog.desc_exitSession")}</p>
</div>
<div className="RoomDialog__active__actions">
<FilledButton
size="large"
variant="outlined"
color="danger"
label={t("roomDialog.button_stopSession")}
startIcon={playerStopFilledIcon}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
}}
/>
</div>
</>
);
};
}
return (
<>
<div className="RoomDialog__inactive__illustration">
<CollabImage />
</div>
<div className="RoomDialog__inactive__header">
{t("labels.liveCollaboration")}
</div>
<div className="RoomDialog__inactive__description">
<strong>{t("roomDialog.desc_intro")}</strong>
{t("roomDialog.desc_privacy")}
</div>
<div className="RoomDialog__inactive__start_session">
<FilledButton
size="large"
label={t("roomDialog.button_startSession")}
startIcon={playerPlayIcon}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();
}}
/>
</div>
</>
);
};
const RoomDialog = (props: RoomModalProps) => {
return (
<Dialog
size="small"
onCloseRequest={handleClose}
title={t("labels.liveCollaboration")}
theme={theme}
onCloseRequest={props.handleClose}
title={false}
theme={props.theme}
>
{renderRoomDialog()}
<div className="RoomDialog">
<RoomModal {...props} />
</div>
</Dialog>
);
};

View File

@ -671,8 +671,8 @@ const ExcalidrawWrapper = () => {
{t("alerts.collabOfflineWarning")}
</div>
)}
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
</Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (
<ErrorDialog onClose={() => setErrorMessage("")}>
{errorMessage}