Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 6356a8c

Browse files
Add notifications and toasts for Element Call calls (#9337)
1 parent 20f5adc commit 6356a8c

File tree

10 files changed

+591
-22
lines changed

10 files changed

+591
-22
lines changed

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@
337337
@import "./views/spaces/_SpacePublicShare.pcss";
338338
@import "./views/terms/_InlineTermsAgreement.pcss";
339339
@import "./views/toasts/_AnalyticsToast.pcss";
340+
@import "./views/toasts/_IncomingCallToast.pcss";
340341
@import "./views/toasts/_IncomingLegacyCallToast.pcss";
341342
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
342343
@import "./views/typography/_Heading.pcss";
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_IncomingCallToast {
18+
position: relative;
19+
display: flex;
20+
flex-direction: row;
21+
pointer-events: initial; /* restore pointer events so the user can accept/decline */
22+
width: 250px;
23+
24+
.mx_IncomingCallToast_content {
25+
display: flex;
26+
flex-direction: column;
27+
margin-left: 8px;
28+
width: 100%;
29+
30+
.mx_IncomingCallToast_info {
31+
margin-bottom: $spacing-16;
32+
33+
.mx_IncomingCallToast_room {
34+
display: inline-block;
35+
36+
font-weight: bold;
37+
font-size: $font-15px;
38+
line-height: $font-24px;
39+
40+
overflow: hidden;
41+
text-overflow: ellipsis;
42+
white-space: nowrap;
43+
44+
margin-bottom: $spacing-4;
45+
}
46+
47+
.mx_IncomingCallToast_message {
48+
font-size: $font-12px;
49+
line-height: $font-15px;
50+
51+
margin-bottom: $spacing-4;
52+
}
53+
54+
.mx_LiveContentSummary {
55+
font-size: $font-12px;
56+
line-height: $font-15px;
57+
58+
.mx_LiveContentSummary_participants::before {
59+
width: 15px;
60+
height: 15px;
61+
}
62+
}
63+
}
64+
65+
.mx_IncomingCallToast_joinButton {
66+
position: relative;
67+
68+
bottom: $spacing-4;
69+
right: $spacing-4;
70+
71+
align-self: flex-end;
72+
73+
box-sizing: border-box;
74+
min-width: 120px;
75+
76+
padding: $spacing-4 0;
77+
78+
line-height: $font-24px;
79+
}
80+
}
81+
82+
.mx_IncomingCallToast_closeButton {
83+
position: absolute;
84+
85+
top: $spacing-4;
86+
right: $spacing-4;
87+
88+
display: flex;
89+
height: 16px;
90+
width: 16px;
91+
92+
&::before {
93+
content: '';
94+
95+
mask-image: url('$(res)/img/cancel.svg');
96+
97+
height: inherit;
98+
width: inherit;
99+
background-color: $secondary-content;
100+
mask-repeat: no-repeat;
101+
mask-size: contain;
102+
mask-position: center;
103+
}
104+
}
105+
}

src/Notifier.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
4747
import LegacyCallHandler from "./LegacyCallHandler";
4848
import VoipUserMapper from "./VoipUserMapper";
4949
import { localNotificationsAreSilenced } from "./utils/notifications";
50+
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
51+
import ToastStore from "./stores/ToastStore";
52+
import { ElementCall } from "./models/Call";
5053

5154
/*
5255
* Dispatches:
@@ -358,7 +361,7 @@ export const Notifier = {
358361

359362
onEvent: function(ev: MatrixEvent) {
360363
if (!this.isSyncing) return; // don't alert for any messages initially
361-
if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return;
364+
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
362365

363366
MatrixClientPeg.get().decryptEventIfNeeded(ev);
364367

@@ -419,6 +422,8 @@ export const Notifier = {
419422

420423
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
421424
if (actions?.notify) {
425+
this._performCustomEventHandling(ev);
426+
422427
if (RoomViewStore.instance.getRoomId() === room.roomId &&
423428
UserActivity.sharedInstance().userActiveRecently() &&
424429
!Modal.hasDialogs()
@@ -436,6 +441,24 @@ export const Notifier = {
436441
}
437442
}
438443
},
444+
445+
/**
446+
* Some events require special handling such as showing in-app toasts
447+
*/
448+
_performCustomEventHandling: function(ev: MatrixEvent) {
449+
if (
450+
ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType())
451+
&& SettingsStore.getValue("feature_group_calls")
452+
) {
453+
ToastStore.sharedInstance().addOrReplaceToast({
454+
key: getIncomingCallToastKey(ev.getStateKey()),
455+
priority: 100,
456+
component: IncomingCallToast,
457+
bodyClassName: "mx_IncomingCallToast",
458+
props: { callEvent: ev },
459+
});
460+
}
461+
},
439462
};
440463

441464
if (!window.mxNotifier) {

src/TextForEvent.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import AccessibleButton from './components/views/elements/AccessibleButton';
4545
import RightPanelStore from './stores/right-panel/RightPanelStore';
4646
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
4747
import { isLocationEvent } from './utils/EventUtils';
48+
import { ElementCall } from "./models/Call";
4849

4950
export function getSenderName(event: MatrixEvent): string {
5051
return event.sender?.name ?? event.getSender() ?? _t("Someone");
@@ -57,6 +58,15 @@ function getRoomMemberDisplayname(event: MatrixEvent, userId = event.getSender()
5758
return member?.name || member?.rawDisplayName || userId || _t("Someone");
5859
}
5960

61+
function textForCallEvent(event: MatrixEvent): () => string {
62+
const roomName = MatrixClientPeg.get().getRoom(event.getRoomId()!).name;
63+
const isSupported = MatrixClientPeg.get().supportsVoip();
64+
65+
return isSupported
66+
? () => _t("Video call started in %(roomName)s.", { roomName })
67+
: () => _t("Video call started in %(roomName)s. (not supported by this browser)", { roomName });
68+
}
69+
6070
// These functions are frequently used just to check whether an event has
6171
// any text to display at all. For this reason they return deferred values
6272
// to avoid the expense of looking up translations when they're not needed.
@@ -798,6 +808,11 @@ for (const evType of ALL_RULE_TYPES) {
798808
stateHandlers[evType] = textForMjolnirEvent;
799809
}
800810

811+
// Add both stable and unstable m.call events
812+
for (const evType of ElementCall.CALL_EVENT_TYPE.names) {
813+
stateHandlers[evType] = textForCallEvent;
814+
}
815+
801816
/**
802817
* Determines whether the given event has text to display.
803818
* @param ev The event

src/components/views/rooms/LiveContentSummary.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import React, { FC } from "react";
1818
import classNames from "classnames";
1919

2020
import { _t } from "../../../languageHandler";
21+
import { Call } from "../../../models/Call";
22+
import { useParticipants } from "../../../hooks/useCall";
2123

2224
export enum LiveContentType {
2325
Video,
@@ -55,3 +57,18 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
5557
</> }
5658
</span>
5759
);
60+
61+
interface LiveContentSummaryWithCallProps {
62+
call: Call;
63+
}
64+
65+
export function LiveContentSummaryWithCall({ call }: LiveContentSummaryWithCallProps) {
66+
const participants = useParticipants(call);
67+
68+
return <LiveContentSummary
69+
type={LiveContentType.Video}
70+
text={_t("Video")}
71+
active={false}
72+
participantCount={participants.size}
73+
/>;
74+
}

src/i18n/strings/en_EN.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,8 @@
470470
"Converts the DM to a room": "Converts the DM to a room",
471471
"Displays action": "Displays action",
472472
"Someone": "Someone",
473+
"Video call started in %(roomName)s.": "Video call started in %(roomName)s.",
474+
"Video call started in %(roomName)s. (not supported by this browser)": "Video call started in %(roomName)s. (not supported by this browser)",
473475
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
474476
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
475477
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
@@ -795,6 +797,11 @@
795797
"Don't miss a reply": "Don't miss a reply",
796798
"Notifications": "Notifications",
797799
"Enable desktop notifications": "Enable desktop notifications",
800+
"Unknown room": "Unknown room",
801+
"Video call started": "Video call started",
802+
"Video": "Video",
803+
"Join": "Join",
804+
"Close": "Close",
798805
"Unknown caller": "Unknown caller",
799806
"Voice call": "Voice call",
800807
"Video call": "Video call",
@@ -1051,7 +1058,6 @@
10511058
"Video devices": "Video devices",
10521059
"Turn off camera": "Turn off camera",
10531060
"Turn on camera": "Turn on camera",
1054-
"Join": "Join",
10551061
"%(count)s people joined|other": "%(count)s people joined",
10561062
"%(count)s people joined|one": "%(count)s person joined",
10571063
"Dial": "Dial",
@@ -1519,7 +1525,6 @@
15191525
"Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s",
15201526
"Server rules": "Server rules",
15211527
"User rules": "User rules",
1522-
"Close": "Close",
15231528
"You have not ignored anyone.": "You have not ignored anyone.",
15241529
"You are currently ignoring:": "You are currently ignoring:",
15251530
"You are not subscribed to any lists": "You are not subscribed to any lists",
@@ -2005,7 +2010,6 @@
20052010
"%(count)s unread messages.|other": "%(count)s unread messages.",
20062011
"%(count)s unread messages.|one": "1 unread message.",
20072012
"Unread messages.": "Unread messages.",
2008-
"Video": "Video",
20092013
"Joining…": "Joining…",
20102014
"Joined": "Joined",
20112015
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",

src/toasts/IncomingCallToast.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { useCallback, useEffect } from 'react';
18+
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
19+
20+
import { _t } from '../languageHandler';
21+
import RoomAvatar from '../components/views/avatars/RoomAvatar';
22+
import AccessibleButton from '../components/views/elements/AccessibleButton';
23+
import { MatrixClientPeg } from "../MatrixClientPeg";
24+
import defaultDispatcher from "../dispatcher/dispatcher";
25+
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
26+
import { Action } from "../dispatcher/actions";
27+
import ToastStore from "../stores/ToastStore";
28+
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
29+
import {
30+
LiveContentSummary,
31+
LiveContentSummaryWithCall,
32+
LiveContentType,
33+
} from "../components/views/rooms/LiveContentSummary";
34+
import { useCall } from "../hooks/useCall";
35+
import { useRoomState } from "../hooks/useRoomState";
36+
import { ButtonEvent } from "../components/views/elements/AccessibleButton";
37+
38+
export const getIncomingCallToastKey = (stateKey: string) => `call_${stateKey}`;
39+
40+
interface Props {
41+
callEvent: MatrixEvent;
42+
}
43+
44+
export function IncomingCallToast({ callEvent }: Props) {
45+
const roomId = callEvent.getRoomId()!;
46+
const room = MatrixClientPeg.get().getRoom(roomId);
47+
const call = useCall(roomId);
48+
49+
const dismissToast = useCallback((): void => {
50+
ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(callEvent.getStateKey()!));
51+
}, [callEvent]);
52+
53+
const latestEvent = useRoomState(room, useCallback((state) => {
54+
return state.getStateEvents(callEvent.getType(), callEvent.getStateKey()!);
55+
}, [callEvent]));
56+
57+
useEffect(() => {
58+
if ("m.terminated" in latestEvent.getContent()) {
59+
dismissToast();
60+
}
61+
}, [latestEvent, dismissToast]);
62+
63+
const onJoinClick = useCallback((e: ButtonEvent): void => {
64+
e.stopPropagation();
65+
66+
defaultDispatcher.dispatch<ViewRoomPayload>({
67+
action: Action.ViewRoom,
68+
room_id: room.roomId,
69+
view_call: true,
70+
metricsTrigger: undefined,
71+
});
72+
dismissToast();
73+
}, [room, dismissToast]);
74+
75+
const onCloseClick = useCallback((e: ButtonEvent): void => {
76+
e.stopPropagation();
77+
78+
dismissToast();
79+
}, [dismissToast]);
80+
81+
return <React.Fragment>
82+
<RoomAvatar
83+
room={room ?? undefined}
84+
height={24}
85+
width={24}
86+
/>
87+
<div className="mx_IncomingCallToast_content">
88+
<div className="mx_IncomingCallToast_info">
89+
<span className="mx_IncomingCallToast_room">
90+
{ room ? room.name : _t("Unknown room") }
91+
</span>
92+
<div className="mx_IncomingCallToast_message">
93+
{ _t("Video call started") }
94+
</div>
95+
{ call
96+
? <LiveContentSummaryWithCall call={call} />
97+
: <LiveContentSummary
98+
type={LiveContentType.Video}
99+
text={_t("Video")}
100+
active={false}
101+
participantCount={0}
102+
/>
103+
}
104+
</div>
105+
<AccessibleButton
106+
className="mx_IncomingCallToast_joinButton"
107+
onClick={onJoinClick}
108+
kind="primary"
109+
>
110+
{ _t("Join") }
111+
</AccessibleButton>
112+
</div>
113+
<AccessibleTooltipButton
114+
className="mx_IncomingCallToast_closeButton"
115+
onClick={onCloseClick}
116+
title={_t("Close")}
117+
/>
118+
</React.Fragment>;
119+
}

0 commit comments

Comments
 (0)