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