diff --git a/package.json b/package.json index e86faef7..58b17ce6 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@sentry/tracing": "^6.13.3", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^0.0.5", - "@vector-im/compound-web": "^0.2.15", + "@vector-im/compound-web": "^0.4.0", "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react": "^4.0.1", "classnames": "^2.3.1", diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index f2605959..78878b71 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,9 +1,11 @@ { + "{{count, number}}|one": "{{count, number}}", + "{{count, number}}|other": "{{count, number}}", "{{count}} stars|one": "{{count}} star", "{{count}} stars|other": "{{count}} stars", "{{displayName}} is presenting": "{{displayName}} is presenting", "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", - "{{names}}, {{name}}": "{{names}}, {{name}}", + "{{names, list(style: short;)}}": "{{names, list(style: short;)}}", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", "<0>Create an account Or <2>Access as a guest": "<0>Create an account Or <2>Access as a guest", @@ -22,7 +24,6 @@ "Call link copied": "Call link copied", "Call type menu": "Call type menu", "Camera": "Camera", - "Change layout": "Change layout", "Close": "Close", "Confirm password": "Confirm password", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", @@ -32,7 +33,6 @@ "Create account": "Create account", "Debug log": "Debug log", "Debug log request": "Debug log request", - "Details": "Details", "Developer": "Developer", "Developer Settings": "Developer Settings", "Display name": "Display name", @@ -40,25 +40,23 @@ "Element Call Home": "Element Call Home", "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.", "Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)", + "Encrypted": "Encrypted", "End call": "End call", "End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Feedback": "Feedback", "Fetching group call timed out.": "Fetching group call timed out.", - "Freedom": "Freedom", "Full screen": "Full screen", "Go": "Go", - "Grid layout menu": "Grid layout menu", + "Grid": "Grid", "Home": "Home", "How did it go?": "How did it go?", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "Include debug logs": "Include debug logs", "Incompatible versions": "Incompatible versions", - "Incompatible versions!": "Incompatible versions!", "Inspector": "Inspector", "Invite": "Invite", - "Invite people": "Invite people", "Join call": "Join call", "Join call now": "Join call now", "Join existing call?": "Join existing call?", @@ -72,6 +70,7 @@ "Microphone on": "Microphone on", "More": "More", "No": "No", + "Not encrypted": "Not encrypted", "Not now, return to home screen": "Not now, return to home screen", "Not registered yet? <2>Create an account": "Not registered yet? <2>Create an account", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}", @@ -91,7 +90,9 @@ "Sending debug logs…": "Sending debug logs…", "Sending…": "Sending…", "Settings": "Settings", + "Share": "Share", "Share screen": "Share screen", + "Share this call": "Share this call", "Sharing screen": "Sharing screen", "Show call inspector": "Show call inspector", "Show connection stats": "Show connection stats", @@ -106,7 +107,6 @@ "Thanks, we received your feedback!": "Thanks, we received your feedback!", "Thanks!": "Thanks!", "This call already exists, would you like to join?": "This call already exists, would you like to join?", - "This call is not end-to-end encrypted.": "This call is not end-to-end encrypted.", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)", "User menu": "User menu", "Username": "Username", diff --git a/src/Avatar.module.css b/src/Avatar.module.css deleted file mode 100644 index accff6ae..00000000 --- a/src/Avatar.module.css +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2022 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.avatar { - position: relative; - color: var(--stopgap-color-on-solid-accent); - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; - font-weight: 600; - overflow: hidden; - flex-shrink: 0; -} - -.avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.avatar svg * { - fill: var(--cpd-color-text-primary); -} - -.avatar span { - padding-top: 1px; -} - -.xs { - width: 22px; - height: 22px; - border-radius: 22px; - font-size: 14px; -} - -.sm { - width: 32px; - height: 32px; - border-radius: 32px; - font-size: 15px; -} - -.md { - width: 36px; - height: 36px; - border-radius: 36px; - font-size: 20px; -} - -.lg { - width: 42px; - height: 42px; - border-radius: 42px; - font-size: 24px; -} - -.xl { - width: 90px; - height: 90px; - border-radius: 90px; - font-size: 48px; -} diff --git a/src/Avatar.tsx b/src/Avatar.tsx index c11d71d4..e23bd909 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -14,23 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useMemo, CSSProperties, HTMLAttributes, FC } from "react"; -import classNames from "classnames"; +import { useMemo, FC } from "react"; +import { Avatar as CompoundAvatar } from "@vector-im/compound-web"; import { getAvatarUrl } from "./matrix-utils"; import { useClient } from "./ClientContext"; -import styles from "./Avatar.module.css"; - -const backgroundColors = [ - "#5C56F5", - "#03B381", - "#368BD6", - "#AC3BA8", - "#E64F7A", - "#FF812D", - "#2DC2C5", - "#74D12C", -]; export enum Size { XS = "xs", @@ -48,50 +36,28 @@ export const sizes = new Map([ [Size.XL, 90], ]); -function hashStringToArrIndex(str: string, arrLength: number) { - let sum = 0; - - for (let i = 0; i < str.length; i++) { - sum += str.charCodeAt(i); - } - - return sum % arrLength; -} - -interface Props extends HTMLAttributes { - bgKey?: string; +interface Props { + id: string; + name: string; + className?: string; src?: string; size?: Size | number; - className?: string; - style?: CSSProperties; - fallback: string; } export const Avatar: FC = ({ - bgKey, - src, - fallback, - size = Size.MD, className, - style = {}, - ...rest + id, + name, + src, + size = Size.MD, }) => { const { client } = useClient(); - const [sizeClass, sizePx, sizeStyle] = useMemo( + const sizePx = useMemo( () => Object.values(Size).includes(size as Size) - ? [styles[size as string], sizes.get(size as Size), {}] - : [ - null, - size as number, - { - width: size, - height: size, - borderRadius: size, - fontSize: Math.round((size as number) / 2), - }, - ], + ? sizes.get(size as Size) + : (size as number), [size] ); @@ -100,28 +66,13 @@ export const Avatar: FC = ({ return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src; }, [client, src, sizePx]); - const backgroundColor = useMemo(() => { - const index = hashStringToArrIndex( - bgKey || fallback || src || "", - backgroundColors.length - ); - return backgroundColors[index]; - }, [bgKey, src, fallback]); - - /* eslint-disable jsx-a11y/alt-text */ return ( -
- {resolvedSrc ? ( - - ) : typeof fallback === "string" ? ( - {fallback} - ) : ( - fallback - )} -
+ ); }; diff --git a/src/E2EELock.tsx b/src/E2EELock.tsx deleted file mode 100644 index 9a9a55e9..00000000 --- a/src/E2EELock.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2023 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { useTranslation } from "react-i18next"; -import { useCallback } from "react"; -import { useObjectRef } from "@react-aria/utils"; -import { useButton } from "@react-aria/button"; - -import styles from "./E2EELock.module.css"; -import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg"; -import { TooltipTrigger } from "./Tooltip"; - -export const E2EELock = () => { - const { t } = useTranslation(); - const tooltip = useCallback( - () => t("This call is not end-to-end encrypted."), - [t] - ); - - return ( - - - - ); -}; - -/** - * This component is a bit of hack - for some reason for the TooltipTrigger to - * work, it needs to contain a component which uses the useButton hook; please - * note that for some reason this also needs to be a separate component and we - * cannot just use the useButton hook inside the E2EELock. - */ -const Icon = () => { - const buttonRef = useObjectRef(); - const { buttonProps } = useButton({}, buttonRef); - - return ( -
- -
- ); -}; diff --git a/src/Facepile.tsx b/src/Facepile.tsx index 0c9ec239..7ed995ce 100644 --- a/src/Facepile.tsx +++ b/src/Facepile.tsx @@ -14,27 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { HTMLAttributes, useMemo } from "react"; -import classNames from "classnames"; +import { HTMLAttributes } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTranslation } from "react-i18next"; +import { AvatarStack } from "@vector-im/compound-web"; -import styles from "./Facepile.module.css"; -import { Avatar, Size, sizes } from "./Avatar"; - -const overlapMap: Partial> = { - [Size.XS]: 2, - [Size.SM]: 4, - [Size.MD]: 8, -}; +import { Avatar, Size } from "./Avatar"; interface Props extends HTMLAttributes { - className: string; + className?: string; client: MatrixClient; members: RoomMember[]; max?: number; - size?: Size; + size?: Size | number; } export function Facepile({ @@ -47,51 +40,27 @@ export function Facepile({ }: Props) { const { t } = useTranslation(); - const _size = sizes.get(size)!; - const _overlap = overlapMap[size]!; - - const title = useMemo(() => { - return members.reduce( - (prev, curr) => - prev === null - ? curr.name - : t("{{names}}, {{name}}", { names: prev, name: curr.name }), - null - ) as string; - }, [members, t]); + const displayedMembers = members.slice(0, max); return ( -
m.name), + })} {...rest} > - {members.slice(0, max).map((member, i) => { + {displayedMembers.map((member, i) => { const avatarUrl = member.getMxcAvatarUrl(); return ( ); })} - {members.length > max && ( - - )} -
+ ); } diff --git a/src/Header.module.css b/src/Header.module.css index e54edff5..84b9df7b 100644 --- a/src/Header.module.css +++ b/src/Header.module.css @@ -28,8 +28,8 @@ limitations under the License. flex: 1; align-items: center; white-space: nowrap; - padding: 0 20px; - height: 64px; + padding-inline: var(--inline-content-inset); + height: 80px; } .headerLogo { @@ -66,51 +66,56 @@ limitations under the License. margin-right: 0; } +.roomHeaderInfo { + display: grid; + column-gap: var(--cpd-space-4x); + grid-template-columns: auto auto; + grid-template-rows: 1fr auto; +} + +.roomHeaderInfo[data-size="sm"] { + grid-template-areas: "avatar name" ". participants"; +} + +.roomHeaderInfo[data-size="lg"] { + grid-template-areas: "avatar name" "avatar participants"; +} + .roomAvatar { - position: relative; - display: none; - justify-content: center; + align-self: flex-start; + grid-area: avatar; +} + +.nameLine { + grid-area: name; + flex-grow: 1; + display: flex; align-items: center; - width: 36px; - height: 36px; - border-radius: 36px; - background-color: #5c56f5; + gap: var(--cpd-space-1x); } -.roomAvatar > * { - fill: white; - stroke: white; -} - -.userName { - font-weight: 600; - margin-right: 8px; - text-overflow: ellipsis; +.nameLine > h1 { + margin: 0; + /* XXX I can't actually get this ellipsis overflow to trigger, because + constraint propagation in a nested flexbox layout is a massive pain */ overflow: hidden; - flex-shrink: 1; + text-overflow: ellipsis; } -.versionMismatchWarning { - padding-left: 15px; +.nameLine > svg { + flex-shrink: 0; } -.versionMismatchWarning::before { - content: ""; - display: inline-block; - position: relative; - top: 1px; - width: 16px; - height: 16px; - mask-image: url("./icons/AlertTriangleFilled.svg"); - mask-repeat: no-repeat; - mask-size: contain; - background-color: var(--cpd-color-icon-critical-primary); - padding-right: 5px; +.participantsLine { + grid-area: participants; + display: flex; + align-items: center; + gap: var(--cpd-space-1-5x); + font: var(--cpd-font-body-sm-medium); } @media (min-width: 800px) { .headerLogo, - .roomAvatar, .leftNav.hideMobile, .rightNav.hideMobile { display: flex; @@ -119,8 +124,4 @@ limitations under the License. .leftNav h3 { font-size: var(--font-size-subtitle); } - - .nav { - height: 76px; - } } diff --git a/src/Header.stories.jsx b/src/Header.stories.jsx deleted file mode 100644 index e5c4473a..00000000 --- a/src/Header.stories.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { GridLayoutMenu } from "./room/GridLayoutMenu"; -import { - Header, - HeaderLogo, - LeftNav, - RightNav, - RoomHeaderInfo, -} from "./Header"; -import { UserMenu } from "./UserMenu"; - -export default { - title: "Header", - component: Header, - parameters: { - layout: "fullscreen", - }, -}; - -export const HomeAnonymous = () => ( -
- - - - - - -
-); - -export const HomeNamedGuest = () => ( -
- - - - - - -
-); - -export const HomeLoggedIn = () => ( -
- - - - - - -
-); - -export const LobbyNamedGuest = () => ( -
- - - - - - -
-); - -export const LobbyLoggedIn = () => ( -
- - - - - - -
-); - -export const InRoomNamedGuest = () => ( -
- - - - - - - -
-); - -export const InRoomLoggedIn = () => ( -
- - - - - - - -
-); - -export const CreateAccount = () => ( -
- - - - -
-); diff --git a/src/Header.tsx b/src/Header.tsx index 8d754b92..aea3da71 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -15,17 +15,18 @@ limitations under the License. */ import classNames from "classnames"; -import { HTMLAttributes, ReactNode, useCallback } from "react"; +import { FC, HTMLAttributes, ReactNode } from "react"; import { Link } from "react-router-dom"; -import { Room } from "matrix-js-sdk/src/models/room"; import { useTranslation } from "react-i18next"; +import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; +import { Heading } from "@vector-im/compound-web"; import styles from "./Header.module.css"; -import { useModalTriggerState } from "./Modal"; -import { Button } from "./button"; import { ReactComponent as Logo } from "./icons/Logo.svg"; -import { Subtitle } from "./typography/Typography"; -import { IncompatibleVersionModal } from "./IncompatibleVersionModal"; +import { Avatar, Size } from "./Avatar"; +import { Facepile } from "./Facepile"; +import { EncryptionLock } from "./room/EncryptionLock"; +import { useMediaQuery } from "./useMediaQuery"; interface HeaderProps extends HTMLAttributes { children: ReactNode; @@ -112,47 +113,52 @@ export function HeaderLogo({ className }: HeaderLogoProps) { ); } -interface RoomHeaderInfo { - roomName: string; +interface RoomHeaderInfoProps { + id: string; + name: string; + avatarUrl: string | null; + encrypted: boolean; + participants: RoomMember[]; + client: MatrixClient; } -export function RoomHeaderInfo({ roomName }: RoomHeaderInfo) { - return ( - <> - - {roomName} - - - ); -} - -interface VersionMismatchWarningProps { - users: Set; - room: Room; -} - -export function VersionMismatchWarning({ - users, - room, -}: VersionMismatchWarningProps) { +export const RoomHeaderInfo: FC = ({ + id, + name, + avatarUrl, + encrypted, + participants, + client, +}) => { const { t } = useTranslation(); - const { modalState, modalProps } = useModalTriggerState(); - - const onDetailsClick = useCallback(() => { - modalState.open(); - }, [modalState]); - - if (users.size === 0) return null; + const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg"; return ( - - {t("Incompatible versions!")} - - {modalState.isOpen && ( - +
+ +
+ + {name} + + +
+ {participants.length > 0 && ( +
+ + {t("{{count, number}}", { count: participants.length })} +
)} - +
); -} +}; diff --git a/src/UserMenu.module.css b/src/UserMenu.module.css index d1db1071..575b71b9 100644 --- a/src/UserMenu.module.css +++ b/src/UserMenu.module.css @@ -24,17 +24,3 @@ limitations under the License. .userButton svg * { fill: var(--cpd-color-icon-primary); } - -.avatar { - width: 24px; - height: 24px; - font-size: var(--font-size-caption); -} - -@media (min-width: 800px) { - .avatar { - width: 32px; - height: 32px; - font-size: var(--font-size-body); - } -} diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 9df3309d..515e71f0 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -35,6 +35,7 @@ interface UserMenuProps { preventNavigation: boolean; isAuthenticated: boolean; isPasswordlessUser: boolean; + userId: string; displayName: string; avatarUrl?: string; onAction: (value: string) => void; @@ -44,6 +45,7 @@ export function UserMenu({ preventNavigation, isAuthenticated, isPasswordlessUser, + userId, displayName, avatarUrl, onAction, @@ -109,10 +111,10 @@ export function UserMenu({ > {isAuthenticated && (!isPasswordlessUser || avatarUrl) ? ( ) : ( diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 6a83133e..a03e5b5a 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -67,6 +67,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { isPasswordlessUser={passwordlessUser} avatarUrl={avatarUrl} onAction={onAction} + userId={client?.getUserId() ?? ""} displayName={displayName || (userName ? userName.replace("@", "") : "")} /> {modalState.isOpen && client && ( diff --git a/src/button/ShareButton.tsx b/src/button/ShareButton.tsx new file mode 100644 index 00000000..2f7f1334 --- /dev/null +++ b/src/button/ShareButton.tsx @@ -0,0 +1,31 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ComponentPropsWithoutRef, FC } from "react"; +import { Button } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; +import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg"; + +export const ShareButton: FC< + Omit, "children"> +> = (props) => { + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/src/button/VolumeIcon.tsx b/src/button/VolumeIcon.tsx index 163699f6..00aebb06 100644 --- a/src/button/VolumeIcon.tsx +++ b/src/button/VolumeIcon.tsx @@ -15,19 +15,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ComponentPropsWithoutRef, FC } from "react"; + import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg"; import { ReactComponent as AudioLow } from "../icons/AudioLow.svg"; import { ReactComponent as Audio } from "../icons/Audio.svg"; -interface Props { +interface Props extends ComponentPropsWithoutRef<"svg"> { /** * Number between 0 and 1 */ volume: number; } -export function VolumeIcon({ volume }: Props) { - if (volume <= 0) return ; - if (volume <= 0.5) return ; - return