First pass at the new video tile designs

Here, I've begun updating the styles of video tiles to match the new designs. Not yet updated: the local volume option is supposed to go inside an overflow menu now, but I haven't gotten to that yet.

To make the outlines on hovered / speaking tiles show up properly, I have to remove the usePageFocusStyle hack, which was preventing CSS outlines from being used for anything other than focus rings. I honestly can't tell what problem it was solving in the first place: focus rings still appear to behave as expected throughout the application.
This commit is contained in:
Robin
2023-09-13 16:19:29 -04:00
parent 1f95ec7a0c
commit 915fb63356
13 changed files with 84 additions and 149 deletions

View File

@@ -116,6 +116,7 @@
"Walkie-talkie call": "Walkie-talkie call",
"Walkie-talkie call name": "Walkie-talkie call name",
"Yes, join call": "Yes, join call",
"You": "You",
"You were disconnected from the call": "You were disconnected from the call",
"Your feedback": "Your feedback",
"Your recent calls": "Your recent calls"

View File

@@ -25,7 +25,6 @@ import { LoginPage } from "./auth/LoginPage";
import { RegisterPage } from "./auth/RegisterPage";
import { RoomPage } from "./room/RoomPage";
import { ClientProvider } from "./ClientContext";
import { usePageFocusStyle } from "./usePageFocusStyle";
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
import { InspectorContextProvider } from "./room/GroupCallInspector";
import { CrashView, LoadingView } from "./FullScreenView";
@@ -48,8 +47,6 @@ export default function App({ history }: AppProps) {
});
});
usePageFocusStyle();
const errorPage = <CrashView />;
return (

View File

@@ -50,14 +50,14 @@ limitations under the License.
background-color: var(--cpd-color-text-action-accent);
}
.button:focus,
.toolbarButton:focus,
.toolbarButtonSecondary:focus,
.iconButton:focus,
.iconCopyButton:focus,
.secondary:focus,
.secondaryHangup:focus,
.copyButton:focus {
.button:focus-visible,
.toolbarButton:focus-visible,
.toolbarButtonSecondary:focus-visible,
.iconButton:focus-visible,
.iconCopyButton:focus-visible,
.secondary:focus-visible,
.secondaryHangup:focus-visible,
.copyButton:focus-visible {
outline: auto;
}
@@ -74,7 +74,7 @@ limitations under the License.
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-icon-primary);
border: 1px solid var(--cpd-color-gray-400);
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
box-shadow: var(--subtle-drop-shadow);
}
.toolbarButton.on,

View File

@@ -39,19 +39,22 @@ limitations under the License.
--font-size-title: calc(24px * var(--font-scale));
--font-size-headline: calc(32px * var(--font-scale));
--cpd-color-border-accent: var(--cpd-color-green-1100);
/* These colors are needed during the transitionary period between the old and
new Compound design systems, but should be removed ASAP */
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
--stopgap-background-85: rgba(255, 255, 255, 0.85);
--stopgap-bgColor3: #444;
--cpd-color-border-accent: var(--cpd-color-green-800);
/* The distance to inset non-full-width content from the edge of the window
along the inline axis */
--inline-content-inset: max(var(--cpd-space-4x), calc((100vw - 1180px) / 2));
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
--subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
}
.cpd-theme-dark {
--cpd-color-border-accent: var(--cpd-color-green-1100);
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
--stopgap-background-85: rgba(16, 19, 23, 0.85);
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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]);
}

View File

@@ -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);
}
}

View File

@@ -957,7 +957,7 @@ function updateTiles(g: Grid, tiles: TileDescriptor<unknown>[]): 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);
}

View File

@@ -178,7 +178,7 @@ export function NewVideoGrid<T>({
from: ({ x, y, width, height }: Tile<T>) => ({
opacity: 0,
scale: 0,
shadow: 1,
shadow: 0,
shadowSpread: 0,
zIndex: 1,
x,
@@ -221,7 +221,7 @@ export function NewVideoGrid<T>({
? {
scale: 1,
zIndex: 1,
shadow: 1,
shadow: 0,
x: tile.x,
y: tile.y,
width: tile.width,

View File

@@ -1082,7 +1082,7 @@ export function VideoGrid<T>({
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<T>({
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,

View File

@@ -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) {

View File

@@ -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<HTMLDivElement, Props>(
}
}, [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<HTMLDivElement, Props>(
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<HTMLDivElement, Props>(
<span>{t("{{displayName}} is presenting", { displayName })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{microphoneMuted === false ? <MicIcon /> : <MicMutedIcon />}
<span title={displayName}>{displayName}</span>
<div className={styles.nameTag}>
<MicIcon
width={20}
height={20}
aria-label={muted ? t("Microphone off") : t("Microphone on")}
data-muted={muted}
/>
<Text as="span" size="sm" weight="medium">
{sfuParticipant.isLocal ? t("You") : displayName}
</Text>
{showConnectionStats && (
<ConnectionQualityIndicator participant={sfuParticipant} />
)}