Update toast styles, improve incoming call notifications (#33043)
* Update design of incoming call notifications * Make toast show avatars of group call participants * Further expand test coverage for call notifications * Update screenshots * Update screenshots * Delete unused variables * Upgrade Element Call to v0.19.2 For the new group call intents. * Consolidate some branches * Apply Compound spacing variables a little more * Fix lints * Exclude Element Call assets from being re-minified to fix build
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
RoomEvent,
|
||||
type IRoomTimelineData,
|
||||
type ISendEventResponse,
|
||||
type IContent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||
@@ -50,12 +51,32 @@ import LegacyCallHandler, { AudioID } from "../../../src/LegacyCallHandler";
|
||||
import { CallEvent } from "../../../src/models/Call";
|
||||
import { type WidgetMessaging } from "../../../src/stores/widgets/WidgetMessaging";
|
||||
|
||||
function makeNotificationEvent(room: Room, content: IContent = {}): MatrixEvent {
|
||||
const ts = Date.now();
|
||||
const notificationContent = {
|
||||
"notification_type": "notification",
|
||||
"m.relation": { rel_type: "m.reference", event_id: "$memberEventId" },
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"lifetime": 3000,
|
||||
"sender_ts": ts,
|
||||
...content,
|
||||
} as unknown as IRTCNotificationContent;
|
||||
return mkEvent({
|
||||
type: EventType.RTCNotification,
|
||||
user: "@userId:matrix.org",
|
||||
content: notificationContent,
|
||||
room: room.roomId,
|
||||
ts,
|
||||
id: "$notificationEventId",
|
||||
event: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("IncomingCallToast", () => {
|
||||
useMockedCalls();
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let notificationEvent: MatrixEvent;
|
||||
|
||||
let alice: RoomMember;
|
||||
let bob: RoomMember;
|
||||
@@ -77,23 +98,6 @@ describe("IncomingCallToast", () => {
|
||||
document.body.appendChild(audio);
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org");
|
||||
const ts = Date.now();
|
||||
const notificationContent = {
|
||||
"notification_type": "notification",
|
||||
"m.relation": { rel_type: "m.reference", event_id: "$memberEventId" },
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"lifetime": 3000,
|
||||
"sender_ts": ts,
|
||||
} as unknown as IRTCNotificationContent;
|
||||
notificationEvent = mkEvent({
|
||||
type: EventType.RTCNotification,
|
||||
user: "@userId:matrix.org",
|
||||
content: notificationContent,
|
||||
room: room.roomId,
|
||||
ts,
|
||||
id: "$notificationEventId",
|
||||
event: true,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
|
||||
@@ -130,7 +134,7 @@ describe("IncomingCallToast", () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderToast = (): string => {
|
||||
const renderToast = (notificationEvent: MatrixEvent = makeNotificationEvent(room)): string => {
|
||||
const callId = randomUUID();
|
||||
call.event.getContent = () =>
|
||||
({
|
||||
@@ -146,27 +150,52 @@ describe("IncomingCallToast", () => {
|
||||
return callId;
|
||||
};
|
||||
|
||||
it("correctly shows all the information", () => {
|
||||
it.each(["video", "voice"])("shows information for a group %s call", (callType: string) => {
|
||||
call.participants = new Map([
|
||||
[alice, new Set("a")],
|
||||
[bob, new Set(["b1", "b2"])],
|
||||
]);
|
||||
renderToast();
|
||||
const notificationEvent = makeNotificationEvent(room, {
|
||||
"m.call.intent": callType === "voice" ? "audio" : "video",
|
||||
});
|
||||
renderToast(notificationEvent);
|
||||
|
||||
screen.getByText("Video call started");
|
||||
screen.getByText("Video");
|
||||
screen.getByLabelText("3 people joined");
|
||||
screen.getByText("Group call started");
|
||||
screen.getByLabelText(callType === "voice" ? "Voice call" : "Video call");
|
||||
screen.getByLabelText("@alice:example.org");
|
||||
screen.getByLabelText("@bob:example.org");
|
||||
screen.getByText("on the call");
|
||||
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getByRole("button", { name: "Close" });
|
||||
screen.getByRole("button", { name: "Ignore" });
|
||||
screen.getByRole("button", { name: "Expand" });
|
||||
});
|
||||
|
||||
it.each(["video", "voice"])("shows information for a DM %s call", (callType: string) => {
|
||||
mocked(dmRoomMap.getUserIdForRoomId).mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? alice.userId : undefined,
|
||||
);
|
||||
try {
|
||||
call.participants = new Map([[alice, new Set("a")]]);
|
||||
const notificationEvent = makeNotificationEvent(room, {
|
||||
"m.call.intent": callType === "voice" ? "audio" : "video",
|
||||
});
|
||||
renderToast(notificationEvent);
|
||||
|
||||
screen.getByText(callType === "voice" ? "Incoming voice call" : "Incoming video call");
|
||||
screen.getByLabelText(callType === "voice" ? "Voice call" : "Video call");
|
||||
screen.getByLabelText("@alice:example.org");
|
||||
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getByRole("button", { name: "Ignore" });
|
||||
screen.getByRole("button", { name: "Expand" });
|
||||
} finally {
|
||||
mocked(dmRoomMap.getUserIdForRoomId).mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it("start ringing on ring notify event", () => {
|
||||
const oldContent = notificationEvent.getContent() as IRTCNotificationContent;
|
||||
(notificationEvent as unknown as { getContent: () => IRTCNotificationContent }).getContent = () => {
|
||||
return { ...oldContent, notification_type: "ring" } as IRTCNotificationContent;
|
||||
};
|
||||
|
||||
const notificationEvent = makeNotificationEvent(room, { notification_type: "ring" });
|
||||
const playMock = jest.spyOn(LegacyCallHandler.instance, "play");
|
||||
render(<IncomingCallToast notificationEvent={notificationEvent} toastKey="" />);
|
||||
expect(playMock).toHaveBeenCalled();
|
||||
@@ -176,21 +205,23 @@ describe("IncomingCallToast", () => {
|
||||
call.destroy();
|
||||
renderToast();
|
||||
|
||||
screen.getByText("Video call started");
|
||||
screen.getByText("Video");
|
||||
screen.getByText("Group call started");
|
||||
screen.getByLabelText("Video call");
|
||||
expect(screen.queryByText("on the call")).toBe(null);
|
||||
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getByRole("button", { name: "Decline" });
|
||||
screen.getByRole("button", { name: "Close" });
|
||||
screen.getByRole("button", { name: "Ignore" });
|
||||
screen.getByRole("button", { name: "Expand" });
|
||||
});
|
||||
|
||||
it("opens the call directly and closes the toast when pressing on the join button", async () => {
|
||||
it("joins with video and closes the toast", async () => {
|
||||
const callId = renderToast();
|
||||
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
|
||||
// click on the avatar (which is the example used for pressing on any area other than the buttons)
|
||||
screen.getByRole("switch", { name: "Join with video", checked: true });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() =>
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
@@ -208,23 +239,23 @@ describe("IncomingCallToast", () => {
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
});
|
||||
|
||||
it("opens the call lobby and closes the toast when configured like that", async () => {
|
||||
it("joins without video and closes the toast", async () => {
|
||||
const callId = renderToast();
|
||||
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", {}));
|
||||
screen.getByRole("switch", { name: "Join with video", checked: false });
|
||||
|
||||
// click on the avatar (which is the example used for pressing on any area other than the buttons)
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() =>
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
skipLobby: true,
|
||||
view_call: true,
|
||||
voiceOnly: false,
|
||||
voiceOnly: true,
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
@@ -276,13 +307,22 @@ describe("IncomingCallToast", () => {
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
});
|
||||
|
||||
it("closes the toast", async () => {
|
||||
it("expands to show the call lobby", async () => {
|
||||
const callId = renderToast();
|
||||
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Close" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Expand" }));
|
||||
await waitFor(() =>
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
view_call: true,
|
||||
voiceOnly: false,
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(toastStore.dismissToast).toHaveBeenCalledWith(getIncomingCallToastKey(callId, room.roomId)),
|
||||
);
|
||||
@@ -316,7 +356,8 @@ describe("IncomingCallToast", () => {
|
||||
});
|
||||
|
||||
it("closes toast when the notification event is redacted", async () => {
|
||||
const callId = renderToast();
|
||||
const notificationEvent = makeNotificationEvent(room);
|
||||
const callId = renderToast(notificationEvent);
|
||||
|
||||
room.emit(MatrixEventEvent.BeforeRedaction, notificationEvent, {} as unknown as MatrixEvent);
|
||||
|
||||
@@ -335,7 +376,8 @@ describe("IncomingCallToast", () => {
|
||||
});
|
||||
|
||||
it("closes toast when a decline event was received", async () => {
|
||||
const callId = renderToast();
|
||||
const notificationEvent = makeNotificationEvent(room);
|
||||
const callId = renderToast(notificationEvent);
|
||||
|
||||
room.emit(
|
||||
RoomEvent.Timeline,
|
||||
@@ -357,7 +399,8 @@ describe("IncomingCallToast", () => {
|
||||
});
|
||||
|
||||
it("does not close toast when a decline event for another user was received", async () => {
|
||||
const callId = renderToast();
|
||||
const notificationEvent = makeNotificationEvent(room);
|
||||
const callId = renderToast(notificationEvent);
|
||||
|
||||
room.emit(
|
||||
RoomEvent.Timeline,
|
||||
@@ -401,7 +444,7 @@ describe("IncomingCallToast", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a decline event when clicking the decline button and only dismiss after sending", async () => {
|
||||
it("sends a decline event when clicking the ignore button and only dismiss after sending", async () => {
|
||||
const callId = renderToast();
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<ISendEventResponse>();
|
||||
@@ -409,7 +452,7 @@ describe("IncomingCallToast", () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Decline" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Ignore" }));
|
||||
|
||||
expect(toastStore.dismissToast).not.toHaveBeenCalledWith(getIncomingCallToastKey(callId, room.roomId));
|
||||
expect(client.sendRtcDecline).toHaveBeenCalledWith("!1:example.org", "$notificationEventId");
|
||||
@@ -422,6 +465,7 @@ describe("IncomingCallToast", () => {
|
||||
});
|
||||
|
||||
it("getNotificationEventSendTs returns the correct ts", () => {
|
||||
const notificationEvent = makeNotificationEvent(room);
|
||||
const eventOriginServerTs = mkEvent({
|
||||
user: "@userId:matrix.org",
|
||||
type: EventType.RTCNotification,
|
||||
|
||||
Reference in New Issue
Block a user