mirror of
https://github.com/vector-im/element-web.git
synced 2025-12-01 07:21:43 +01:00
Support using Element Call for voice calls in DMs (#30817)
* Add voiceOnly options. * tweaks * Nearly working demo * Lots of minor fixes * Better working version * remove unused payload * bits and pieces * Cleanup based on new hints * Simple refactor for skipLobby (and remove returnToLobby) * Tidyup * Remove unused tests * Update tests for voice calls * Add video room support. * Add a test for video rooms * tidy * remove console log line * lint and tests * Bunch of fixes * Fixes * Use correct title * make linter happier * Update tests * cleanup * Drop only * update snaps * Document * lint * Update snapshots * Remove duplicate test * add brackets * fix jest
This commit is contained in:
parent
3d683ec5c6
commit
f3a880f1c3
@ -9,7 +9,7 @@ import type { EventType, Preset } from "matrix-js-sdk/src/matrix";
|
|||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import type { Credentials } from "../../plugins/homeserver";
|
import type { Credentials } from "../../plugins/homeserver";
|
||||||
import type { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
|
|
||||||
function assertCommonCallParameters(
|
function assertCommonCallParameters(
|
||||||
url: URLSearchParams,
|
url: URLSearchParams,
|
||||||
@ -27,27 +27,28 @@ function assertCommonCallParameters(
|
|||||||
expect(hash.get("preload")).toEqual("false");
|
expect(hash.get("preload")).toEqual("false");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
|
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification", intent?: string) {
|
||||||
const resp = await bot.sendStateEvent(
|
const resp = await bot.sendStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
"org.matrix.msc3401.call.member",
|
"org.matrix.msc3401.call.member",
|
||||||
{
|
{
|
||||||
application: "m.call",
|
"application": "m.call",
|
||||||
call_id: "",
|
"call_id": "",
|
||||||
device_id: "OiDFxsZrjz",
|
"m.call.intent": intent,
|
||||||
expires: 180000000,
|
"device_id": "OiDFxsZrjz",
|
||||||
foci_preferred: [
|
"expires": 180000000,
|
||||||
|
"foci_preferred": [
|
||||||
{
|
{
|
||||||
livekit_alias: roomId,
|
livekit_alias: roomId,
|
||||||
livekit_service_url: "https://example.org",
|
livekit_service_url: "https://example.org",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
focus_active: {
|
"focus_active": {
|
||||||
focus_selection: "oldest_membership",
|
focus_selection: "oldest_membership",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
},
|
},
|
||||||
scope: "m.room",
|
"scope": "m.room",
|
||||||
},
|
},
|
||||||
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
|
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
|
||||||
);
|
);
|
||||||
@ -64,6 +65,7 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n
|
|||||||
event_id: resp.event_id,
|
event_id: resp.event_id,
|
||||||
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
||||||
},
|
},
|
||||||
|
"m.call.intent": intent,
|
||||||
"notification_type": notification,
|
"notification_type": notification,
|
||||||
"sender_ts": 1758611895996,
|
"sender_ts": 1758611895996,
|
||||||
});
|
});
|
||||||
@ -103,15 +105,21 @@ test.describe("Element Call", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Group Chat", () => {
|
test.describe("Group Chat", () => {
|
||||||
|
let charlie: Bot;
|
||||||
test.use({
|
test.use({
|
||||||
room: async ({ page, app, user, bot }, use) => {
|
room: async ({ page, app, user, homeserver, bot }, use) => {
|
||||||
const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
|
charlie = new Bot(page, homeserver, { displayName: "Charlie" });
|
||||||
|
await charlie.prepareClient();
|
||||||
|
const roomId = await app.client.createRoom({
|
||||||
|
name: "TestRoom",
|
||||||
|
invite: [bot.credentials.userId, charlie.credentials.userId],
|
||||||
|
});
|
||||||
await use({ roomId });
|
await use({ roomId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
test("should be able to start a video call", async ({ page, user, room, app }) => {
|
test("should be able to start a video call", async ({ page, user, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Video call" }).click();
|
await page.getByRole("button", { name: "Video call" }).click();
|
||||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||||
@ -126,9 +134,16 @@ test.describe("Element Call", () => {
|
|||||||
expect(hash.get("skipLobby")).toEqual(null);
|
expect(hash.get("skipLobby")).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should NOT be able to start a voice call", async ({ page, user, room, app }) => {
|
||||||
|
// Voice calls do not exist in group rooms
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "Voice call" })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
|
test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Video call" }).click();
|
await page.getByRole("button", { name: "Video call" }).click();
|
||||||
await page.keyboard.down("Shift");
|
await page.keyboard.down("Shift");
|
||||||
@ -147,8 +162,8 @@ test.describe("Element Call", () => {
|
|||||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
// Allow bob to create a call
|
// Allow bob to create a call
|
||||||
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
|
||||||
// Fake a start of a call
|
// Fake a start of a call
|
||||||
await sendRTCState(bot, room.roomId);
|
await sendRTCState(bot, room.roomId);
|
||||||
const button = page.getByTestId("join-call-button");
|
const button = page.getByTestId("join-call-button");
|
||||||
@ -156,7 +171,6 @@ test.describe("Element Call", () => {
|
|||||||
// And test joining
|
// And test joining
|
||||||
await button.click();
|
await button.click();
|
||||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
console.log(frameUrlStr);
|
|
||||||
await expect(frameUrlStr).toBeDefined();
|
await expect(frameUrlStr).toBeDefined();
|
||||||
const url = new URL(frameUrlStr);
|
const url = new URL(frameUrlStr);
|
||||||
const hash = new URLSearchParams(url.hash.slice(1));
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
@ -168,29 +182,29 @@ test.describe("Element Call", () => {
|
|||||||
|
|
||||||
[true, false].forEach((skipLobbyToggle) => {
|
[true, false].forEach((skipLobbyToggle) => {
|
||||||
test(
|
test(
|
||||||
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
|
`should be able to join a call via incoming video call toast (skipLobby=${skipLobbyToggle})`,
|
||||||
{ tag: ["@screenshot"] },
|
{ tag: ["@screenshot"] },
|
||||||
async ({ page, user, bot, room, app }) => {
|
async ({ page, user, bot, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
// Allow bob to create a call
|
// Allow bob to create a call
|
||||||
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
|
||||||
// Fake a start of a call
|
// Fake a start of a call
|
||||||
await sendRTCState(bot, room.roomId, "notification");
|
await sendRTCState(bot, room.roomId, "notification", "video");
|
||||||
const toast = page.locator(".mx_Toast_toast");
|
const toast = page.locator(".mx_Toast_toast");
|
||||||
const button = toast.getByRole("button", { name: "Join" });
|
const button = toast.getByRole("button", { name: "Join" });
|
||||||
|
|
||||||
if (skipLobbyToggle) {
|
if (skipLobbyToggle) {
|
||||||
await toast.getByRole("switch").check();
|
await toast.getByRole("switch").check();
|
||||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
|
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-checked.png`);
|
||||||
} else {
|
} else {
|
||||||
await toast.getByRole("switch").uncheck();
|
await toast.getByRole("switch").uncheck();
|
||||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
|
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-unchecked.png`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// And test joining
|
// And test joining
|
||||||
await button.click();
|
await button.click();
|
||||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
console.log(frameUrlStr);
|
|
||||||
await expect(frameUrlStr).toBeDefined();
|
await expect(frameUrlStr).toBeDefined();
|
||||||
const url = new URL(frameUrlStr);
|
const url = new URL(frameUrlStr);
|
||||||
const hash = new URLSearchParams(url.hash.slice(1));
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
@ -201,6 +215,34 @@ test.describe("Element Call", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
`should be able to join a call via incoming voice call toast`,
|
||||||
|
{ tag: ["@screenshot"] },
|
||||||
|
async ({ page, user, bot, room, app }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
// Allow bob to create a call
|
||||||
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
|
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||||
|
// Fake a start of a call
|
||||||
|
await sendRTCState(bot, room.roomId, "notification", "audio");
|
||||||
|
const toast = page.locator(".mx_Toast_toast");
|
||||||
|
const button = toast.getByRole("button", { name: "Join" });
|
||||||
|
|
||||||
|
await expect(toast).toMatchScreenshot(`incoming-call-group-voice-toast.png`);
|
||||||
|
|
||||||
|
// And test joining
|
||||||
|
await button.click();
|
||||||
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
|
await expect(frameUrlStr).toBeDefined();
|
||||||
|
const url = new URL(frameUrlStr);
|
||||||
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
|
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||||
|
|
||||||
|
expect(hash.get("intent")).toEqual("join_existing");
|
||||||
|
expect(hash.get("skipLobby")).toEqual("true");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("DMs", () => {
|
test.describe("DMs", () => {
|
||||||
@ -253,7 +295,6 @@ test.describe("Element Call", () => {
|
|||||||
|
|
||||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
// Allow bob to create a call
|
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||||
// Fake a start of a call
|
// Fake a start of a call
|
||||||
await sendRTCState(bot, room.roomId);
|
await sendRTCState(bot, room.roomId);
|
||||||
@ -262,7 +303,6 @@ test.describe("Element Call", () => {
|
|||||||
// And test joining
|
// And test joining
|
||||||
await button.click();
|
await button.click();
|
||||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
console.log(frameUrlStr);
|
|
||||||
await expect(frameUrlStr).toBeDefined();
|
await expect(frameUrlStr).toBeDefined();
|
||||||
const url = new URL(frameUrlStr);
|
const url = new URL(frameUrlStr);
|
||||||
const hash = new URLSearchParams(url.hash.slice(1));
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
@ -278,24 +318,31 @@ test.describe("Element Call", () => {
|
|||||||
{ tag: ["@screenshot"] },
|
{ tag: ["@screenshot"] },
|
||||||
async ({ page, user, bot, room, app }) => {
|
async ({ page, user, bot, room, app }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
await app.viewRoomById(room.roomId);
|
||||||
// Allow bob to create a call
|
|
||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||||
// Fake a start of a call
|
// Fake a start of a call
|
||||||
await sendRTCState(bot, room.roomId, "ring");
|
await sendRTCState(bot, room.roomId, "ring", "video");
|
||||||
const toast = page.locator(".mx_Toast_toast");
|
const toast = page.locator(".mx_Toast_toast");
|
||||||
const button = toast.getByRole("button", { name: "Join" });
|
const button = toast.getByRole("button", { name: "Accept" });
|
||||||
if (skipLobbyToggle) {
|
if (skipLobbyToggle) {
|
||||||
await toast.getByRole("switch").check();
|
await toast.getByRole("switch").check();
|
||||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
|
|
||||||
} else {
|
} else {
|
||||||
await toast.getByRole("switch").uncheck();
|
await toast.getByRole("switch").uncheck();
|
||||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
|
|
||||||
}
|
}
|
||||||
|
await expect(toast).toMatchScreenshot(
|
||||||
|
`incoming-call-dm-video-toast-${skipLobbyToggle ? "checked" : "unchecked"}.png`,
|
||||||
|
{
|
||||||
|
// Hide UserId
|
||||||
|
css: `
|
||||||
|
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// And test joining
|
// And test joining
|
||||||
await button.click();
|
await button.click();
|
||||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
console.log(frameUrlStr);
|
|
||||||
await expect(frameUrlStr).toBeDefined();
|
await expect(frameUrlStr).toBeDefined();
|
||||||
const url = new URL(frameUrlStr);
|
const url = new URL(frameUrlStr);
|
||||||
const hash = new URLSearchParams(url.hash.slice(1));
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
@ -306,6 +353,39 @@ test.describe("Element Call", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
`should be able to join a call via incoming voice call toast`,
|
||||||
|
{ tag: ["@screenshot"] },
|
||||||
|
async ({ page, user, bot, room, app }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||||
|
// Fake a start of a call
|
||||||
|
await sendRTCState(bot, room.roomId, "ring", "audio");
|
||||||
|
const toast = page.locator(".mx_Toast_toast");
|
||||||
|
const button = toast.getByRole("button", { name: "Accept" });
|
||||||
|
|
||||||
|
await expect(toast).toMatchScreenshot(`incoming-call-dm-voice-toast.png`, {
|
||||||
|
// Hide UserId
|
||||||
|
css: `
|
||||||
|
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// And test joining
|
||||||
|
await button.click();
|
||||||
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
|
await expect(frameUrlStr).toBeDefined();
|
||||||
|
const url = new URL(frameUrlStr);
|
||||||
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
|
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||||
|
|
||||||
|
expect(hash.get("intent")).toEqual("join_existing_dm_voice");
|
||||||
|
expect(hash.get("skipLobby")).toEqual("true");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Video Rooms", () => {
|
test.describe("Video Rooms", () => {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@ -25,6 +25,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_LiveContentSummary_text_voice::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/voice-call.svg");
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_LiveContentSummary_text_active {
|
&.mx_LiveContentSummary_text_active {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
import dispatcher from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
@ -19,7 +20,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
|||||||
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||||
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
||||||
import { type ConnectionState } from "../../../models/Call";
|
import { CallEvent, type ConnectionState } from "../../../models/Call";
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
@ -67,6 +68,10 @@ export interface RoomListItemViewState {
|
|||||||
* Whether there are participants in the call.
|
* Whether there are participants in the call.
|
||||||
*/
|
*/
|
||||||
hasParticipantInCall: boolean;
|
hasParticipantInCall: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the call is a voice or video call.
|
||||||
|
*/
|
||||||
|
callType: CallType | undefined;
|
||||||
/**
|
/**
|
||||||
* Pre-rendered and translated preview for the latest message in the room, or undefined
|
* Pre-rendered and translated preview for the latest message in the room, or undefined
|
||||||
* if no preview should be shown.
|
* if no preview should be shown.
|
||||||
@ -123,10 +128,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
// EC video call or video room
|
// EC video call or video room
|
||||||
const call = useCall(room.roomId);
|
const call = useCall(room.roomId);
|
||||||
const connectionState = useConnectionState(call);
|
const connectionState = useConnectionState(call);
|
||||||
const hasParticipantInCall = useParticipantCount(call) > 0;
|
const participantCount = useParticipantCount(call);
|
||||||
const callConnectionState = call ? connectionState : null;
|
const callConnectionState = call ? connectionState : null;
|
||||||
|
|
||||||
const showNotificationDecoration = hasVisibleNotification || hasParticipantInCall;
|
const showNotificationDecoration = hasVisibleNotification || participantCount > 0;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
@ -138,6 +143,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
});
|
});
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
|
const [callType, setCallType] = useState<CallType>(CallType.Video);
|
||||||
|
useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
notificationState,
|
notificationState,
|
||||||
@ -148,9 +156,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
isBold,
|
isBold,
|
||||||
isVideoRoom,
|
isVideoRoom,
|
||||||
callConnectionState,
|
callConnectionState,
|
||||||
hasParticipantInCall,
|
hasParticipantInCall: participantCount > 0,
|
||||||
messagePreview,
|
messagePreview,
|
||||||
showNotificationDecoration,
|
showNotificationDecoration,
|
||||||
|
callType: call ? callType : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,12 +10,10 @@ import React, { type FC } from "react";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { type Call } from "../../../models/Call";
|
|
||||||
import { useParticipantCount } from "../../../hooks/useCall";
|
|
||||||
|
|
||||||
export enum LiveContentType {
|
export enum LiveContentType {
|
||||||
Video,
|
Video,
|
||||||
// More coming soon
|
Voice,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -33,6 +31,7 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
|||||||
<span
|
<span
|
||||||
className={classNames("mx_LiveContentSummary_text", {
|
className={classNames("mx_LiveContentSummary_text", {
|
||||||
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
|
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
|
||||||
|
mx_LiveContentSummary_text_voice: type === LiveContentType.Voice,
|
||||||
mx_LiveContentSummary_text_active: active,
|
mx_LiveContentSummary_text_active: active,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -51,16 +50,3 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface LiveContentSummaryWithCallProps {
|
|
||||||
call: Call;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
|
|
||||||
<LiveContentSummary
|
|
||||||
type={LiveContentType.Video}
|
|
||||||
text={_t("common|video")}
|
|
||||||
active={false}
|
|
||||||
participantCount={useParticipantCount(call)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/ic
|
|||||||
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
||||||
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
|
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
|
||||||
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
||||||
|
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { Flex } from "@element-hq/web-shared-components";
|
import { Flex } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||||
@ -24,9 +26,9 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
|||||||
*/
|
*/
|
||||||
notificationState: RoomNotificationState;
|
notificationState: RoomNotificationState;
|
||||||
/**
|
/**
|
||||||
* Whether the room has a video call.
|
* Whether the room has a voice or video call.
|
||||||
*/
|
*/
|
||||||
hasVideoCall: boolean;
|
callType?: CallType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +36,7 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
|||||||
*/
|
*/
|
||||||
export function NotificationDecoration({
|
export function NotificationDecoration({
|
||||||
notificationState,
|
notificationState,
|
||||||
hasVideoCall,
|
callType,
|
||||||
...props
|
...props
|
||||||
}: NotificationDecorationProps): JSX.Element | null {
|
}: NotificationDecorationProps): JSX.Element | null {
|
||||||
// Listen to the notification state and update the component when it changes
|
// Listen to the notification state and update the component when it changes
|
||||||
@ -58,7 +60,7 @@ export function NotificationDecoration({
|
|||||||
muted: notificationState.muted,
|
muted: notificationState.muted,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
|
if (!hasAnyNotificationOrActivity && !muted && !callType) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -69,7 +71,12 @@ export function NotificationDecoration({
|
|||||||
data-testid="notification-decoration"
|
data-testid="notification-decoration"
|
||||||
>
|
>
|
||||||
{isUnsentMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
|
{isUnsentMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
|
||||||
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
{callType === CallType.Video && (
|
||||||
|
<VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
|
||||||
|
)}
|
||||||
|
{callType === CallType.Voice && (
|
||||||
|
<VoiceCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
|
||||||
|
)}
|
||||||
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||||
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||||
{(isMention || isNotification) && <UnreadCounter count={count || null} />}
|
{(isMention || isNotification) && <UnreadCounter count={count || null} />}
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
|||||||
<NotificationDecoration
|
<NotificationDecoration
|
||||||
notificationState={vm.notificationState}
|
notificationState={vm.notificationState}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
hasVideoCall={vm.hasParticipantInCall}
|
callType={vm.callType}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export enum Action {
|
|||||||
UpdateSystemFont = "update_system_font",
|
UpdateSystemFont = "update_system_font",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes room based on payload parameters. Should be used with JoinRoomPayload.
|
* Changes room based on payload parameters. Should be used with ViewRoomPayload.
|
||||||
*/
|
*/
|
||||||
ViewRoom = "view_room",
|
ViewRoom = "view_room",
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ interface BaseViewRoomPayload extends Pick<ActionPayload, "action"> {
|
|||||||
clear_search?: boolean; // Whether to clear the room list search
|
clear_search?: boolean; // Whether to clear the room list search
|
||||||
view_call?: boolean; // Whether to view the call or call lobby for the room
|
view_call?: boolean; // Whether to view the call or call lobby for the room
|
||||||
skipLobby?: boolean; // Whether to skip the call lobby when showing the call (only supported for element calls)
|
skipLobby?: boolean; // Whether to skip the call lobby when showing the call (only supported for element calls)
|
||||||
|
voiceOnly?: boolean; // Whether the call is voice only (only supported for element calls)
|
||||||
opts?: JoinRoomPayload["opts"];
|
opts?: JoinRoomPayload["opts"];
|
||||||
|
|
||||||
deferred_action?: ActionPayload; // Action to fire after MatrixChat handles this ViewRoom action
|
deferred_action?: ActionPayload; // Action to fire after MatrixChat handles this ViewRoom action
|
||||||
|
|||||||
@ -142,11 +142,6 @@ export const useRoomCall = (
|
|||||||
// If there are multiple options, the user will be prompted to choose.
|
// If there are multiple options, the user will be prompted to choose.
|
||||||
const callOptions = useMemo((): PlatformCallType[] => {
|
const callOptions = useMemo((): PlatformCallType[] => {
|
||||||
const options: PlatformCallType[] = [];
|
const options: PlatformCallType[] = [];
|
||||||
if (memberCount <= 2) {
|
|
||||||
options.push(PlatformCallType.LegacyCall);
|
|
||||||
} else if (mayEditWidgets || hasJitsiWidget) {
|
|
||||||
options.push(PlatformCallType.JitsiCall);
|
|
||||||
}
|
|
||||||
if (groupCallsEnabled) {
|
if (groupCallsEnabled) {
|
||||||
if (hasGroupCall || mayCreateElementCalls) {
|
if (hasGroupCall || mayCreateElementCalls) {
|
||||||
options.push(PlatformCallType.ElementCall);
|
options.push(PlatformCallType.ElementCall);
|
||||||
@ -155,6 +150,11 @@ export const useRoomCall = (
|
|||||||
return [PlatformCallType.ElementCall];
|
return [PlatformCallType.ElementCall];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (memberCount <= 2) {
|
||||||
|
options.push(PlatformCallType.LegacyCall);
|
||||||
|
} else if (mayEditWidgets || hasJitsiWidget) {
|
||||||
|
options.push(PlatformCallType.JitsiCall);
|
||||||
|
}
|
||||||
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
||||||
// only allow joining the ongoing Element call if there is one.
|
// only allow joining the ongoing Element call if there is one.
|
||||||
return [PlatformCallType.ElementCall];
|
return [PlatformCallType.ElementCall];
|
||||||
@ -231,7 +231,7 @@ export const useRoomCall = (
|
|||||||
if (widget && promptPinWidget) {
|
if (widget && promptPinWidget) {
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||||
} else {
|
} else {
|
||||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
|
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[promptPinWidget, room, widget],
|
[promptPinWidget, room, widget],
|
||||||
@ -244,7 +244,7 @@ export const useRoomCall = (
|
|||||||
} else {
|
} else {
|
||||||
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
||||||
// to the defaults of the call implementation.
|
// to the defaults of the call implementation.
|
||||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
|
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[widget, promptPinWidget, room],
|
[widget, promptPinWidget, room],
|
||||||
@ -279,7 +279,13 @@ export const useRoomCall = (
|
|||||||
const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;
|
const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;
|
||||||
|
|
||||||
// We hide the voice call button if it'd have the same effect as the video call button
|
// We hide the voice call button if it'd have the same effect as the video call button
|
||||||
let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall);
|
let hideVoiceCallButton =
|
||||||
|
isManagedHybridWidgetEnabled(room) ||
|
||||||
|
// Disable voice calls if Legacy calls are disabled
|
||||||
|
(!callOptions.includes(PlatformCallType.LegacyCall) &&
|
||||||
|
// Disable voice calls in ECall if the room is a group (we only present video calls for groups of users)
|
||||||
|
(!callOptions.includes(PlatformCallType.ElementCall) || memberCount > 2));
|
||||||
|
|
||||||
let hideVideoCallButton = false;
|
let hideVideoCallButton = false;
|
||||||
// We hide both buttons if:
|
// We hide both buttons if:
|
||||||
// - they require widgets but widgets are disabled
|
// - they require widgets but widgets are disabled
|
||||||
|
|||||||
@ -603,6 +603,7 @@
|
|||||||
"video": "Video",
|
"video": "Video",
|
||||||
"video_room": "Video room",
|
"video_room": "Video room",
|
||||||
"view_message": "View message",
|
"view_message": "View message",
|
||||||
|
"voice": "Voice",
|
||||||
"warning": "Warning"
|
"warning": "Warning"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
@ -4096,9 +4097,11 @@
|
|||||||
"user_busy_description": "The user you called is busy.",
|
"user_busy_description": "The user you called is busy.",
|
||||||
"user_is_presenting": "%(sharerName)s is presenting",
|
"user_is_presenting": "%(sharerName)s is presenting",
|
||||||
"video_call": "Video call",
|
"video_call": "Video call",
|
||||||
|
"video_call_incoming": "Incoming video call",
|
||||||
"video_call_started": "Video call started",
|
"video_call_started": "Video call started",
|
||||||
"video_call_using": "Video call using:",
|
"video_call_using": "Video call using:",
|
||||||
"voice_call": "Voice call",
|
"voice_call": "Voice call",
|
||||||
|
"voice_call_incoming": "Incoming voice call",
|
||||||
"you_are_presenting": "You are presenting"
|
"you_are_presenting": "You are presenting"
|
||||||
},
|
},
|
||||||
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",
|
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",
|
||||||
|
|||||||
@ -84,6 +84,7 @@ export enum CallEvent {
|
|||||||
Participants = "participants",
|
Participants = "participants",
|
||||||
Close = "close",
|
Close = "close",
|
||||||
Destroy = "destroy",
|
Destroy = "destroy",
|
||||||
|
CallTypeChanged = "call_type_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallEventHandlerMap {
|
interface CallEventHandlerMap {
|
||||||
@ -94,6 +95,7 @@ interface CallEventHandlerMap {
|
|||||||
) => void;
|
) => void;
|
||||||
[CallEvent.Close]: () => void;
|
[CallEvent.Close]: () => void;
|
||||||
[CallEvent.Destroy]: () => void;
|
[CallEvent.Destroy]: () => void;
|
||||||
|
[CallEvent.CallTypeChanged]: (callType: CallType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,6 +105,18 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
protected readonly widgetUid: string;
|
protected readonly widgetUid: string;
|
||||||
protected readonly room: Room;
|
protected readonly room: Room;
|
||||||
|
|
||||||
|
private _callType: CallType = CallType.Video;
|
||||||
|
public get callType(): CallType {
|
||||||
|
return this._callType;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected set callType(callType: CallType) {
|
||||||
|
if (this._callType !== callType) {
|
||||||
|
this.emit(CallEvent.CallTypeChanged, callType);
|
||||||
|
}
|
||||||
|
this._callType = callType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time after which device member state should be considered expired.
|
* The time after which device member state should be considered expired.
|
||||||
*/
|
*/
|
||||||
@ -544,7 +558,24 @@ export enum ElementCallIntent {
|
|||||||
StartCall = "start_call",
|
StartCall = "start_call",
|
||||||
JoinExisting = "join_existing",
|
JoinExisting = "join_existing",
|
||||||
StartCallDM = "start_call_dm",
|
StartCallDM = "start_call_dm",
|
||||||
|
StartCallDMVoice = "start_call_dm_voice",
|
||||||
JoinExistingDM = "join_existing_dm",
|
JoinExistingDM = "join_existing_dm",
|
||||||
|
JoinExistingDMVoice = "join_existing_dm_voice",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters to be passed during widget creation.
|
||||||
|
* These parameters are hints only, and may not be accepted by the implementation.
|
||||||
|
*/
|
||||||
|
export interface WidgetGenerationParameters {
|
||||||
|
/**
|
||||||
|
* Skip showing the lobby screen of a call.
|
||||||
|
*/
|
||||||
|
skipLobby?: boolean;
|
||||||
|
/**
|
||||||
|
* Does the user intent to start a voice call?
|
||||||
|
*/
|
||||||
|
voiceOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -586,7 +617,12 @@ export class ElementCall extends Call {
|
|||||||
* @param client The current client.
|
* @param client The current client.
|
||||||
* @param roomId The room ID for the call.
|
* @param roomId The room ID for the call.
|
||||||
*/
|
*/
|
||||||
private static appendRoomParams(params: URLSearchParams, client: MatrixClient, roomId: string): void {
|
private static appendRoomParams(
|
||||||
|
params: URLSearchParams,
|
||||||
|
client: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
{ voiceOnly }: WidgetGenerationParameters,
|
||||||
|
): void {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
// If the room isn't known, or the room is a video room then skip setting an intent.
|
// If the room isn't known, or the room is a video room then skip setting an intent.
|
||||||
@ -610,13 +646,17 @@ export class ElementCall extends Call {
|
|||||||
// is released and upgraded.
|
// is released and upgraded.
|
||||||
if (isDM) {
|
if (isDM) {
|
||||||
if (hasCallStarted) {
|
if (hasCallStarted) {
|
||||||
params.append("intent", ElementCallIntent.JoinExistingDM);
|
params.append(
|
||||||
|
"intent",
|
||||||
|
voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM,
|
||||||
|
);
|
||||||
params.append("preload", "false");
|
params.append("preload", "false");
|
||||||
} else {
|
} else {
|
||||||
params.append("intent", ElementCallIntent.StartCallDM);
|
params.append("intent", voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM);
|
||||||
params.append("preload", "false");
|
params.append("preload", "false");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Group chats do not have a voice option.
|
||||||
if (hasCallStarted) {
|
if (hasCallStarted) {
|
||||||
params.append("intent", ElementCallIntent.JoinExisting);
|
params.append("intent", ElementCallIntent.JoinExisting);
|
||||||
params.append("preload", "false");
|
params.append("preload", "false");
|
||||||
@ -717,7 +757,7 @@ export class ElementCall extends Call {
|
|||||||
.forEach((font) => params.append("font", font));
|
.forEach((font) => params.append("font", font));
|
||||||
}
|
}
|
||||||
this.appendAnalyticsParams(params, client);
|
this.appendAnalyticsParams(params, client);
|
||||||
this.appendRoomParams(params, client, roomId);
|
this.appendRoomParams(params, client, roomId, opts);
|
||||||
|
|
||||||
const replacedUrl = params.toString().replace(/%24/g, "$");
|
const replacedUrl = params.toString().replace(/%24/g, "$");
|
||||||
url.hash = `#?${replacedUrl}`;
|
url.hash = `#?${replacedUrl}`;
|
||||||
@ -751,11 +791,43 @@ export class ElementCall extends Call {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the correct intent for a widget, so that Element Call presents the correct
|
||||||
|
* default config.
|
||||||
|
* @param client The matrix client.
|
||||||
|
* @param roomId
|
||||||
|
* @param voiceOnly Should the call be voice-only, or video (default).
|
||||||
|
*/
|
||||||
|
public static getWidgetIntent(client: MatrixClient, roomId: string, voiceOnly?: boolean): ElementCallIntent {
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (room !== null && !isVideoRoom(room)) {
|
||||||
|
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
|
||||||
|
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
|
||||||
|
if (isDM) {
|
||||||
|
if (hasCallStarted) {
|
||||||
|
return voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM;
|
||||||
|
} else {
|
||||||
|
return voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasCallStarted) {
|
||||||
|
return ElementCallIntent.JoinExisting;
|
||||||
|
} else {
|
||||||
|
return ElementCallIntent.StartCall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If unknown, default to joining an existing call.
|
||||||
|
return ElementCallIntent.JoinExisting;
|
||||||
|
}
|
||||||
|
|
||||||
private static getWidgetData(
|
private static getWidgetData(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
currentData: IWidgetData,
|
currentData: IWidgetData,
|
||||||
overwriteData: IWidgetData,
|
overwriteData: IWidgetData,
|
||||||
|
voiceOnly?: boolean,
|
||||||
): IWidgetData {
|
): IWidgetData {
|
||||||
let perParticipantE2EE = false;
|
let perParticipantE2EE = false;
|
||||||
if (
|
if (
|
||||||
@ -763,9 +835,13 @@ export class ElementCall extends Call {
|
|||||||
!SettingsStore.getValue("feature_disable_call_per_sender_encryption")
|
!SettingsStore.getValue("feature_disable_call_per_sender_encryption")
|
||||||
)
|
)
|
||||||
perParticipantE2EE = true;
|
perParticipantE2EE = true;
|
||||||
|
|
||||||
|
const intent = ElementCall.getWidgetIntent(client, roomId, voiceOnly);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentData,
|
...currentData,
|
||||||
...overwriteData,
|
...overwriteData,
|
||||||
|
intent,
|
||||||
perParticipantE2EE,
|
perParticipantE2EE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -791,7 +867,7 @@ export class ElementCall extends Call {
|
|||||||
this.updateParticipants();
|
this.updateParticipants();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get(room: Room): ElementCall | null {
|
public static get(room: Room, voiceOnly?: boolean): ElementCall | null {
|
||||||
const apps = WidgetStore.instance.getApps(room.roomId);
|
const apps = WidgetStore.instance.getApps(room.roomId);
|
||||||
const hasEcWidget = apps.some((app) => WidgetType.CALL.matches(app.type));
|
const hasEcWidget = apps.some((app) => WidgetType.CALL.matches(app.type));
|
||||||
const session = room.client.matrixRTC.getRoomSession(room);
|
const session = room.client.matrixRTC.getRoomSession(room);
|
||||||
@ -874,7 +950,10 @@ export class ElementCall extends Call {
|
|||||||
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
|
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly onMembershipChanged = (): void => this.updateParticipants();
|
private readonly onMembershipChanged = (): void => {
|
||||||
|
this.updateParticipants();
|
||||||
|
this.callType = this.session.getConsensusCallIntent() === "audio" ? CallType.Voice : CallType.Video;
|
||||||
|
};
|
||||||
|
|
||||||
private updateParticipants(): void {
|
private updateParticipants(): void {
|
||||||
const participants = new Map<RoomMember, Set<string>>();
|
const participants = new Map<RoomMember, Set<string>>();
|
||||||
|
|||||||
@ -365,7 +365,9 @@ export class RoomViewStore extends EventEmitter {
|
|||||||
call.presented = true;
|
call.presented = true;
|
||||||
// Immediately start the call. This will connect to all required widget events
|
// Immediately start the call. This will connect to all required widget events
|
||||||
// and allow the widget to show the lobby.
|
// and allow the widget to show the lobby.
|
||||||
if (call.connectionState === ConnectionState.Disconnected) call.start({ skipLobby: payload.skipLobby });
|
if (call.connectionState === ConnectionState.Disconnected) {
|
||||||
|
call.start({ skipLobby: payload.skipLobby, voiceOnly: payload.voiceOnly });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If we switch to a different room from the call, we are no longer presenting it
|
// If we switch to a different room from the call, we are no longer presenting it
|
||||||
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
|
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
|
|||||||
import CrossIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
import CrossIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import { VoiceCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import { AvatarWithDetails } from "@element-hq/web-shared-components";
|
import { AvatarWithDetails } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
@ -23,12 +24,8 @@ import defaultDispatcher from "../dispatcher/dispatcher";
|
|||||||
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import {
|
import { LiveContentSummary, LiveContentType } from "../components/views/rooms/LiveContentSummary";
|
||||||
LiveContentSummary,
|
import { useCall, useJoinCallButtonDisabledTooltip, useParticipantCount } from "../hooks/useCall";
|
||||||
LiveContentSummaryWithCall,
|
|
||||||
LiveContentType,
|
|
||||||
} from "../components/views/rooms/LiveContentSummary";
|
|
||||||
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
|
|
||||||
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
|
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
|
||||||
import { useDispatcher } from "../hooks/useDispatcher";
|
import { useDispatcher } from "../hooks/useDispatcher";
|
||||||
import { type ActionPayload } from "../dispatcher/payloads";
|
import { type ActionPayload } from "../dispatcher/payloads";
|
||||||
@ -36,6 +33,7 @@ import { type Call, CallEvent } from "../models/Call";
|
|||||||
import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
|
import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
|
||||||
import { useEventEmitter } from "../hooks/useEventEmitter";
|
import { useEventEmitter } from "../hooks/useEventEmitter";
|
||||||
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
||||||
|
import DMRoomMap from "../utils/DMRoomMap";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the key for the incoming call toast. A combination of the event ID and room ID.
|
* Get the key for the incoming call toast. A combination of the event ID and room ID.
|
||||||
@ -71,9 +69,15 @@ interface JoinCallButtonWithCallProps {
|
|||||||
onClick: (e: ButtonEvent) => void;
|
onClick: (e: ButtonEvent) => void;
|
||||||
call: Call | null;
|
call: Call | null;
|
||||||
disabledTooltip: string | undefined;
|
disabledTooltip: string | undefined;
|
||||||
|
isRinging: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButtonWithCallProps): JSX.Element {
|
function JoinCallButtonWithCall({
|
||||||
|
onClick,
|
||||||
|
call,
|
||||||
|
disabledTooltip,
|
||||||
|
isRinging,
|
||||||
|
}: JoinCallButtonWithCallProps): JSX.Element {
|
||||||
let disTooltip = disabledTooltip;
|
let disTooltip = disabledTooltip;
|
||||||
const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call);
|
const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||||
disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined;
|
disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined;
|
||||||
@ -88,7 +92,7 @@ function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButt
|
|||||||
Icon={CheckIcon}
|
Icon={CheckIcon}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{_t("action|join")}
|
{isRinging ? _t("action|accept") : _t("action|join")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@ -152,7 +156,7 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
|||||||
// This section can race, so we use a ref to keep track of whether we have started trying to play.
|
// This section can race, so we use a ref to keep track of whether we have started trying to play.
|
||||||
// This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously
|
// This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously
|
||||||
// and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing.
|
// and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing.
|
||||||
const isRingToast = notificationContent.notification_type == "ring";
|
const isRingToast = notificationContent.notification_type === "ring";
|
||||||
if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) {
|
if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) {
|
||||||
// Start ringing if not already.
|
// Start ringing if not already.
|
||||||
soundHasStarted.current = true;
|
soundHasStarted.current = true;
|
||||||
@ -243,10 +247,11 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
|||||||
room_id: room?.roomId,
|
room_id: room?.roomId,
|
||||||
view_call: true,
|
view_call: true,
|
||||||
skipLobby: ("shiftKey" in e && e.shiftKey) || skipLobbyToggle,
|
skipLobby: ("shiftKey" in e && e.shiftKey) || skipLobbyToggle,
|
||||||
|
voiceOnly: notificationContent["m.call.intent"] === "audio",
|
||||||
metricsTrigger: undefined,
|
metricsTrigger: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[room, skipLobbyToggle],
|
[room, skipLobbyToggle, notificationContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dismiss on closing toast.
|
// Dismiss on closing toast.
|
||||||
@ -262,34 +267,53 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
|||||||
useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall);
|
useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall);
|
||||||
useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange);
|
useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange);
|
||||||
useEventEmitter(room, RoomEvent.Timeline, onTimelineChange);
|
useEventEmitter(room, RoomEvent.Timeline, onTimelineChange);
|
||||||
|
const isVoice = notificationContent["m.call.intent"] === "audio";
|
||||||
|
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
const participantCount = useParticipantCount(call);
|
||||||
|
const detailsInformation =
|
||||||
|
notificationContent.notification_type === "ring" ? (
|
||||||
|
<span>{otherUserId}</span>
|
||||||
|
) : (
|
||||||
|
<LiveContentSummary
|
||||||
|
type={isVoice ? LiveContentType.Voice : LiveContentType.Video}
|
||||||
|
text={isVoice ? _t("common|voice") : _t("common|video")}
|
||||||
|
active={false}
|
||||||
|
participantCount={participantCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const callLiveContentSummary = call ? (
|
|
||||||
<LiveContentSummaryWithCall call={call} />
|
|
||||||
) : (
|
|
||||||
<LiveContentSummary
|
|
||||||
type={LiveContentType.Video}
|
|
||||||
text={_t("common|video")}
|
|
||||||
active={false}
|
|
||||||
participantCount={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<>
|
<>
|
||||||
<div className="mx_IncomingCallToast_content">
|
<div className="mx_IncomingCallToast_content">
|
||||||
<div className="mx_IncomingCallToast_message">
|
{isVoice ? (
|
||||||
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
<div className="mx_IncomingCallToast_message">
|
||||||
{_t("voip|video_call_started")}
|
<VoiceCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
||||||
</div>
|
{_t("voip|voice_call_incoming")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mx_IncomingCallToast_message">
|
||||||
|
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
||||||
|
{notificationContent.notification_type === "ring"
|
||||||
|
? _t("voip|video_call_incoming")
|
||||||
|
: _t("voip|video_call_started")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<AvatarWithDetails
|
<AvatarWithDetails
|
||||||
avatar={<RoomAvatar room={room ?? undefined} size="32px" />}
|
avatar={<RoomAvatar room={room ?? undefined} size="32px" />}
|
||||||
details={callLiveContentSummary}
|
details={detailsInformation}
|
||||||
title={room ? room.name : _t("voip|call_toast_unknown_room")}
|
title={room ? room.name : _t("voip|call_toast_unknown_room")}
|
||||||
|
className="mx_IncomingCallToast_AvatarWithDetails"
|
||||||
/>
|
/>
|
||||||
<div className="mx_IncomingCallToast_toggleWithLabel">
|
{!isVoice && (
|
||||||
<span>{_t("voip|skip_lobby_toggle_option")}</span>
|
<div className="mx_IncomingCallToast_toggleWithLabel">
|
||||||
<ToggleInput onChange={(e) => setSkipLobbyToggle(e.target.checked)} checked={skipLobbyToggle} />
|
<span>{_t("voip|skip_lobby_toggle_option")}</span>
|
||||||
</div>
|
<ToggleInput
|
||||||
|
onChange={(e) => setSkipLobbyToggle(e.target.checked)}
|
||||||
|
checked={skipLobbyToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mx_IncomingCallToast_buttons">
|
<div className="mx_IncomingCallToast_buttons">
|
||||||
<DeclineCallButtonWithNotificationEvent
|
<DeclineCallButtonWithNotificationEvent
|
||||||
notificationEvent={notificationEvent}
|
notificationEvent={notificationEvent}
|
||||||
@ -299,6 +323,7 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
|||||||
<JoinCallButtonWithCall
|
<JoinCallButtonWithCall
|
||||||
onClick={onJoinClick}
|
onClick={onJoinClick}
|
||||||
call={call}
|
call={call}
|
||||||
|
isRinging={notificationContent.notification_type === "ring"}
|
||||||
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
|
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,7 +27,8 @@ export const placeCall = async (
|
|||||||
room: Room,
|
room: Room,
|
||||||
callType: CallType,
|
callType: CallType,
|
||||||
platformCallType: PlatformCallType,
|
platformCallType: PlatformCallType,
|
||||||
skipLobby?: boolean,
|
skipLobby: boolean | undefined,
|
||||||
|
voiceOnly: boolean,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { analyticsName } = getPlatformCallTypeProps(platformCallType);
|
const { analyticsName } = getPlatformCallTypeProps(platformCallType);
|
||||||
PosthogTrackers.trackInteraction(analyticsName);
|
PosthogTrackers.trackInteraction(analyticsName);
|
||||||
@ -39,6 +40,7 @@ export const placeCall = async (
|
|||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
view_call: true,
|
view_call: true,
|
||||||
|
voiceOnly,
|
||||||
skipLobby,
|
skipLobby,
|
||||||
metricsTrigger: undefined,
|
metricsTrigger: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -181,6 +181,7 @@ export function setUpClientRoomAndStores(): {
|
|||||||
const roomSession = new MockEventEmitter({
|
const roomSession = new MockEventEmitter({
|
||||||
memberships: [],
|
memberships: [],
|
||||||
getOldestMembership: jest.fn().mockReturnValue(undefined),
|
getOldestMembership: jest.fn().mockReturnValue(undefined),
|
||||||
|
getConsensusCallIntent: jest.fn().mockReturnValue(undefined),
|
||||||
room,
|
room,
|
||||||
}) as Mocked<MatrixRTCSession>;
|
}) as Mocked<MatrixRTCSession>;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen } from "jest-matrix-react";
|
import { render, screen } from "jest-matrix-react";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||||
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
|
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
|
||||||
@ -22,7 +23,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
|
|
||||||
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
|
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
|
||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||||
render(<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />);
|
render(<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />);
|
||||||
expect(screen.queryByTestId("notification-decoration")).toBeNull();
|
expect(screen.queryByTestId("notification-decoration")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -39,7 +40,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -49,7 +50,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -59,7 +60,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -69,7 +70,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -78,7 +79,7 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -87,14 +88,21 @@ describe("<NotificationDecoration />", () => {
|
|||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
|
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("should render the video decoration", () => {
|
it("should render the video call decoration", () => {
|
||||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={true} />,
|
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Video} />,
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it("should render the audio call decoration", () => {
|
||||||
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Voice} />,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
|||||||
import { render, screen, waitFor } from "jest-matrix-react";
|
import { render, screen, waitFor } from "jest-matrix-react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||||
import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
|
import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
|
||||||
@ -64,6 +65,7 @@ describe("<RoomListItemView />", () => {
|
|||||||
isBold: false,
|
isBold: false,
|
||||||
isVideoRoom: false,
|
isVideoRoom: false,
|
||||||
callConnectionState: null,
|
callConnectionState: null,
|
||||||
|
callType: CallType.Video,
|
||||||
hasParticipantInCall: false,
|
hasParticipantInCall: false,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
showNotificationDecoration: false,
|
showNotificationDecoration: false,
|
||||||
|
|||||||
@ -103,6 +103,17 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
|||||||
data-testid="notification-decoration"
|
data-testid="notification-decoration"
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
fill="var(--cpd-color-icon-accent-primary)"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
<span
|
<span
|
||||||
class="_unread-counter_9mg0k_8"
|
class="_unread-counter_9mg0k_8"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -16,6 +16,28 @@ exports[`<NotificationDecoration /> should render the activity decoration 1`] =
|
|||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<NotificationDecoration /> should render the audio call decoration 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="_flex_4dswl_9"
|
||||||
|
data-testid="notification-decoration"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="var(--cpd-color-icon-accent-primary)"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<NotificationDecoration /> should render the invitation decoration 1`] = `
|
exports[`<NotificationDecoration /> should render the invitation decoration 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
@ -142,7 +164,7 @@ exports[`<NotificationDecoration /> should render the unset message decoration 1
|
|||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<NotificationDecoration /> should render the video decoration 1`] = `
|
exports[`<NotificationDecoration /> should render the video call decoration 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="_flex_4dswl_9"
|
class="_flex_4dswl_9"
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
|||||||
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
|
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
|
||||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||||
|
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||||
|
|
||||||
const CallView = wrapInMatrixClientContext(_CallView);
|
const CallView = wrapInMatrixClientContext(_CallView);
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ describe("CallView", () => {
|
|||||||
|
|
||||||
stubClient();
|
stubClient();
|
||||||
client = mocked(MatrixClientPeg.safeGet());
|
client = mocked(MatrixClientPeg.safeGet());
|
||||||
|
DMRoomMap.makeShared(client);
|
||||||
|
|
||||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
|||||||
@ -188,6 +188,7 @@ describe("IncomingCallToast", () => {
|
|||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
skipLobby: true,
|
skipLobby: true,
|
||||||
view_call: true,
|
view_call: true,
|
||||||
|
voiceOnly: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@ -215,6 +216,7 @@ describe("IncomingCallToast", () => {
|
|||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
skipLobby: false,
|
skipLobby: false,
|
||||||
view_call: true,
|
view_call: true,
|
||||||
|
voiceOnly: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@ -239,6 +241,7 @@ describe("IncomingCallToast", () => {
|
|||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
skipLobby: true,
|
skipLobby: true,
|
||||||
view_call: true,
|
view_call: true,
|
||||||
|
voiceOnly: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user