diff --git a/package.json b/package.json index f0ba0fa7..33395b98 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 6fe6020f..c3899a65 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,8 +1,11 @@ { - "{{count}} stars|one": "{{count}} star", + "{{count, number}}|one": "{{count, number}}", + "{{count, number}}|other": "{{count, number}}", + "{{count}} stars|one": "{{count}} stars", "{{count}} stars|other": "{{count}} stars", "{{displayName}} is presenting": "{{displayName}} is presenting", "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", + "{{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", @@ -21,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.", @@ -38,22 +40,20 @@ "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", - "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", "Inspector": "Inspector", - "Invite": "Invite", - "Invite people": "Invite people", "Join call": "Join call", "Join call now": "Join call now", "Join existing call?": "Join existing call?", @@ -67,6 +67,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", "Password": "Password", @@ -85,7 +86,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", @@ -100,7 +103,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/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 new file mode 100644 index 00000000..7ed995ce --- /dev/null +++ b/src/Facepile.tsx @@ -0,0 +1,66 @@ +/* +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. +*/ + +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 { Avatar, Size } from "./Avatar"; + +interface Props extends HTMLAttributes { + className?: string; + client: MatrixClient; + members: RoomMember[]; + max?: number; + size?: Size | number; +} + +export function Facepile({ + className, + client, + members, + max = 3, + size = Size.XS, + ...rest +}: Props) { + const { t } = useTranslation(); + + const displayedMembers = members.slice(0, max); + + return ( + m.name), + })} + {...rest} + > + {displayedMembers.map((member, i) => { + const avatarUrl = member.getMxcAvatarUrl(); + return ( + + ); + })} + + ); +} 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 80a6ab15..aea3da71 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -15,13 +15,18 @@ limitations under the License. */ import classNames from "classnames"; -import { HTMLAttributes, ReactNode } from "react"; +import { FC, HTMLAttributes, ReactNode } from "react"; import { Link } from "react-router-dom"; 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 { ReactComponent as Logo } from "./icons/Logo.svg"; -import { Subtitle } from "./typography/Typography"; +import { Avatar, Size } from "./Avatar"; +import { Facepile } from "./Facepile"; +import { EncryptionLock } from "./room/EncryptionLock"; +import { useMediaQuery } from "./useMediaQuery"; interface HeaderProps extends HTMLAttributes { children: ReactNode; @@ -108,16 +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) { +export const RoomHeaderInfo: FC = ({ + id, + name, + avatarUrl, + encrypted, + participants, + client, +}) => { + const { t } = useTranslation(); + const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg"; + return ( - <> - - {roomName} - - +
+ +
+ + {name} + + +
+ {participants.length > 0 && ( +
+ + {t("{{count, number}}", { count: participants.length })} +
+ )} +
); -} +}; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index ac02c6ed..2b8049e5 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -13,7 +13,7 @@ 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 { forwardRef, useCallback } from "react"; +import { forwardRef } from "react"; import { PressEvent } from "@react-types/shared"; import classNames from "classnames"; import { useButton } from "@react-aria/button"; @@ -27,13 +27,11 @@ import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-t import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg"; import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg"; import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg"; -import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg"; import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg"; import styles from "./Button.module.css"; import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg"; import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg"; -import { TooltipTrigger } from "../Tooltip"; import { VolumeIcon } from "./VolumeIcon"; export type ButtonVariant = @@ -146,11 +144,13 @@ export function MicButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon; + const label = muted ? t("Microphone off") : t("Microphone on"); return ( - + ); @@ -165,11 +165,13 @@ export function VideoButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const Icon = muted ? VideoCallOffIcon : VideoCallIcon; + const label = muted ? t("Video off") : t("Video on"); return ( - + ); @@ -186,11 +188,12 @@ export function ScreenshareButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const label = enabled ? t("Sharing screen") : t("Share screen"); return ( - + ); @@ -213,7 +216,7 @@ export function HangupButton({ className={classNames(styles.hangupButton, className)} {...rest} > - + ); @@ -232,28 +235,7 @@ export function SettingsButton({ return ( - - ); -} - -export function InviteButton({ - className, - variant = "toolbar", - ...rest -}: { - className?: string; - variant?: string; - // TODO: add all props for ); @@ -268,14 +250,13 @@ interface AudioButtonProps extends Omit { export function AudioButton({ volume, ...rest }: AudioButtonProps) { const { t } = useTranslation(); - const tooltip = useCallback(() => t("Local volume"), [t]); return ( - + - + ); } @@ -288,15 +269,14 @@ export function FullscreenButton({ ...rest }: FullscreenButtonProps) { const { t } = useTranslation(); - const tooltip = useCallback(() => { - return fullscreen ? t("Exit full screen") : t("Full screen"); - }, [fullscreen, t]); + const Icon = fullscreen ? FullscreenExit : Fullscreen; + const label = fullscreen ? t("Exit full screen") : t("Full screen"); return ( - + - + ); } 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