From c8f5be7e4f9884033393593b1c4538ed751311a4 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 24 Feb 2026 11:54:41 +0100 Subject: [PATCH 01/21] remove deprecated `analyticsID` url param --- docs/url-params.md | 52 +++++++++++++++++++++++----------------------- src/UrlParams.ts | 3 +-- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/docs/url-params.md b/docs/url-params.md index a474daed..e24e9823 100644 --- a/docs/url-params.md +++ b/docs/url-params.md @@ -46,32 +46,32 @@ possible to support encryption. These parameters are relevant to both [widget](./embedded-standalone.md) and [standalone](./embedded-standalone.md) modes: -| Name | Values | Required for widget | Required for SPA | Description | -| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. | -| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. | -| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. | -| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. | -| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. | -| `displayName` | | No | No | Display name used for auto-registration. | -| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. | -| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. | -| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. | -| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. | -| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. | -| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. | -| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. | -| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) | -| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. | -| `controlledAudioDevices` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the [global JS controls for audio devices](./controls.md#audio-devices) should be enabled, allowing the list of audio devices to be controlled by the app hosting Element Call. | -| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. | -| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. | -| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) | -| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | -| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. | -| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. | -| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. | -| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. | +| Name | Values | Required for widget | Required for SPA | Description | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. | +| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. | +| `posthogUserId` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. | +| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. | +| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. | +| `displayName` | | No | No | Display name used for auto-registration. | +| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. | +| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. | +| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. | +| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. | +| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. | +| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. | +| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. | +| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) | +| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. | +| `controlledAudioDevices` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the [global JS controls for audio devices](./controls.md#audio-devices) should be enabled, allowing the list of audio devices to be controlled by the app hosting Element Call. | +| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. | +| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. | +| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) | +| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | +| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. | +| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. | +| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. | +| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. | ### Widget-only parameters diff --git a/src/UrlParams.ts b/src/UrlParams.ts index f8ee22fb..31101197 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -473,8 +473,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { homeserver: !isWidget ? parser.getParam("homeserver") : null, posthogApiHost: parser.getParam("posthogApiHost"), posthogApiKey: parser.getParam("posthogApiKey"), - posthogUserId: - parser.getParam("posthogUserId") ?? parser.getParam("analyticsID"), + posthogUserId: parser.getParam("posthogUserId"), rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"), sentryDsn: parser.getParam("sentryDsn"), sentryEnvironment: parser.getParam("sentryEnvironment"), From 8e7e0992949f2a3812fc38bd691be2bba2a163b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 01:11:01 +0000 Subject: [PATCH 02/21] Update docker/login-action digest to c94ce9f --- .github/workflows/build-and-publish-docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-docker.yaml b/.github/workflows/build-and-publish-docker.yaml index 68f7131c..f6eeb7c2 100644 --- a/.github/workflows/build-and-publish-docker.yaml +++ b/.github/workflows/build-and-publish-docker.yaml @@ -69,7 +69,7 @@ jobs: services/-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to oci.element.io Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 if: github.event_name != 'pull_request' with: registry: oci-push.vpn.infra.element.io From 6e0f48e99b260ece266b84abb79e020854c3f666 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 2 Mar 2026 15:04:18 +0100 Subject: [PATCH 03/21] fix: add id-token permission as its required by tailscale login (#3768) * Push docker images to oci.element.io * prettier * add id-token permission as its required by tailscale login --- .github/workflows/build-and-publish-docker.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish-docker.yaml b/.github/workflows/build-and-publish-docker.yaml index f6eeb7c2..6e8f01f5 100644 --- a/.github/workflows/build-and-publish-docker.yaml +++ b/.github/workflows/build-and-publish-docker.yaml @@ -20,7 +20,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write # required to upload release asset - packages: write + packages: write # needed for publishing packages to GHCR + id-token: write # needed for login into tailscale with GitHub OIDC Token steps: - name: Check it out uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 From 00f880108e29e629b0c4aae835b06fd55f96eb19 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 2 Mar 2026 17:03:01 +0100 Subject: [PATCH 04/21] fix: add id-token permission as its required by tailscale login (part 2) (#3770) * Push docker images to oci.element.io * prettier * add id-token permission as its required by tailscale login --- .github/workflows/build.yaml | 1 + .github/workflows/pr-deploy.yaml | 1 + .github/workflows/publish.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9b86215e..4f9e80f2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -49,6 +49,7 @@ jobs: permissions: contents: write packages: write + id-token: write uses: ./.github/workflows/build-and-publish-docker.yaml with: artifact_run_id: ${{ github.run_id }} diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index fe934162..62b37aca 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -60,6 +60,7 @@ jobs: permissions: contents: write packages: write + id-token: write uses: ./.github/workflows/build-and-publish-docker.yaml with: artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 7f2c58fe..ade91019 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -55,6 +55,7 @@ jobs: permissions: contents: write packages: write + id-token: write uses: ./.github/workflows/build-and-publish-docker.yaml with: artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }} From 2cf4a38c1743ad2895c5c4ebfba8e1b7d474c76e Mon Sep 17 00:00:00 2001 From: Jake Janicke Date: Tue, 3 Mar 2026 16:47:41 -0600 Subject: [PATCH 05/21] Add volume logic to RemoteScreenShareViewModel Signed-off-by: Jake Janicke --- src/state/media/RemoteScreenShareViewModel.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/state/media/RemoteScreenShareViewModel.ts b/src/state/media/RemoteScreenShareViewModel.ts index eff6d9c1..78fc0494 100644 --- a/src/state/media/RemoteScreenShareViewModel.ts +++ b/src/state/media/RemoteScreenShareViewModel.ts @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type RemoteParticipant } from "livekit-client"; -import { map } from "rxjs"; +import { Track, type RemoteParticipant } from "livekit-client"; +import { map, of, switchMap } from "rxjs"; import { type Behavior } from "../Behavior"; import { @@ -16,13 +16,17 @@ import { createBaseScreenShare, } from "./ScreenShareViewModel"; import { type ObservableScope } from "../ObservableScope"; +import { createVolumeControls, type VolumeControls } from "../VolumeControls"; +import { observeTrackReference$ } from "../observeTrackReference"; -export interface RemoteScreenShareViewModel extends BaseScreenShareViewModel { +export interface RemoteScreenShareViewModel + extends BaseScreenShareViewModel, VolumeControls { local: false; /** * Whether this screen share's video should be displayed. */ videoEnabled$: Behavior; + audioEnabled$: Behavior; } export interface RemoteScreenShareInputs extends BaseScreenShareInputs { @@ -36,9 +40,30 @@ export function createRemoteScreenShare( ): RemoteScreenShareViewModel { return { ...createBaseScreenShare(scope, inputs), + ...createVolumeControls(scope, { + pretendToBeDisconnected$, + sink$: scope.behavior( + inputs.participant$.pipe( + map( + (p) => (volume) => + p?.setVolume(volume, Track.Source.ScreenShareAudio), + ), + ), + ), + }), local: false, videoEnabled$: scope.behavior( pretendToBeDisconnected$.pipe(map((disconnected) => !disconnected)), ), + audioEnabled$: scope.behavior( + inputs.participant$.pipe( + switchMap((p) => + p + ? observeTrackReference$(p, Track.Source.ScreenShareAudio) + : of(null), + ), + map(Boolean), + ), + ), }; } From 72520dbb3f8f9a08701c207d79047e38160f4e4a Mon Sep 17 00:00:00 2001 From: Jake Janicke Date: Wed, 4 Mar 2026 01:41:16 -0600 Subject: [PATCH 06/21] Add screen share volume slider UI Signed-off-by: Jake Janicke --- locales/en/app.json | 1 + src/tile/SpotlightTile.module.css | 26 +++++++++ src/tile/SpotlightTile.tsx | 94 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/locales/en/app.json b/locales/en/app.json index 0b0ac7b4..6ec4859e 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -256,6 +256,7 @@ "mute_for_me": "Mute for me", "muted_for_me": "Muted for me", "volume": "Volume", + "screen_share_volume": "Screen share volume", "waiting_for_media": "Waiting for media..." } } diff --git a/src/tile/SpotlightTile.module.css b/src/tile/SpotlightTile.module.css index 622496d2..43c3d53c 100644 --- a/src/tile/SpotlightTile.module.css +++ b/src/tile/SpotlightTile.module.css @@ -108,6 +108,32 @@ Please see LICENSE in the repository root for full details. z-index: 1; } +.volumeSlider { + width: 100%; +} + +/* Disable the hover effect for the screen share volume menu button */ +.volumeMenuItem:hover { + background: transparent; + cursor: default; +} + +.volumeMenuItem { + gap: var(--cpd-space-3x); +} + +.menuMuteButton { + background: none; + border: none; + cursor: pointer; + display: flex; +} + +/* Make icons change color with the theme */ +.menuMuteButton > svg { + color: var(--cpd-color-icon-primary); +} + .expand > svg { display: block; color: var(--cpd-color-icon-primary); diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 75c69479..93a1fbd9 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -20,6 +20,8 @@ import { CollapseIcon, ChevronLeftIcon, ChevronRightIcon, + VolumeOffIcon, + VolumeOnIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { animated } from "@react-spring/web"; import { type Observable, map } from "rxjs"; @@ -27,6 +29,7 @@ import { useObservableRef } from "observable-hooks"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { type TrackReferenceOrPlaceholder } from "@livekit/components-core"; +import { Menu, MenuItem } from "@vector-im/compound-web"; import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react"; import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react"; @@ -45,6 +48,8 @@ import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel"; import { type RemoteScreenShareViewModel } from "../state/media/RemoteScreenShareViewModel"; import { type MediaViewModel } from "../state/media/MediaViewModel"; +import { Slider } from "../Slider"; +import { constant } from "../state/Behavior"; interface SpotlightItemBaseProps { ref?: Ref; @@ -260,6 +265,33 @@ export const SpotlightTile: FC = ({ const visibleIndex = media.findIndex((vm) => vm.id === visibleId); const canGoBack = visibleIndex > 0; const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1; + const currentMedia = media[visibleIndex]; + // isScreenShare only needs to check "audioEnabled$" but I wanted to be more specific + // just in case more models are added in the future, since screen shares always have video + const isScreenShare = + currentMedia && + "audioEnabled$" in currentMedia && + "videoEnabled$" in currentMedia; + + const hasAudio$ = useBehavior( + isScreenShare && currentMedia?.audioEnabled$ + ? currentMedia.audioEnabled$ + : constant(false), + ); + const isLocalScreenShare = isScreenShare && currentMedia.local; + const screenShareLocallyMuted = useBehavior( + isScreenShare + ? (currentMedia as RemoteScreenShareViewModel).playbackMuted$ + : constant(false), + ); + const ScreenShareVolumeIcon = screenShareLocallyMuted + ? VolumeOffIcon + : VolumeOnIcon; + const screenShareVolume = useBehavior( + isScreenShare + ? (currentMedia as RemoteScreenShareViewModel).playbackVolume$ + : constant(0), + ); const isFullscreen = useCallback((): boolean => { const rootElement = document.body; @@ -328,6 +360,7 @@ export const SpotlightTile: FC = ({ }, [latestVisibleId, latestMedia, setScrollToId]); const ToggleExpandIcon = expanded ? CollapseIcon : ExpandIcon; + const [openVolumeMenu, setOpenVolumeMenu] = useState(false); return ( = ({ ))}
+ {/* + Show volume slider only when the tile is a screenshare, has audio, + is in spotlight mode, and isn't your own screen share. + */} + {isScreenShare && + hasAudio$ && + onToggleExpanded && + !isLocalScreenShare && ( + + + + } + > + + + + ( + currentMedia as RemoteScreenShareViewModel + ).adjustPlaybackVolume(v) + } + onValueCommit={() => + ( + currentMedia as RemoteScreenShareViewModel + ).commitPlaybackVolume() + } + /> + + + )} + } + > + + + + + + ) + ); +}; + interface Props { ref?: Ref; vm: SpotlightTileViewModel; @@ -263,37 +332,9 @@ export const SpotlightTile: FC = ({ const latestMedia = useLatest(media); const latestVisibleId = useLatest(visibleId); const visibleIndex = media.findIndex((vm) => vm.id === visibleId); + const visibleMedia = media.at(visibleIndex); const canGoBack = visibleIndex > 0; const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1; - const currentMedia = media[visibleIndex]; - // only "audioEnabled$" needs to be checked but I wanted to be more specific just in - // case more models are added in the future, since screen shares always have video - const currentScreenShare = - currentMedia && - "audioEnabled$" in currentMedia && - "videoEnabled$" in currentMedia - ? (currentMedia as RemoteScreenShareViewModel) - : null; - - const isScreenShare = currentScreenShare != null; - - const hasAudio$ = useBehavior( - currentScreenShare?.audioEnabled$ ?? constant(false), - ); - - const isLocalScreenShare = currentScreenShare?.local ?? false; - - const screenShareLocallyMuted = useBehavior( - currentScreenShare?.playbackMuted$ ?? constant(false), - ); - - const ScreenShareVolumeIcon = screenShareLocallyMuted - ? VolumeOffIcon - : VolumeOnIcon; - - const screenShareVolume = useBehavior( - currentScreenShare?.playbackVolume$ ?? constant(0), - ); const isFullscreen = useCallback((): boolean => { const rootElement = document.body; @@ -362,7 +403,6 @@ export const SpotlightTile: FC = ({ }, [latestVisibleId, latestMedia, setScrollToId]); const ToggleExpandIcon = expanded ? CollapseIcon : ExpandIcon; - const [openVolumeMenu, setOpenVolumeMenu] = useState(false); return ( = ({ /> ))}
-
- {/* - Show volume slider only when the tile is a screenshare, has audio, - is in spotlight mode, and isn't your own screen share. - */} - {isScreenShare && - hasAudio$ && - onToggleExpanded && - !isLocalScreenShare && ( - - - - } - > - - - - ( - currentMedia as RemoteScreenShareViewModel - ).adjustPlaybackVolume(v) - } - onValueCommit={() => - ( - currentMedia as RemoteScreenShareViewModel - ).commitPlaybackVolume() - } - /> - - - )} - +
+ {visibleMedia?.type === "screen share" && !visibleMedia.local && ( + + )} + {platform === "desktop" && ( + + )} {onToggleExpanded && (
@@ -760,12 +726,13 @@ export const InCallView: FC = ({ const allConnections = useBehavior(vm.allConnections$); return ( + // The onClick handler here exists to control the visibility of the footer, + // and the footer is also viewable by moving focus into it, so this is fine. + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx index 6cddc95f..ca6aa467 100644 --- a/src/room/LayoutToggle.tsx +++ b/src/room/LayoutToggle.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 ChangeEvent, type FC, type TouchEvent, useCallback } from "react"; +import { type ChangeEvent, type FC, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Tooltip } from "@vector-im/compound-web"; import { @@ -22,15 +22,9 @@ interface Props { layout: Layout; setLayout: (layout: Layout) => void; className?: string; - onTouchEnd?: (e: TouchEvent) => void; } -export const LayoutToggle: FC = ({ - layout, - setLayout, - className, - onTouchEnd, -}) => { +export const LayoutToggle: FC = ({ layout, setLayout, className }) => { const { t } = useTranslation(); const onChange = useCallback( @@ -47,7 +41,6 @@ export const LayoutToggle: FC = ({ value="spotlight" checked={layout === "spotlight"} onChange={onChange} - onTouchEnd={onTouchEnd} /> @@ -58,7 +51,6 @@ export const LayoutToggle: FC = ({ value="grid" checked={layout === "grid"} onChange={onChange} - onTouchEnd={onTouchEnd} /> diff --git a/src/tile/SpotlightTile.module.css b/src/tile/SpotlightTile.module.css index 178d13f3..fe13f906 100644 --- a/src/tile/SpotlightTile.module.css +++ b/src/tile/SpotlightTile.module.css @@ -84,7 +84,6 @@ Please see LICENSE in the repository root for full details. .expand { appearance: none; cursor: pointer; - opacity: 0; padding: var(--cpd-space-2x); border: none; border-radius: var(--cpd-radius-pill-effect); @@ -148,17 +147,21 @@ Please see LICENSE in the repository root for full details. } } -.expand:active { +.expand:active, .expand[data-state="open"] { background: var(--cpd-color-gray-100); } @media (hover) { + .tile > div > button { + opacity: 0; + } .tile:hover > div > button { opacity: 1; } } -.tile:has(:focus-visible) > div > button { +.tile:has(:focus-visible) > div > button, +.tile > div:has([data-state="open"]) > button { opacity: 1; } From 885a523e9189aa41bb83045f36e7fe9b0bdafe9b Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 9 Mar 2026 10:44:22 +0100 Subject: [PATCH 20/21] Fix formatting --- src/tile/SpotlightTile.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tile/SpotlightTile.module.css b/src/tile/SpotlightTile.module.css index fe13f906..af0e0add 100644 --- a/src/tile/SpotlightTile.module.css +++ b/src/tile/SpotlightTile.module.css @@ -147,7 +147,8 @@ Please see LICENSE in the repository root for full details. } } -.expand:active, .expand[data-state="open"] { +.expand:active, +.expand[data-state="open"] { background: var(--cpd-color-gray-100); } From 8f418ce9fd5c1d2090b242adf7801368b11231bb Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 9 Mar 2026 13:59:39 +0100 Subject: [PATCH 21/21] fix cla link --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e68c0f42..787ddc73 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,4 +36,4 @@ Uncomment this markdown table below and edit the last line `|||`: - [ ] Pull request includes screenshots or videos if containing UI changes - [ ] Tests written for new code (and old code if feasible). - [ ] Linter and other CI checks pass. -- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) +- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-call)