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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
||||||
import { type PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
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 { _t } from "./languageHandler";
|
||||||
import * as Roles from "./Roles";
|
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
|
// 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.
|
// to avoid the expense of looking up translations when they're not needed.
|
||||||
|
|
||||||
function textForCallInviteEvent(event: MatrixEvent, client: MatrixClient): (() => string) | null {
|
function getCallInviteText(isVoice: boolean, isSupported: boolean, senderName: string): () => string {
|
||||||
const senderName = getSenderName(event);
|
// This logic could be reduced down to dynamic string keys, however other languages
|
||||||
// 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
|
|
||||||
// can have a hard time translating those strings. In an effort to make translations easier
|
// 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.
|
// and more accurate, we use explicit strings for each combination.
|
||||||
if (isVoice && isSupported) {
|
if (isVoice) {
|
||||||
return () => _t("timeline|m.call.invite|voice_call", { senderName });
|
return isSupported
|
||||||
} else if (isVoice && !isSupported) {
|
? () => _t("timeline|m.call.invite|voice_call", { senderName })
|
||||||
return () => _t("timeline|m.call.invite|voice_call_unsupported", { senderName });
|
: () => _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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
enum Modification {
|
||||||
@@ -881,6 +903,7 @@ const handlers: IHandlers = {
|
|||||||
[EventType.RoomMessage]: textForMessageEvent,
|
[EventType.RoomMessage]: textForMessageEvent,
|
||||||
[EventType.Sticker]: textForMessageEvent,
|
[EventType.Sticker]: textForMessageEvent,
|
||||||
[EventType.CallInvite]: textForCallInviteEvent,
|
[EventType.CallInvite]: textForCallInviteEvent,
|
||||||
|
[EventType.RTCNotification]: textForRTCNotificationEvent,
|
||||||
[M_POLL_START.name]: textForPollStartEvent,
|
[M_POLL_START.name]: textForPollStartEvent,
|
||||||
[M_POLL_END.name]: textForPollEndEvent,
|
[M_POLL_END.name]: textForPollEndEvent,
|
||||||
[M_POLL_START.altName]: textForPollStartEvent,
|
[M_POLL_START.altName]: textForPollStartEvent,
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ describe("Notifier", () => {
|
|||||||
decryptEventIfNeeded: jest.fn(),
|
decryptEventIfNeeded: jest.fn(),
|
||||||
getRoom: jest.fn(),
|
getRoom: jest.fn(),
|
||||||
getPushActionsForEvent: 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),
|
supportsThreads: jest.fn().mockReturnValue(false),
|
||||||
matrixRTC: {
|
matrixRTC: {
|
||||||
on: jest.fn(),
|
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()", () => {
|
describe("textForMemberEvent()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
|
|||||||
Reference in New Issue
Block a user