Incoming Element Calls now trigger a regular OS notification (#33499)
* Fix missing OS-level desktop notification for Element Call (MSC4075) * Add Unit-test for textForCallInviteEvent and lint fixes * Refactor call invite notification * prettier fix * Added mock supportsVoip for notifier-test
This commit is contained in:
@@ -23,6 +23,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
||||
import { type PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import { _t } from "./languageHandler";
|
||||
import * as Roles from "./Roles";
|
||||
@@ -60,26 +61,47 @@ function textForCallEvent(event: MatrixEvent, client: MatrixClient): () => strin
|
||||
// any text to display at all. For this reason they return deferred values
|
||||
// to avoid the expense of looking up translations when they're not needed.
|
||||
|
||||
function textForCallInviteEvent(event: MatrixEvent, client: MatrixClient): (() => string) | null {
|
||||
const senderName = getSenderName(event);
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
const isVoice = !event.getContent().offer?.sdp?.includes("m=video");
|
||||
const isSupported = client.supportsVoip();
|
||||
|
||||
// This ladder could be reduced down to a couple string variables, however other languages
|
||||
function getCallInviteText(isVoice: boolean, isSupported: boolean, senderName: string): () => string {
|
||||
// This logic could be reduced down to dynamic string keys, however other languages
|
||||
// can have a hard time translating those strings. In an effort to make translations easier
|
||||
// and more accurate, we break out the string-based variables to a couple booleans.
|
||||
if (isVoice && isSupported) {
|
||||
return () => _t("timeline|m.call.invite|voice_call", { senderName });
|
||||
} else if (isVoice && !isSupported) {
|
||||
return () => _t("timeline|m.call.invite|voice_call_unsupported", { senderName });
|
||||
} else if (!isVoice && isSupported) {
|
||||
return () => _t("timeline|m.call.invite|video_call", { senderName });
|
||||
} else if (!isVoice && !isSupported) {
|
||||
return () => _t("timeline|m.call.invite|video_call_unsupported", { senderName });
|
||||
// and more accurate, we use explicit strings for each combination.
|
||||
if (isVoice) {
|
||||
return isSupported
|
||||
? () => _t("timeline|m.call.invite|voice_call", { senderName })
|
||||
: () => _t("timeline|m.call.invite|voice_call_unsupported", { senderName });
|
||||
}
|
||||
|
||||
return null;
|
||||
return isSupported
|
||||
? () => _t("timeline|m.call.invite|video_call", { senderName })
|
||||
: () => _t("timeline|m.call.invite|video_call_unsupported", { senderName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the textual content for incoming legacy WebRTC call events.
|
||||
*/
|
||||
function textForCallInviteEvent(event: MatrixEvent, client: MatrixClient): (() => string) | null {
|
||||
const senderName = getSenderName(event);
|
||||
const content = event.getContent();
|
||||
|
||||
// Fallback for legacy WebRTC signaling
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
const isVoice = !content.offer?.sdp?.includes("m=video");
|
||||
const isSupported = client.supportsVoip();
|
||||
|
||||
return getCallInviteText(isVoice, isSupported, senderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the textual content for incoming MatrixRTC notifications (MSC4075).
|
||||
*/
|
||||
function textForRTCNotificationEvent(event: MatrixEvent, client: MatrixClient): (() => string) | null {
|
||||
const senderName = getSenderName(event);
|
||||
const content = event.getContent<IRTCNotificationContent>();
|
||||
|
||||
const isVoice = content["m.call.intent"] === "audio";
|
||||
const isSupported = client.supportsVoip();
|
||||
|
||||
return getCallInviteText(isVoice, isSupported, senderName);
|
||||
}
|
||||
|
||||
enum Modification {
|
||||
@@ -881,6 +903,7 @@ const handlers: IHandlers = {
|
||||
[EventType.RoomMessage]: textForMessageEvent,
|
||||
[EventType.Sticker]: textForMessageEvent,
|
||||
[EventType.CallInvite]: textForCallInviteEvent,
|
||||
[EventType.RTCNotification]: textForRTCNotificationEvent,
|
||||
[M_POLL_START.name]: textForPollStartEvent,
|
||||
[M_POLL_END.name]: textForPollEndEvent,
|
||||
[M_POLL_START.altName]: textForPollStartEvent,
|
||||
|
||||
@@ -131,6 +131,9 @@ describe("Notifier", () => {
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
// Mock required because TextForEvent now evaluates supportsVoip for RTCNotification to trigger OS popups.
|
||||
// The true/false value is arbitrary here, as this test only verifies the in-app toast creation, not the OS text output.
|
||||
supportsVoip: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(false),
|
||||
matrixRTC: {
|
||||
on: jest.fn(),
|
||||
|
||||
@@ -492,6 +492,48 @@ describe("TextForEvent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("textForCallInviteEvent()", () => {
|
||||
let mockClient: MatrixClient;
|
||||
let callInviteEvent: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
mockClient = MatrixClientPeg.safeGet();
|
||||
|
||||
mocked(mockClient.supportsVoip).mockReturnValue(true);
|
||||
|
||||
callInviteEvent = {
|
||||
getSender: jest.fn().mockReturnValue("@alice:matrix.org"),
|
||||
sender: { name: "Alice" },
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getType: jest.fn().mockReturnValue(EventType.CallInvite),
|
||||
isState: jest.fn().mockReturnValue(false),
|
||||
} as unknown as MatrixEvent;
|
||||
});
|
||||
|
||||
it("returns correct message for legacy voice call invite", () => {
|
||||
mocked(callInviteEvent).getContent.mockReturnValue({ offer: { sdp: "m=audio" } });
|
||||
expect(textForEvent(callInviteEvent, mockClient)).toEqual("Alice placed a voice call.");
|
||||
});
|
||||
|
||||
it("returns correct message for legacy video call invite", () => {
|
||||
mocked(callInviteEvent).getContent.mockReturnValue({ offer: { sdp: "m=video" } });
|
||||
expect(textForEvent(callInviteEvent, mockClient)).toEqual("Alice placed a video call.");
|
||||
});
|
||||
|
||||
it("returns correct message for MSC4075 voice call", () => {
|
||||
mocked(callInviteEvent).getType.mockReturnValue(EventType.RTCNotification);
|
||||
mocked(callInviteEvent).getContent.mockReturnValue({ "m.call.intent": "audio" });
|
||||
expect(textForEvent(callInviteEvent, mockClient)).toEqual("Alice placed a voice call.");
|
||||
});
|
||||
|
||||
it("returns correct message for MSC4075 video call", () => {
|
||||
mocked(callInviteEvent).getType.mockReturnValue(EventType.RTCNotification);
|
||||
mocked(callInviteEvent).getContent.mockReturnValue({ "m.call.intent": "video" });
|
||||
expect(textForEvent(callInviteEvent, mockClient)).toEqual("Alice placed a video call.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("textForMemberEvent()", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
|
||||
Reference in New Issue
Block a user