mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-09 18:29:21 +00:00
Move ringing status indicator to header on mobile
On mobile, the ringing status indicator is supposed to display in the header rather than on a tile. The exact layout differs between Android and iOS. To get it right I had to refactor AppBar to use CSS grid templates. (Also, I changed my mind about the exact ringing data I needed out of CallViewModel - sorry. A little move of the ringtone audio renderer into its own component was necessary to accommodate that.)
This commit is contained in:
@@ -29,15 +29,13 @@ import {
|
||||
UserProfileIcon,
|
||||
VolumeOffSolidIcon,
|
||||
SwitchCameraSolidIcon,
|
||||
VideoCallSolidIcon,
|
||||
VoiceCallSolidIcon,
|
||||
EndCallIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
ContextMenu,
|
||||
MenuItem,
|
||||
ToggleMenuItem,
|
||||
Menu,
|
||||
Text,
|
||||
} from "@vector-im/compound-web";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
|
||||
@@ -53,6 +51,7 @@ import { type LocalUserMediaViewModel } from "../state/media/LocalUserMediaViewM
|
||||
import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaViewModel";
|
||||
import { type UserMediaViewModel } from "../state/media/UserMediaViewModel";
|
||||
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||
import { RingingStatus } from "./RingingStatus";
|
||||
|
||||
interface TileProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
@@ -75,9 +74,6 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const pickupState = useBehavior(vm.pickupState$);
|
||||
|
||||
return (
|
||||
<MediaView
|
||||
className={classNames(className, styles.tile)}
|
||||
@@ -85,13 +81,9 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
||||
userId={vm.userId}
|
||||
unencryptedWarning={false}
|
||||
status={
|
||||
pickupState === "ringing"
|
||||
? {
|
||||
text: t("video_tile.calling"),
|
||||
Icon:
|
||||
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
|
||||
}
|
||||
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
|
||||
<Text as="span" size="sm" weight="medium">
|
||||
<RingingStatus vm={vm} />
|
||||
</Text>
|
||||
}
|
||||
videoEnabled={false}
|
||||
videoFit="cover"
|
||||
|
||||
@@ -121,18 +121,22 @@ unconditionally select the container so we can use cqmin units */
|
||||
.status {
|
||||
grid-area: status;
|
||||
color: var(--cpd-color-text-primary);
|
||||
display: flex;
|
||||
flex-wrap: none;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
margin-block-start: calc(var(--cpd-space-3x) - var(--fg-inset));
|
||||
margin-inline-start: calc(var(--cpd-space-4x) - var(--fg-inset));
|
||||
}
|
||||
|
||||
.status svg {
|
||||
color: var(--cpd-color-icon-tertiary);
|
||||
span {
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--cpd-color-icon-tertiary);
|
||||
vertical-align: center;
|
||||
margin-inline-end: 3px;
|
||||
block-size: 1.2em;
|
||||
inline-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.reactions {
|
||||
|
||||
@@ -7,13 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||
import { animated } from "@react-spring/web";
|
||||
import {
|
||||
type FC,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
type ComponentType,
|
||||
type SVGAttributes,
|
||||
} from "react";
|
||||
import { type FC, type ComponentProps, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { VideoTrack } from "@livekit/components-react";
|
||||
@@ -43,7 +37,7 @@ interface Props extends ComponentProps<typeof animated.div> {
|
||||
userId: string;
|
||||
videoEnabled: boolean;
|
||||
unencryptedWarning: boolean;
|
||||
status?: { text: string; Icon: ComponentType<SVGAttributes<SVGElement>> };
|
||||
status?: ReactNode;
|
||||
showNameTags: boolean;
|
||||
nameTagLeadingIcon?: ReactNode;
|
||||
displayName: string;
|
||||
@@ -180,14 +174,7 @@ export const MediaView: FC<Props> = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{status && (
|
||||
<div className={styles.status}>
|
||||
<status.Icon width={16} height={16} aria-hidden />
|
||||
<Text as="span" size="sm" weight="medium">
|
||||
{status.text}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{status && <div className={styles.status}>{status}</div>}
|
||||
{/* TODO: Bring this back once encryption status is less broken */}
|
||||
{/*encryptionStatus !== EncryptionStatus.Okay && (
|
||||
<div className={styles.status}>
|
||||
|
||||
41
src/tile/RingingStatus.tsx
Normal file
41
src/tile/RingingStatus.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations 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 {
|
||||
VideoCallSolidIcon,
|
||||
VoiceCallSolidIcon,
|
||||
EndCallIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
|
||||
interface Props {
|
||||
vm: RingingMediaViewModel;
|
||||
}
|
||||
|
||||
export const RingingStatus: FC<Props> = ({ vm }) => {
|
||||
const { t } = useTranslation();
|
||||
const pickupState = useBehavior(vm.pickupState$);
|
||||
const Icon =
|
||||
pickupState === "ringing"
|
||||
? vm.intent === "video"
|
||||
? VideoCallSolidIcon
|
||||
: VoiceCallSolidIcon
|
||||
: EndCallIcon;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon aria-hidden />
|
||||
{pickupState === "ringing"
|
||||
? t("video_tile.calling")
|
||||
: t("video_tile.call_ended")}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect, vi } from "vitest";
|
||||
import { act, isInaccessible, render, screen } from "@testing-library/react";
|
||||
import { isInaccessible, render, screen } from "@testing-library/react";
|
||||
import { axe } from "vitest-axe";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -177,11 +177,6 @@ test("SpotlightTile displays ringing media", async () => {
|
||||
);
|
||||
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
// Alice should be in the spotlight with the right status
|
||||
// Alice should be in the spotlight
|
||||
screen.getByText("Alice");
|
||||
screen.getByText("Calling…");
|
||||
|
||||
// Now we time out ringing to Alice
|
||||
act(() => pickupState$.next("timeout"));
|
||||
screen.getByText("Call ended");
|
||||
});
|
||||
|
||||
@@ -24,9 +24,6 @@ import {
|
||||
VolumeOnIcon,
|
||||
VolumeOffSolidIcon,
|
||||
VolumeOnSolidIcon,
|
||||
VideoCallSolidIcon,
|
||||
VoiceCallSolidIcon,
|
||||
EndCallIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { animated } from "@react-spring/web";
|
||||
import { type Observable, map } from "rxjs";
|
||||
@@ -210,22 +207,10 @@ const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
|
||||
vm,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const pickupState = useBehavior(vm.pickupState$);
|
||||
|
||||
return (
|
||||
<MediaView
|
||||
video={undefined}
|
||||
unencryptedWarning={false}
|
||||
status={
|
||||
pickupState === "ringing"
|
||||
? {
|
||||
text: t("video_tile.calling"),
|
||||
Icon:
|
||||
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
|
||||
}
|
||||
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
|
||||
}
|
||||
videoEnabled={false}
|
||||
videoFit="cover"
|
||||
mirror={false}
|
||||
|
||||
Reference in New Issue
Block a user