Call Tile - Support declined call tile (#33371)
* Extract shared types and css * Add CallDeclinedTileView * Add storybook and view tests * Support declined event in view model * Render declined view from tile factory * Update snapshots * Add 10px padding to top and bottom * Distinguish between call declined by us and other users * Support `isCallDeclinedByUs` in view model * Update tests * Add better comments * Rename getInitial to generateSnapshot
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
M_POLL_START,
|
M_POLL_START,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
|
CallDeclinedTileView,
|
||||||
CallStartedTileView,
|
CallStartedTileView,
|
||||||
EncryptionEventView,
|
EncryptionEventView,
|
||||||
HiddenBodyView,
|
HiddenBodyView,
|
||||||
@@ -56,7 +57,7 @@ import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/Te
|
|||||||
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
||||||
import { ViewSourceEventViewModel } from "../viewmodels/room/timeline/event-tile/body/ViewSourceEventViewModel";
|
import { ViewSourceEventViewModel } from "../viewmodels/room/timeline/event-tile/body/ViewSourceEventViewModel";
|
||||||
import { ElementCallEventType } from "../call-types";
|
import { ElementCallEventType } from "../call-types";
|
||||||
import { CallStartedTileViewModel } from "../viewmodels/room/timeline/event-tile/call/CallStartedTileViewModel";
|
import { CallTileViewModel } from "../viewmodels/room/timeline/event-tile/call/CallTileViewModel";
|
||||||
|
|
||||||
// Subset of EventTile's IProps plus some mixins
|
// Subset of EventTile's IProps plus some mixins
|
||||||
export interface EventTileTypeProps extends Pick<
|
export interface EventTileTypeProps extends Pick<
|
||||||
@@ -187,9 +188,9 @@ function RoomAvatarEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
const RoomAvatarEventFactory: Factory = (ref, props) => <RoomAvatarEventWrappedView ref={ref} {...props} />;
|
const RoomAvatarEventFactory: Factory = (ref, props) => <RoomAvatarEventWrappedView ref={ref} {...props} />;
|
||||||
|
|
||||||
function CallStartedTileViewWrapped({ mxEvent }: IBodyProps): JSX.Element {
|
function CallStartedTileViewWrapped({ mxEvent, getRelationsForEvent }: IBodyProps): JSX.Element {
|
||||||
const vm = useCreateAutoDisposedViewModel(() => new CallStartedTileViewModel({ mxEvent }));
|
const vm = useCreateAutoDisposedViewModel(() => new CallTileViewModel({ mxEvent, getRelationsForEvent }));
|
||||||
return <CallStartedTileView vm={vm} />;
|
return vm.isCallDeclined ? <CallDeclinedTileView vm={vm} /> : <CallStartedTileView vm={vm} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CallStartedEventFactory: Factory = (ref, props) => {
|
export const CallStartedEventFactory: Factory = (ref, props) => {
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { BaseViewModel, CallType, type CallStartedTileViewSnapshot } from "@element-hq/web-shared-components";
|
|
||||||
|
|
||||||
import type { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import type { IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
|
||||||
import { formatTime } from "../../../../../DateUtils";
|
|
||||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
|
||||||
import type { SettingUpdatedPayload } from "../../../../../dispatcher/payloads/SettingUpdatedPayload";
|
|
||||||
import type { ActionPayload } from "../../../../../dispatcher/payloads";
|
|
||||||
import { Action } from "../../../../../dispatcher/actions";
|
|
||||||
|
|
||||||
export interface CallStartedTileViewModelProps {
|
|
||||||
mxEvent: MatrixEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIntentFromEvent(event: MatrixEvent): CallStartedTileViewSnapshot["type"] {
|
|
||||||
const content = event.getContent<IRTCNotificationContent>();
|
|
||||||
const intentInContent = content["m.call.intent"];
|
|
||||||
switch (intentInContent) {
|
|
||||||
case "audio":
|
|
||||||
return CallType.Voice;
|
|
||||||
case "video":
|
|
||||||
default:
|
|
||||||
return CallType.Video;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimeFromEvent(event: MatrixEvent, showTwelveHour: boolean): CallStartedTileViewSnapshot["timestamp"] {
|
|
||||||
const content = event.getContent<IRTCNotificationContent>();
|
|
||||||
const senderTs = content["sender_ts"];
|
|
||||||
const originServerTs = event.getTs();
|
|
||||||
const ts = Math.abs(senderTs - originServerTs) > 20000 ? originServerTs : senderTs;
|
|
||||||
|
|
||||||
const date = new Date(ts);
|
|
||||||
const timestamp = formatTime(date, showTwelveHour);
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialSnapshot(event: MatrixEvent): CallStartedTileViewSnapshot {
|
|
||||||
const type = getIntentFromEvent(event);
|
|
||||||
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
|
||||||
const timestamp = getTimeFromEvent(event, showTwelveHour);
|
|
||||||
return { type, timestamp };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSettingsChangedPayload(payload: ActionPayload): payload is SettingUpdatedPayload {
|
|
||||||
return payload.action === Action.SettingUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel for a timeline tile that indicates the start of an element call.
|
|
||||||
*/
|
|
||||||
export class CallStartedTileViewModel extends BaseViewModel<
|
|
||||||
CallStartedTileViewSnapshot,
|
|
||||||
CallStartedTileViewModelProps
|
|
||||||
> {
|
|
||||||
public constructor(props: CallStartedTileViewModelProps) {
|
|
||||||
super(props, getInitialSnapshot(props.mxEvent));
|
|
||||||
SettingsStore.monitorSetting("showTwelveHourTimestamps", null);
|
|
||||||
const token = defaultDispatcher.register(this.onAction);
|
|
||||||
this.disposables.track(() => {
|
|
||||||
defaultDispatcher.unregister(token);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload): void => {
|
|
||||||
if (!isSettingsChangedPayload(payload) || payload.settingName !== "showTwelveHourTimestamps") return;
|
|
||||||
const showTwelveHour = (payload.newValue as boolean) ?? false;
|
|
||||||
const timestamp = getTimeFromEvent(this.props.mxEvent, showTwelveHour);
|
|
||||||
this.snapshot.merge({ timestamp });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 { BaseViewModel, CallType, type CallTileViewSnapshot } from "@element-hq/web-shared-components";
|
||||||
|
import { EventType, type MatrixEvent, MatrixEventEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
import { formatTime } from "../../../../../DateUtils";
|
||||||
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
|
import type { SettingUpdatedPayload } from "../../../../../dispatcher/payloads/SettingUpdatedPayload";
|
||||||
|
import type { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import type { GetRelationsForEvent } from "../../../../../components/views/rooms/EventTile";
|
||||||
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
|
|
||||||
|
export interface CallTileViewModelProps {
|
||||||
|
/**
|
||||||
|
* Event of type `org.matrix.msc4075.rtc.notification`.
|
||||||
|
*/
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
/**
|
||||||
|
* Helper to fetch related events from a given event.
|
||||||
|
*/
|
||||||
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntentFromEvent(event: MatrixEvent): CallTileViewSnapshot["type"] {
|
||||||
|
const content = event.getContent<IRTCNotificationContent>();
|
||||||
|
const intentInContent = content["m.call.intent"];
|
||||||
|
switch (intentInContent) {
|
||||||
|
case "audio":
|
||||||
|
return CallType.Voice;
|
||||||
|
case "video":
|
||||||
|
default:
|
||||||
|
return CallType.Video;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTs(event: MatrixEvent): number {
|
||||||
|
if (event.getType() === EventType.RTCNotification) {
|
||||||
|
/**
|
||||||
|
* According to the spec:
|
||||||
|
* Receivers SHOULD use origin_server_ts if |sender_ts - origin_server_ts| > 20000 ms.
|
||||||
|
*/
|
||||||
|
const content = event.getContent<IRTCNotificationContent>();
|
||||||
|
const senderTs = content["sender_ts"];
|
||||||
|
const originServerTs = event.getTs();
|
||||||
|
const ts = Math.abs(senderTs - originServerTs) > 20000 ? originServerTs : senderTs;
|
||||||
|
return ts;
|
||||||
|
} else return event.getTs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeFromEvent(event: MatrixEvent, showTwelveHour: boolean): CallTileViewSnapshot["timestamp"] {
|
||||||
|
const ts = getTs(event);
|
||||||
|
const date = new Date(ts);
|
||||||
|
const timestamp = formatTime(date, showTwelveHour);
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSnapshot(
|
||||||
|
event: MatrixEvent,
|
||||||
|
getRelationsForEvent?: GetRelationsForEvent,
|
||||||
|
): { snapshot: CallTileViewSnapshot; declineEvent: MatrixEvent | null } {
|
||||||
|
const type = getIntentFromEvent(event);
|
||||||
|
const declineEvent = getDeclinedEvent(event, getRelationsForEvent);
|
||||||
|
let isCallDeclinedByUs: boolean | undefined;
|
||||||
|
if (declineEvent) {
|
||||||
|
isCallDeclinedByUs = declineEvent.getSender() === MatrixClientPeg.get()?.getUserId();
|
||||||
|
}
|
||||||
|
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||||
|
const timestamp = getTimeFromEvent(declineEvent ?? event, showTwelveHour);
|
||||||
|
return { snapshot: { type, timestamp, isCallDeclinedByUs }, declineEvent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSettingsChangedPayload(payload: ActionPayload): payload is SettingUpdatedPayload {
|
||||||
|
return payload.action === Action.SettingUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a declined event that is related to the given rtc notification event.
|
||||||
|
* @param event rtc notification event
|
||||||
|
*/
|
||||||
|
function getDeclinedEvent(event: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): MatrixEvent | null {
|
||||||
|
const eventId = event.getId();
|
||||||
|
if (eventId && getRelationsForEvent) {
|
||||||
|
const relations = getRelationsForEvent(eventId, RelationType.Reference, EventType.RTCDecline)?.getRelations();
|
||||||
|
if (relations) return relations[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common view-model for call tiles; currently used to render:
|
||||||
|
* 1. A tile that indicates that a call occurred (call tombstone).
|
||||||
|
* 2. A tile that indicates that a call was declined.
|
||||||
|
*/
|
||||||
|
export class CallTileViewModel extends BaseViewModel<CallTileViewSnapshot, CallTileViewModelProps> {
|
||||||
|
/**
|
||||||
|
* The decline event associated with this call, if any.
|
||||||
|
*/
|
||||||
|
private declineEvent: MatrixEvent | null;
|
||||||
|
|
||||||
|
public constructor(props: CallTileViewModelProps) {
|
||||||
|
const { declineEvent, snapshot } = generateSnapshot(props.mxEvent, props.getRelationsForEvent);
|
||||||
|
super(props, snapshot);
|
||||||
|
this.declineEvent = declineEvent;
|
||||||
|
|
||||||
|
// Listen to the changes on settings so that we can update the timestamp format (12H vs 24H).
|
||||||
|
SettingsStore.monitorSetting("showTwelveHourTimestamps", null);
|
||||||
|
const token = defaultDispatcher.register(this.onAction);
|
||||||
|
this.disposables.track(() => {
|
||||||
|
defaultDispatcher.unregister(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a relation is added to the event, recompute the state.
|
||||||
|
this.disposables.trackListener(props.mxEvent, MatrixEventEvent.RelationsCreated, () => {
|
||||||
|
const { declineEvent, snapshot } = generateSnapshot(props.mxEvent, props.getRelationsForEvent);
|
||||||
|
this.declineEvent = declineEvent;
|
||||||
|
this.snapshot.set(snapshot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload): void => {
|
||||||
|
if (!isSettingsChangedPayload(payload) || payload.settingName !== "showTwelveHourTimestamps") return;
|
||||||
|
const showTwelveHour = (payload.newValue as boolean) ?? false;
|
||||||
|
const timestamp = getTimeFromEvent(this.declineEvent ?? this.props.mxEvent, showTwelveHour);
|
||||||
|
this.snapshot.merge({ timestamp });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the call associated with this vm has been declined.
|
||||||
|
*/
|
||||||
|
public get isCallDeclined(): boolean {
|
||||||
|
return !!this.declineEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { CallType } from "@element-hq/web-shared-components";
|
|
||||||
import { waitFor } from "jest-matrix-react";
|
|
||||||
|
|
||||||
import { mkEvent } from "../../test-utils";
|
|
||||||
import { CallStartedTileViewModel } from "../../../src/viewmodels/room/timeline/event-tile/call/CallStartedTileViewModel";
|
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
|
|
||||||
function getMockedRtcNotificationEvent(intent: string, senderTs: number, serverTs: number): MatrixEvent {
|
|
||||||
const mockEvent = mkEvent({
|
|
||||||
type: EventType.RTCNotification,
|
|
||||||
user: "@foo:m.org",
|
|
||||||
content: {
|
|
||||||
"m.call.intent": intent,
|
|
||||||
"sender_ts": senderTs,
|
|
||||||
},
|
|
||||||
ts: serverTs,
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
return mockEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("CallStartedTileViewModel", () => {
|
|
||||||
it("should set voice intent in state", () => {
|
|
||||||
const mxEvent = getMockedRtcNotificationEvent("audio", 1752583130365, 1752583130365);
|
|
||||||
const vm = new CallStartedTileViewModel({ mxEvent });
|
|
||||||
const { type } = vm.getSnapshot();
|
|
||||||
expect(type).toStrictEqual(CallType.Voice);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set video intent in state", () => {
|
|
||||||
const mxEvent = getMockedRtcNotificationEvent("video", 1752583130365, 1752583130365);
|
|
||||||
const vm = new CallStartedTileViewModel({ mxEvent });
|
|
||||||
const { type } = vm.getSnapshot();
|
|
||||||
expect(type).toStrictEqual(CallType.Video);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should calculate time string correctly", () => {
|
|
||||||
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
|
||||||
const vm = new CallStartedTileViewModel({ mxEvent });
|
|
||||||
const { timestamp } = vm.getSnapshot();
|
|
||||||
expect(timestamp).toStrictEqual("17:55");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should calculate time string correctly when configured to use 12 hour format", async () => {
|
|
||||||
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
|
||||||
await SettingsStore.setValue("showTwelveHourTimestamps", null, SettingLevel.DEVICE, true);
|
|
||||||
const vm = new CallStartedTileViewModel({ mxEvent });
|
|
||||||
const { timestamp } = vm.getSnapshot();
|
|
||||||
expect(timestamp).toStrictEqual("5:55 PM");
|
|
||||||
SettingsStore.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should change timestamp format when setting is modified", async () => {
|
|
||||||
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
|
||||||
const vm = new CallStartedTileViewModel({ mxEvent });
|
|
||||||
expect(vm.getSnapshot().timestamp).toStrictEqual("17:55");
|
|
||||||
await SettingsStore.setValue("showTwelveHourTimestamps", null, SettingLevel.DEVICE, true);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(vm.getSnapshot().timestamp).toStrictEqual("5:55 PM");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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 { EventType, type MatrixEvent, MatrixEventEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CallType } from "@element-hq/web-shared-components";
|
||||||
|
import { waitFor } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import { mkEvent, stubClient } from "../../test-utils";
|
||||||
|
import { CallTileViewModel } from "../../../src/viewmodels/room/timeline/event-tile/call/CallTileViewModel";
|
||||||
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
|
function getMockedRtcNotificationEvent(intent: string, senderTs: number, serverTs: number): MatrixEvent {
|
||||||
|
const mockEvent = mkEvent({
|
||||||
|
type: EventType.RTCNotification,
|
||||||
|
user: "@foo:m.org",
|
||||||
|
content: {
|
||||||
|
"m.call.intent": intent,
|
||||||
|
"sender_ts": senderTs,
|
||||||
|
},
|
||||||
|
ts: serverTs,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
return mockEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMockedRtcDeclineEvent(rtcNotificationEvent: MatrixEvent, sender = "@foo:m.org"): MatrixEvent {
|
||||||
|
const mockEvent = mkEvent({
|
||||||
|
type: EventType.RTCDecline,
|
||||||
|
user: sender,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.reference",
|
||||||
|
event_id: rtcNotificationEvent.getId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ts: 924285416000,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
return mockEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CallTileViewModel", () => {
|
||||||
|
it("should set voice intent in state", () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("audio", 1752583130365, 1752583130365);
|
||||||
|
const vm = new CallTileViewModel({ mxEvent });
|
||||||
|
const { type } = vm.getSnapshot();
|
||||||
|
expect(type).toStrictEqual(CallType.Voice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set video intent in state", () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("video", 1752583130365, 1752583130365);
|
||||||
|
const vm = new CallTileViewModel({ mxEvent });
|
||||||
|
const { type } = vm.getSnapshot();
|
||||||
|
expect(type).toStrictEqual(CallType.Video);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should calculate time string correctly", () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
||||||
|
const vm = new CallTileViewModel({ mxEvent });
|
||||||
|
const { timestamp } = vm.getSnapshot();
|
||||||
|
expect(timestamp).toStrictEqual("17:55");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should calculate time string correctly when configured to use 12 hour format", async () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
||||||
|
await SettingsStore.setValue("showTwelveHourTimestamps", null, SettingLevel.DEVICE, true);
|
||||||
|
const vm = new CallTileViewModel({ mxEvent });
|
||||||
|
const { timestamp } = vm.getSnapshot();
|
||||||
|
expect(timestamp).toStrictEqual("5:55 PM");
|
||||||
|
SettingsStore.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change timestamp format when setting is modified", async () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("video", 924285348000, 924285348000);
|
||||||
|
const vm = new CallTileViewModel({ mxEvent });
|
||||||
|
expect(vm.getSnapshot().timestamp).toStrictEqual("17:55");
|
||||||
|
await SettingsStore.setValue("showTwelveHourTimestamps", null, SettingLevel.DEVICE, true);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vm.getSnapshot().timestamp).toStrictEqual("5:55 PM");
|
||||||
|
});
|
||||||
|
SettingsStore.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("On call declined", () => {
|
||||||
|
it("should calculate isCallDeclined correctly", () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("audio", 1752583130365, 1752583130365);
|
||||||
|
// When there's no decline event, isCallDeclined = false
|
||||||
|
const vm1 = new CallTileViewModel({ mxEvent, getRelationsForEvent: jest.fn() });
|
||||||
|
expect(vm1.isCallDeclined).toStrictEqual(false);
|
||||||
|
|
||||||
|
// When there's a decline event, isCallDeclined = true
|
||||||
|
const declineEvent = getMockedRtcDeclineEvent(mxEvent);
|
||||||
|
const getRelationsForEvent = jest.fn().mockReturnValue({
|
||||||
|
getRelations: () => [declineEvent],
|
||||||
|
});
|
||||||
|
const vm2 = new CallTileViewModel({ mxEvent, getRelationsForEvent });
|
||||||
|
expect(vm2.isCallDeclined).toStrictEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should calculate isCallDeclinedByUs correctly", () => {
|
||||||
|
const cli = stubClient();
|
||||||
|
cli.getUserId = jest.fn().mockReturnValue("@bar:m.org");
|
||||||
|
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("audio", 924285348000, 924285348000);
|
||||||
|
const declineEvent: MatrixEvent[] = [];
|
||||||
|
const getRelationsForEvent = jest.fn().mockReturnValue({
|
||||||
|
getRelations: () => declineEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Decline event sent by somebody else
|
||||||
|
declineEvent.push(getMockedRtcDeclineEvent(mxEvent));
|
||||||
|
const vm = new CallTileViewModel({ mxEvent, getRelationsForEvent });
|
||||||
|
expect(vm.getSnapshot().isCallDeclinedByUs).toStrictEqual(false);
|
||||||
|
|
||||||
|
// Decline event sent by us
|
||||||
|
declineEvent.pop();
|
||||||
|
declineEvent.push(getMockedRtcDeclineEvent(mxEvent, MatrixClientPeg.get()!.getUserId()!));
|
||||||
|
const vm2 = new CallTileViewModel({ mxEvent, getRelationsForEvent });
|
||||||
|
expect(vm2.getSnapshot().isCallDeclinedByUs).toStrictEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should recompute state when call is declined", () => {
|
||||||
|
const mxEvent = getMockedRtcNotificationEvent("audio", 924285348000, 924285348000);
|
||||||
|
const declineEvent: MatrixEvent[] = [];
|
||||||
|
const getRelationsForEvent = jest.fn().mockReturnValue({
|
||||||
|
getRelations: () => declineEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
// No decline event yet, so timestamp should be based on rtc notification event.
|
||||||
|
const vm = new CallTileViewModel({ mxEvent, getRelationsForEvent });
|
||||||
|
expect(vm.getSnapshot().timestamp).toStrictEqual("17:55");
|
||||||
|
|
||||||
|
// Decline event arrives, timestamp should update to be that of the decline event.
|
||||||
|
declineEvent.push(getMockedRtcDeclineEvent(mxEvent));
|
||||||
|
mxEvent.emit(MatrixEventEvent.RelationsCreated, RelationType.Reference, EventType.RTCDecline);
|
||||||
|
expect(vm.getSnapshot().timestamp).toStrictEqual("17:56");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -203,6 +203,10 @@
|
|||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"call_tile": {
|
"call_tile": {
|
||||||
|
"declined": {
|
||||||
|
"call_declined": "Call declined",
|
||||||
|
"call_declined_by_us": "You declined a call"
|
||||||
|
},
|
||||||
"video_call_title": "Video call",
|
"video_call_title": "Video call",
|
||||||
"voice_call_title": "Voice call"
|
"voice_call_title": "Voice call"
|
||||||
},
|
},
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||||
|
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||||
|
import { CallType, type CallTileViewSnapshot } from "../common/types";
|
||||||
|
import { CallDeclinedTileView } from "./CallDeclinedTileView";
|
||||||
|
|
||||||
|
const CallDeclinedTileViewWrapperImpl = ({ ...rest }: CallTileViewSnapshot): React.ReactNode => {
|
||||||
|
const vm = useMockedViewModel(rest, {});
|
||||||
|
return <CallDeclinedTileView vm={vm} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallDeclinedTileViewWrapper = withViewDocs(CallDeclinedTileViewWrapperImpl, CallDeclinedTileView);
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Timeline/Timeline Event/Call/CallDeclinedTileView",
|
||||||
|
component: CallDeclinedTileViewWrapper,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
argTypes: {
|
||||||
|
type: {
|
||||||
|
options: [CallType.Video, CallType.Voice],
|
||||||
|
control: { type: "select" },
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
type: CallType.Voice,
|
||||||
|
timestamp: "12:36",
|
||||||
|
isCallDeclinedByUs: false,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=11217-3914&t=jv0JnUoKJUW1Ko96-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CallDeclinedTileViewWrapper>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CallDeclinedByUs: Story = {
|
||||||
|
args: {
|
||||||
|
type: CallType.Voice,
|
||||||
|
isCallDeclinedByUs: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
+34
@@ -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 "./CallDeclinedTileView.stories";
|
||||||
|
|
||||||
|
const { VideoCall, VoiceCall, CallDeclinedByUs } = composeStories(Stories);
|
||||||
|
|
||||||
|
describe("CallDeclinedTileView", () => {
|
||||||
|
describe("renders the tile", () => {
|
||||||
|
it("voice call", () => {
|
||||||
|
const { container } = render(<VoiceCall />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("video call", () => {
|
||||||
|
const { container } = render(<VideoCall />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("call declined by us", () => {
|
||||||
|
const { container } = render(<CallDeclinedByUs />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
VideoCallDeclinedSolidIcon,
|
||||||
|
VoiceCallDeclinedSolidIcon,
|
||||||
|
} 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 "../common/CallTileView.module.css";
|
||||||
|
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
||||||
|
import { type CallTileViewSnapshot, CallType } from "../common/types";
|
||||||
|
|
||||||
|
export type CallDeclinedTileViewModel = ViewModel<CallTileViewSnapshot>;
|
||||||
|
|
||||||
|
export interface CallDeclinedTileViewProps {
|
||||||
|
vm: CallDeclinedTileViewModel;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconForCallType(type: CallType): React.ReactNode {
|
||||||
|
switch (type) {
|
||||||
|
case CallType.Video:
|
||||||
|
return <VideoCallDeclinedSolidIcon className={styles.icon} width={20} height={20} />;
|
||||||
|
case CallType.Voice:
|
||||||
|
return <VoiceCallDeclinedSolidIcon className={styles.icon} width={20} height={20} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View for a timeline tile that indicates that a call was declined.
|
||||||
|
*/
|
||||||
|
export function CallDeclinedTileView({ vm, className }: CallDeclinedTileViewProps): React.ReactNode {
|
||||||
|
const { translate: _t } = useI18n();
|
||||||
|
const { type, timestamp, isCallDeclinedByUs } = 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}>
|
||||||
|
{isCallDeclinedByUs
|
||||||
|
? _t("timeline|call_tile|declined|call_declined_by_us")
|
||||||
|
: _t("timeline|call_tile|declined|call_declined")}
|
||||||
|
</div>
|
||||||
|
<div className={styles.time}>{timestamp}</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`CallDeclinedTileView > renders the tile > call declined by us 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Flex-module_flex CallTileView-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="CallTileView-module_icon"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.623 3.04a1.07 1.07 0 0 1 1.086.929l.542 3.954q.039.27-.038.504a1.1 1.1 0 0 1-.272.427l-1.64 1.64Q7.806 11.5 8.456 12.4c.433.601 1.444 1.697 1.444 1.697.013.012 1.098 1.014 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.194-.194.426-.27a1.1 1.1 0 0 1 .504-.04l3.953.543q.407.058.67.358.26.301.26.728l.04 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.116-6.56q0-.426.329-.756Q3.67 3 4.095 3zM20.25 3q.405 0 .707.3.3.301.3.708t-.3.707l-1.414 1.414 1.414 1.414q.3.3.3.707t-.3.707-.707.3-.707-.3l-1.414-1.414-1.414 1.414q-.3.3-.707.3t-.707-.3T15 8.25q0-.406.3-.707l1.415-1.414L15.3 4.715q-.3-.3-.301-.707 0-.407.3-.707t.71-.301q.405 0 .707.3l1.414 1.415L19.543 3.3q.3-.3.707-.301"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_title"
|
||||||
|
>
|
||||||
|
You declined a call
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_time"
|
||||||
|
>
|
||||||
|
12:36
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CallDeclinedTileView > renders the tile > video call 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Flex-module_flex CallTileView-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="CallTileView-module_icon"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 8a4 4 0 0 1 4-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-4zm10.828 6.828q.3-.3.3-.707 0-.405-.3-.707L11.414 12l1.414-1.414q.3-.3.3-.707t-.3-.707-.707-.301q-.405 0-.707.3L10 10.587 8.586 9.172q-.3-.3-.707-.301-.407 0-.707.3t-.3.708q0 .405.3.707L8.586 12l-1.414 1.414q-.3.3-.3.707t.3.707.707.3q.405 0 .707-.3L10 13.414l1.414 1.414q.3.3.707.3t.707-.3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_title"
|
||||||
|
>
|
||||||
|
Call declined
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_time"
|
||||||
|
>
|
||||||
|
12:36
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CallDeclinedTileView > renders the tile > voice call 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Flex-module_flex CallTileView-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="CallTileView-module_icon"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.623 3.04a1.07 1.07 0 0 1 1.086.929l.542 3.954q.039.27-.038.504a1.1 1.1 0 0 1-.272.427l-1.64 1.64Q7.806 11.5 8.456 12.4c.433.601 1.444 1.697 1.444 1.697.013.012 1.098 1.014 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.194-.194.426-.27a1.1 1.1 0 0 1 .504-.04l3.953.543q.407.058.67.358.26.301.26.728l.04 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.116-6.56q0-.426.329-.756Q3.67 3 4.095 3zM20.25 3q.405 0 .707.3.3.301.3.708t-.3.707l-1.414 1.414 1.414 1.414q.3.3.3.707t-.3.707-.707.3-.707-.3l-1.414-1.414-1.414 1.414q-.3.3-.707.3t-.707-.3T15 8.25q0-.406.3-.707l1.415-1.414L15.3 4.715q-.3-.3-.301-.707 0-.407.3-.707t.71-.301q.405 0 .707.3l1.414 1.415L19.543 3.3q.3-.3.707-.301"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_title"
|
||||||
|
>
|
||||||
|
Call declined
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="CallTileView-module_time"
|
||||||
|
>
|
||||||
|
12:36
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
+3
-2
@@ -8,11 +8,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { CallStartedTileView, type CallStartedTileViewSnapshot, CallType } from "./CallStartedTileView";
|
import { CallStartedTileView } from "./CallStartedTileView";
|
||||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||||
|
import { CallType, type CallTileViewSnapshot } from "../common/types";
|
||||||
|
|
||||||
const CallStartedTileViewWrapperImpl = ({ ...rest }: CallStartedTileViewSnapshot): React.ReactNode => {
|
const CallStartedTileViewWrapperImpl = ({ ...rest }: CallTileViewSnapshot): React.ReactNode => {
|
||||||
const vm = useMockedViewModel(rest, {});
|
const vm = useMockedViewModel(rest, {});
|
||||||
return <CallStartedTileView vm={vm} />;
|
return <CallStartedTileView vm={vm} />;
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-27
@@ -11,35 +11,11 @@ import classnames from "classnames";
|
|||||||
|
|
||||||
import { useViewModel, type ViewModel } from "../../../../../core/viewmodel";
|
import { useViewModel, type ViewModel } from "../../../../../core/viewmodel";
|
||||||
import { Flex } from "../../../../../core/utils/Flex";
|
import { Flex } from "../../../../../core/utils/Flex";
|
||||||
import styles from "./CallStartedTileView.module.css";
|
import styles from "../common/CallTileView.module.css";
|
||||||
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
||||||
|
import { type CallTileViewSnapshot, CallType } from "../common/types";
|
||||||
|
|
||||||
/**
|
export type CallStartedTileViewModel = ViewModel<CallTileViewSnapshot>;
|
||||||
* 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 {
|
export interface CallStartedTileViewProps {
|
||||||
vm: CallStartedTileViewModel;
|
vm: CallStartedTileViewModel;
|
||||||
|
|||||||
+8
-8
@@ -3,11 +3,11 @@
|
|||||||
exports[`CallStartedTileView > renders the tile > video call 1`] = `
|
exports[`CallStartedTileView > renders the tile > video call 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="Flex-module_flex CallStartedTileView-module_container"
|
class="Flex-module_flex CallTileView-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;"
|
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
|
<svg
|
||||||
class="CallStartedTileView-module_icon"
|
class="CallTileView-module_icon"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -19,12 +19,12 @@ exports[`CallStartedTileView > renders the tile > video call 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
class="CallStartedTileView-module_title"
|
class="CallTileView-module_title"
|
||||||
>
|
>
|
||||||
Video call
|
Video call
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="CallStartedTileView-module_time"
|
class="CallTileView-module_time"
|
||||||
>
|
>
|
||||||
12:36
|
12:36
|
||||||
</div>
|
</div>
|
||||||
@@ -35,11 +35,11 @@ exports[`CallStartedTileView > renders the tile > video call 1`] = `
|
|||||||
exports[`CallStartedTileView > renders the tile > voice call 1`] = `
|
exports[`CallStartedTileView > renders the tile > voice call 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="Flex-module_flex CallStartedTileView-module_container"
|
class="Flex-module_flex CallTileView-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;"
|
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
|
<svg
|
||||||
class="CallStartedTileView-module_icon"
|
class="CallTileView-module_icon"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -51,12 +51,12 @@ exports[`CallStartedTileView > renders the tile > voice call 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
class="CallStartedTileView-module_title"
|
class="CallTileView-module_title"
|
||||||
>
|
>
|
||||||
Voice call
|
Voice call
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="CallStartedTileView-module_time"
|
class="CallTileView-module_time"
|
||||||
>
|
>
|
||||||
12:36
|
12:36
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@
|
|||||||
border-radius: var(--cpd-space-2x);
|
border-radius: var(--cpd-space-2x);
|
||||||
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The snapshot that both the call started and call declined tiles expect.
|
||||||
|
*/
|
||||||
|
export type CallTileViewSnapshot = {
|
||||||
|
/**
|
||||||
|
* What type of call this tile needs to render for.
|
||||||
|
*/
|
||||||
|
type: CallType;
|
||||||
|
/**
|
||||||
|
* Time when this call was started.
|
||||||
|
*/
|
||||||
|
timestamp: string;
|
||||||
|
/**
|
||||||
|
* Whether this call was declined by our user.
|
||||||
|
* Undefined if not rendering a declined call tile.
|
||||||
|
*/
|
||||||
|
isCallDeclinedByUs?: boolean;
|
||||||
|
};
|
||||||
@@ -6,3 +6,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./CallStartedTile/CallStartedTileView";
|
export * from "./CallStartedTile/CallStartedTileView";
|
||||||
|
export * from "./CallDeclinedTile/CallDeclinedTileView";
|
||||||
|
export * from "./common/types";
|
||||||
|
|||||||
Reference in New Issue
Block a user