From 78b506dd919882aaa549d50c7882bb342f35408f Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:48:19 +0000 Subject: [PATCH 1/8] Add screen share audio profile setting Introduce a ScreenShareAudioProfile enum with four quality levels (Music, MusicStereo, MusicHighQuality, MusicHighQualityStereo) and a persisted setting that defaults to MusicHighQuality. Stereo profiles implicitly enable stereo capture instead of requiring a separate toggle. --- src/settings/settings.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/settings/settings.ts b/src/settings/settings.ts index a674f1aae0..d4534803a2 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -145,3 +145,16 @@ export const customLivekitUrl = new Setting( "custom-livekit-url", null, ); + +export enum ScreenShareAudioProfile { + Music = "music", + MusicStereo = "musicStereo", + MusicHighQuality = "musicHighQuality", + MusicHighQualityStereo = "musicHighQualityStereo", +} + +export const screenShareAudioProfile = new Setting( + "screen-share-audio-profile", + ScreenShareAudioProfile.MusicHighQuality, +); + From c6f774e91a54d4515fc5de4dd132fb79b1d1f837 Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:48:53 +0000 Subject: [PATCH 2/8] Apply screen share audio profile when toggling screen share Read the user's screen share audio profile setting and pass it as publish options to setScreenShareEnabled. Disable audio processing (AGC, echo cancellation, noise suppression) for screen share audio, and derive stereo capture from the selected profile rather than a separate setting. --- .../CallViewModel/localMember/LocalMember.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index eb641ca7cd..08ade3dd75 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -12,6 +12,7 @@ import { type ScreenShareCaptureOptions, RoomEvent, MediaDeviceFailure, + AudioPresets, } from "livekit-client"; import { observeParticipantEvents } from "@livekit/components-core"; import { @@ -53,7 +54,11 @@ import { import { ElementWidgetActions, widget } from "../../../widget.ts"; import { getUrlParams } from "../../../UrlParams.ts"; import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts"; -import { MatrixRTCMode } from "../../../settings/settings.ts"; +import { + MatrixRTCMode, + screenShareAudioProfile, + ScreenShareAudioProfile, +} from "../../../settings/settings.ts"; import { Config } from "../../../config/Config.ts"; import { ConnectionState, @@ -637,12 +642,35 @@ export const createLocalMembership$ = ({ !getUrlParams().hideScreensharing ) { toggleScreenSharing = (): void => { + const profile = screenShareAudioProfile.getValue(); + const stereo = + profile === ScreenShareAudioProfile.MusicStereo || + profile === ScreenShareAudioProfile.MusicHighQualityStereo; + const screenshareSettings: ScreenShareCaptureOptions = { - audio: true, + audio: { + autoGainControl: false, + echoCancellation: false, + noiseSuppression: false, + channelCount: stereo ? 2 : 1, + }, selfBrowserSurface: "include", surfaceSwitching: "include", systemAudio: "include", }; + + const profileMap = { + [ScreenShareAudioProfile.Music]: AudioPresets.music, + [ScreenShareAudioProfile.MusicStereo]: AudioPresets.musicStereo, + [ScreenShareAudioProfile.MusicHighQuality]: + AudioPresets.musicHighQuality, + [ScreenShareAudioProfile.MusicHighQualityStereo]: + AudioPresets.musicHighQualityStereo, + }; + const audioPreset = + profileMap[screenShareAudioProfile.getValue()] ?? + AudioPresets.musicHighQuality; + const targetScreenshareState = !sharingScreen$.value; logger.info( `toggleScreenSharing called. Switching ${ @@ -658,7 +686,10 @@ export const createLocalMembership$ = ({ // is still initializing or publishing tracks, because there's no // technical reason to disallow this. LiveKit will publish if it can. participant$.value - ?.setScreenShareEnabled(targetScreenshareState, screenshareSettings) + ?.setScreenShareEnabled(targetScreenshareState, screenshareSettings, { + audioPreset, + forceStereo: stereo, + }) .catch(logger.error); }; } From 8b3a57b95bff33dad0c24bdbccf41da3e21027f2 Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:49:06 +0000 Subject: [PATCH 3/8] Add screen share settings tab to settings modal Add a dedicated Screen Share tab with radio buttons for selecting the audio quality profile (Music 48kbps, Music Stereo 64kbps, Music High Quality 96kbps, Music HQ Stereo 96kbps). Add corresponding translation keys. --- locales/en/app.json | 8 +++ src/settings/SettingsModal.tsx | 114 ++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 0b0ac7b4dc..fc5bcc6e06 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -232,6 +232,14 @@ "reactions_show_label": "Show reactions", "show_hand_raised_timer_description": "Show a timer when a participant raises their hand", "show_hand_raised_timer_label": "Show hand raise duration" + }, + "screen_share_tab_title": "Screen share", + "screen_share_tab": { + "audio_profile_heading": "Audio quality", + "audio_profile_music": "Music (48 kbps)", + "audio_profile_music_stereo": "Music Stereo (64 kbps)", + "audio_profile_music_high_quality": "Music High Quality (96 kbps)", + "audio_profile_music_high_quality_stereo": "Music HQ Stereo (96 kbps)" } }, "star_rating_input_label_one": "{{count}} star", diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 2b4078aa50..4a4b88d30d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,10 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type FC, type ReactNode, useEffect, useState } from "react"; +import { + type ChangeEvent, + type FC, + type ReactNode, + useCallback, + useEffect, + useId, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk"; -import { Button, Root as Form, Separator } from "@vector-im/compound-web"; +import { + Button, + InlineField, + Label, + RadioControl, + Root as Form, + Separator, +} from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; import { Modal } from "../Modal"; @@ -24,6 +39,8 @@ import { soundEffectVolume as soundEffectVolumeSetting, backgroundBlur as backgroundBlurSetting, developerMode, + screenShareAudioProfile as screenShareAudioProfileSetting, + ScreenShareAudioProfile, } from "./settings"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; @@ -38,6 +55,7 @@ import { useBehavior } from "../useBehavior"; type SettingsTab = | "audio" | "video" + | "screen_share" | "profile" | "preferences" | "feedback" @@ -107,6 +125,17 @@ export const SettingsModal: FC = ({ const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume); const [showDeveloperSettingsTab] = useSetting(developerMode); + const [screenShareAudioProfile, setScreenShareAudioProfile] = useSetting( + screenShareAudioProfileSetting, + ); + const screenShareAudioProfileRadioGroup = useId(); + const onScreenShareAudioProfileChange = useCallback( + (e: ChangeEvent) => { + setScreenShareAudioProfile(e.target.value as ScreenShareAudioProfile); + }, + [setScreenShareAudioProfile], + ); + const { available: isRageshakeAvailable } = useSubmitRageshake(); // For controlled devices, we will not show the input section: @@ -169,6 +198,85 @@ export const SettingsModal: FC = ({ ), }; + const screenShareTab: Tab = { + key: "screen_share", + name: t("settings.screen_share_tab_title"), + content: ( + <> +

{t("settings.screen_share_tab.audio_profile_heading")}

+
+ + } + > + + + + } + > + + + + } + > + + + + } + > + + +
+ + ), + }; + const videoTab: Tab = { key: "video", name: t("common.video"), @@ -217,7 +325,7 @@ export const SettingsModal: FC = ({ ), }; - const tabs = [audioTab, videoTab]; + const tabs = [audioTab, videoTab, screenShareTab]; if (widget === null) tabs.push(profileTab); tabs.push(preferencesTab); if (isRageshakeAvailable || import.meta.env.VITE_PACKAGE === "full") { From 4b83ce493bfbcc12f3815003f43c864fc263b68b Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:55:57 +0000 Subject: [PATCH 4/8] Add opt-in screen share viewing state to CallViewModel Screen shares no longer appear in spotlight automatically. A new BehaviorSubject tracks which screen share IDs the user has accepted. Local screen shares bypass opt-in and always appear in spotlight. Modifies spotlight$, pip$, and hasRemoteScreenShares$ to filter through accepted shares. Stale accepted IDs are cleaned up when screen shares end. Exposes screenShares$, acceptedScreenShareIds$, acceptScreenShare(), and dismissScreenShare() on the CallViewModel interface. Co-Authored-By: Claude Opus 4.6 --- src/state/CallViewModel/CallViewModel.ts | 87 +++++++++++++++++++++--- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index c19c4818dc..a66c2379d6 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -15,6 +15,7 @@ import { } from "livekit-client"; import { type Room as MatrixRoom } from "matrix-js-sdk"; import { + BehaviorSubject, catchError, combineLatest, distinctUntilChanged, @@ -371,6 +372,24 @@ export interface CallViewModel { * Shortcut for not requireing to parse and combine connectionState.matrix and connectionState.livekit */ connected$: Behavior; + + // Screen share opt-in + /** + * All active screen shares (for the preview panel). + */ + screenShares$: Behavior; + /** + * Set of screen share IDs the user has opted into viewing. + */ + acceptedScreenShareIds$: Behavior>; + /** + * Opt in to viewing a remote screen share. + */ + acceptScreenShare: (id: string) => void; + /** + * Stop viewing a remote screen share. + */ + dismissScreenShare: (id: string) => void; } /** @@ -819,6 +838,51 @@ export function createCallViewModel$( ), ); + // Screen share opt-in state + const acceptedScreenShareIds$ = new BehaviorSubject>( + new Set(), + ); + const acceptScreenShare = (id: string): void => { + const next = new Set(acceptedScreenShareIds$.value); + next.add(id); + acceptedScreenShareIds$.next(next); + }; + const dismissScreenShare = (id: string): void => { + const next = new Set(acceptedScreenShareIds$.value); + next.delete(id); + acceptedScreenShareIds$.next(next); + }; + + // Clean up accepted IDs when screen shares disappear + screenShares$ + .pipe( + map((shares) => new Set(shares.map((s) => s.id))), + scope.bind(), + ) + .subscribe((activeIds) => { + const accepted = acceptedScreenShareIds$.value; + let changed = false; + for (const id of accepted) { + if (!activeIds.has(id)) { + accepted.delete(id); + changed = true; + } + } + if (changed) acceptedScreenShareIds$.next(new Set(accepted)); + }); + + /** + * Screen shares the user has opted into viewing (plus local screen shares + * which always bypass opt-in). + */ + const acceptedScreenShares$ = scope.behavior( + combineLatest([screenShares$, acceptedScreenShareIds$]).pipe( + map(([shares, acceptedIds]) => + shares.filter((s) => s.local || acceptedIds.has(s.id)), + ), + ), + ); + const joinSoundEffect$ = userMedia$.pipe( pairwise(), filter( @@ -932,9 +996,9 @@ export function createCallViewModel$( ); const spotlight$ = scope.behavior( - screenShares$.pipe( - switchMap((screenShares) => { - if (screenShares.length > 0) return of(screenShares); + acceptedScreenShares$.pipe( + switchMap((acceptedScreenShares) => { + if (acceptedScreenShares.length > 0) return of(acceptedScreenShares); return spotlightSpeaker$.pipe( map((speaker) => (speaker ? [speaker] : [])), @@ -947,12 +1011,12 @@ export function createCallViewModel$( const pip$ = scope.behavior( combineLatest([ // TODO This also needs epoch logic to dedupe the screenshares and mediaItems emits - screenShares$, + acceptedScreenShares$, spotlightSpeaker$, mediaItems$, ]).pipe( - switchMap(([screenShares, spotlight, mediaItems]) => { - if (screenShares.length > 0) { + switchMap(([acceptedScreenShares, spotlight, mediaItems]) => { + if (acceptedScreenShares.length > 0) { return spotlightSpeaker$; } if (!spotlight || spotlight.local) { @@ -973,11 +1037,7 @@ export function createCallViewModel$( ); const hasRemoteScreenShares$ = scope.behavior( - spotlight$.pipe( - map((spotlight) => - spotlight.some((vm) => vm.type === "screen share" && !vm.local), - ), - ), + acceptedScreenShares$.pipe(map((shares) => shares.some((vm) => !vm.local))), ); const pipEnabled$ = scope.behavior(setPipEnabled$, false); @@ -1555,6 +1615,11 @@ export function createCallViewModel$( reconnecting$: localMembership.reconnecting$, livekitRoomItems$, connected$: localMembership.connected$, + + screenShares$, + acceptedScreenShareIds$, + acceptScreenShare, + dismissScreenShare, }; } From 74bc17aec95ef42060c43892a9dbd5fd91911a7e Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:56:28 +0000 Subject: [PATCH 5/8] Add screen share preview panel component New panel shows a grid of remote screen share tiles with live video previews (no audio) and sharer display names. Clicking a tile toggles opt-in. Accepted shares show a "Viewing" badge. Adds translation keys for the preview panel UI. Co-Authored-By: Claude Opus 4.6 --- locales/en/app.json | 4 + src/room/ScreenSharePreviewPanel.module.css | 70 ++++++++++++++++ src/room/ScreenSharePreviewPanel.tsx | 88 +++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/room/ScreenSharePreviewPanel.module.css create mode 100644 src/room/ScreenSharePreviewPanel.tsx diff --git a/locales/en/app.json b/locales/en/app.json index fc5bcc6e06..96459aee5f 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -193,6 +193,10 @@ "room_auth_view_continue_button": "Continue", "room_auth_view_ssla_caption": "By clicking \"Join call now\", you agree to our <2>Software and Services License Agreement (SSLA)", "screenshare_button_label": "Share screen", + "screenshare_preview_button_label": "Screen shares", + "screenshare_preview_panel_title": "Active screen shares", + "screenshare_stop_viewing": "Stop viewing", + "screenshare_tile_viewing": "Viewing", "settings": { "audio_tab": { "effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.", diff --git a/src/room/ScreenSharePreviewPanel.module.css b/src/room/ScreenSharePreviewPanel.module.css new file mode 100644 index 0000000000..5d6f8c0148 --- /dev/null +++ b/src/room/ScreenSharePreviewPanel.module.css @@ -0,0 +1,70 @@ +/* +Copyright 2026 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +.panel { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: var(--cpd-space-4x); + padding: var(--cpd-space-4x); +} + +.tile { + position: relative; + aspect-ratio: 16 / 9; + border-radius: var(--cpd-space-3x); + overflow: hidden; + cursor: pointer; + border: 2px solid transparent; + transition: + border-color 0.15s, + box-shadow 0.15s; + background: var(--cpd-color-bg-subtle-secondary); +} + +.tile:hover { + border-color: var(--cpd-color-border-interactive-hovered); +} + +.tile.accepted { + border-color: var(--cpd-color-border-interactive-primary); + box-shadow: 0 0 0 1px var(--cpd-color-border-interactive-primary); +} + +.tile video { + width: 100%; + height: 100%; + object-fit: contain; +} + +.tileOverlay { + position: absolute; + inset-block-end: 0; + inset-inline: 0; + display: flex; + align-items: center; + gap: var(--cpd-space-2x); + padding: var(--cpd-space-2x) var(--cpd-space-3x); + background: linear-gradient(transparent, rgba(0, 0, 0, 0.6)); + color: var(--cpd-color-text-on-solid-primary); +} + +.tileOverlay .badge { + display: inline-flex; + align-items: center; + gap: var(--cpd-space-1x); + padding: var(--cpd-space-1x) var(--cpd-space-2x); + border-radius: var(--cpd-radius-pill-effect); + background: var(--cpd-color-bg-action-primary-rest); + font-size: var(--cpd-font-size-body-xs); + color: var(--cpd-color-text-on-solid-primary); +} + +.empty { + padding: var(--cpd-space-8x); + text-align: center; + color: var(--cpd-color-text-secondary); +} diff --git a/src/room/ScreenSharePreviewPanel.tsx b/src/room/ScreenSharePreviewPanel.tsx new file mode 100644 index 0000000000..c35ea534b7 --- /dev/null +++ b/src/room/ScreenSharePreviewPanel.tsx @@ -0,0 +1,88 @@ +/* +Copyright 2026 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 FC } from "react"; +import { useTranslation } from "react-i18next"; +import classNames from "classnames"; +import { Text } from "@vector-im/compound-web"; +import { VideoTrack } from "@livekit/components-react"; + +import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel"; +import { useBehavior } from "../useBehavior"; +import styles from "./ScreenSharePreviewPanel.module.css"; + +interface PreviewTileProps { + vm: ScreenShareViewModel; + accepted: boolean; + onToggle: () => void; +} + +const PreviewTile: FC = ({ vm, accepted, onToggle }) => { + const { t } = useTranslation(); + const displayName = useBehavior(vm.displayName$); + const video = useBehavior(vm.video$); + + return ( + + ); +}; + +interface ScreenSharePreviewPanelProps { + screenShares: ScreenShareViewModel[]; + acceptedIds: Set; + onAccept: (id: string) => void; + onDismiss: (id: string) => void; +} + +export const ScreenSharePreviewPanel: FC = ({ + screenShares, + acceptedIds, + onAccept, + onDismiss, +}) => { + const remoteShares = screenShares.filter((s) => !s.local); + + return ( +
+ {remoteShares.map((vm) => { + const accepted = acceptedIds.has(vm.id); + return ( + { + if (accepted) { + onDismiss(vm.id); + } else { + onAccept(vm.id); + } + }} + /> + ); + })} +
+ ); +}; From 680be67719b89461f50ce5e612f13379e730618b Mon Sep 17 00:00:00 2001 From: Alison Jenkins <1176328+alisonjenkins@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:56:39 +0000 Subject: [PATCH 6/8] Add screen share view button and wire preview panel in toolbar Adds ScreenShareViewButton to the toolbar when remote screen shares are active. The button opens a modal containing the preview panel where users can opt in to individual screen shares. The button only renders when there are remote screen shares available. Co-Authored-By: Claude Opus 4.6 --- src/button/Button.tsx | 28 ++++++++++++++++++++++++++++ src/room/InCallView.tsx | 31 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 3136e2da26..e8e4035d27 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -17,6 +17,7 @@ import { EndCallIcon, ShareScreenSolidIcon, SettingsSolidIcon, + VisibilityOnIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import styles from "./Button.module.css"; @@ -130,3 +131,30 @@ export const SettingsButton: FC> = ( ); }; + +interface ScreenShareViewButtonProps extends ComponentPropsWithoutRef<"button"> { + count: number; + open: boolean; +} + +export const ScreenShareViewButton: FC = ({ + count, + open, + ...props +}) => { + const { t } = useTranslation(); + const label = t("screenshare_preview_button_label"); + + return ( + + + + ); +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 135745eb37..8bd960e3eb 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -40,6 +40,7 @@ import { ShareScreenButton, SettingsButton, ReactionToggleButton, + ScreenShareViewButton, } from "../button"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { type HeaderStyle, useUrlParams } from "../UrlParams"; @@ -107,6 +108,8 @@ import ringtoneOgg from "../sound/ringtone.ogg?url"; import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx"; import { type Layout } from "../state/layout-types.ts"; import { ObservableScope } from "../state/ObservableScope.ts"; +import { ScreenSharePreviewPanel } from "./ScreenSharePreviewPanel.tsx"; +import { Modal } from "../Modal.tsx"; const logger = rootLogger.getChild("[InCallView]"); @@ -272,6 +275,10 @@ export const InCallView: FC = ({ const earpieceMode = useBehavior(vm.earpieceMode$); const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$); const sharingScreen = useBehavior(vm.sharingScreen$); + const allScreenShares = useBehavior(vm.screenShares$); + const acceptedScreenShareIds = useBehavior(vm.acceptedScreenShareIds$); + const remoteScreenShareCount = allScreenShares.filter((s) => !s.local).length; + const [screenSharePanelOpen, setScreenSharePanelOpen] = useState(false); const ringOverlay = useBehavior(vm.ringOverlay$); const fatalCallError = useBehavior(vm.fatalError$); @@ -571,6 +578,7 @@ export const InCallView: FC = ({ vm={model} expanded={spotlightExpanded} onToggleExpanded={onToggleExpanded} + onDismissScreenShare={(id) => vm.dismissScreenShare(id)} targetWidth={targetWidth} targetHeight={targetHeight} showIndicators={showSpotlightIndicatorsValue} @@ -692,6 +700,17 @@ export const InCallView: FC = ({ />, ); } + if (remoteScreenShareCount > 0) { + buttons.push( + setScreenSharePanelOpen((prev) => !prev)} + onTouchEnd={onControlsTouchEnd} + />, + ); + } if (supportsReactions) { buttons.push( = ({ {footer} {layout.type !== "pip" && ( <> + setScreenSharePanelOpen(false)} + > + vm.acceptScreenShare(id)} + onDismiss={(id) => vm.dismissScreenShare(id)} + /> + Date: Fri, 27 Feb 2026 22:56:45 +0000 Subject: [PATCH 7/8] Add stop viewing button to spotlight for remote screen shares Adds a dismiss button on remote screen share items in the spotlight tile. Clicking it calls dismissScreenShare to remove the share from the accepted set, returning the user to the grid or next accepted share. Co-Authored-By: Claude Opus 4.6 --- src/tile/SpotlightTile.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 75c69479ae..44ac875a55 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -20,6 +20,7 @@ import { CollapseIcon, ChevronLeftIcon, ChevronRightIcon, + CloseIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { animated } from "@react-spring/web"; import { type Observable, map } from "rxjs"; @@ -229,6 +230,7 @@ interface Props { vm: SpotlightTileViewModel; expanded: boolean; onToggleExpanded: (() => void) | null; + onDismissScreenShare?: (id: string) => void; targetWidth: number; targetHeight: number; showIndicators: boolean; @@ -242,6 +244,7 @@ export const SpotlightTile: FC = ({ vm, expanded, onToggleExpanded, + onDismissScreenShare, targetWidth, targetHeight, showIndicators, @@ -366,6 +369,18 @@ export const SpotlightTile: FC = ({ ))}
+ {onDismissScreenShare && + media.find((m) => m.id === visibleId)?.type === "screen share" && + !media.find((m) => m.id === visibleId)?.local && ( + + )} + } + side="left" + align="start" + > + + {/* TODO: Figure out how to make this slider keyboard accessible */} + + + + + ); +}; + interface SpotlightItemProps { ref?: Ref; vm: MediaViewModel; @@ -332,6 +405,14 @@ export const SpotlightTile: FC = ({ const ToggleExpandIcon = expanded ? CollapseIcon : ExpandIcon; + const visibleVm = media.find((m) => m.id === visibleId); + const visibleRemoteScreenShare = + visibleVm && + visibleVm.type === "screen share" && + !visibleVm.local + ? visibleVm + : undefined; + return ( = ({ ))}
+ {visibleRemoteScreenShare && ( + + )} {onDismissScreenShare && media.find((m) => m.id === visibleId)?.type === "screen share" && !media.find((m) => m.id === visibleId)?.local && (