{!mobile && !hideHeader && (
-
-
+
+
)}
{buttons}
@@ -472,8 +482,11 @@ function findMatrixMember(
function useParticipantTiles(
livekitRoom: Room,
- matrixRoom: MatrixRoom
+ matrixRoom: MatrixRoom,
+ connState: ECConnectionState
): TileDescriptor
[] {
+ const previousTiles = useRef[]>([]);
+
const sfuParticipants = useParticipants({
room: livekitRoom,
});
@@ -555,5 +568,44 @@ function useParticipantTiles(
return allGhosts ? [] : tiles;
}, [matrixRoom, sfuParticipants]);
- return items;
+ // We carry over old tiles from the previous focus for some time after a focus switch
+ // so that the video tiles don't all disappear and reappear.
+ // This is set to true when the state transitions to Switching Focus and remains
+ // true for a short time after it changes (ie. connState is only switching focus for
+ // the time it takes us to reconnect to the conference).
+ // If there are still members that haven't reconnected after that time, they'll just
+ // appear to disconnect and will reappear once they reconnect.
+ const [isSwitchingFocus, setIsSwitchingFocus] = useState(false);
+
+ useEffect(() => {
+ if (connState === ECAddonConnectionState.ECSwitchingFocus) {
+ setIsSwitchingFocus(true);
+ } else if (isSwitchingFocus) {
+ setTimeout(() => {
+ setIsSwitchingFocus(false);
+ }, POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS);
+ }
+ }, [connState, setIsSwitchingFocus, isSwitchingFocus]);
+
+ if (
+ connState === ECAddonConnectionState.ECSwitchingFocus ||
+ isSwitchingFocus
+ ) {
+ logger.debug("Switching focus: injecting previous tiles");
+
+ // inject the previous tile for members that haven't rejoined yet
+ const newItems = items.slice(0);
+ const rejoined = new Set(newItems.map((p) => p.id));
+
+ for (const prevTile of previousTiles.current) {
+ if (!rejoined.has(prevTile.id)) {
+ newItems.push(prevTile);
+ }
+ }
+
+ return newItems;
+ } else {
+ previousTiles.current = items;
+ return items;
+ }
}
diff --git a/src/room/LayoutToggle.module.css b/src/room/LayoutToggle.module.css
index 08df7b17..e54e9447 100644
--- a/src/room/LayoutToggle.module.css
+++ b/src/room/LayoutToggle.module.css
@@ -34,7 +34,7 @@ limitations under the License.
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
background: var(--cpd-color-bg-action-secondary-rest);
- box-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
+ box-shadow: var(--small-drop-shadow);
}
@media (hover: hover) {
diff --git a/src/usePageFocusStyle.module.css b/src/usePageFocusStyle.module.css
deleted file mode 100644
index 7e92738a..00000000
--- a/src/usePageFocusStyle.module.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-Copyright 2022 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-.hideFocus * {
- outline: none !important;
-}
diff --git a/src/usePageFocusStyle.ts b/src/usePageFocusStyle.ts
deleted file mode 100644
index f433d3db..00000000
--- a/src/usePageFocusStyle.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-Copyright 2022 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { useEffect } from "react";
-import { useFocusVisible } from "@react-aria/interactions";
-
-import styles from "./usePageFocusStyle.module.css";
-
-export function usePageFocusStyle(): void {
- const { isFocusVisible } = useFocusVisible();
-
- useEffect(() => {
- const classList = document.body.classList;
- const hasClass = classList.contains(styles.hideFocus);
-
- if (isFocusVisible && hasClass) {
- classList.remove(styles.hideFocus);
- } else if (!isFocusVisible && !hasClass) {
- classList.add(styles.hideFocus);
- }
-
- return () => {
- classList.remove(styles.hideFocus);
- };
- }, [isFocusVisible]);
-}
diff --git a/src/video-grid/BigGrid.module.css b/src/video-grid/BigGrid.module.css
index cde593a5..2201295d 100644
--- a/src/video-grid/BigGrid.module.css
+++ b/src/video-grid/BigGrid.module.css
@@ -16,14 +16,13 @@ limitations under the License.
.bigGrid {
display: grid;
- grid-auto-rows: 163px;
- gap: 8px;
+ grid-auto-rows: 130px;
+ gap: var(--cpd-space-2x);
}
@media (min-width: 800px) {
.bigGrid {
- grid-auto-rows: 183px;
- column-gap: 18px;
- row-gap: 21px;
+ grid-auto-rows: 135px;
+ gap: var(--cpd-space-5x);
}
}
diff --git a/src/video-grid/BigGrid.tsx b/src/video-grid/BigGrid.tsx
index d8f7a859..b6a5b6dc 100644
--- a/src/video-grid/BigGrid.tsx
+++ b/src/video-grid/BigGrid.tsx
@@ -957,7 +957,7 @@ function updateTiles(g: Grid, tiles: TileDescriptor[]): Grid {
}
function updateBounds(g: Grid, bounds: RectReadOnly): Grid {
- const columns = Math.max(2, Math.floor(bounds.width * 0.0045));
+ const columns = Math.max(2, Math.floor(bounds.width * 0.0055));
return columns === g.columns ? g : resize(g, columns);
}
diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx
index 3363c573..26402507 100644
--- a/src/video-grid/NewVideoGrid.tsx
+++ b/src/video-grid/NewVideoGrid.tsx
@@ -178,7 +178,7 @@ export function NewVideoGrid({
from: ({ x, y, width, height }: Tile) => ({
opacity: 0,
scale: 0,
- shadow: 1,
+ shadow: 0,
shadowSpread: 0,
zIndex: 1,
x,
@@ -221,7 +221,7 @@ export function NewVideoGrid({
? {
scale: 1,
zIndex: 1,
- shadow: 1,
+ shadow: 0,
x: tile.x,
y: tile.y,
width: tile.width,
diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx
index a4dbe09f..8eb11a3d 100644
--- a/src/video-grid/VideoGrid.tsx
+++ b/src/video-grid/VideoGrid.tsx
@@ -1082,7 +1082,7 @@ export function VideoGrid({
y?: number;
width?: number;
height?: number;
- } = { shadow: 1, scale: 0, opacity: 0 };
+ } = { shadow: 0, scale: 0, opacity: 0 };
let reset = false;
if (!tilePositionsWereValid) {
@@ -1105,7 +1105,7 @@ export function VideoGrid({
scale: remove ? 0 : 1,
opacity: remove ? 0 : 1,
zIndex: tilePosition.zIndex,
- shadow: 1,
+ shadow: oneOnOneLayout && tile.item.local ? 1 : 0,
shadowSpread: oneOnOneLayout && tile.item.local ? 1 : 0,
from,
reset,
diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css
index d83c91bb..ca0278c9 100644
--- a/src/video-grid/VideoTile.module.css
+++ b/src/video-grid/VideoTile.module.css
@@ -20,14 +20,11 @@ limitations under the License.
top: 0;
container-name: videoTile;
container-type: size;
- --tileRadius: 8px;
- border-radius: var(--tileRadius);
+ border-radius: var(--cpd-space-4x);
overflow: hidden;
cursor: pointer;
-
- /* HACK: This has no visual effect due to the short duration, but allows the
- JS to detect movement via the transform property for audio spatialization */
- transition: transform 0.000000001s;
+ outline: 2px solid rgba(0, 0, 0, 0);
+ transition: outline-radius ease 0.15s, outline-color ease 0.15s;
}
.videoTile * {
@@ -45,21 +42,14 @@ limitations under the License.
transform: scaleX(-1);
}
-.videoTile::after {
- position: absolute;
- top: -1px;
- left: -1px;
- right: -1px;
- bottom: -1px;
- content: "";
- border-radius: var(--tileRadius);
- box-shadow: inset 0 0 0 4px var(--cpd-color-border-accent) !important;
- opacity: 0;
- transition: opacity ease 0.15s;
+.videoTile.speaking {
+ outline: 4px solid var(--cpd-color-border-accent);
}
-.videoTile.speaking::after {
- opacity: 1;
+@media (hover: hover) {
+ .videoTile:hover {
+ outline: 2px solid var(--cpd-color-gray-1400);
+ }
}
.videoTile.maximised {
@@ -73,30 +63,47 @@ limitations under the License.
object-fit: contain;
}
-.infoBubble {
+.nameTag {
position: absolute;
- height: 24px;
- padding: 0 8px;
+ inset-inline-start: var(--cpd-space-1x);
+ inset-block-end: var(--cpd-space-1x);
+ padding: var(--cpd-space-1x);
+ padding-block: var(--cpd-space-1x);
color: var(--cpd-color-text-primary);
- background-color: var(--stopgap-background-85);
+ /* TODO: un-hardcode this color. It comes from the dark theme. */
+ background-color: rgba(237, 244, 252, 0.79);
display: flex;
align-items: center;
- justify-content: center;
- border-radius: 4px;
+ border-radius: var(--cpd-radius-pill-effect);
user-select: none;
max-width: calc(100% - 48px);
overflow: hidden;
z-index: 1;
+ box-shadow: var(--small-drop-shadow);
}
-.infoBubble > svg {
- height: 16px;
- width: 16px;
- margin-right: 4px;
+:global(.cpd-theme-dark) .nameTag {
+ /* TODO: un-hardcode this color. It comes from the light theme. */
+ background-color: rgba(2, 7, 13, 0.77);
}
-.infoBubble > svg * {
- fill: var(--cpd-color-icon-primary);
+.nameTag > svg {
+ flex-shrink: 0;
+}
+
+.nameTag > svg[data-muted="true"] {
+ color: var(--cpd-color-icon-secondary);
+}
+
+.nameTag > svg[data-muted="false"] {
+ color: var(--cpd-color-icon-primary);
+}
+
+.nameTag span {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ padding-inline: var(--cpd-space-2x);
}
.toolbar {
@@ -137,24 +144,6 @@ limitations under the License.
height: 16px;
}
-.memberName {
- left: 16px;
- bottom: 16px;
-}
-
-.memberName > :last-child {
- margin-right: 0px;
-}
-
-.memberName span {
- font-size: var(--font-size-caption);
- font-weight: 400;
- line-height: var(--font-size-body);
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-
.videoMutedOverlay {
width: 100%;
height: 100%;
@@ -192,12 +181,6 @@ limitations under the License.
background-color: rgba(0, 0, 0, 0.5);
}
-@media (min-width: 800px) {
- .videoTile {
- --tileRadius: 20px;
- }
-}
-
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container videoTile (width > 0) {
diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx
index ceaf7e1e..04d1b297 100644
--- a/src/video-grid/VideoTile.tsx
+++ b/src/video-grid/VideoTile.tsx
@@ -28,11 +28,12 @@ import {
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk/src/models/room-member";
+import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
+import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
+import { Text } from "@vector-im/compound-web";
import { Avatar } from "../Avatar";
import styles from "./VideoTile.module.css";
-import { ReactComponent as MicIcon } from "../icons/Mic.svg";
-import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { useReactiveState } from "../useReactiveState";
import { AudioButton, FullscreenButton } from "../button/Button";
import { useModalTriggerState } from "../Modal";
@@ -102,12 +103,15 @@ export const VideoTile = forwardRef(
}
}, [member, setDisplayName]);
- const { isMuted: microphoneMuted } = useMediaTrack(
- content === TileContent.UserMedia
- ? Track.Source.Microphone
- : Track.Source.ScreenShareAudio,
- sfuParticipant
- );
+ const muted =
+ useMediaTrack(
+ content === TileContent.UserMedia
+ ? Track.Source.Microphone
+ : Track.Source.ScreenShareAudio,
+ sfuParticipant
+ ).isMuted !== false;
+
+ const MicIcon = muted ? MicOffSolidIcon : MicOnSolidIcon;
const onFullscreen = useCallback(() => {
onToggleFullscreen(data.id);
@@ -153,7 +157,6 @@ export const VideoTile = forwardRef(
sfuParticipant.isSpeaking &&
content === TileContent.UserMedia &&
showSpeakingIndicator,
- [styles.muted]: microphoneMuted,
[styles.screenshare]: content === TileContent.ScreenShare,
[styles.maximised]: maximised,
})}
@@ -182,9 +185,16 @@ export const VideoTile = forwardRef(
{t("{{displayName}} is presenting", { displayName })}
) : (
-
- {microphoneMuted === false ?
:
}
-
{displayName}
+
+
+
+ {sfuParticipant.isLocal ? t("You") : displayName}
+
{showConnectionStats && (
)}