Skip to content

Commit 830c891

Browse files
committed
Make toast show avatars of group call participants
1 parent c0c0111 commit 830c891

6 files changed

Lines changed: 51 additions & 33 deletions

File tree

apps/web/res/css/views/toasts/_IncomingCallToast.pcss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ Please see LICENSE files in the repository root for full details.
4040
}
4141
}
4242

43+
.mx_IncomingCallToast_avatars {
44+
display: inline-block;
45+
vertical-align: top;
46+
}
47+
4348
.mx_IncomingCallToast_buttons {
4449
display: flex;
4550
gap: var(--cpd-space-2x);

apps/web/src/components/views/rooms/LiveContentSummary.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ Please see LICENSE files in the repository root for full details.
88

99
import React, { type FC } from "react";
1010
import classNames from "classnames";
11-
import { GroupIcon, VideoCallSolidIcon, VoiceCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
11+
import { GroupIcon, VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
1212

1313
import { _t } from "../../../languageHandler";
1414

1515
export enum LiveContentType {
1616
Video,
17-
Voice,
1817
}
1918

2019
interface Props {
@@ -27,14 +26,14 @@ interface Props {
2726
/**
2827
* Summary line used to call out live, interactive content such as calls.
2928
*/
30-
export const LiveContentSummary: FC<Props> = ({ type, text, active, participantCount }) => (
29+
export const LiveContentSummary: FC<Props> = ({ text, active, participantCount }) => (
3130
<span className="mx_LiveContentSummary">
3231
<span
3332
className={classNames("mx_LiveContentSummary_text", {
3433
mx_LiveContentSummary_text_active: active,
3534
})}
3635
>
37-
{type === LiveContentType.Video ? <VideoCallSolidIcon /> : <VoiceCallSolidIcon />}
36+
<VideoCallSolidIcon />
3837
{text}
3938
</span>
4039
{participantCount > 0 && (

apps/web/src/hooks/useCall.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,7 @@ export const useParticipantCount = (call: Call | null): number => {
5050
}, [participants]);
5151
};
5252

53-
export const useParticipatingMembers = (call: Call): RoomMember[] => {
53+
export const useParticipatingMembers = (call: Call | null): RoomMember[] => {
5454
const participants = useParticipants(call);
55-
56-
return useMemo(() => {
57-
const members: RoomMember[] = [];
58-
for (const [member, devices] of participants) {
59-
// Repeat the member for as many devices as they're using
60-
for (let i = 0; i < devices.size; i++) members.push(member);
61-
}
62-
return members;
63-
}, [participants]);
55+
return useMemo(() => [...participants.keys()], [participants]);
6456
};

apps/web/src/i18n/strings/en_EN.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3891,6 +3891,16 @@
38913891
"call_held": "%(peerName)s held the call",
38923892
"call_held_resume": "You held the call <a>Resume</a>",
38933893
"call_held_switch": "You held the call <a>Switch</a>",
3894+
"call_members": {
3895+
"exhaustive": {
3896+
"one": "<avatars/> on the call",
3897+
"other": "<avatars/> on the call"
3898+
},
3899+
"overflow": {
3900+
"one": "<avatars/> +%(overflowCount)s on the call",
3901+
"other": "<avatars/> +%(overflowCount)s on the call"
3902+
}
3903+
},
38943904
"call_toast_unknown_room": "Unknown room",
38953905
"camera_disabled": "Your camera is turned off",
38963906
"camera_enabled": "Your camera is still enabled",

apps/web/src/toasts/IncomingCallToast.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React, { type JSX, useCallback, useEffect, useRef, useState, useId } from "react";
9+
import React, { type JSX, type ReactNode, useCallback, useEffect, useRef, useState, useId } from "react";
1010
import {
1111
type Room,
1212
type MatrixEvent,
@@ -15,7 +15,7 @@ import {
1515
EventType,
1616
MatrixEventEvent,
1717
} from "matrix-js-sdk/src/matrix";
18-
import { Button, Form, Heading, InlineField, Label, ToggleInput, Tooltip } from "@vector-im/compound-web";
18+
import { AvatarStack, Button, Form, Heading, InlineField, Label, ToggleInput, Tooltip } from "@vector-im/compound-web";
1919
import { logger } from "matrix-js-sdk/src/logger";
2020
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
2121
import {
@@ -34,8 +34,7 @@ import defaultDispatcher from "../dispatcher/dispatcher";
3434
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
3535
import { Action } from "../dispatcher/actions";
3636
import ToastStore from "../stores/ToastStore";
37-
import { LiveContentSummary, LiveContentType } from "../components/views/rooms/LiveContentSummary";
38-
import { useCall, useParticipantCount } from "../hooks/useCall";
37+
import { useCall, useParticipatingMembers } from "../hooks/useCall";
3938
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
4039
import { useDispatcher } from "../hooks/useDispatcher";
4140
import { type ActionPayload } from "../dispatcher/payloads";
@@ -44,6 +43,7 @@ import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
4443
import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter";
4544
import { CallStore, CallStoreEvent } from "../stores/CallStore";
4645
import DMRoomMap from "../utils/DMRoomMap";
46+
import MemberAvatar from "../components/views/avatars/MemberAvatar";
4747

4848
/**
4949
* Get the key for the incoming call toast. A combination of the call ID and room ID.
@@ -279,19 +279,30 @@ export function IncomingCallToast({ notificationEvent, toastKey }: Props): JSX.E
279279
useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall);
280280
useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange);
281281
useEventEmitter(room, RoomEvent.Timeline, onTimelineChange);
282+
282283
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(roomId);
283-
const participantCount = useParticipantCount(call);
284-
const detailsInformation =
285-
notificationContent.notification_type === "ring" ? (
286-
<span>{otherUserId}</span>
287-
) : (
288-
<LiveContentSummary
289-
type={isVoice ? LiveContentType.Voice : LiveContentType.Video}
290-
text={isVoice ? _t("common|voice") : _t("common|video")}
291-
active={false}
292-
participantCount={participantCount}
293-
/>
294-
);
284+
const members = useParticipatingMembers(call);
285+
const avatars = (): ReactNode => (
286+
<AvatarStack className="mx_IncomingCallToast_avatars">
287+
{members.slice(0, 3).map((m) => (
288+
<MemberAvatar key={m.userId} size="20px" member={m} aria-label={m.name} />
289+
))}
290+
</AvatarStack>
291+
);
292+
293+
let detailsInformation: ReactNode;
294+
if (notificationContent.notification_type === "ring") {
295+
detailsInformation = <span>{otherUserId}</span>;
296+
} else if (members.length > 0) {
297+
detailsInformation =
298+
members.length > 3
299+
? _t(
300+
"voip|call_members|overflow",
301+
{ count: members.length, overflowCount: members.length - 3 },
302+
{ avatars },
303+
)
304+
: _t("voip|call_members|exhaustive", { count: members.length }, { avatars });
305+
}
295306

296307
const Icon = isVoice ? VoiceCallSolidIcon : VideoCallSolidIcon;
297308
const iconLabel = isVoice ? _t("voip|voice_call") : _t("voip|video_call");

apps/web/test/unit-tests/toasts/IncomingCallToast-test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ describe("IncomingCallToast", () => {
154154
renderToast();
155155

156156
screen.getByText("Group call started");
157-
screen.getByText("Video");
158-
screen.getByLabelText("3 people joined");
157+
screen.getByLabelText("Video call");
158+
screen.getByLabelText("@alice:example.org");
159+
screen.getByLabelText("@bob:example.org");
159160

160161
screen.getByRole("button", { name: "Join" });
161162
screen.getByRole("button", { name: "Ignore" });
@@ -178,7 +179,7 @@ describe("IncomingCallToast", () => {
178179
renderToast();
179180

180181
screen.getByText("Group call started");
181-
screen.getByText("Video");
182+
screen.getByLabelText("Video call");
182183

183184
screen.getByRole("button", { name: "Join" });
184185
screen.getByRole("button", { name: "Ignore" });

0 commit comments

Comments
 (0)