Skip to content

Commit 0783f27

Browse files
authored
Add decline button to call notification toast (use new notification event) (#30729)
* Add decline button to call notification toast (use new notification event) - This make EW incompatible with the old style notify events. Signed-off-by: Timo K <[email protected]> * update styling for call toast Signed-off-by: Timo K <[email protected]> * skip lobby on join button click / dont skip lobby on toast click Signed-off-by: Timo K <[email protected]> * dismiss toast on remote decline Signed-off-by: Timo K <[email protected]> * fixup docstring and event_id Signed-off-by: Timo K <[email protected]> * Add tests Signed-off-by: Timo K <[email protected]> * remove unused var Signed-off-by: Timo K <[email protected]> * test that decline event gets sent Signed-off-by: Timo K <[email protected]> * make "go to lobby" accessible via keyboard (fix sonar cloud) Signed-off-by: Timo K <[email protected]> * remove keyboard input Signed-off-by: Timo K <[email protected]> * fix lint Signed-off-by: Timo K <[email protected]> * use actual button Signed-off-by: Timo K <[email protected]> * review style + toggle for join immediately Signed-off-by: Timo K <[email protected]> * fix `getNotificationEventSendTs` Signed-off-by: Timo K <[email protected]> * use story component Signed-off-by: Timo K <[email protected]> * english text Signed-off-by: Timo K <[email protected]> * dont use legacy toggle Signed-off-by: Timo K <[email protected]> * fix lint Signed-off-by: Timo K <[email protected]> * review Signed-off-by: Timo K <[email protected]> * review (mostly docs) Signed-off-by: Timo K <[email protected]> --------- Signed-off-by: Timo K <[email protected]>
1 parent 3c13f55 commit 0783f27

File tree

14 files changed

+635
-190
lines changed

14 files changed

+635
-190
lines changed
9.51 KB
Loading

res/css/views/toasts/_IncomingCallToast.pcss

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,76 +11,52 @@ Please see LICENSE files in the repository root for full details.
1111
display: flex;
1212
flex-direction: row;
1313
pointer-events: initial; /* restore pointer events so the user can accept/decline */
14-
width: 250px;
1514

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

1817
.mx_IncomingCallToast_content {
1918
display: flex;
2019
flex-direction: column;
21-
margin-left: 8px;
20+
gap: var(--cpd-space-4x);
21+
padding: var(--cpd-space-3x);
2222
width: 100%;
2323
overflow: hidden;
2424

25-
.mx_IncomingCallToast_info {
26-
margin-bottom: $spacing-16;
27-
28-
.mx_IncomingCallToast_room {
29-
display: inline-block;
30-
31-
font-weight: var(--cpd-font-weight-semibold);
32-
font-size: $font-15px;
33-
line-height: $font-24px;
34-
35-
/* Prevent overlap with the close button */
36-
width: calc(100% - $closeButtonSize - 2 * $spacing-4);
37-
overflow: hidden;
38-
text-overflow: ellipsis;
39-
white-space: nowrap;
40-
41-
margin-bottom: $spacing-4;
42-
}
43-
44-
.mx_IncomingCallToast_message {
45-
font-size: $font-12px;
46-
line-height: $font-15px;
47-
48-
margin-bottom: $spacing-4;
49-
}
25+
.mx_IncomingCallToast_message {
26+
font-size: var(--cpd-font-size-body-lg);
27+
line-height: var(--cpd-font-size-heading-sm);
28+
width: calc(100% - $closeButtonSize - 2 * var(--cpd-space-1x));
29+
font-weight: var(--cpd-font-weight-semibold);
30+
}
5031

51-
.mx_LiveContentSummary {
52-
font-size: $font-12px;
53-
line-height: $font-15px;
32+
.mx_LiveContentSummary_participants::before {
33+
width: 15px;
34+
height: 15px;
35+
}
5436

55-
.mx_LiveContentSummary_participants::before {
56-
width: 15px;
57-
height: 15px;
58-
}
59-
}
37+
.mx_IncomingCallToast_buttons {
38+
display: flex;
39+
gap: var(--cpd-space-2x);
6040
}
6141

62-
.mx_IncomingCallToast_joinButton {
42+
.mx_IncomingCallToast_actionButton {
6343
position: relative;
6444

65-
bottom: $spacing-4;
66-
right: $spacing-4;
67-
6845
align-self: flex-end;
6946

7047
box-sizing: border-box;
7148
min-width: 120px;
7249

73-
padding: $spacing-4 0;
74-
75-
line-height: $font-24px;
50+
padding: var(--cpd-space-1x) 0;
51+
padding-right: var(--cpd-space-4x);
52+
line-height: var(--cpd-space-6x);
7653
}
7754
}
7855

7956
.mx_IncomingCallToast_closeButton {
8057
position: absolute;
8158

82-
top: $spacing-4;
83-
right: $spacing-4;
59+
right: 0;
8460

8561
display: flex;
8662
height: $closeButtonSize;
@@ -99,4 +75,10 @@ Please see LICENSE files in the repository root for full details.
9975
mask-position: center;
10076
}
10177
}
78+
.mx_IncomingCallToast_toggleWithLabel {
79+
display: flex;
80+
span {
81+
flex-grow: 1;
82+
}
83+
}
10284
}

src/Notifier.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from "matrix-js-sdk/src/matrix";
2626
import { logger } from "matrix-js-sdk/src/logger";
2727
import { type PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
28-
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
28+
import { type IRTCNotificationContent, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
2929

3030
import { MatrixClientPeg } from "./MatrixClientPeg";
3131
import { PosthogAnalytics } from "./PosthogAnalytics";
@@ -45,7 +45,7 @@ import { mediaFromMxc } from "./customisations/Media";
4545
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
4646
import { SdkContextClass } from "./contexts/SDKContext";
4747
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
48-
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
48+
import { getIncomingCallToastKey, getNotificationEventSendTs, IncomingCallToast } from "./toasts/IncomingCallToast";
4949
import ToastStore from "./stores/ToastStore";
5050
import { stripPlainReply } from "./utils/Reply";
5151
import { BackgroundAudio } from "./audio/BackgroundAudio";
@@ -486,41 +486,33 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
486486
private performCustomEventHandling(ev: MatrixEvent): void {
487487
const cli = MatrixClientPeg.safeGet();
488488
const room = cli.getRoom(ev.getRoomId());
489-
const type = ev.getType();
490489
const thisUserHasConnectedDevice =
491490
room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId());
492491

493-
if (EventType.GroupCallMemberPrefix === type && thisUserHasConnectedDevice) {
494-
const content = ev.getContent();
495-
496-
if (typeof content.call_id !== "string") {
497-
logger.warn(
498-
"Received malformatted GroupCallMemberPrefix event. Did not contain 'call_id' of type 'string'",
499-
);
500-
return;
501-
}
502-
// One of our devices has joined the call, so dismiss it.
503-
ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(content.call_id, room.roomId));
504-
}
505-
// Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification
506-
else if (EventType.CallNotify === type && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
507-
const content = ev.getContent();
492+
if (EventType.RTCNotification === ev.getType() && !thisUserHasConnectedDevice) {
493+
const content = ev.getContent() as IRTCNotificationContent;
508494
const roomId = ev.getRoomId();
495+
const eventId = ev.getId();
509496

510-
if (typeof content.call_id !== "string") {
511-
logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
497+
// Check maximum age of a call notification event that will trigger a ringing notification
498+
if (Date.now() - getNotificationEventSendTs(ev) > content.lifetime) {
499+
logger.warn("Received outdated RTCNotification event.");
512500
return;
513501
}
514502
if (!roomId) {
515-
logger.warn("Could not get roomId for CallNotify event");
503+
logger.warn("Could not get roomId for RTCNotification event");
504+
return;
505+
}
506+
if (!eventId) {
507+
logger.warn("Could not get eventId for RTCNotification event");
516508
return;
517509
}
518510
ToastStore.sharedInstance().addOrReplaceToast({
519-
key: getIncomingCallToastKey(content.call_id, roomId),
511+
key: getIncomingCallToastKey(eventId, roomId),
520512
priority: 100,
521513
component: IncomingCallToast,
522514
bodyClassName: "mx_IncomingCallToast",
523-
props: { notifyEvent: ev },
515+
props: { notificationEvent: ev },
524516
});
525517
}
526518
}

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3987,6 +3987,7 @@
39873987
"connection_lost": "Connectivity to the server has been lost",
39883988
"connection_lost_description": "You cannot place calls without a connection to the server.",
39893989
"consulting": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
3990+
"decline_call": "Decline",
39903991
"default_device": "Default Device",
39913992
"dial": "Dial",
39923993
"dialpad": "Dialpad",
@@ -4038,6 +4039,7 @@
40384039
"show_sidebar_button": "Show sidebar",
40394040
"silence": "Silence call",
40404041
"silenced": "Notifications silenced",
4042+
"skip_lobby_toggle_option": "Join immediately",
40414043
"start_screenshare": "Start sharing your screen",
40424044
"stop_screenshare": "Stop sharing your screen",
40434045
"too_many_calls": "Too Many Calls",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.avatarWithDetails {
9+
display: flex;
10+
align-items: center;
11+
12+
border-radius: 12px;
13+
background-color: var(--cpd-color-gray-200);
14+
padding: var(--cpd-space-2x);
15+
gap: var(--cpd-space-2x);
16+
17+
.title {
18+
display: inline-block;
19+
20+
font-weight: var(--cpd-font-weight-semibold);
21+
font-size: var(--cpd-font-size-body-md);
22+
23+
overflow: hidden;
24+
text-overflow: ellipsis;
25+
white-space: nowrap;
26+
}
27+
28+
.details {
29+
font-size: var(--cpd-font-size-body-sm);
30+
}
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { type Meta, type StoryObj } from "@storybook/react-vite/*";
10+
11+
import { AvatarWithDetails } from "./AvatarWithDetails";
12+
13+
const meta = {
14+
title: "Avatar/AvatarWithDetails",
15+
component: AvatarWithDetails,
16+
tags: ["autodocs"],
17+
args: {
18+
avatar: <div style={{ width: 40, height: 40, backgroundColor: "#888", borderRadius: "50%" }} />,
19+
details: "Details about the avatar go here",
20+
title: "Room Name",
21+
},
22+
} satisfies Meta<typeof AvatarWithDetails>;
23+
24+
export default meta;
25+
type Story = StoryObj<typeof meta>;
26+
export const Default: Story = {};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { composeStories } from "@storybook/react-vite";
9+
import { render } from "jest-matrix-react";
10+
import React from "react";
11+
12+
import * as stories from "./AvatarWithDetails.stories.tsx";
13+
14+
const { Default } = composeStories(stories);
15+
16+
describe("AvatarWithDetails", () => {
17+
it("renders a textual event", () => {
18+
const { container } = render(<Default />);
19+
expect(container).toMatchSnapshot();
20+
});
21+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { type ComponentProps, type ElementType, type JSX, type PropsWithChildren } from "react";
9+
import React from "react";
10+
import classNames from "classnames";
11+
12+
import styles from "./AvatarWithDetails.module.css";
13+
import { Flex } from "../../utils/Flex";
14+
15+
export type AvatarWithDetailsProps<C extends ElementType> = {
16+
/**
17+
* The HTML tag.
18+
* @default "div"
19+
*/
20+
as?: C;
21+
/**
22+
* The CSS class name.
23+
*/
24+
className?: string;
25+
/**
26+
* The title/label next to the avatar. Usually the user or room name.
27+
*/
28+
title: string;
29+
/**
30+
* A label with details to display under the avatar title.
31+
* Commonly used to display the number of participants in a room.
32+
*/
33+
details: React.ReactNode;
34+
/** The avatar to display. */
35+
avatar: React.ReactNode;
36+
} & ComponentProps<C>;
37+
38+
/**
39+
* A component to display an avatar with a title next to it in a grey box.
40+
*
41+
* @example
42+
* ```tsx
43+
* <AvatarWithDetails title="Room Name" details="10 participants" className="custom-class" />
44+
* ```
45+
*/
46+
export function AvatarWithDetails<C extends React.ElementType = "div">({
47+
as,
48+
className,
49+
details,
50+
avatar,
51+
title,
52+
...props
53+
}: PropsWithChildren<AvatarWithDetailsProps<C>>): JSX.Element {
54+
const Component = as || "div";
55+
56+
return (
57+
<Component className={classNames(styles.avatarWithDetails, className)} {...props}>
58+
{avatar}
59+
<Flex direction="column">
60+
<span className={styles.title}>{title}</span>
61+
<span className={styles.details}>{details}</span>
62+
</Flex>
63+
</Component>
64+
);
65+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`AvatarWithDetails renders a textual event 1`] = `
4+
<div>
5+
<div
6+
class="avatarWithDetails"
7+
>
8+
<div
9+
style="width: 40px; height: 40px; background-color: rgb(136, 136, 136); border-radius: 50%;"
10+
/>
11+
<div
12+
class="flex"
13+
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;"
14+
>
15+
<span
16+
class="title"
17+
>
18+
Room Name
19+
</span>
20+
<span
21+
class="details"
22+
>
23+
Details about the avatar go here
24+
</span>
25+
</div>
26+
</div>
27+
</div>
28+
`;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
export { AvatarWithDetails } from "./AvatarWithDetails";

0 commit comments

Comments
 (0)