diff --git a/.github/workflows/netlify-pr.yaml b/.github/workflows/netlify-pr.yaml index f1d4dced..616c290b 100644 --- a/.github/workflows/netlify-pr.yaml +++ b/.github/workflows/netlify-pr.yaml @@ -6,6 +6,7 @@ on: - completed branches-ignore: - "main" + - "livekit" jobs: deploy: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' diff --git a/config/nginx.conf b/config/nginx.conf index f7253465..5dc69b2f 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -21,5 +21,9 @@ server { expires 1w; add_header Cache-Control "public, no-transform"; } + + location /apple-app-site-association { + default_type application/json; + } } diff --git a/package.json b/package.json index 1eca1e81..acc6068d 100644 --- a/package.json +++ b/package.json @@ -48,6 +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.4.0", "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react": "^4.0.1", "classnames": "^2.3.1", @@ -58,7 +59,7 @@ "i18next-http-backend": "^1.4.4", "livekit-client": "^1.12.3", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fc2671d8538a3339ca5925ef42e3ba6102f7e8f2", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca", "matrix-widget-api": "^1.3.1", "mermaid": "^9.0.0", "normalize.css": "^8.0.1", @@ -121,7 +122,7 @@ "vite-plugin-svgr": "^3.2.0" }, "jest": { - "testEnvironment": "jsdom", + "testEnvironment": "./test/environment.ts", "testMatch": [ "/test/**/*-test.[jt]s?(x)" ], diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json new file mode 100644 index 00000000..0eb37bf0 --- /dev/null +++ b/public/.well-known/assetlinks.json @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "io.element.android.x.debug", + "sha256_cert_fingerprints": [ + "B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E" + ] + } + } +] diff --git a/public/apple-app-site-association b/public/apple-app-site-association new file mode 100644 index 00000000..b27b5c71 --- /dev/null +++ b/public/apple-app-site-association @@ -0,0 +1,15 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appIDs": [ + "7J4U792NQT.io.element.elementx", + "7J4U792NQT.io.element.elementx.nightly", + "7J4U792NQT.io.element.elementx.pr" + ], + "paths": ["*"] + } + ] + } +} diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 30c770dd..c3899a65 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,9 +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}}, {{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.", @@ -39,34 +40,34 @@ "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?", - "Leave": "Leave", "Loading…": "Loading…", "Local volume": "Local volume", "Logging in…": "Logging in…", "Login": "Login", "Login to your account": "Login to your account", "Microphone": "Microphone", + "Microphone off": "Microphone off", + "Microphone on": "Microphone on", "More": "More", - "Mute microphone": "Mute microphone", "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,14 +86,16 @@ "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", "Sign in": "Sign in", "Sign out": "Sign out", "Speaker": "Speaker", "Spotlight": "Spotlight", - "Stop sharing screen": "Stop sharing screen", "Submit": "Submit", "Submit feedback": "Submit feedback", "Submitting…": "Submitting…", @@ -100,17 +103,15 @@ "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)", - "Turn off camera": "Turn off camera", - "Turn on camera": "Turn on camera", - "Unmute microphone": "Unmute microphone", "User menu": "User menu", "Username": "Username", "Version: {{version}}": "Version: {{version}}", "Video": "Video", "Video call": "Video call", "Video call name": "Video call name", + "Video off": "Video off", + "Video on": "Video on", "Waiting for other participants…": "Waiting for other participants…", "Walkie-talkie call": "Walkie-talkie call", "Walkie-talkie call name": "Walkie-talkie call name", 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 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/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/Button.module.css b/src/button/Button.module.css index 0ec4b534..b1b712e1 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -63,6 +63,7 @@ limitations under the License. .toolbarButton:disabled { background-color: var(--cpd-color-bg-action-primary-disabled); + box-shadow: none; } .toolbarButton, @@ -70,33 +71,39 @@ limitations under the License. width: 50px; height: 50px; border-radius: 50px; - background-color: var(--cpd-color-bg-subtle-secondary); -} - -.toolbarButton:hover, -.toolbarButtonSecondary:hover { - background-color: var(--cpd-color-bg-action-secondary-hovered); -} - -.toolbarButton:active, -.toolbarButtonSecondary:active { - background-color: var(--cpd-color-bg-action-secondary-pressed); + background-color: var(--cpd-color-bg-canvas-default); + color: var(--cpd-color-icon-primary); + border: 1px solid var(--cpd-color-gray-400); + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); } .toolbarButton.on, .toolbarButton.off { background-color: var(--cpd-color-bg-action-primary-rest); + color: var(--cpd-color-icon-on-solid-primary); } .toolbarButtonSecondary.on { background-color: var(--cpd-color-text-success-primary); } +.toolbarButton:active, +.toolbarButtonSecondary:active { + background-color: var(--cpd-color-bg-subtle-primary); + border: none; + box-shadow: none; +} + +.toolbarButton.on:active, +.toolbarButton.off:active { + background-color: var(--cpd-color-bg-action-primary-pressed); +} + .iconButton:not(.stroke) svg * { fill: var(--cpd-color-bg-action-primary-rest); } -.iconButton:not(.stroke):hover svg * { +.iconButton:not(.stroke):tertiary svg * { fill: var(--cpd-color-icon-accent-tertiary); } @@ -110,31 +117,12 @@ limitations under the License. .hangupButton { background-color: var(--cpd-color-bg-critical-primary); + border-color: var(--cpd-color-border-critical-subtle); + color: var(--stopgap-color-on-solid-accent); } -.hangupButton:hover { - background-color: var(--cpd-color-bg-critical-hovered); -} - -.toolbarButton.hangupButton svg * { - fill: var(--stopgap-color-on-solid-accent); -} - -.toolbarButton svg *, -.toolbarButtonSecondary svg * { - fill: var(--cpd-color-icon-primary); -} - -.toolbarButton.on svg * { - fill: var(--cpd-color-icon-accent-tertiary); -} - -.toolbarButton.off svg * { - fill: var(--cpd-color-icon-on-solid-primary); -} - -.toolbarButtonSecondary.on svg * { - fill: var(--stopgap-color-on-solid-accent); +.hangupButton:active { + background-color: var(--cpd-color-bg-critical-pressed); } .secondary, @@ -196,10 +184,6 @@ limitations under the License. fill: var(--cpd-color-icon-secondary); } -.iconCopyButton:hover svg * { - fill: var(--cpd-color-icon-accent-tertiary); -} - .iconCopyButton.on svg *, .iconCopyButton.on:hover svg * { fill: transparent; @@ -212,10 +196,6 @@ limitations under the License. border-radius: 8px; } -.dropdownButton:hover { - background-color: var(--cpd-color-bg-action-secondary-hovered); -} - .dropdownButton:active, .dropdownButton.on { background-color: var(--cpd-color-bg-action-secondary-pressed); @@ -239,3 +219,33 @@ limitations under the License. color: var(--cpd-color-text-action-accent); cursor: pointer; } + +@media (hover: hover) { + .toolbarButton:hover, + .toolbarButtonSecondary:hover { + background-color: var(--cpd-color-bg-subtle-primary); + border: none; + box-shadow: none; + } + + .toolbarButton.on:hover, + .toolbarButton.off:hover { + background-color: var(--cpd-color-bg-action-primary-hovered); + } + + .iconButton:not(.stroke):hover svg * { + fill: var(--cpd-color-icon-accent-tertiary); + } + + .hangupButton:hover { + background-color: var(--cpd-color-bg-critical-hovered); + } + + .iconCopyButton:hover svg * { + fill: var(--cpd-color-icon-accent-tertiary); + } + + .dropdownButton:hover { + background-color: var(--cpd-color-bg-action-secondary-hovered); + } +} diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 2de62a80..2b8049e5 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -13,26 +13,25 @@ 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"; import { mergeProps, useObjectRef } from "@react-aria/utils"; import { useTranslation } from "react-i18next"; +import { Tooltip } from "@vector-im/compound-web"; +import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg"; +import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg"; +import { ReactComponent as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg"; +import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-tokens/icons/video-call-off.svg"; +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 ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg"; import styles from "./Button.module.css"; -import { ReactComponent as MicIcon } from "../icons/Mic.svg"; -import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg"; -import { ReactComponent as VideoIcon } from "../icons/Video.svg"; -import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg"; -import { ReactComponent as HangupIcon } from "../icons/Hangup.svg"; -import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg"; -import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; -import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg"; -import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg"; 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 = @@ -129,7 +128,7 @@ export const Button = forwardRef( > <> {children} - {variant === "dropdown" && } + {variant === "dropdown" && } ); @@ -145,15 +144,15 @@ export function MicButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon; + const label = muted ? t("Microphone off") : t("Microphone on"); return ( - (muted ? t("Unmute microphone") : t("Mute microphone"))} - > - - + ); } @@ -166,15 +165,15 @@ export function VideoButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const Icon = muted ? VideoCallOffIcon : VideoCallIcon; + const label = muted ? t("Video off") : t("Video on"); return ( - (muted ? t("Turn on camera") : t("Turn off camera"))} - > - - + ); } @@ -189,15 +188,14 @@ export function ScreenshareButton({ [index: string]: unknown; }) { const { t } = useTranslation(); + const label = enabled ? t("Sharing screen") : t("Share screen"); return ( - (enabled ? t("Stop sharing screen") : t("Share screen"))} - > - - + ); } @@ -210,18 +208,17 @@ export function HangupButton({ [index: string]: unknown; }) { const { t } = useTranslation(); - const tooltip = useCallback(() => t("Leave"), [t]); return ( - + - + ); } @@ -234,36 +231,13 @@ export function SettingsButton({ [index: string]: unknown; }) { const { t } = useTranslation(); - const tooltip = useCallback(() => t("Settings"), [t]); return ( - + - - ); -} - -export function InviteButton({ - className, - variant = "toolbar", - ...rest -}: { - className?: string; - variant?: string; - // TODO: add all props for - + ); } @@ -276,14 +250,13 @@ interface AudioButtonProps extends Omit { export function AudioButton({ volume, ...rest }: AudioButtonProps) { const { t } = useTranslation(); - const tooltip = useCallback(() => t("Local volume"), [t]); return ( - + - + ); } @@ -296,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