Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 27 additions & 45 deletions res/css/views/toasts/_IncomingCallToast.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,52 @@ Please see LICENSE files in the repository root for full details.
display: flex;
flex-direction: row;
pointer-events: initial; /* restore pointer events so the user can accept/decline */
width: 250px;

$closeButtonSize: 16px;
$closeButtonSize: var(--cpd-space-4x);

.mx_IncomingCallToast_content {
display: flex;
flex-direction: column;
margin-left: 8px;
gap: var(--cpd-space-4x);
padding: var(--cpd-space-3x);
width: 100%;
overflow: hidden;

.mx_IncomingCallToast_info {
margin-bottom: $spacing-16;

.mx_IncomingCallToast_room {
display: inline-block;

font-weight: var(--cpd-font-weight-semibold);
font-size: $font-15px;
line-height: $font-24px;

/* Prevent overlap with the close button */
width: calc(100% - $closeButtonSize - 2 * $spacing-4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

margin-bottom: $spacing-4;
}

.mx_IncomingCallToast_message {
font-size: $font-12px;
line-height: $font-15px;

margin-bottom: $spacing-4;
}
.mx_IncomingCallToast_message {
font-size: $font-17px;
line-height: $font-20px;
width: calc(100% - $closeButtonSize - 2 * var(--cpd-space-1x));
font-weight: var(--cpd-font-weight-semibold);
}

.mx_LiveContentSummary {
font-size: $font-12px;
line-height: $font-15px;
.mx_LiveContentSummary_participants::before {
width: 15px;
height: 15px;
}

.mx_LiveContentSummary_participants::before {
width: 15px;
height: 15px;
}
}
.mx_IncomingCallToast_buttons {
display: flex;
gap: var(--cpd-space-2x);
}

.mx_IncomingCallToast_joinButton {
.mx_IncomingCallToast_actionButton {
position: relative;

bottom: $spacing-4;
right: $spacing-4;

align-self: flex-end;

box-sizing: border-box;
min-width: 120px;

padding: $spacing-4 0;

line-height: $font-24px;
padding: var(--cpd-space-1x) 0;
padding-right: var(--cpd-space-4x);
line-height: var(--cpd-space-6x);
}
}

.mx_IncomingCallToast_closeButton {
position: absolute;

top: $spacing-4;
right: $spacing-4;
right: 0;

display: flex;
height: $closeButtonSize;
Expand All @@ -99,4 +75,10 @@ Please see LICENSE files in the repository root for full details.
mask-position: center;
}
}
.mx_IncomingCallToast_toggleWithLabel {
display: flex;
span {
flex-grow: 1;
}
}
}
38 changes: 15 additions & 23 deletions src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { type PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import { type IRTCNotificationContent, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";

import { MatrixClientPeg } from "./MatrixClientPeg";
import { PosthogAnalytics } from "./PosthogAnalytics";
Expand All @@ -45,7 +45,7 @@ import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import { getIncomingCallToastKey, getNotificationEventSendTs, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore";
import { stripPlainReply } from "./utils/Reply";
import { BackgroundAudio } from "./audio/BackgroundAudio";
Expand Down Expand Up @@ -486,41 +486,33 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
private performCustomEventHandling(ev: MatrixEvent): void {
const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(ev.getRoomId());
const type = ev.getType();
const thisUserHasConnectedDevice =
room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId());

if (EventType.GroupCallMemberPrefix === type && thisUserHasConnectedDevice) {
const content = ev.getContent();

if (typeof content.call_id !== "string") {
logger.warn(
"Received malformatted GroupCallMemberPrefix event. Did not contain 'call_id' of type 'string'",
);
return;
}
// One of our devices has joined the call, so dismiss it.
ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(content.call_id, room.roomId));
}
// Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification
else if (EventType.CallNotify === type && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
const content = ev.getContent();
if (EventType.RTCNotification === ev.getType() && !thisUserHasConnectedDevice) {
const content = ev.getContent() as IRTCNotificationContent;
const roomId = ev.getRoomId();
const eventId = ev.getId();

if (typeof content.call_id !== "string") {
logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
// Check maximum age of a call notification event that will trigger a ringing notification
if (Date.now() - getNotificationEventSendTs(ev) > content.lifetime) {
logger.warn("Received outdated RTCNotification event.");
return;
}
if (!roomId) {
logger.warn("Could not get roomId for CallNotify event");
logger.warn("Could not get roomId for RTCNotification event");
return;
}
if (!eventId) {
logger.warn("Could not get eventId for RTCNotification event");
return;
}
ToastStore.sharedInstance().addOrReplaceToast({
key: getIncomingCallToastKey(content.call_id, roomId),
key: getIncomingCallToastKey(eventId, roomId),
priority: 100,
component: IncomingCallToast,
bodyClassName: "mx_IncomingCallToast",
props: { notifyEvent: ev },
props: { notificationEvent: ev },
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3989,6 +3989,7 @@
"connection_lost": "Connectivity to the server has been lost",
"connection_lost_description": "You cannot place calls without a connection to the server.",
"consulting": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
"decline_call": "Decline",
"default_device": "Default Device",
"dial": "Dial",
"dialpad": "Dialpad",
Expand Down Expand Up @@ -4040,6 +4041,7 @@
"show_sidebar_button": "Show sidebar",
"silence": "Silence call",
"silenced": "Notifications silenced",
"skip_lobby_toggle_option": "Join immediately",
"start_screenshare": "Start sharing your screen",
"stop_screenshare": "Stop sharing your screen",
"too_many_calls": "Too Many Calls",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2025 New Vector 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.
*/

.avatarWithDetails {
display: flex;
align-items: center;

border-radius: 12px;
background-color: var(--cpd-color-gray-200);
padding: var(--cpd-space-2x);
gap: var(--cpd-space-2x);

.room {
display: inline-block;

font-weight: var(--cpd-font-weight-semibold);
font-size: var(--cpd-font-size-body-md);

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.details {
font-size: var(--cpd-font-size-body-sm);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2025 New Vector 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 { AvatarWithDetails } from "./AvatarWithDetails";
import type { Meta, StoryFn } from "@storybook/react-vite";

export default {
title: "Avatar/AvatarWithDetails",
component: AvatarWithDetails,
tags: ["autodocs"],
args: {
avatar: <div style={{ width: 40, height: 40, backgroundColor: "#888", borderRadius: "50%" }} />,
details: "Details about the avatar go here",
roomName: "Room Name",
},
} as Meta<typeof AvatarWithDetails>;

const Template: StoryFn<typeof AvatarWithDetails> = (args) => <AvatarWithDetails {...args} />;

export const Default = Template.bind({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright 2025 New Vector 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 { render } from "jest-matrix-react";
import React from "react";

import * as stories from "./AvatarWithDetails.stories.tsx";

const { Default } = composeStories(stories);

describe("AvatarWithDetails", () => {
it("renders a textual event", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2025 New Vector 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 ComponentProps, type ElementType, type JSX, type PropsWithChildren } from "react";
import React from "react";
import classNames from "classnames";

import styles from "./AvatarWithDetails.module.css";
import { Flex } from "../../utils/Flex";

export type AvatarWithDetailsProps<C extends ElementType> = {
/**
* The HTML tag.
* @default "div"
*/
as?: C;
/**
* The CSS class name.
*/
className?: string;
roomName: string;
avatar: React.ReactNode;
details: React.ReactNode;
} & ComponentProps<C>;

/**
* A component to display the body of a media message.
*
* @example
* ```tsx
* <MediaBody as="p" className="custom-class">Media body content</MediaBody>
* ```
*/
export function AvatarWithDetails<C extends React.ElementType = "div">({
as,
className,
details,
avatar,
roomName,
...props
}: PropsWithChildren<AvatarWithDetailsProps<C>>): JSX.Element {
const Component = as || "div";

// Keep Mx_MediaBody to support the compatibility with existing timeline and the all the layout
return (
<Component className={classNames(styles.avatarWithDetails, className)} {...props}>
{avatar}
<Flex direction="column">
<span className={styles.room}>{roomName}</span>
<span className={styles.details}>{details}</span>
</Flex>
</Component>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AvatarWithDetails renders a textual event 1`] = `
<div>
<div
class="avatarWithDetails"
>
<div
style="width: 40px; height: 40px; background-color: rgb(136, 136, 136); border-radius: 50%;"
/>
<div
class="flex"
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;"
>
<span
class="room"
>
Room Name
</span>
<span
class="details"
>
Details about the avatar go here
</span>
</div>
</div>
</div>
`;
8 changes: 8 additions & 0 deletions src/shared-components/avatar/AvatarWithDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2025 New Vector 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 { AvatarWithDetails } from "./AvatarWithDetails";
Loading
Loading