diff --git a/knip.ts b/knip.ts index dcc7231c19..0dcf8b8c18 100644 --- a/knip.ts +++ b/knip.ts @@ -30,8 +30,6 @@ export default { // We obviously use this, but if the package has been linked with pnpm link, // then Knip will flag it as a false positive // https://github.com/webpro-nl/knip/issues/766 - "@vector-im/compound-web", - "matrix-widget-api", // Used by oxlint "eslint-plugin-element-call", "eslint-plugin-storybook", diff --git a/package.json b/package.json index bd4769d919..54bf085242 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "dev:full": "vite", "dev:embedded": "vite --config vite-embedded.config.js", "build": "pnpm build:full", - "build:full": "NODE_OPTIONS=--max-old-space-size=16384 vite build", + "build:full": "vite build", "build:full:production": "pnpm build:full", "build:full:development": "pnpm build:full --mode development", - "build:embedded": "pnpm build:full --config vite-embedded.config.js", + "build:embedded": "pnpm build:full --config vite-embedded.config.ts", "build:embedded:production": "pnpm build:embedded", "build:embedded:development": "pnpm build:embedded --mode development", "build:sdk:development": "pnpm build:sdk --mode development", @@ -145,6 +145,7 @@ }, "packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f", "dependencies": { - "@jitsi/rnnoise-wasm": "0.2.1" + "@jitsi/rnnoise-wasm": "0.2.1", + "@phosphor-icons/react": "^2.1.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02d8978f46..b5bd3cf689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@jitsi/rnnoise-wasm': specifier: 0.2.1 version: 0.2.1 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) devDependencies: '@codecov/vite-plugin': specifier: ^1.3.0 @@ -2046,6 +2049,13 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -7946,6 +7956,11 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.6 optional: true + '@phosphor-icons/react@2.1.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/src/AppBar.tsx b/src/AppBar.tsx index 18314dd325..0d02bc6d12 100644 --- a/src/AppBar.tsx +++ b/src/AppBar.tsx @@ -18,11 +18,7 @@ import { } from "react"; import classNames from "classnames"; import { Heading, IconButton, Text, Tooltip } from "@vector-im/compound-web"; -import { - ArrowLeftIcon, - ChevronLeftIcon, - CollapseIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { ArrowLeft, CaretLeft, CornersIn } from "@phosphor-icons/react"; import { useTranslation } from "react-i18next"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -81,7 +77,7 @@ export const AppBar: FC = ({ children }) => { ], ); - const BackIcon = platform === "android" ? ArrowLeftIcon : ChevronLeftIcon; + const BackIcon = platform === "android" ? ArrowLeft : CaretLeft; return ( <> @@ -100,7 +96,7 @@ export const AppBar: FC = ({ children }) => { {primaryButtonIcon === "back" ? ( ) : ( - + )} diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index eb84010e52..8eb496536a 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -10,7 +10,7 @@ import classNames from "classnames"; import { useTranslation } from "react-i18next"; import * as Sentry from "@sentry/react"; import { logger } from "matrix-js-sdk/lib/logger"; -import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Warning } from "@phosphor-icons/react"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; import styles from "./FullScreenView.module.css"; @@ -67,7 +67,9 @@ export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => { ) : ( ( + + )} title={t("error.generic")} rageshake fatal diff --git a/src/Header.tsx b/src/Header.tsx index cffc340235..7d838cb62d 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -10,7 +10,7 @@ import { type Ref, type FC, type HTMLAttributes, type ReactNode } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Heading, Text } from "@vector-im/compound-web"; -import { UserProfileIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { User } from "@phosphor-icons/react"; import styles from "./Header.module.css"; import Logo from "./icons/Logo.svg?react"; @@ -166,7 +166,7 @@ export const RoomHeaderInfo: FC = ({ {(participantCount ?? 0) > 0 && (
- = ({ data-testid="modal_close" aria-label={t("action.close")} > - + )}
diff --git a/src/RTCConnectionStats.tsx b/src/RTCConnectionStats.tsx index ea9df3f5e0..e94a98610b 100644 --- a/src/RTCConnectionStats.tsx +++ b/src/RTCConnectionStats.tsx @@ -7,10 +7,7 @@ Please see LICENSE in the repository root for full details. import { useState, type FC } from "react"; import { Button, Text } from "@vector-im/compound-web"; -import { - MicOnSolidIcon, - VideoCallSolidIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Microphone, VideoCamera } from "@phosphor-icons/react"; import classNames from "classnames"; import { Modal } from "./Modal"; @@ -89,7 +86,7 @@ export const RTCConnectionStats: FC = ({ onClick={() => showFullModal("audio")} size="md" kind="tertiary" - Icon={MicOnSolidIcon} + Icon={(props) => } > {"jitter" in audio && typeof audio.jitter === "number" && ( @@ -105,7 +102,7 @@ export const RTCConnectionStats: FC = ({ onClick={() => showFullModal("video")} size="md" kind="tertiary" - Icon={VideoCallSolidIcon} + Icon={(props) => } > {!!video?.framesPerSecond && ( diff --git a/src/RichError.tsx b/src/RichError.tsx index 699486e25b..3a8e93ad1e 100644 --- a/src/RichError.tsx +++ b/src/RichError.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { useTranslation } from "react-i18next"; -import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { ArrowSquareOut } from "@phosphor-icons/react"; import type { FC, ReactNode } from "react"; import { ErrorView } from "./ErrorView"; @@ -34,7 +34,7 @@ const OpenElsewhere: FC = () => { return (

diff --git a/src/__snapshots__/AppBar.test.tsx.snap b/src/__snapshots__/AppBar.test.tsx.snap index 4821894817..beee342059 100644 --- a/src/__snapshots__/AppBar.test.tsx.snap +++ b/src/__snapshots__/AppBar.test.tsx.snap @@ -22,12 +22,12 @@ exports[`AppBar > renders 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -65,12 +65,12 @@ exports[`AppBar > renders with title and subtitle 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 0e29512c68..9cc1891832 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -14,18 +14,20 @@ import { Tooltip, } from "@vector-im/compound-web"; import { - MicOnSolidIcon, - MicOffSolidIcon, - SpinnerIcon, - VideoCallSolidIcon, - VideoCallOffSolidIcon, - EndCallIcon, - ShareScreenSolidIcon, - OverflowHorizontalIcon, - OverflowVerticalIcon, - VolumeOnSolidIcon, - VolumeOffSolidIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; + Microphone, + MicrophoneSlash, + Spinner, + VideoCamera, + VideoCameraSlash, + PhoneDisconnect, + MonitorArrowUp, +} from "@phosphor-icons/react"; +import { + DotsThreeOutlineVertical, + DotsThreeOutline, + SpeakerHigh, + SpeakerSlash, +} from "@phosphor-icons/react"; import styles from "./Button.module.css"; import callFooterStyles from "../components/CallFooter.module.css"; @@ -39,7 +41,11 @@ interface MicButtonProps extends ComponentPropsWithoutRef<"button"> { export const MicButton: FC = ({ enabled, busy, ...props }) => { const { t } = useTranslation(); - const Icon = busy ? SpinnerIcon : enabled ? MicOnSolidIcon : MicOffSolidIcon; + const Icon = busy + ? Spinner + : enabled + ? (p: any) => + : (p: any) => ; const label = enabled ? t("mute_microphone_button_label") : t("unmute_microphone_button_label"); @@ -76,10 +82,10 @@ export const VideoButton: FC = ({ }) => { const { t } = useTranslation(); const Icon = busy - ? SpinnerIcon + ? Spinner : enabled - ? VideoCallSolidIcon - : VideoCallOffSolidIcon; + ? (p: any) => + : (p: any) => ; const label = enabled ? t("stop_video_button_label") : t("start_video_button_label"); @@ -121,7 +127,7 @@ export const ShareScreenButton: FC = ({ = ({ @@ -171,7 +177,13 @@ export const LoudspeakerButton: FC = ({ + ) : ( + + ) + } {...props} kind={loudspeakerModeEnabled ? "secondary" : "primary"} aria-checked={loudspeakerModeEnabled} @@ -201,15 +213,16 @@ export const SettingsIconButton: FC = ({ ...props }) => { const { t } = useTranslation(); - const Icon = - platform === "android" ? OverflowVerticalIcon : OverflowHorizontalIcon; return ( - + ); @@ -231,9 +244,18 @@ export const SettingsButton: FC = ({ { + const IconComp = + platform === "android" + ? DotsThreeOutlineVertical + : DotsThreeOutline; + return ( + + ); + }} kind={"secondary"} {...props} /> diff --git a/src/button/DeafenButton.tsx b/src/button/DeafenButton.tsx new file mode 100644 index 0000000000..1053b7091b --- /dev/null +++ b/src/button/DeafenButton.tsx @@ -0,0 +1,52 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type ComponentPropsWithoutRef, type FC } from "react"; +import classNames from "classnames"; +import { Button as CpdButton, Tooltip } from "@vector-im/compound-web"; +import { Headphones, SpeakerSlash, Spinner } from "@phosphor-icons/react"; + +import styles from "./Button.module.css"; + +interface DeafenButtonProps extends ComponentPropsWithoutRef<"button"> { + enabled: boolean; + busy?: boolean; + size?: "md" | "lg"; +} + +export const DeafenButton: FC = ({ + enabled, + busy, + ...props +}) => { + const Icon = busy + ? Spinner + : enabled + ? (p: any) => + : (p: any) => ; + + // Using generic labels for now, these can be added to i18n later if needed + const label = enabled ? "Deafen" : "Undeafen"; + + return ( + + + + ); +}; diff --git a/src/button/InviteButton.tsx b/src/button/InviteButton.tsx index 66ff57e2c4..466e63a041 100644 --- a/src/button/InviteButton.tsx +++ b/src/button/InviteButton.tsx @@ -8,14 +8,14 @@ Please see LICENSE in the repository root for full details. import { type ComponentPropsWithoutRef, type FC } from "react"; import { Button } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; -import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { UserPlus } from "@phosphor-icons/react"; export const InviteButton: FC< Omit, "children"> > = (props) => { const { t } = useTranslation(); return ( - ); diff --git a/src/button/ReactionToggleButton.module.css b/src/button/ReactionToggleButton.module.css index 90c6af0219..dac2cdc4be 100644 --- a/src/button/ReactionToggleButton.module.css +++ b/src/button/ReactionToggleButton.module.css @@ -34,8 +34,8 @@ div.reactionPopupMenuRoot { } div.reactionPopupMenuRoot.reactionPopupMenuModal > div > div { - padding-inline: var(--cpd-space-6x); - padding-block: var(--cpd-space-6x); + padding-inline: var(--cpd-space-2x); + padding-block: var(--cpd-space-2x); } .reactionPopupMenu section { diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index c71642e97f..82062eff8d 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -6,12 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { Button as CpdButton, Tooltip, Alert } from "@vector-im/compound-web"; -import { - RaisedHandSolidIcon, - ChevronDownIcon, - ChevronUpIcon, - ReactionSolidIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { CaretDown, CaretUp, Smiley, HandPalm } from "@phosphor-icons/react"; import { type ComponentPropsWithoutRef, type FC, @@ -53,7 +48,7 @@ const InnerButton: FC = ({ raised, open, ...props }) => { aria-haspopup kind={raised || open ? "primary" : "secondary"} iconOnly - Icon={raised ? RaisedHandSolidIcon : ReactionSolidIcon} + Icon={raised ? HandPalm : Smiley} {...props} /> @@ -102,7 +97,7 @@ export function ReactionPopupMenu({ aria-label={label} onClick={() => toggleRaisedHand()} iconOnly - Icon={RaisedHandSolidIcon} + Icon={HandPalm} /> @@ -153,10 +148,15 @@ export function ReactionPopupMenu({ aria-label={ isFullyExpanded ? t("action.show_less") : t("action.show_more") } - Icon={isFullyExpanded ? ChevronUpIcon : ChevronDownIcon} kind="tertiary" onClick={() => setExpanded(!isFullyExpanded)} - /> + > + {isFullyExpanded ? ( + + ) : ( + + )} + diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index a1e319d95c..31295e9a88 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -20,14 +20,12 @@ exports[`Can close reaction dialog 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -54,14 +52,12 @@ exports[`Can fully expand emoji picker 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -85,14 +81,12 @@ exports[`Can lower hand 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -119,14 +113,12 @@ exports[`Can open menu 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -150,12 +142,12 @@ exports[`Can raise hand 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/src/button/index.ts b/src/button/index.ts index f1db314e59..0a910862e9 100644 --- a/src/button/index.ts +++ b/src/button/index.ts @@ -6,5 +6,6 @@ Please see LICENSE in the repository root for full details. */ export * from "./Button"; +export * from "./DeafenButton"; export * from "./LinkButton"; export * from "./ReactionToggleButton"; diff --git a/src/components/CallFooter.stories.tsx b/src/components/CallFooter.stories.tsx index a6b509fab2..a33e5b7600 100644 --- a/src/components/CallFooter.stories.tsx +++ b/src/components/CallFooter.stories.tsx @@ -86,6 +86,7 @@ export const Default: Story = { setLayoutMode: fn(), openSettings: fn(), toggleAudio: fn(), + toggleAudioOutput: fn(), toggleVideo: fn(), toggleScreenSharing: fn(), toggleBlur: fn(), @@ -102,11 +103,16 @@ export const Default: Story = { debugTileLayout: false, tileStoreGeneration: undefined, audioOptions: [], + audioOutputOptions: [], videoOptions: [], selectedAudio: undefined, + selectedAudioOutput: undefined, selectedVideo: undefined, selectAudioButtonOption: undefined, + selectAudioOutputButtonOption: undefined, selectVideoButtonOption: undefined, + audioOutputEnabled: true, + audioOutputBusy: false, }, parameters: { layout: "fullscreen", @@ -131,6 +137,7 @@ export const Default: Story = { setLayoutMode: fnArgType, openSettings: fnArgType, toggleAudio: fnArgType, + toggleAudioOutput: fnArgType, toggleVideo: fnArgType, hangup: fnArgType, }, diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index f952601dca..a92998628e 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -7,10 +7,7 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; -import { - SpotlightIcon, - GridIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Presentation, GridFour } from "@phosphor-icons/react"; import { Switch } from "@vector-im/compound-web"; import { t } from "i18next"; @@ -20,6 +17,7 @@ import { EndCallButton, MicButton, VideoButton, + DeafenButton, ShareScreenButton, SettingsButton, ReactionToggleButton, @@ -58,6 +56,7 @@ export type FooterSnapshot = FooterActions & FooterState; export interface FooterActions { /** Also controls if the audioMute button is disabled */ toggleAudio: (() => void) | undefined; + toggleAudioOutput: (() => void) | undefined; /** Also controls if the videoMute button is disabled */ toggleVideo: (() => void) | undefined; toggleBlur: (() => void) | undefined; @@ -73,6 +72,8 @@ export interface FooterActions { export interface FooterState { audioEnabled: boolean; audioBusy: boolean; + audioOutputEnabled: boolean; + audioOutputBusy: boolean; videoEnabled: boolean; videoBusy: boolean; videoBlurEnabled: boolean; @@ -103,11 +104,14 @@ export interface FooterState { /** Providing no options `[]` or `undefined` will imply that we dont have a audio fast switcher */ audioOptions: MenuOptions[]; + audioOutputOptions: MenuOptions[]; /** Providing no options `[]` or `undefined` will imply that we dont have a audio fast switcher */ videoOptions: MenuOptions[]; selectedAudio: string | undefined; + selectedAudioOutput: string | undefined; selectedVideo: string | undefined; selectAudioButtonOption: ((deviceId: string) => void) | undefined; + selectAudioOutputButtonOption: ((deviceId: string) => void) | undefined; selectVideoButtonOption: ((option: string) => void) | undefined; } @@ -125,9 +129,12 @@ export const CallFooter: FC = ({ ref, children, vm }) => { const openSettings = useBehavior(vm.openSettings$); const audioEnabled = useBehavior(vm.audioEnabled$); const audioBusy = useBehavior(vm.audioBusy$); + const audioOutputEnabled = useBehavior(vm.audioOutputEnabled$); + const audioOutputBusy = useBehavior(vm.audioOutputBusy$); const videoEnabled = useBehavior(vm.videoEnabled$); const videoBusy = useBehavior(vm.videoBusy$); const toggleAudio = useBehavior(vm.toggleAudio$); + const toggleAudioOutput = useBehavior(vm.toggleAudioOutput$); const toggleVideo = useBehavior(vm.toggleVideo$); const sharingScreen = useBehavior(vm.sharingScreen$); const toggleScreenSharing = useBehavior(vm.toggleScreenSharing$); @@ -140,8 +147,13 @@ export const CallFooter: FC = ({ ref, children, vm }) => { const videoOptions = useBehavior(vm.videoOptions$); const selectedVideo = useBehavior(vm.selectedVideo$); const audioOptions = useBehavior(vm.audioOptions$); + const audioOutputOptions = useBehavior(vm.audioOutputOptions$); const selectedAudio = useBehavior(vm.selectedAudio$); + const selectedAudioOutput = useBehavior(vm.selectedAudioOutput$); const selectAudioButtonOption = useBehavior(vm.selectAudioButtonOption$); + const selectAudioOutputButtonOption = useBehavior( + vm.selectAudioOutputButtonOption$, + ); const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$); const toggleBlur = useBehavior(vm.toggleBlur$); const videoBlurEnabled = useBehavior(vm.videoBlurEnabled$); @@ -193,36 +205,68 @@ export const CallFooter: FC = ({ ref, children, vm }) => { ); } - if ((videoOptions?.length ?? 0) > 0) { + if ((audioOutputOptions?.length ?? 0) > 0) { buttons.push( , ); } else { buttons.push( - , ); } + if (toggleVideo !== undefined) { + if ((videoOptions?.length ?? 0) > 0) { + buttons.push( + , + ); + } else { + buttons.push( + , + ); + } + } + if (toggleScreenSharing !== undefined) { buttons.push( = ({ ref, children, vm }) => { aria-label={t("layout_switch_label")} leftLabel={t("layout_spotlight_label")} leftValue="spotlight" - leftIcon={SpotlightIcon} + leftIcon={Presentation} rightLabel={t("layout_grid_label")} rightValue="grid" - rightIcon={GridIcon} + rightIcon={GridFour} className={styles.layout} value={layoutMode} onChange={setLayoutMode} diff --git a/src/components/CallFooterViewModel.tsx b/src/components/CallFooterViewModel.tsx index 2f64cc83b6..1d403367b0 100644 --- a/src/components/CallFooterViewModel.tsx +++ b/src/components/CallFooterViewModel.tsx @@ -14,7 +14,9 @@ import { type MediaDevices } from "../state/MediaDevices"; import { backgroundBlur as backgroundBlurSettings, debugTileLayout as debugTileLayoutSetting, + muteAllAudio as muteAllAudioSetting, } from "../settings/settings"; +import { muteAllAudio$ } from "../state/MuteAllAudioModel"; import { type Behavior, constant } from "../state/Behavior"; import type { ObservableScope } from "../state/ObservableScope"; import { type MuteStates } from "../state/MuteStates"; @@ -35,6 +37,9 @@ function buildMuteBehaviors( | "audioEnabled$" | "audioBusy$" | "toggleAudio$" + | "audioOutputEnabled$" + | "audioOutputBusy$" + | "toggleAudioOutput$" | "videoEnabled$" | "videoBusy$" | "toggleVideo$" @@ -45,6 +50,13 @@ function buildMuteBehaviors( toggleAudio$: scope.behavior( muteStates.audio.toggle$.pipe(map((t) => t ?? undefined)), ), + audioOutputEnabled$: scope.behavior( + muteAllAudio$.pipe(map((muted) => !muted)), + ), + audioOutputBusy$: constant(false), + toggleAudioOutput$: constant(() => + muteAllAudioSetting.setValue(!muteAllAudioSetting.getValue()), + ), videoEnabled$: muteStates.video.enabled$, videoBusy$: muteStates.video.syncing$, toggleVideo$: scope.behavior( @@ -67,6 +79,9 @@ function buildDeviceBehaviors( | "audioOptions$" | "selectedAudio$" | "selectAudioButtonOption$" + | "audioOutputOptions$" + | "selectedAudioOutput$" + | "selectAudioOutputButtonOption$" | "videoOptions$" | "selectedVideo$" | "selectVideoButtonOption$" @@ -94,6 +109,26 @@ function buildDeviceBehaviors( mediaDevices.audioInput.selected$.pipe(map((s) => s?.id)), ), selectAudioButtonOption$: constant(mediaDevices.audioInput.select), + audioOutputOptions$: scope.behavior( + disableSwitcher$.pipe( + switchMap((disable) => + disable + ? constant([] as MenuOptions[]) + : mediaDevices.audioOutput.available$.pipe( + map((available) => + [...available.entries()].map(([id, label]) => ({ + id, + label, + })), + ), + ), + ), + ), + ), + selectedAudioOutput$: scope.behavior( + mediaDevices.audioOutput.selected$.pipe(map((s) => s?.id)), + ), + selectAudioOutputButtonOption$: constant(mediaDevices.audioOutput.select), videoOptions$: scope.behavior( disableSwitcher$.pipe( switchMap((disable) => @@ -253,11 +288,14 @@ export function createLobbyFooterViewModel( debugTileLayout: false, showFooter: true, toggleAudio: undefined, + toggleAudioOutput: undefined, toggleVideo: undefined, setLayoutMode: undefined, toggleScreenSharing: undefined, audioEnabled: undefined, audioBusy: false, + audioOutputEnabled: undefined, + audioOutputBusy: false, videoEnabled: undefined, videoBusy: false, layoutMode: undefined, @@ -267,10 +305,13 @@ export function createLobbyFooterViewModel( reactionData: undefined, tileStoreGeneration: undefined, audioOptions: undefined, + audioOutputOptions: undefined, videoOptions: undefined, selectedAudio: undefined, + selectedAudioOutput: undefined, selectedVideo: undefined, selectAudioButtonOption: undefined, + selectAudioOutputButtonOption: undefined, selectVideoButtonOption: undefined, }), ...buildMuteBehaviors(scope, muteStates), diff --git a/src/components/MediaMuteAndSwitchButton.module.css b/src/components/MediaMuteAndSwitchButton.module.css index e5bba2383f..5bfca5be34 100644 --- a/src/components/MediaMuteAndSwitchButton.module.css +++ b/src/components/MediaMuteAndSwitchButton.module.css @@ -20,13 +20,41 @@ Please see LICENSE in the repository root for full details. color: var(--cpd-color-icon-on-solid-primary); } .menuButton { - width: 40px; + width: 24px !important; + padding: 0 !important; background-color: transparent !important; } .itemIcon { color: var(--cpd-color-text-secondary); } +.menuItem { + display: flex !important; + align-items: center !important; + padding: 8px 16px !important; + gap: 12px; +} + +.menuItem > :nth-child(2) { + flex-grow: 1; + text-align: left; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.scrollableArea { + max-height: 400px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; +} + .rotate { animation: spinner 1.5s linear infinite; } diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx index bd220330f3..47643c8919 100644 --- a/src/components/MediaMuteAndSwitchButton.tsx +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type ComponentType, useState, type FC, useEffect } from "react"; +import { useState, type FC, useEffect } from "react"; import { Button, Menu, @@ -13,23 +13,27 @@ import { ToggleMenuItem, } from "@vector-im/compound-web"; import { - CheckIcon, - ChevronUpIcon, - ChevronDownIcon, - MicOnIcon, - SpinnerIcon, - VideoCallIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; + Microphone, + Spinner, + VideoCamera, + Check, + CaretUp, + CaretDown, + Headphones, +} from "@phosphor-icons/react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import styles from "./MediaMuteAndSwitchButton.module.css"; -import { MicButton, VideoButton } from "../button"; -import { type DeviceLabel } from "../state/MediaDevices"; +import { MicButton, VideoButton, DeafenButton } from "../button"; +import { + type DeviceLabel, + type AudioOutputDeviceLabel, +} from "../state/MediaDevices"; import { useMediaDevices } from "../MediaDevicesContext"; export interface MenuOptions { - label: DeviceLabel; + label: DeviceLabel | AudioOutputDeviceLabel; id: string; } @@ -42,7 +46,7 @@ export interface MediaMuteAndSwitchButtonProps { onMuteClick?: () => void; /** True while mute/unmute operation is syncing. */ busy?: boolean; - iconsAndLabels: "video" | "audio"; + iconsAndLabels: "video" | "audio" | "audioOutput"; /** The options available for the media device selector modal */ options?: MenuOptions[]; /** The option that will currently be rendered as the selected option */ @@ -122,24 +126,44 @@ export const MediaMuteAndSwitchButton: FC = ({ /> ); break; + case "audioOutput": + button = ( + { + onMuteClick?.(); + e.preventDefault(); + e.stopPropagation(); + }} + disabled={isBusy || onMuteClick === undefined} + data-testid="incall_deafen" + /> + ); + break; } - let IconOptions: ComponentType> | undefined; + let IconOptions: React.ElementType; let optionsButtonLabel: string; let numberedLabel: (number: number) => string; switch (iconsAndLabels) { case "video": - IconOptions = VideoCallIcon; + IconOptions = VideoCamera; optionsButtonLabel = t("settings.devices.camera"); numberedLabel = (n): string => t("settings.devices.camera_numbered", { n }); break; case "audio": - IconOptions = MicOnIcon; + IconOptions = Microphone; optionsButtonLabel = t("settings.devices.microphone"); numberedLabel = (n): string => t("settings.devices.microphone_numbered", { n }); break; + case "audioOutput": + IconOptions = Headphones; + optionsButtonLabel = "Audio Output Options"; + numberedLabel = (n): string => `Speaker ${n}`; + break; } return ( @@ -164,63 +188,82 @@ export const MediaMuteAndSwitchButton: FC = ({ [styles.menuButton]: true, [styles.chevronIconOpen]: menuOpen, })} - Icon={menuOpen ? ChevronUpIcon : ChevronDownIcon} + Icon={menuOpen ? CaretUp : CaretDown} kind={"tertiary"} size="lg" aria-label={optionsButtonLabel} /> } > - {options?.map(({ label, id }) => { - let labelText: string; - switch (label.type) { - case "name": - labelText = label.name; - break; - case "number": - labelText = numberedLabel(label.number); - break; - } - return ( - - ) - } +

+ {options?.map(({ label, id }) => { + let labelText: string = ""; + switch (label.type) { + case "name": + labelText = label.name; + break; + case "number": + labelText = numberedLabel(label.number); + break; + case "speaker": + labelText = t("settings.devices.loudspeaker") ?? "Speaker"; + break; + case "earpiece": + labelText = t("settings.devices.handset") ?? "Earpiece"; + break; + case "default": + labelText = label.name + ? `${t("settings.devices.default", "Default")} (${label.name})` + : t("settings.devices.default", "Default"); + break; + } + return ( + + +
+ ) : undefined + } + onSelect={(e) => { + e.preventDefault(); + if (id === selectedOption) return; + setPlannedSelection(id); + onSelect?.(id); + }} + key={id} + > +
+ {selectedOption === id && } + {selectedOption !== id && plannedSelection === id && ( + + )} +
+ + ); + })} + {(toggles?.length ?? 0) > 0 &&
} + {toggles?.map((toggle) => ( + { + videoBlurToggleClick?.(); e.preventDefault(); - if (id === selectedOption) return; - setPlannedSelection(id); - onSelect?.(id); }} - key={id} - > - {selectedOption === id && } - {selectedOption !== id && plannedSelection === id && ( - - )} - - ); - })} - {(toggles?.length ?? 0) > 0 &&
} - {toggles?.map((toggle) => ( - { - videoBlurToggleClick?.(); - e.preventDefault(); - }} - checked={toggle.enabled ?? false} - key={toggle.id} - /> - ))} + checked={toggle.enabled ?? false} + key={toggle.id} + /> + ))} + ); diff --git a/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap index 8fe77ef11a..ce6a14d56e 100644 --- a/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap +++ b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap @@ -21,12 +21,12 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -48,12 +48,12 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index 44422587ef..f9f11fecd5 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -10,7 +10,7 @@ import { type RoomMember, type Room, type MatrixClient } from "matrix-js-sdk"; import { type FC, useCallback, type MouseEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconButton, Text } from "@vector-im/compound-web"; -import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { X } from "@phosphor-icons/react"; import classNames from "classnames"; import { Avatar, Size } from "../Avatar"; @@ -84,7 +84,7 @@ const CallTile: FC = ({ name, avatarUrl, room, client }) => { disabled={isLeaving} aria-label={t("action.remove")} > - + ); diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index f9b147076f..1f81e1db09 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -17,11 +17,7 @@ import { import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { Button, Menu, MenuItem } from "@vector-im/compound-web"; -import { - DeleteIcon, - EditIcon, - ShareIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Trash, PencilSimple, Upload } from "@phosphor-icons/react"; import { Avatar, Size } from "../Avatar"; import styles from "./AvatarInputField.module.css"; @@ -111,7 +107,7 @@ export const AvatarInputField: FC = ({ trigger={ @@ -343,24 +336,24 @@ exports[`InCallView > rendering > renders 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -392,14 +385,12 @@ exports[`InCallView > rendering > renders 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -416,12 +407,12 @@ exports[`InCallView > rendering > renders 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -441,12 +432,12 @@ exports[`InCallView > rendering > renders 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > rendering > renders 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > diff --git a/src/room/__snapshots__/LobbyView.test.tsx.snap b/src/room/__snapshots__/LobbyView.test.tsx.snap index ac89651b9f..b947ce8577 100644 --- a/src/room/__snapshots__/LobbyView.test.tsx.snap +++ b/src/room/__snapshots__/LobbyView.test.tsx.snap @@ -22,12 +22,12 @@ exports[`LobbyView > renders with AppBar android 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -117,12 +117,13 @@ exports[`LobbyView > renders with AppBar android 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -144,12 +145,13 @@ exports[`LobbyView > renders with AppBar android 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -169,24 +171,24 @@ exports[`LobbyView > renders with AppBar android 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -216,12 +218,12 @@ exports[`LobbyView > renders with AppBar android 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -253,12 +255,12 @@ exports[`LobbyView > renders with AppBar ios 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -348,12 +350,13 @@ exports[`LobbyView > renders with AppBar ios 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -375,12 +378,13 @@ exports[`LobbyView > renders with AppBar ios 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -400,24 +404,24 @@ exports[`LobbyView > renders with AppBar ios 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -447,12 +451,12 @@ exports[`LobbyView > renders with AppBar ios 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -505,12 +509,12 @@ exports[`LobbyView > renders with header and participant count 1`] = ` data-encrypted="false" fill="currentColor" height="16" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="16" xmlns="http://www.w3.org/2000/svg" > @@ -522,18 +526,12 @@ exports[`LobbyView > renders with header and participant count 1`] = ` aria-label="Participants" fill="currentColor" height="20" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="20" xmlns="http://www.w3.org/2000/svg" > - - renders with header and participant count 1`] = ` aria-hidden="true" fill="currentColor" height="1em" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="1em" xmlns="http://www.w3.org/2000/svg" > @@ -756,12 +755,13 @@ exports[`LobbyView > renders with header and participant count 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + style="transform: scale(0.75); transform-origin: center;" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -781,24 +781,24 @@ exports[`LobbyView > renders with header and participant count 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -828,12 +828,12 @@ exports[`LobbyView > renders with header and participant count 1`] = ` aria-hidden="true" fill="currentColor" height="24" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 2cd0d40b03..a494e9381e 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -28,11 +28,7 @@ import { import { logger } from "matrix-js-sdk/lib/logger"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { useTranslation } from "react-i18next"; -import { - AdminIcon, - CloseIcon, - EndCallIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Shield, X, PhoneDisconnect } from "@phosphor-icons/react"; import { widget } from "../widget"; @@ -136,7 +132,7 @@ export const useLoadGroupCall = ( const bannedError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( - AdminIcon, + Shield, t("group_call_loader.banned_heading"), t("group_call_loader.banned_body"), leaveReason(), @@ -146,7 +142,7 @@ export const useLoadGroupCall = ( const knockRejectError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( - CloseIcon, + X, t("group_call_loader.knock_reject_heading"), t("group_call_loader.knock_reject_body"), leaveReason(), @@ -156,7 +152,7 @@ export const useLoadGroupCall = ( const removeNoticeError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( - EndCallIcon, + PhoneDisconnect, t("group_call_loader.call_ended_heading"), t("group_call_loader.call_ended_body"), leaveReason(), diff --git a/src/state/MuteStates.ts b/src/state/MuteStates.ts index d89cb8442a..8050a46cb2 100644 --- a/src/state/MuteStates.ts +++ b/src/state/MuteStates.ts @@ -27,6 +27,8 @@ import { type MediaDevices, type MediaDevice } from "../state/MediaDevices"; import { ElementWidgetActions, widget } from "../widget"; import { type ObservableScope } from "./ObservableScope"; import { type Behavior, constant } from "./Behavior"; +import { muteAllAudio as muteAllAudioSetting } from "../settings/settings"; +import { muteAllAudio$ } from "./MuteAllAudioModel"; interface MuteStateData { enabled$: Observable; @@ -220,8 +222,12 @@ export class MuteStates { if (widget !== null) { // Sync our mute states with the hosting client const widgetApiState$ = combineLatest( - [this.audio.enabled$, this.video.enabled$], - (audio, video) => ({ audio_enabled: audio, video_enabled: video }), + [this.audio.enabled$, this.video.enabled$, muteAllAudio$], + (audio, video, muteAllAudio) => ({ + audio_enabled: audio, + video_enabled: video, + audio_output_enabled: !muteAllAudio, + }), ); widgetApiState$.pipe(this.scope.bind()).subscribe((state) => { widget!.api.transport @@ -266,6 +272,21 @@ export class MuteStates { newState.video_enabled = ev.detail.data.video_enabled; setVideoEnabled(newState.video_enabled); } + if ( + ev.detail.data.audio_output_enabled != null && + typeof ev.detail.data.audio_output_enabled === "boolean" + ) { + (newState as any).audio_output_enabled = + ev.detail.data.audio_output_enabled; + if ( + muteAllAudioSetting.getValue() === + ev.detail.data.audio_output_enabled + ) { + muteAllAudioSetting.setValue( + !ev.detail.data.audio_output_enabled, + ); + } + } widget!.api.transport.reply(ev.detail, newState); }); } diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 657bf0bc88..1544e41da1 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -19,17 +19,15 @@ import { type animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { - MicOnSolidIcon, - MicOffSolidIcon, - MicOffIcon, - OverflowHorizontalIcon, - VolumeOnIcon, - VolumeOffIcon, - VisibilityOnIcon, - UserProfileIcon, - VolumeOffSolidIcon, - SwitchCameraSolidIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; + User, + SpeakerSlash, + SpeakerHigh, + ArrowsClockwise, + Microphone, + MicrophoneSlash, + DotsThreeOutline, + Eye, +} from "@phosphor-icons/react"; import { ContextMenu, MenuItem, @@ -155,10 +153,10 @@ const UserMediaTile: FC = ({ }, [targetWidth, targetHeight, vm]); const AudioIcon = playbackMuted - ? VolumeOffSolidIcon + ? (props: any) => : audioEnabled - ? MicOnSolidIcon - : MicOffSolidIcon; + ? (props: any) => + : (props: any) => ; const audioIconLabel = playbackMuted ? t("video_tile.muted_for_me") : audioEnabled @@ -196,13 +194,22 @@ const UserMediaTile: FC = ({ [styles.handRaised]: !showSpeaking && handRaised, })} nameTagLeadingIcon={ - + playbackMuted ? ( + + ) : ( + + ) } displayName={displayName} mxcAvatarUrl={mxcAvatarUrl} @@ -218,7 +225,15 @@ const UserMediaTile: FC = ({ aria-label={t("common.options")} tabIndex={focusable ? undefined : -1} > - + } side="left" @@ -293,13 +308,13 @@ const LocalUserMediaTile: FC = ({ onClick={switchCamera} tabIndex={focusable ? undefined : -1} > - + ) } menuStart={ = ({ menuEnd={ onOpenProfile && ( @@ -347,7 +362,7 @@ const RemoteUserMediaTile: FC = ({ [vm], ); - const VolumeIcon = playbackMuted ? VolumeOffIcon : VolumeOnIcon; + const VolumeIcon = playbackMuted ? SpeakerSlash : SpeakerHigh; return ( = ({ menuStart={ <> = ({ isTriggerInteractive={false} nonInteractiveTriggerTabIndex={focusable ? undefined : -1} > - = ({ vm }) => { const Icon = pickupState === "ringing" ? vm.intent === "video" - ? VideoCallSolidIcon - : VoiceCallSolidIcon - : EndCallIcon; + ? VideoCamera + : Phone + : PhoneDisconnect; return ( <> - + {pickupState === "ringing" ? t("video_tile.calling") : t("video_tile.call_ended")} diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 7f6b446a37..f1e0427460 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -20,11 +20,8 @@ import { CollapseIcon, ChevronLeftIcon, ChevronRightIcon, - VolumeOffIcon, - VolumeOnIcon, - VolumeOffSolidIcon, - VolumeOnSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { SpeakerHigh, SpeakerSlash } from "@phosphor-icons/react"; import { animated } from "@react-spring/web"; import { type Observable, map } from "rxjs"; import { useObservableRef } from "observable-hooks"; @@ -323,11 +320,6 @@ const ScreenShareVolumeButton: FC = ({ vm }) => { const playbackMuted = useBehavior(vm.playbackMuted$); const playbackVolume = useBehavior(vm.playbackVolume$); - const VolumeIcon = playbackMuted ? VolumeOffIcon : VolumeOnIcon; - const VolumeSolidIcon = playbackMuted - ? VolumeOffSolidIcon - : VolumeOnSolidIcon; - const [volumeMenuOpen, setVolumeMenuOpen] = useState(false); const onMuteButtonClick = useCallback(() => vm.togglePlaybackMuted(), [vm]); const onVolumeChange = useCallback( @@ -349,7 +341,11 @@ const ScreenShareVolumeButton: FC = ({ vm }) => { className={styles.expand} aria-label={t("video_tile.screen_share_volume")} > - + {playbackMuted ? ( + + ) : ( + + )} } > @@ -361,7 +357,11 @@ const ScreenShareVolumeButton: FC = ({ vm }) => { hideChevron={true} >