Call Tile - Render a tile showing that a call was started (#32988)

* Ignore specific directories

Otherwise newly generated screenshots will be ignored.

* Create a CallStartedTileView

This view will render a tile that shows when an EC call was started in
the timeline.

* Add storybook tests

* Add vite tests

* Export the view from shared-components package

* Add a viewmodel for driving the view

* Support rendering the tile in the tile factory

* Fix tile rendering

* Add comment to explain css height

* Use semantic token for gap

* Update snapshot

* Use min-height over height

This will scale the element if the user sets a custom font size.

* Don't show timestamp for call started tile

Timestamp is already shown as a part of the tile content.

* Fix broken tile on bubble layout

The tile should be full-width and not shown within a bubble.

* Fix tests

* Update storybook title
This commit is contained in:
R Midhun Suresh
2026-05-11 21:12:51 +00:00
committed by GitHub
parent c62acc6634
commit 2933b51fea
16 changed files with 448 additions and 1 deletions
@@ -199,6 +199,10 @@
"n_minutes_ago": "%(num)s minutes ago"
},
"timeline": {
"call_tile": {
"video_call_title": "Video call",
"voice_call_title": "Voice call"
},
"decryption_failure": {
"blocked": "The sender has blocked you from receiving this message because your device is unverified",
"historical_event_no_key_backup": "Historical messages are not available on this device",
+1
View File
@@ -39,6 +39,7 @@ export * from "./room/timeline/TimelineSeparator";
export * from "./room/timeline/event-tile/actions/ActionBarView";
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
export * from "./room/timeline/event-tile/call";
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
export * from "./room/timeline/event-tile/EventTileView/MKeyVerificationRequestView";
export * from "./room/timeline/event-tile/EventTileView/PinnedMessageBadge";
@@ -0,0 +1,30 @@
/*
* 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 {
/* This is the height of the tile as per design */
min-height: 39px;
width: 100%;
border: 1px solid var(--cpd-color-border-interactive-secondary);
border-radius: var(--cpd-space-2x);
padding: var(--cpd-space-2x) var(--cpd-space-3x);
box-sizing: border-box;
}
.title {
font: var(--cpd-font-body-md-semibold);
flex: 1;
}
.time {
font: var(--cpd-font-body-xs-regular);
color: var(--cpd-color-text-secondary);
}
.icon {
color: var(--cpd-color-icon-secondary);
}
@@ -0,0 +1,62 @@
/*
* 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 { CallStartedTileView, type CallStartedTileViewSnapshot, CallType } from "./CallStartedTileView";
import { useMockedViewModel } from "../../../../../core/viewmodel";
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
const CallStartedTileViewWrapperImpl = ({ ...rest }: CallStartedTileViewSnapshot): React.ReactNode => {
const vm = useMockedViewModel(rest, {});
return <CallStartedTileView vm={vm} />;
};
const CallStartedTileViewWrapper = withViewDocs(CallStartedTileViewWrapperImpl, CallStartedTileView);
const meta = {
title: "Timeline/Timeline Event/Call/CallStartedTileView",
component: CallStartedTileViewWrapper,
tags: ["autodocs"],
argTypes: {
type: {
options: [CallType.Video, CallType.Voice],
control: { type: "select" },
},
timestamp: {
control: { type: "text" },
},
},
args: {
type: CallType.Voice,
timestamp: "12:36",
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=11217-3901&t=OvT1LOc5wH4kXt0a-4",
},
},
} satisfies Meta<typeof CallStartedTileViewWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const VoiceCall: Story = {
args: {
type: CallType.Voice,
},
};
export const VideoCall: Story = {
args: {
type: CallType.Video,
},
};
@@ -0,0 +1,29 @@
/*
* 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 "./CallStartedTileView.stories";
const { VideoCall, VoiceCall } = composeStories(Stories);
describe("CallStartedTileView", () => {
describe("renders the tile", () => {
it("voice call", () => {
const { container } = render(<VoiceCall />);
expect(container).toMatchSnapshot();
});
it("video call", () => {
const { container } = render(<VideoCall />);
expect(container).toMatchSnapshot();
});
});
});
@@ -0,0 +1,77 @@
/*
* 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 { VideoCallSolidIcon, VoiceCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import classnames from "classnames";
import { useViewModel, type ViewModel } from "../../../../../core/viewmodel";
import { Flex } from "../../../../../core/utils/Flex";
import styles from "./CallStartedTileView.module.css";
import { useI18n } from "../../../../../core/i18n/i18nContext";
/**
* Represents whether a call is a voice call or video call.
*/
export const enum CallType {
/**
* This is a voice call.
*/
Voice = "voice",
/**
* This is a video call.
*/
Video = "video",
}
export type CallStartedTileViewSnapshot = {
/**
* What type of call this tile needs to render for.
*/
type: CallType;
/**
* Time when this call was started.
*/
timestamp: string;
};
export type CallStartedTileViewModel = ViewModel<CallStartedTileViewSnapshot>;
export interface CallStartedTileViewProps {
vm: CallStartedTileViewModel;
className?: string;
}
function getIconForCallType(type: CallType): React.ReactNode {
switch (type) {
case CallType.Video:
return <VideoCallSolidIcon className={styles.icon} width={20} height={20} />;
case CallType.Voice:
return <VoiceCallSolidIcon className={styles.icon} width={20} height={20} />;
}
}
/**
* View for a timeline tile that indicates the start of an element call.
*/
export function CallStartedTileView({ vm, className }: CallStartedTileViewProps): React.ReactNode {
const { translate: _t } = useI18n();
const { type, timestamp } = useViewModel(vm);
const classNames = classnames(className, styles.container);
return (
<Flex className={classNames} align="center" gap="var(--cpd-space-2x)">
{getIconForCallType(type)}
<div className={styles.title}>
{type === CallType.Voice
? _t("timeline|call_tile|voice_call_title")
: _t("timeline|call_tile|video_call_title")}
</div>
<div className={styles.time}>{timestamp}</div>
</Flex>
);
}
@@ -0,0 +1,65 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CallStartedTileView > renders the tile > video call 1`] = `
<div>
<div
class="Flex-module_flex CallStartedTileView-module_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<svg
class="CallStartedTileView-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
class="CallStartedTileView-module_title"
>
Video call
</div>
<div
class="CallStartedTileView-module_time"
>
12:36
</div>
</div>
</div>
`;
exports[`CallStartedTileView > renders the tile > voice call 1`] = `
<div>
<div
class="Flex-module_flex CallStartedTileView-module_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<svg
class="CallStartedTileView-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
class="CallStartedTileView-module_title"
>
Voice call
</div>
<div
class="CallStartedTileView-module_time"
>
12:36
</div>
</div>
</div>
`;
@@ -0,0 +1,8 @@
/*
* 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.
*/
export * from "./CallStartedTile/CallStartedTileView";