mirror of
https://github.com/vector-im/element-call.git
synced 2026-06-30 18:02:56 +00:00
Show ringing status even on spotlight tiles (except in app bar mode)
This commit is contained in:
@@ -333,7 +333,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
|
||||
useAppBarHidden(!showHeader);
|
||||
useAppBarSubtitle(ringingVm && <RingingStatus vm={ringingVm} />);
|
||||
useAppBarSubtitle(
|
||||
ringingVm && vm.ringingStatusLocation === "app_bar" && (
|
||||
<RingingStatus vm={ringingVm} />
|
||||
),
|
||||
);
|
||||
|
||||
let header: ReactNode = null;
|
||||
switch (headerStyle) {
|
||||
@@ -428,6 +432,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showRingingStatus = vm.ringingStatusLocation === "tile";
|
||||
|
||||
return model instanceof GridTileViewModel ? (
|
||||
<GridTile
|
||||
@@ -440,6 +445,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={showRingingStatus}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
@@ -452,6 +458,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight={targetHeight}
|
||||
showIndicators={showSpotlightIndicators}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={showRingingStatus}
|
||||
focusable={!contentObscured}
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
@@ -486,6 +493,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight={gridBounds.height}
|
||||
showIndicators={false}
|
||||
showNameTags={showNameTags}
|
||||
showRingingStatus={vm.ringingStatusLocation === "tile"}
|
||||
focusable={!contentObscured}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
|
||||
@@ -232,6 +232,10 @@ export interface CallViewModel {
|
||||
* View model for info relating to ringing, timing out, calling back, etc.
|
||||
*/
|
||||
ringingVm$: Behavior<RingingMediaViewModel | null>;
|
||||
/**
|
||||
* Which visual element the ringing status should be shown in.
|
||||
*/
|
||||
ringingStatusLocation: "app_bar" | "tile";
|
||||
/** Observable that emits when the user should leave the call (hangup pressed, widget action, error).
|
||||
* THIS DOES NOT LEAVE THE CALL YET. The only way to leave the call (send the hangup event) is
|
||||
* - by ending the scope
|
||||
@@ -1701,6 +1705,8 @@ export function createCallViewModel$(
|
||||
return {
|
||||
autoLeave$: autoLeave$,
|
||||
ringingVm$: ringingMedia$,
|
||||
ringingStatusLocation:
|
||||
urlParams.header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||
leave$: leave$,
|
||||
hangup: (): void => userHangup$.next(),
|
||||
join: localMembership.requestJoinAndPublish,
|
||||
|
||||
@@ -77,6 +77,7 @@ test("GridTile is accessible", async () => {
|
||||
targetHeight={200}
|
||||
showSpeakingIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable
|
||||
/>
|
||||
</ReactionsSenderProvider>,
|
||||
@@ -108,6 +109,7 @@ test("GridTile displays ringing media", async () => {
|
||||
targetHeight={200}
|
||||
showSpeakingIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable
|
||||
/>
|
||||
</ReactionsSenderProvider>,
|
||||
|
||||
@@ -67,10 +67,12 @@ interface TileProps {
|
||||
|
||||
interface RingingMediaTileProps extends TileProps {
|
||||
vm: RingingMediaViewModel;
|
||||
showStatus: boolean;
|
||||
}
|
||||
|
||||
const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
||||
vm,
|
||||
showStatus,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
@@ -81,10 +83,13 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
|
||||
userId={vm.userId}
|
||||
unencryptedWarning={false}
|
||||
status={
|
||||
<Text as="span" size="sm" weight="medium">
|
||||
<RingingStatus vm={vm} />
|
||||
</Text>
|
||||
showStatus && (
|
||||
<Text as="span" size="sm" weight="medium">
|
||||
<RingingStatus vm={vm} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
avatarStyle="translucent"
|
||||
videoEnabled={false}
|
||||
videoFit="cover"
|
||||
mirror={false}
|
||||
@@ -392,6 +397,7 @@ interface GridTileProps {
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
showSpeakingIndicators: boolean;
|
||||
showNameTags: boolean;
|
||||
showRingingStatus: boolean;
|
||||
focusable: boolean;
|
||||
}
|
||||
|
||||
@@ -399,6 +405,7 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
ref: theirRef,
|
||||
vm,
|
||||
showSpeakingIndicators,
|
||||
showRingingStatus,
|
||||
onOpenProfile,
|
||||
...props
|
||||
}) => {
|
||||
@@ -415,6 +422,7 @@ export const GridTile: FC<GridTileProps> = ({
|
||||
vm={media}
|
||||
displayName={displayName}
|
||||
mxcAvatarUrl={mxcAvatarUrl}
|
||||
showStatus={showRingingStatus}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -54,11 +54,12 @@ Please see LICENSE in the repository root for full details.
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
opacity: 100%;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.translucent {
|
||||
.avatar[data-style="translucent"] {
|
||||
opacity: 50%;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* CSS makes us put a condition here, even though all we want to do is
|
||||
|
||||
@@ -42,6 +42,7 @@ interface Props extends ComponentProps<typeof animated.div> {
|
||||
nameTagLeadingIcon?: ReactNode;
|
||||
displayName: string;
|
||||
mxcAvatarUrl: string | undefined;
|
||||
avatarStyle?: "solid" | "translucent";
|
||||
focusable: boolean;
|
||||
primaryButton?: ReactNode;
|
||||
raisedHandTime?: Date;
|
||||
@@ -71,6 +72,7 @@ export const MediaView: FC<Props> = ({
|
||||
nameTagLeadingIcon,
|
||||
displayName,
|
||||
mxcAvatarUrl,
|
||||
avatarStyle = "solid",
|
||||
focusable,
|
||||
primaryButton,
|
||||
status,
|
||||
@@ -124,11 +126,8 @@ export const MediaView: FC<Props> = ({
|
||||
name={displayName}
|
||||
size={avatarSize}
|
||||
src={mxcAvatarUrl}
|
||||
className={classNames(styles.avatar, {
|
||||
// When the avatar is overlaid with a status, make it translucent
|
||||
// for readability
|
||||
[styles.translucent]: status,
|
||||
})}
|
||||
data-style={avatarStyle}
|
||||
className={styles.avatar}
|
||||
style={{ display: video && videoEnabled ? "none" : "initial" }}
|
||||
/>
|
||||
{video?.publication !== undefined && (
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect, vi } from "vitest";
|
||||
import { isInaccessible, render, screen } from "@testing-library/react";
|
||||
import { act, 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";
|
||||
@@ -65,6 +65,7 @@ test("SpotlightTile is accessible", async () => {
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable={true}
|
||||
/>,
|
||||
);
|
||||
@@ -107,6 +108,7 @@ test("Screen share volume UI is shown when screen share has audio", async () =>
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable
|
||||
/>
|
||||
</TooltipProvider>,
|
||||
@@ -137,6 +139,7 @@ test("Screen share volume UI is hidden when screen share has no audio", async ()
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable
|
||||
/>,
|
||||
);
|
||||
@@ -172,11 +175,17 @@ test("SpotlightTile displays ringing media", async () => {
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
showNameTags
|
||||
showRingingStatus
|
||||
focusable={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
// Alice should be in the spotlight
|
||||
// Alice should be in the spotlight with the right status
|
||||
screen.getByText("Alice");
|
||||
screen.getByText("Calling…");
|
||||
|
||||
// Now we time out ringing to Alice
|
||||
act(() => pickupState$.next("timeout"));
|
||||
screen.getByText("Call ended");
|
||||
});
|
||||
|
||||
@@ -31,7 +31,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 { Menu, MenuItem, Text } from "@vector-im/compound-web";
|
||||
|
||||
import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react";
|
||||
import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react";
|
||||
@@ -53,6 +53,7 @@ import { type MediaViewModel } from "../state/media/MediaViewModel";
|
||||
import { Slider } from "../Slider";
|
||||
import { platform } from "../Platform";
|
||||
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
|
||||
import { RingingStatus } from "./RingingStatus";
|
||||
|
||||
interface SpotlightItemBaseProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
@@ -201,16 +202,26 @@ const SpotlightMemberMediaItem: FC<SpotlightMemberMediaItemProps> = ({
|
||||
|
||||
interface SpotlightRingingMediaItemProps extends SpotlightItemBaseProps {
|
||||
vm: RingingMediaViewModel;
|
||||
showStatus: boolean;
|
||||
}
|
||||
|
||||
const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
|
||||
vm,
|
||||
showStatus,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<MediaView
|
||||
video={undefined}
|
||||
unencryptedWarning={false}
|
||||
status={
|
||||
showStatus && (
|
||||
<Text as="span" size="md" weight="medium">
|
||||
<RingingStatus vm={vm} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
avatarStyle="translucent"
|
||||
videoEnabled={false}
|
||||
videoFit="cover"
|
||||
mirror={false}
|
||||
@@ -231,6 +242,7 @@ interface SpotlightItemProps {
|
||||
*/
|
||||
targetHeight: number;
|
||||
showNameTags: boolean;
|
||||
showRingingStatus: boolean;
|
||||
focusable: boolean;
|
||||
intersectionObserver$: Observable<IntersectionObserver>;
|
||||
/**
|
||||
@@ -246,6 +258,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
showNameTags,
|
||||
showRingingStatus,
|
||||
focusable,
|
||||
intersectionObserver$,
|
||||
snap,
|
||||
@@ -287,7 +300,11 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||
};
|
||||
|
||||
return vm.type === "ringing" ? (
|
||||
<SpotlightRingingMediaItem vm={vm} {...baseProps} />
|
||||
<SpotlightRingingMediaItem
|
||||
vm={vm}
|
||||
showStatus={showRingingStatus}
|
||||
{...baseProps}
|
||||
/>
|
||||
) : (
|
||||
<SpotlightMemberMediaItem vm={vm} {...baseProps} />
|
||||
);
|
||||
@@ -371,6 +388,7 @@ interface Props {
|
||||
targetHeight: number;
|
||||
showIndicators: boolean;
|
||||
showNameTags: boolean;
|
||||
showRingingStatus: boolean;
|
||||
focusable: boolean;
|
||||
className?: string;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
@@ -385,6 +403,7 @@ export const SpotlightTile: FC<Props> = ({
|
||||
targetHeight,
|
||||
showIndicators,
|
||||
showNameTags,
|
||||
showRingingStatus,
|
||||
focusable = true,
|
||||
className,
|
||||
style,
|
||||
@@ -495,6 +514,7 @@ export const SpotlightTile: FC<Props> = ({
|
||||
vm={vm}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
showRingingStatus={showRingingStatus}
|
||||
showNameTags={showNameTags}
|
||||
focusable={focusable}
|
||||
intersectionObserver$={intersectionObserver$}
|
||||
|
||||
Reference in New Issue
Block a user