Ongoing call tiles - Implement the required views (#34197)
* Add a common view snapshot type This will contain the common snapshot values used by both dm ongoing call tile view and room ongoing call tile view. * Implement the common join button component * Implement the call icon component * Implement the common container component * Support single digit minutes in existing clock component * Implement the view for duration component * Add static profile pics to be used in storybook tests * Implement view for member avatar component * Implement view for face pile component * Implement RoomOngoingCallTileView * Implement DmOngoingCallTileView * Support the new tiles in RootTileView * Export all the views * Update all the screenshots * Add a comment about the 10px margin * Add comments to explain css heights and widths
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -27,3 +27,10 @@ export const LotOfSeconds: Story = {
|
||||
seconds: 99999999999999,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleDigitMinutes: Story = {
|
||||
args: {
|
||||
seconds: 100,
|
||||
minutesMaxLength: 1,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { describe, it, expect } from "vitest";
|
||||
|
||||
import * as stories from "./Clock.stories.tsx";
|
||||
|
||||
const { Default, LotOfSeconds } = composeStories(stories);
|
||||
const { Default, LotOfSeconds, SingleDigitMinutes } = composeStories(stories);
|
||||
|
||||
describe("Clock", () => {
|
||||
it("renders the clock", () => {
|
||||
@@ -24,4 +24,9 @@ describe("Clock", () => {
|
||||
const { container } = render(<LotOfSeconds />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the clock with single digit minute", () => {
|
||||
const { container } = render(<SingleDigitMinutes />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,22 @@ export interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "r
|
||||
* The number of seconds to display.
|
||||
*/
|
||||
seconds: number;
|
||||
|
||||
/**
|
||||
* The number of positions to pad the minutes part.
|
||||
*
|
||||
* @example
|
||||
* If minutesMaxLength = 1, the clock will show 5:31 instead of 05:31.
|
||||
*/
|
||||
minutesMaxLength?: number;
|
||||
|
||||
/**
|
||||
* The number of positions to pad the hour part.
|
||||
*
|
||||
* @example
|
||||
* If hoursMaxLength = 1, the clock will show 1:05:31 instead of 01:05:31.
|
||||
*/
|
||||
hoursMaxLength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +44,7 @@ export interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "r
|
||||
* <Clock seconds={125} />
|
||||
* ```
|
||||
*/
|
||||
export function Clock({ seconds, className, ...rest }: Props): JSX.Element {
|
||||
export function Clock({ seconds, className, minutesMaxLength, hoursMaxLength, ...rest }: Props): JSX.Element {
|
||||
// Memoize current second to avoid recalculating the duration when seconds changes slightly (e.g. 1.2 -> 1.3)
|
||||
const currentSecond = useMemo(() => Math.floor(seconds), [seconds]);
|
||||
const duration = useMemo(() => calculateDuration(currentSecond), [currentSecond]);
|
||||
@@ -40,7 +56,10 @@ export function Clock({ seconds, className, ...rest }: Props): JSX.Element {
|
||||
className={classNames("mx_Clock", className)}
|
||||
{...rest}
|
||||
>
|
||||
{formatSeconds(seconds)}
|
||||
{formatSeconds(seconds, {
|
||||
minutesMaxLength,
|
||||
hoursMaxLength,
|
||||
})}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,3 +21,14 @@ exports[`Clock > renders the clock with a lot of seconds 1`] = `
|
||||
</time>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Clock > renders the clock with single digit minute 1`] = `
|
||||
<div>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { type FacePileViewSnapshot, FacePileView } from "./FacePileView";
|
||||
import { withViewDocs } from "../../../.storybook/withViewDocs";
|
||||
import profileLink1 from "../../../static/profile-pics/profile1.png";
|
||||
import profileLink2 from "../../../static/profile-pics/profile2.png";
|
||||
import profileLink3 from "../../../static/profile-pics/profile3.png";
|
||||
import { MockViewModel, useMockedViewModel } from "../viewmodel";
|
||||
import { type MemberAvatarViewSnapshot } from "../MemberAvatar/MemberAvatarView";
|
||||
|
||||
const FacePileViewWrapperImpl = (snapshot: FacePileViewSnapshot): React.ReactNode => {
|
||||
const vm = useMockedViewModel(snapshot, {});
|
||||
return <FacePileView vm={vm} />;
|
||||
};
|
||||
|
||||
const FacePileViewWrapper = withViewDocs(FacePileViewWrapperImpl, FacePileView);
|
||||
|
||||
type MockProp = { id: string; name: string; url?: string };
|
||||
|
||||
export class MockMemberAvatarViewModel extends MockViewModel<MemberAvatarViewSnapshot> {
|
||||
public constructor({ id, name, url }: MockProp) {
|
||||
super({
|
||||
id,
|
||||
name,
|
||||
size: "20px",
|
||||
title: id,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Core/FacePileView",
|
||||
component: FacePileViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
memberAvatarViewModels: [
|
||||
new MockMemberAvatarViewModel({ id: "@bob:m.org", name: "Bob", url: profileLink1 }),
|
||||
new MockMemberAvatarViewModel({ id: "@riley:m.org", name: "Riley", url: profileLink2 }),
|
||||
new MockMemberAvatarViewModel({ id: "@andi:m.org", name: "Andi", url: profileLink3 }),
|
||||
],
|
||||
},
|
||||
} satisfies Meta<typeof FacePileViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { AvatarStack } from "@vector-im/compound-web";
|
||||
|
||||
import { type MemberAvatarViewModel, MemberAvatarView } from "../MemberAvatar/MemberAvatarView";
|
||||
import { useViewModel, type ViewModel } from "../viewmodel";
|
||||
|
||||
export interface FacePileViewSnapshot {
|
||||
/**
|
||||
* The sub vms for the member avatars.
|
||||
*/
|
||||
memberAvatarViewModels: MemberAvatarViewModel[];
|
||||
}
|
||||
|
||||
export type FacePileViewModel = ViewModel<FacePileViewSnapshot>;
|
||||
|
||||
export interface FacePileViewProps {
|
||||
vm: FacePileViewModel;
|
||||
|
||||
/**
|
||||
* Additional class names for this component.
|
||||
*/
|
||||
classNames?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* View that renders a face pile view.
|
||||
*/
|
||||
export function FacePileView(props: FacePileViewProps): React.ReactNode {
|
||||
const { memberAvatarViewModels } = useViewModel(props.vm);
|
||||
if (memberAvatarViewModels.length === 0) return null;
|
||||
return (
|
||||
<AvatarStack className={props.classNames}>
|
||||
{memberAvatarViewModels.map((vm) => (
|
||||
<MemberAvatarView vm={vm} key={vm.getSnapshot().id} />
|
||||
))}
|
||||
</AvatarStack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { type MemberAvatarViewSnapshot, MemberAvatarView } from "./MemberAvatarView";
|
||||
import { withViewDocs } from "../../../.storybook/withViewDocs";
|
||||
import profile from "../../../static/profile-pics/profile3.png";
|
||||
import { useMockedViewModel } from "../viewmodel";
|
||||
|
||||
const MemberAvatarViewWrapperImpl = (snapshot: MemberAvatarViewSnapshot): React.ReactNode => {
|
||||
const vm = useMockedViewModel(snapshot, {});
|
||||
return <MemberAvatarView vm={vm} />;
|
||||
};
|
||||
|
||||
const MemberAvatarViewWrapper = withViewDocs(MemberAvatarViewWrapperImpl, MemberAvatarView);
|
||||
|
||||
const meta = {
|
||||
title: "Core/MemberAvatarView",
|
||||
component: MemberAvatarViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
url: {
|
||||
control: "text",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
id: "@bob:m.org",
|
||||
name: "Bob",
|
||||
size: "20px",
|
||||
title: "@bob:m.org",
|
||||
url: profile,
|
||||
},
|
||||
} satisfies Meta<typeof MemberAvatarViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Avatar } from "@vector-im/compound-web";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../viewmodel";
|
||||
|
||||
export interface MemberAvatarViewSnapshot {
|
||||
/**
|
||||
* The display name of this member.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The mxid of this member.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The avatar url.
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* Size of the avatar.
|
||||
*/
|
||||
size: string;
|
||||
/**
|
||||
* Title passed to the avatar container (button or span).
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export type MemberAvatarViewModel = ViewModel<MemberAvatarViewSnapshot>;
|
||||
|
||||
interface MemberAvatarViewProps {
|
||||
vm: MemberAvatarViewModel;
|
||||
|
||||
/**
|
||||
* Additional class names for this component.
|
||||
*/
|
||||
classNames?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* View for rendering the avatar for a given member.
|
||||
*/
|
||||
export function MemberAvatarView(props: MemberAvatarViewProps): React.ReactNode {
|
||||
const { name, id, url, size } = useViewModel(props.vm);
|
||||
|
||||
return <Avatar className={props.classNames} name={name} id={id} src={url} size={size} />;
|
||||
}
|
||||
@@ -9,16 +9,21 @@
|
||||
* Formats a number of seconds into a human-readable string.
|
||||
* @param inSeconds
|
||||
*/
|
||||
export function formatSeconds(inSeconds: number): string {
|
||||
export function formatSeconds(
|
||||
inSeconds: number,
|
||||
opts?: { hoursMaxLength?: number; minutesMaxLength?: number },
|
||||
): string {
|
||||
const isNegative = inSeconds < 0;
|
||||
inSeconds = Math.abs(inSeconds);
|
||||
|
||||
const hours = Math.floor(inSeconds / (60 * 60))
|
||||
.toFixed(0)
|
||||
.padStart(2, "0");
|
||||
.padStart(opts?.hoursMaxLength ?? 2, "0");
|
||||
|
||||
const minutes = Math.floor((inSeconds % (60 * 60)) / 60)
|
||||
.toFixed(0)
|
||||
.padStart(2, "0");
|
||||
.padStart(opts?.minutesMaxLength ?? 2, "0");
|
||||
|
||||
const seconds = Math.floor((inSeconds % (60 * 60)) % 60)
|
||||
.toFixed(0)
|
||||
.padStart(2, "0");
|
||||
|
||||
@@ -233,6 +233,20 @@
|
||||
"call_declined": "Call declined",
|
||||
"call_declined_by_us": "You declined a call"
|
||||
},
|
||||
"ongoing": {
|
||||
"common": {
|
||||
"call_started_by": "%(startedByDisplayName)s started a call",
|
||||
"join_button": "Join"
|
||||
},
|
||||
"dm": {
|
||||
"call_started": "Call started",
|
||||
"title": "Call in progress"
|
||||
},
|
||||
"room": {
|
||||
"join_count": "%(joinedCount)s joined",
|
||||
"title": "Group call in progress"
|
||||
}
|
||||
},
|
||||
"tombstone": {
|
||||
"room": {
|
||||
"title": "Group call ended"
|
||||
|
||||
@@ -11,6 +11,8 @@ export * from "./audio/Clock";
|
||||
export * from "./audio/PlayPauseButton";
|
||||
export * from "./audio/SeekBar";
|
||||
export * from "./core/AvatarWithDetails";
|
||||
export * from "./core/MemberAvatar/MemberAvatarView.tsx";
|
||||
export * from "./core/FacePile/FacePileView.tsx";
|
||||
export * from "./core/roving";
|
||||
export * from "./room/composer/Banner";
|
||||
export * from "./room/composer/UploadButton";
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import React from "react";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
||||
import { RoomOngoingCallTileView, type RoomCallStartedTileViewModel } from "./ongoing/room/RoomOngoingCallTileView";
|
||||
import { DmOngoingCallTileView, type DmOngoingCallTileViewModel } from "./ongoing/dm/DmOngoingCallTileView";
|
||||
import {
|
||||
RoomTombstoneCallTileView,
|
||||
type RoomTombstoneCallTileViewModel,
|
||||
@@ -18,6 +20,8 @@ import { DmTombstoneCallTileView, type DmTombstoneCallTileViewModel } from "./to
|
||||
* Map from tile type to view model.
|
||||
*/
|
||||
interface TileTypeToViewModelMap {
|
||||
"ongoing-call-room": RoomCallStartedTileViewModel;
|
||||
"ongoing-call-dm": DmOngoingCallTileViewModel;
|
||||
"tombstone-call-room": RoomTombstoneCallTileViewModel;
|
||||
"tombstone-call-dm": DmTombstoneCallTileViewModel;
|
||||
}
|
||||
@@ -39,6 +43,10 @@ interface Props {
|
||||
export function RootCallTileView({ vm }: Props): React.ReactNode {
|
||||
const { tileType, tileViewModel } = useViewModel(vm);
|
||||
switch (tileType) {
|
||||
case "ongoing-call-room":
|
||||
return <RoomOngoingCallTileView vm={tileViewModel as TileTypeToViewModelMap["ongoing-call-room"]} />;
|
||||
case "ongoing-call-dm":
|
||||
return <DmOngoingCallTileView vm={tileViewModel as TileTypeToViewModelMap["ongoing-call-dm"]} />;
|
||||
case "tombstone-call-room":
|
||||
return <RoomTombstoneCallTileView vm={tileViewModel as TileTypeToViewModelMap["tombstone-call-room"]} />;
|
||||
case "tombstone-call-dm":
|
||||
|
||||
@@ -7,5 +7,9 @@
|
||||
|
||||
export * from "./tombstone/room/RoomTombstoneCallTileView";
|
||||
export * from "./tombstone/dm/DmTombstoneCallTileView";
|
||||
export type * from "./ongoing/common";
|
||||
export * from "./ongoing/room/RoomOngoingCallTileView";
|
||||
export * from "./ongoing/components/Duration/DurationView";
|
||||
export * from "./ongoing/dm/DmOngoingCallTileView";
|
||||
export * from "./RootCallTileView";
|
||||
export * from "./common";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type FacePileViewSnapshot } from "../../../../../core/FacePile/FacePileView";
|
||||
import { type MemberAvatarViewSnapshot } from "../../../../../core/MemberAvatar/MemberAvatarView";
|
||||
import { MockViewModel } from "../../../../../core/viewmodel";
|
||||
|
||||
type MockProp = { id: string; name: string; url?: string };
|
||||
|
||||
export class MockMemberAvatarViewModel extends MockViewModel<MemberAvatarViewSnapshot> {
|
||||
public constructor({ id, name, url }: MockProp) {
|
||||
super({
|
||||
id,
|
||||
name,
|
||||
size: "20px",
|
||||
title: id,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class MockFacePileViewModel extends MockViewModel<FacePileViewSnapshot> {
|
||||
public constructor(items: MockProp[]) {
|
||||
const memberAvatarViewModels = items.slice(0, 3).map((item) => new MockMemberAvatarViewModel(item));
|
||||
super({ memberAvatarViewModels });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.title {
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
color: var(--cpd-color-text-primary);
|
||||
/* This height is from the figma design */
|
||||
min-height: 23px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
/* Width specified to allow the children to overflow and render with ellipsis */
|
||||
min-width: 47px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.facepile {
|
||||
min-width: fit-content;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type FacePileViewModel } from "../../../../../core/FacePile/FacePileView";
|
||||
import { type MemberAvatarViewModel } from "../../../../../core/MemberAvatar/MemberAvatarView";
|
||||
import { type ViewModel } from "../../../../../core/viewmodel";
|
||||
import { type CallDirection } from "../common";
|
||||
import { type DurationViewModel } from "./components/Duration/DurationView";
|
||||
|
||||
/**
|
||||
* This snapshot type contains state used by both RoomOngoingCallTileView and
|
||||
* DmOngoingCallTileView.
|
||||
*/
|
||||
export interface CommonOngoingCallTileViewSnapshot {
|
||||
/**
|
||||
* The display name of whoever started this call.
|
||||
*/
|
||||
startedByDisplayName: string;
|
||||
|
||||
/**
|
||||
* Vm for rendering the duration of this call.
|
||||
*/
|
||||
durationViewModel?: DurationViewModel;
|
||||
|
||||
/**
|
||||
* Avatar vm for the user who started this call.
|
||||
*/
|
||||
memberAvatarViewModel: MemberAvatarViewModel;
|
||||
|
||||
/**
|
||||
* Face pile view-model for the participants on this call.
|
||||
*/
|
||||
facePileViewModel: FacePileViewModel;
|
||||
|
||||
/**
|
||||
* Whether this is an incoming or outgoing call.
|
||||
*/
|
||||
callDirection: CallDirection;
|
||||
|
||||
/**
|
||||
* Whether our user has joined this call.
|
||||
*/
|
||||
isJoined: boolean;
|
||||
|
||||
/**
|
||||
* Whether our user can join this call or not.
|
||||
*/
|
||||
isJoinable: boolean;
|
||||
|
||||
/**
|
||||
* Whether this call has participants other than who started the call.
|
||||
*/
|
||||
callHasOtherParticipants: boolean;
|
||||
}
|
||||
|
||||
export interface CommonOngoingCallTileViewAction {
|
||||
/**
|
||||
* Join this call
|
||||
*/
|
||||
join: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export type CallStartedTileFooViewModel = ViewModel<CommonOngoingCallTileViewSnapshot>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.container {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--cpd-space-2x);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type { CallType } from "../../../common";
|
||||
import { Flex } from "../../../../../../../core/utils/Flex";
|
||||
import styles from "./CallIcon.module.css";
|
||||
import { Icon } from "../Icon/Icon";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The type of call.
|
||||
*/
|
||||
callType: CallType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common call icon component that is used by ongoing call tiles.
|
||||
*/
|
||||
export function CallIcon(props: Props): React.ReactNode {
|
||||
return (
|
||||
<Flex align="center" justify="center" className={styles.container}>
|
||||
<Icon classNames={styles.icon} callType={props.callType} height={20} width={20} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.container {
|
||||
font: var(--cpd-font-body-xs-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
min-width: 30px;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./DurationView.module.css";
|
||||
import { useViewModel, type ViewModel } from "../../../../../../../core/viewmodel";
|
||||
import { Clock } from "../../../../../../../audio/Clock";
|
||||
|
||||
export interface DurationViewSnapshot {
|
||||
/**
|
||||
* The number of seconds that this call has been ongoing for.
|
||||
*/
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export type DurationViewModel = ViewModel<DurationViewSnapshot>;
|
||||
|
||||
interface Props {
|
||||
vm: DurationViewModel;
|
||||
|
||||
/**
|
||||
* Additional class names for this component.
|
||||
*/
|
||||
classNames?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* View to show the duration of the call.
|
||||
*/
|
||||
export function DurationView(props: Props): React.ReactNode {
|
||||
const { duration } = useViewModel(props.vm);
|
||||
const classes = classNames(styles.container, props.classNames);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
(<Clock seconds={duration} minutesMaxLength={1} className={classes} />)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentType, type SVGAttributes } from "react";
|
||||
import { VideoCallSolidIcon, VoiceCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { CallType } from "../../../common";
|
||||
|
||||
interface Props extends SVGAttributes<SVGGElement> {
|
||||
/**
|
||||
* The type of this call i.e voice or video.
|
||||
*/
|
||||
callType: CallType;
|
||||
/**
|
||||
* The extra class names to add to the icon.
|
||||
*/
|
||||
classNames?: string;
|
||||
/**
|
||||
* Height of the icon
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* Width of the icon
|
||||
*/
|
||||
width: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the correct svg icon based on call type.
|
||||
*/
|
||||
export const Icon: ComponentType<Props> = ({ callType, classNames, height, width, ...rest }: Props) => {
|
||||
switch (callType) {
|
||||
case CallType.Video:
|
||||
return <VideoCallSolidIcon className={classNames} width={width} height={height} {...rest} />;
|
||||
case CallType.Voice:
|
||||
return <VoiceCallSolidIcon className={classNames} width={width} height={height} {...rest} />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import React, { type ComponentProps, type SVGAttributes } from "react";
|
||||
|
||||
import { Icon } from "../Icon/Icon";
|
||||
import type { CallType } from "../../../common";
|
||||
import { useI18n } from "../../../../../../../core/i18n/i18nContext";
|
||||
|
||||
interface Props extends ComponentProps<typeof Button> {
|
||||
join: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
callType: CallType;
|
||||
}
|
||||
|
||||
function withIcon(callType: CallType) {
|
||||
return function ButtonIcon(props: SVGAttributes<SVGGElement>) {
|
||||
return <Icon callType={callType} {...props} height={20} width={20} />;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Join button used by all ongoing call tiles.
|
||||
*/
|
||||
export function JoinButton({ join, callType, ...rest }: Props): React.ReactNode {
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<Button onClick={join} Icon={withIcon(callType)} size="md" {...rest}>
|
||||
{_t("timeline|call_tile|ongoing|common|join_button")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.container {
|
||||
min-height: 70px;
|
||||
padding: var(--cpd-space-3x);
|
||||
border: solid 1px var(--cpd-color-border-interactive-primary);
|
||||
border-radius: var(--cpd-space-2x);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
/* All call tiles have a 10px margin at the top and bottom */
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import styles from "./TileContainer.module.css";
|
||||
import { Flex } from "../../../../../../../core/utils/Flex";
|
||||
|
||||
/**
|
||||
* Common container inside which all ongoing call tiles render.
|
||||
*/
|
||||
export function TileContainer({ children }: React.PropsWithChildren): React.ReactNode {
|
||||
return (
|
||||
<Flex direction="row" align="center" className={styles.container}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { type DmOngoingCallTileViewSnapshot, DmOngoingCallTileView } from "./DmOngoingCallTileView";
|
||||
import { MockViewModel, useMockedViewModel } from "../../../../../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../../../../../.storybook/withViewDocs";
|
||||
import { type CommonOngoingCallTileViewAction } from "../common";
|
||||
import { CallDirection, CallType } from "../../common";
|
||||
import profileLink1 from "../../../../../../../static/profile-pics/profile1.png";
|
||||
import profileLink2 from "../../../../../../../static/profile-pics/profile2.png";
|
||||
import { MockFacePileViewModel, MockMemberAvatarViewModel } from "../call-mocks";
|
||||
|
||||
const DmOngoingCallTileViewWrapperImpl = ({
|
||||
join,
|
||||
...rest
|
||||
}: DmOngoingCallTileViewSnapshot & CommonOngoingCallTileViewAction): React.ReactNode => {
|
||||
const vm = useMockedViewModel(rest, { join });
|
||||
return <DmOngoingCallTileView vm={vm} />;
|
||||
};
|
||||
|
||||
const DmOngoingCallTileViewWrapper = withViewDocs(DmOngoingCallTileViewWrapperImpl, DmOngoingCallTileView);
|
||||
|
||||
const meta = {
|
||||
title: "Timeline/Timeline Event/Call/Ongoing/DmOngoingCallTileView",
|
||||
component: DmOngoingCallTileViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
callDirection: {
|
||||
options: [CallDirection.Incoming, CallDirection.Outgoing],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
callType: {
|
||||
options: [CallType.Voice, CallType.Video],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
callDirection: CallDirection.Incoming,
|
||||
startedByDisplayName: "Bob",
|
||||
durationViewModel: new MockViewModel({ duration: 100 }),
|
||||
isJoinable: true,
|
||||
isJoined: false,
|
||||
join: () => {},
|
||||
memberAvatarViewModel: new MockMemberAvatarViewModel({ id: "@bob:m.org", name: "Bob", url: profileLink1 }),
|
||||
facePileViewModel: new MockFacePileViewModel([
|
||||
{ id: "@bob:m.org", name: "Bob", url: profileLink1 },
|
||||
{ id: "@riley:m.org", name: "Riley", url: profileLink2 },
|
||||
]),
|
||||
callType: CallType.Voice,
|
||||
callHasOtherParticipants: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=11217-3901&t=OvT1LOc5wH4kXt0a-4",
|
||||
},
|
||||
visOptions: {
|
||||
mask: [".duration"],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof DmOngoingCallTileViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const IncomingVoiceCall: Story = {
|
||||
args: {
|
||||
callDirection: CallDirection.Incoming,
|
||||
callType: CallType.Voice,
|
||||
},
|
||||
};
|
||||
|
||||
export const IncomingVideoCall: Story = {
|
||||
args: {
|
||||
callDirection: CallDirection.Incoming,
|
||||
callType: CallType.Video,
|
||||
},
|
||||
};
|
||||
|
||||
export const OutgoingVoiceCall: Story = {
|
||||
args: {
|
||||
callDirection: CallDirection.Outgoing,
|
||||
callType: CallType.Voice,
|
||||
},
|
||||
};
|
||||
|
||||
export const OutgoingVideoCall: Story = {
|
||||
args: {
|
||||
callDirection: CallDirection.Outgoing,
|
||||
callType: CallType.Video,
|
||||
},
|
||||
};
|
||||
|
||||
export const VoiceCallInProgress: Story = {
|
||||
args: {
|
||||
callType: CallType.Voice,
|
||||
isJoined: true,
|
||||
callHasOtherParticipants: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoCallInProgress: Story = {
|
||||
args: {
|
||||
callType: CallType.Video,
|
||||
isJoined: true,
|
||||
callHasOtherParticipants: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import React from "react";
|
||||
import { render } from "@test-utils";
|
||||
|
||||
import * as Stories from "./DmOngoingCallTileView.stories";
|
||||
|
||||
const {
|
||||
IncomingVideoCall,
|
||||
IncomingVoiceCall,
|
||||
OutgoingVideoCall,
|
||||
OutgoingVoiceCall,
|
||||
VideoCallInProgress,
|
||||
VoiceCallInProgress,
|
||||
} = composeStories(Stories);
|
||||
|
||||
describe("DmOngoingCallTileView", () => {
|
||||
describe("renders the tile", () => {
|
||||
it("IncomingVideoCall", () => {
|
||||
const { container } = render(<IncomingVideoCall />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("IncomingVoiceCall", () => {
|
||||
const { container } = render(<IncomingVoiceCall />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("OutgoingVideoCall", () => {
|
||||
const { container } = render(<OutgoingVideoCall />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("OutgoingVoiceCall", () => {
|
||||
const { container } = render(<OutgoingVoiceCall />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("VoiceCallInProgress", () => {
|
||||
const { container } = render(<VoiceCallInProgress />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("VideoCallInProgress", () => {
|
||||
const { container } = render(<VideoCallInProgress />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../../../../../../core/viewmodel";
|
||||
import { TileContainer } from "../components/TileContainer/TileContainer";
|
||||
import { CallIcon } from "../components/CallIcon/CallIcon";
|
||||
import { CallDirection, type CallType } from "../../common";
|
||||
import { Flex } from "../../../../../../core/utils/Flex";
|
||||
import commonStyles from "../common.module.css";
|
||||
import { JoinButton } from "../components/JoinButton/JoinButton";
|
||||
import { DurationView } from "../components/Duration/DurationView";
|
||||
import { MemberAvatarView } from "../../../../../../core/MemberAvatar/MemberAvatarView";
|
||||
import { type CommonOngoingCallTileViewSnapshot, type CommonOngoingCallTileViewAction } from "../common";
|
||||
import { FacePileView } from "../../../../../../core/FacePile/FacePileView";
|
||||
import { useI18n } from "../../../../../../core/i18n/i18nContext";
|
||||
|
||||
export interface DmOngoingCallTileViewSnapshot extends CommonOngoingCallTileViewSnapshot {
|
||||
callType: CallType;
|
||||
}
|
||||
|
||||
export type DmOngoingCallTileViewModel = ViewModel<DmOngoingCallTileViewSnapshot> & CommonOngoingCallTileViewAction;
|
||||
|
||||
interface Props {
|
||||
vm: DmOngoingCallTileViewModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* View that renders the tile content for an ongoing call in a DM.
|
||||
*/
|
||||
export function DmOngoingCallTileView(props: Props): React.ReactNode {
|
||||
const snapshot = useViewModel(props.vm);
|
||||
const { callType, callDirection, isJoinable, isJoined, durationViewModel } = snapshot;
|
||||
let content: React.ReactNode;
|
||||
if (snapshot.callHasOtherParticipants) {
|
||||
content = <CallJoinedContent snapshot={snapshot} />;
|
||||
} else if (callDirection === CallDirection.Incoming) {
|
||||
content = <IncomingCallContent snapshot={snapshot} />;
|
||||
} else {
|
||||
content = <OutgoingCallContent snapshot={snapshot} />;
|
||||
}
|
||||
return (
|
||||
<TileContainer>
|
||||
<Flex align="center" gap="var(--cpd-space-3x)" className={commonStyles.content}>
|
||||
<CallIcon callType={callType} />
|
||||
<Flex gap="6px" align="center" className={commonStyles.content}>
|
||||
{content}
|
||||
</Flex>
|
||||
<Flex align="center" gap="var(--cpd-space-3x)" wrap="wrap-reverse">
|
||||
{durationViewModel && <DurationView classNames="duration" vm={durationViewModel} />}
|
||||
{!isJoined && (
|
||||
<JoinButton disabled={!isJoinable} callType={callType} join={(ev) => props.vm.join(ev)} />
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</TileContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function IncomingCallContent({ snapshot }: { snapshot: DmOngoingCallTileViewSnapshot }): React.ReactNode {
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<>
|
||||
<MemberAvatarView classNames={commonStyles.avatar} vm={snapshot.memberAvatarViewModel} />
|
||||
<div className={commonStyles.title}>
|
||||
{_t("timeline|call_tile|ongoing|common|call_started_by", {
|
||||
startedByDisplayName: snapshot.startedByDisplayName,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OutgoingCallContent({ snapshot }: { snapshot: DmOngoingCallTileViewSnapshot }): React.ReactNode {
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<>
|
||||
<MemberAvatarView classNames={commonStyles.avatar} vm={snapshot.memberAvatarViewModel} />
|
||||
<div className={commonStyles.title}>{_t("timeline|call_tile|ongoing|dm|call_started")}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CallJoinedContent({ snapshot }: { snapshot: DmOngoingCallTileViewSnapshot }): React.ReactNode {
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<>
|
||||
<FacePileView classNames={commonStyles.facepile} vm={snapshot.facePileViewModel} />
|
||||
<div className={commonStyles.title}>{_t("timeline|call_tile|ongoing|dm|title")}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > IncomingVideoCall 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8 common-module_avatar"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Bob started a call
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > IncomingVoiceCall 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8 common-module_avatar"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Bob started a call
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > OutgoingVideoCall 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8 common-module_avatar"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Call started
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > OutgoingVoiceCall 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8 common-module_avatar"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Call started
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > VideoCallInProgress 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_va14e_109 common-module_facepile"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@riley:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="3"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile2.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Call in progress
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DmOngoingCallTileView > renders the tile > VoiceCallInProgress 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_va14e_109 common-module_facepile"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@riley:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="3"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile2.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Call in progress
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.subContainer {
|
||||
/* This height is from the figma design */
|
||||
min-height: 23px;
|
||||
width: 100%;
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.startedTextContainer {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { type RoomOngoingCallTileViewSnapshot, RoomOngoingCallTileView } from "./RoomOngoingCallTileView";
|
||||
import { MockViewModel, useMockedViewModel } from "../../../../../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../../../../../.storybook/withViewDocs";
|
||||
import { type CommonOngoingCallTileViewAction } from "../common";
|
||||
import { CallDirection } from "../../common";
|
||||
import profileLink1 from "../../../../../../../static/profile-pics/profile1.png";
|
||||
import profileLink2 from "../../../../../../../static/profile-pics/profile2.png";
|
||||
import profileLink3 from "../../../../../../../static/profile-pics/profile3.png";
|
||||
import { MockFacePileViewModel, MockMemberAvatarViewModel } from "../call-mocks";
|
||||
|
||||
const RoomOngoingCallTileViewWrapperImpl = ({
|
||||
join,
|
||||
...rest
|
||||
}: RoomOngoingCallTileViewSnapshot & CommonOngoingCallTileViewAction): React.ReactNode => {
|
||||
const vm = useMockedViewModel(rest, { join });
|
||||
return <RoomOngoingCallTileView vm={vm} />;
|
||||
};
|
||||
|
||||
const RoomOngoingCallTileViewWrapper = withViewDocs(RoomOngoingCallTileViewWrapperImpl, RoomOngoingCallTileView);
|
||||
|
||||
const meta = {
|
||||
title: "Timeline/Timeline Event/Call/Ongoing/RoomOngoingCallTileView",
|
||||
component: RoomOngoingCallTileViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
callDirection: {
|
||||
options: [CallDirection.Incoming, CallDirection.Outgoing],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
isCallIgnored: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
callDirection: CallDirection.Incoming,
|
||||
startedByDisplayName: "Bob",
|
||||
durationViewModel: new MockViewModel({ duration: 100 }),
|
||||
isCallIgnored: false,
|
||||
totalParticipants: 8,
|
||||
callHasOtherParticipants: true,
|
||||
isJoinable: true,
|
||||
isJoined: false,
|
||||
join: () => {},
|
||||
memberAvatarViewModel: new MockMemberAvatarViewModel({ id: "@bob:m.org", name: "Bob", url: profileLink1 }),
|
||||
facePileViewModel: new MockFacePileViewModel([
|
||||
{ id: "@bob:m.org", name: "Bob", url: profileLink1 },
|
||||
{ id: "@riley:m.org", name: "Riley", url: profileLink2 },
|
||||
{ id: "@andi:m.org", name: "Andi", url: profileLink3 },
|
||||
{ id: "@foo1:m.org", name: "Foo 1" },
|
||||
{ id: "@foo2:m.org", name: "Foo 2" },
|
||||
{ id: "@foo3:m.org", name: "Foo 3" },
|
||||
{ id: "@foo4:m.org", name: "Foo 4" },
|
||||
{ id: "@foo5:m.org", name: "Foo 5" },
|
||||
]),
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=11217-3901&t=OvT1LOc5wH4kXt0a-4",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof RoomOngoingCallTileViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const RoomCallWithoutOtherParticipants: Story = {
|
||||
args: {
|
||||
callHasOtherParticipants: false,
|
||||
totalParticipants: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const RoomCallIgnored: Story = {
|
||||
args: {
|
||||
isCallIgnored: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const RoomCallJoined: Story = {
|
||||
args: {
|
||||
isJoined: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import React from "react";
|
||||
import { render } from "@test-utils";
|
||||
|
||||
import * as Stories from "./RoomOngoingCallTileView.stories";
|
||||
|
||||
const { RoomCallIgnored, RoomCallJoined, RoomCallWithoutOtherParticipants } = composeStories(Stories);
|
||||
|
||||
describe("RoomOngoingCallTileView", () => {
|
||||
describe("renders the tile", () => {
|
||||
it("RoomCallIgnored", () => {
|
||||
const { container } = render(<RoomCallIgnored />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("RoomCallJoined", () => {
|
||||
const { container } = render(<RoomCallJoined />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("RoomCallWithoutOtherParticipants", () => {
|
||||
const { container } = render(<RoomCallWithoutOtherParticipants />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../../../../../../core/viewmodel";
|
||||
import { TileContainer } from "../components/TileContainer/TileContainer";
|
||||
import { CallIcon } from "../components/CallIcon/CallIcon";
|
||||
import { CallType } from "../../common";
|
||||
import { Flex } from "../../../../../../core/utils/Flex";
|
||||
import commonStyles from "../common.module.css";
|
||||
import styles from "./RoomOngoingCallTileView.module.css";
|
||||
import { JoinButton } from "../components/JoinButton/JoinButton";
|
||||
import { DurationView } from "../components/Duration/DurationView";
|
||||
import { MemberAvatarView } from "../../../../../../core/MemberAvatar/MemberAvatarView";
|
||||
import { FacePileView } from "../../../../../../core/FacePile/FacePileView";
|
||||
import type { CommonOngoingCallTileViewAction, CommonOngoingCallTileViewSnapshot } from "../common";
|
||||
import { useI18n } from "../../../../../../core/i18n/i18nContext";
|
||||
|
||||
export interface RoomOngoingCallTileViewSnapshot extends CommonOngoingCallTileViewSnapshot {
|
||||
/**
|
||||
* The total number of participants in this call.
|
||||
*/
|
||||
totalParticipants: number;
|
||||
|
||||
/**
|
||||
* Whether the user ignored this call.
|
||||
*/
|
||||
isCallIgnored?: boolean;
|
||||
}
|
||||
|
||||
export type RoomCallStartedTileViewModel = ViewModel<RoomOngoingCallTileViewSnapshot> & CommonOngoingCallTileViewAction;
|
||||
|
||||
interface Props {
|
||||
vm: RoomCallStartedTileViewModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* View that renders the tile content for an ongoing call in a room.
|
||||
*/
|
||||
export function RoomOngoingCallTileView(props: Props): React.ReactNode {
|
||||
const snapshot = useViewModel(props.vm);
|
||||
const { isJoinable, isJoined, isCallIgnored, callHasOtherParticipants, durationViewModel } = snapshot;
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<TileContainer>
|
||||
<Flex align="center" gap="var(--cpd-space-3x)" className={commonStyles.content}>
|
||||
<CallIcon callType={CallType.Video} />
|
||||
<Flex direction="column" className={commonStyles.content}>
|
||||
<div className={commonStyles.title}>{_t("timeline|call_tile|ongoing|room|title")}</div>
|
||||
|
||||
{callHasOtherParticipants || isCallIgnored ? (
|
||||
<CallJoinedOrIgnoredContent snapshot={snapshot} />
|
||||
) : (
|
||||
<CallStartedContent snapshot={snapshot} />
|
||||
)}
|
||||
</Flex>
|
||||
<Flex align="center" gap="var(--cpd-space-3x)" wrap="wrap-reverse">
|
||||
{durationViewModel && <DurationView classNames="duration" vm={durationViewModel} />}
|
||||
{!isJoined && (
|
||||
<JoinButton
|
||||
callType={CallType.Video}
|
||||
disabled={!isJoinable}
|
||||
join={(event) => {
|
||||
props.vm.join(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</TileContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the content of the tile when the call has participants other than who started the call or
|
||||
* the call was ignored (by clicking decline button on the toast).
|
||||
*/
|
||||
function CallJoinedOrIgnoredContent({ snapshot }: { snapshot: RoomOngoingCallTileViewSnapshot }): React.ReactNode {
|
||||
const { facePileViewModel, totalParticipants } = snapshot;
|
||||
const { translate: _t } = useI18n();
|
||||
const joinedCount = totalParticipants;
|
||||
return (
|
||||
<Flex className={styles.subContainer} gap="6px" align="center">
|
||||
<FacePileView classNames={commonStyles.facepile} vm={facePileViewModel} />
|
||||
{_t("timeline|call_tile|ongoing|room|join_count", { joinedCount })}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the content of the tile when the call has just started.
|
||||
*/
|
||||
function CallStartedContent({ snapshot }: { snapshot: RoomOngoingCallTileViewSnapshot }): React.ReactNode {
|
||||
const { memberAvatarViewModel, startedByDisplayName } = snapshot;
|
||||
const { translate: _t } = useI18n();
|
||||
return (
|
||||
<Flex className={styles.subContainer} gap="6px" align="center">
|
||||
<MemberAvatarView classNames={commonStyles.avatar} vm={memberAvatarViewModel} />
|
||||
<div className={styles.startedTextContainer}>
|
||||
{_t("timeline|call_tile|ongoing|common|call_started_by", { startedByDisplayName })}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`RoomOngoingCallTileView > renders the tile > RoomCallIgnored 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Group call in progress
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex RoomOngoingCallTileView-module_subContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_va14e_109 common-module_facepile"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@riley:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="3"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile2.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@andi:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="4"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile3.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
8 joined
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomOngoingCallTileView > renders the tile > RoomCallJoined 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Group call in progress
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex RoomOngoingCallTileView-module_subContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_va14e_109 common-module_facepile"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@riley:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="3"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile2.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-label="@andi:m.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="4"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile3.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
8 joined
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomOngoingCallTileView > renders the tile > RoomCallWithoutOtherParticipants 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Flex-module_flex TileContainer-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="Flex-module_flex CallIcon-module_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="CallIcon-module_icon"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex common-module_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="common-module_title"
|
||||
>
|
||||
Group call in progress
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex RoomOngoingCallTileView-module_subContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 6px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="@bob:m.org"
|
||||
class="_avatar_va14e_8 common-module_avatar"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/profile-pics/profile1.png"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="RoomOngoingCallTileView-module_startedTextContainer"
|
||||
>
|
||||
Bob started a call
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Flex-module_flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: wrap-reverse;"
|
||||
>
|
||||
<div
|
||||
class="DurationView-module_container duration"
|
||||
>
|
||||
(
|
||||
<time
|
||||
class="mx_Clock DurationView-module_container duration"
|
||||
datetime="PT1M40S"
|
||||
>
|
||||
1:40
|
||||
</time>
|
||||
)
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -13,6 +13,7 @@
|
||||
border-radius: var(--cpd-space-2x);
|
||||
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
||||
box-sizing: border-box;
|
||||
/* All call tiles have a 10px margin at the top and bottom */
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 303 KiB |