diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index 06bd675e19..71340d44f6 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -162,7 +162,7 @@ describe("Spotlight", () => { cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => { cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => { room1Id = _room1Id; - cy.inviteUser(room1Id, bot1.getUserId()); + bot1.joinRoom(room1Id); cy.visit("/#/room/" + room1Id); }); bot2.createRoom({ name: room2Name, visibility: Visibility.Public }) diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 1cc6848ec5..5af2d07d79 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -73,7 +73,10 @@ describe("Threads", () => { it("should be usable for a conversation", () => { let bot: MatrixClient; - cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => { + cy.getBot(synapse, { + displayName: "BotBob", + autoAcceptInvites: false, + }).then(_bot => { bot = _bot; }); @@ -81,6 +84,7 @@ describe("Threads", () => { cy.createRoom({}).then(_roomId => { roomId = _roomId; cy.inviteUser(roomId, bot.getUserId()); + bot.joinRoom(roomId); cy.visit("/#/room/" + roomId); }); diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 17fb679f24..6f9eb010ec 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -337,6 +337,7 @@ @import "./views/spaces/_SpacePublicShare.pcss"; @import "./views/terms/_InlineTermsAgreement.pcss"; @import "./views/toasts/_AnalyticsToast.pcss"; +@import "./views/toasts/_IncomingCallToast.pcss"; @import "./views/toasts/_IncomingLegacyCallToast.pcss"; @import "./views/toasts/_NonUrgentEchoFailureToast.pcss"; @import "./views/typography/_Heading.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss b/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss index 546d4f7ea1..a092112d8a 100644 --- a/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss +++ b/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss @@ -22,7 +22,7 @@ limitations under the License. padding: 0 $spacing-8 $spacing-8 0; } -.mx_DeviceTypeIcon_deviceIcon { +.mx_DeviceTypeIcon_deviceIconWrapper { --background-color: $system; --icon-color: $secondary-content; @@ -36,11 +36,16 @@ limitations under the License. background-color: var(--background-color); } -.mx_DeviceTypeIcon_selected .mx_DeviceTypeIcon_deviceIcon { +.mx_DeviceTypeIcon_selected .mx_DeviceTypeIcon_deviceIconWrapper { --background-color: $primary-content; --icon-color: $background; } +.mx_DeviceTypeIcon_deviceIcon { + height: 24px; + width: 24px; +} + .mx_DeviceTypeIcon_verificationIcon { position: absolute; bottom: 0; diff --git a/res/css/views/toasts/_IncomingCallToast.pcss b/res/css/views/toasts/_IncomingCallToast.pcss new file mode 100644 index 0000000000..e66e1c31d4 --- /dev/null +++ b/res/css/views/toasts/_IncomingCallToast.pcss @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_IncomingCallToast { + position: relative; + display: flex; + flex-direction: row; + pointer-events: initial; /* restore pointer events so the user can accept/decline */ + width: 250px; + + .mx_IncomingCallToast_content { + display: flex; + flex-direction: column; + margin-left: 8px; + width: 100%; + + .mx_IncomingCallToast_info { + margin-bottom: $spacing-16; + + .mx_IncomingCallToast_room { + display: inline-block; + + font-weight: bold; + font-size: $font-15px; + line-height: $font-24px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + margin-bottom: $spacing-4; + } + + .mx_IncomingCallToast_message { + font-size: $font-12px; + line-height: $font-15px; + + margin-bottom: $spacing-4; + } + + .mx_LiveContentSummary { + font-size: $font-12px; + line-height: $font-15px; + + .mx_LiveContentSummary_participants::before { + width: 15px; + height: 15px; + } + } + } + + .mx_IncomingCallToast_joinButton { + position: relative; + + bottom: $spacing-4; + right: $spacing-4; + + align-self: flex-end; + + box-sizing: border-box; + min-width: 120px; + + padding: $spacing-4 0; + + line-height: $font-24px; + } + } + + .mx_IncomingCallToast_closeButton { + position: absolute; + + top: $spacing-4; + right: $spacing-4; + + display: flex; + height: 16px; + width: 16px; + + &::before { + content: ''; + + mask-image: url('$(res)/img/cancel.svg'); + + height: inherit; + width: inherit; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } +} diff --git a/res/img/element-icons/settings/desktop.svg b/res/img/element-icons/settings/desktop.svg new file mode 100644 index 0000000000..7d6ca10079 --- /dev/null +++ b/res/img/element-icons/settings/desktop.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/mobile.svg b/res/img/element-icons/settings/mobile.svg new file mode 100644 index 0000000000..45170b2c15 --- /dev/null +++ b/res/img/element-icons/settings/mobile.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/web.svg b/res/img/element-icons/settings/web.svg new file mode 100644 index 0000000000..95bd1ba24e --- /dev/null +++ b/res/img/element-icons/settings/web.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Notifier.ts b/src/Notifier.ts index 8c7a8e4bed..875402d982 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -47,6 +47,9 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import LegacyCallHandler from "./LegacyCallHandler"; import VoipUserMapper from "./VoipUserMapper"; import { localNotificationsAreSilenced } from "./utils/notifications"; +import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; +import ToastStore from "./stores/ToastStore"; +import { ElementCall } from "./models/Call"; /* * Dispatches: @@ -358,7 +361,7 @@ export const Notifier = { onEvent: function(ev: MatrixEvent) { if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return; + if (ev.getSender() === MatrixClientPeg.get().getUserId()) return; MatrixClientPeg.get().decryptEventIfNeeded(ev); @@ -419,6 +422,8 @@ export const Notifier = { const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions?.notify) { + this._performCustomEventHandling(ev); + if (RoomViewStore.instance.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently() && !Modal.hasDialogs() @@ -436,6 +441,24 @@ export const Notifier = { } } }, + + /** + * Some events require special handling such as showing in-app toasts + */ + _performCustomEventHandling: function(ev: MatrixEvent) { + if ( + ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType()) + && SettingsStore.getValue("feature_group_calls") + ) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: getIncomingCallToastKey(ev.getStateKey()), + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { callEvent: ev }, + }); + } + }, }; if (!window.mxNotifier) { diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 6be8dee332..361edcb1e2 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -45,6 +45,7 @@ import AccessibleButton from './components/views/elements/AccessibleButton'; import RightPanelStore from './stores/right-panel/RightPanelStore'; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { isLocationEvent } from './utils/EventUtils'; +import { ElementCall } from "./models/Call"; export function getSenderName(event: MatrixEvent): string { return event.sender?.name ?? event.getSender() ?? _t("Someone"); @@ -57,6 +58,15 @@ function getRoomMemberDisplayname(event: MatrixEvent, userId = event.getSender() return member?.name || member?.rawDisplayName || userId || _t("Someone"); } +function textForCallEvent(event: MatrixEvent): () => string { + const roomName = MatrixClientPeg.get().getRoom(event.getRoomId()!).name; + const isSupported = MatrixClientPeg.get().supportsVoip(); + + return isSupported + ? () => _t("Video call started in %(roomName)s.", { roomName }) + : () => _t("Video call started in %(roomName)s. (not supported by this browser)", { roomName }); +} + // These functions are frequently used just to check whether an event has // 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. @@ -798,6 +808,11 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } +// Add both stable and unstable m.call events +for (const evType of ElementCall.CALL_EVENT_TYPE.names) { + stateHandlers[evType] = textForCallEvent; +} + /** * Determines whether the given event has text to display. * @param ev The event diff --git a/src/components/views/rooms/LiveContentSummary.tsx b/src/components/views/rooms/LiveContentSummary.tsx index 95adf54f13..34ee825268 100644 --- a/src/components/views/rooms/LiveContentSummary.tsx +++ b/src/components/views/rooms/LiveContentSummary.tsx @@ -18,6 +18,8 @@ import React, { FC } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; +import { Call } from "../../../models/Call"; +import { useParticipants } from "../../../hooks/useCall"; export enum LiveContentType { Video, @@ -55,3 +57,18 @@ export const LiveContentSummary: FC = ({ type, text, active, participantC } ); + +interface LiveContentSummaryWithCallProps { + call: Call; +} + +export function LiveContentSummaryWithCall({ call }: LiveContentSummaryWithCallProps) { + const participants = useParticipants(call); + + return ; +} diff --git a/src/components/views/settings/devices/DeviceTypeIcon.tsx b/src/components/views/settings/devices/DeviceTypeIcon.tsx index 03b921f711..5ae30485eb 100644 --- a/src/components/views/settings/devices/DeviceTypeIcon.tsx +++ b/src/components/views/settings/devices/DeviceTypeIcon.tsx @@ -18,6 +18,9 @@ import React from 'react'; import classNames from 'classnames'; import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg'; +import { Icon as DesktopIcon } from '../../../../../res/img/element-icons/settings/desktop.svg'; +import { Icon as WebIcon } from '../../../../../res/img/element-icons/settings/web.svg'; +import { Icon as MobileIcon } from '../../../../../res/img/element-icons/settings/mobile.svg'; import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg'; import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg'; import { _t } from '../../../../languageHandler'; @@ -30,33 +33,51 @@ interface Props { deviceType?: DeviceType; } +const deviceTypeIcon: Record>> = { + [DeviceType.Desktop]: DesktopIcon, + [DeviceType.Mobile]: MobileIcon, + [DeviceType.Web]: WebIcon, + [DeviceType.Unknown]: UnknownDeviceIcon, +}; +const deviceTypeLabel: Record = { + [DeviceType.Desktop]: _t('Desktop session'), + [DeviceType.Mobile]: _t('Mobile session'), + [DeviceType.Web]: _t('Web session'), + [DeviceType.Unknown]: _t('Unknown session type'), +}; + export const DeviceTypeIcon: React.FC = ({ isVerified, isSelected, deviceType, -}) => ( -
- { /* TODO(kerrya) all devices have an unknown type until PSG-650 */ } - - { - isVerified - ? { + const Icon = deviceTypeIcon[deviceType] || deviceTypeIcon[DeviceType.Unknown]; + const label = deviceTypeLabel[deviceType] || deviceTypeLabel[DeviceType.Unknown]; + return ( +
+
+ - : - } -
); +
+ { + isVerified + ? + : + } +
); +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2fc9052b43..d95362a2fb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -470,6 +470,8 @@ "Converts the DM to a room": "Converts the DM to a room", "Displays action": "Displays action", "Someone": "Someone", + "Video call started in %(roomName)s.": "Video call started in %(roomName)s.", + "Video call started in %(roomName)s. (not supported by this browser)": "Video call started in %(roomName)s. (not supported by this browser)", "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)", "%(senderName)s placed a video call.": "%(senderName)s placed a video call.", @@ -795,6 +797,11 @@ "Don't miss a reply": "Don't miss a reply", "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", + "Unknown room": "Unknown room", + "Video call started": "Video call started", + "Video": "Video", + "Join": "Join", + "Close": "Close", "Unknown caller": "Unknown caller", "Voice call": "Voice call", "Video call": "Video call", @@ -1051,7 +1058,6 @@ "Video devices": "Video devices", "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", - "Join": "Join", "%(count)s people joined|other": "%(count)s people joined", "%(count)s people joined|one": "%(count)s person joined", "Dial": "Dial", @@ -1519,7 +1525,6 @@ "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", - "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "You are currently ignoring:": "You are currently ignoring:", "You are not subscribed to any lists": "You are not subscribed to any lists", @@ -1729,7 +1734,10 @@ "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Verified": "Verified", "Unverified": "Unverified", - "Unknown device type": "Unknown device type", + "Desktop session": "Desktop session", + "Mobile session": "Mobile session", + "Web session": "Web session", + "Unknown session type": "Unknown session type", "Verified session": "Verified session", "This session is ready for secure messaging.": "This session is ready for secure messaging.", "Unverified session": "Unverified session", @@ -2002,7 +2010,6 @@ "%(count)s unread messages.|other": "%(count)s unread messages.", "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", - "Video": "Video", "Joining…": "Joining…", "Joined": "Joined", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx new file mode 100644 index 0000000000..faff195226 --- /dev/null +++ b/src/toasts/IncomingCallToast.tsx @@ -0,0 +1,119 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useEffect } from 'react'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { _t } from '../languageHandler'; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import AccessibleButton from '../components/views/elements/AccessibleButton'; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; +import { Action } from "../dispatcher/actions"; +import ToastStore from "../stores/ToastStore"; +import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; +import { + LiveContentSummary, + LiveContentSummaryWithCall, + LiveContentType, +} from "../components/views/rooms/LiveContentSummary"; +import { useCall } from "../hooks/useCall"; +import { useRoomState } from "../hooks/useRoomState"; +import { ButtonEvent } from "../components/views/elements/AccessibleButton"; + +export const getIncomingCallToastKey = (stateKey: string) => `call_${stateKey}`; + +interface Props { + callEvent: MatrixEvent; +} + +export function IncomingCallToast({ callEvent }: Props) { + const roomId = callEvent.getRoomId()!; + const room = MatrixClientPeg.get().getRoom(roomId); + const call = useCall(roomId); + + const dismissToast = useCallback((): void => { + ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(callEvent.getStateKey()!)); + }, [callEvent]); + + const latestEvent = useRoomState(room, useCallback((state) => { + return state.getStateEvents(callEvent.getType(), callEvent.getStateKey()!); + }, [callEvent])); + + useEffect(() => { + if ("m.terminated" in latestEvent.getContent()) { + dismissToast(); + } + }, [latestEvent, dismissToast]); + + const onJoinClick = useCallback((e: ButtonEvent): void => { + e.stopPropagation(); + + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + view_call: true, + metricsTrigger: undefined, + }); + dismissToast(); + }, [room, dismissToast]); + + const onCloseClick = useCallback((e: ButtonEvent): void => { + e.stopPropagation(); + + dismissToast(); + }, [dismissToast]); + + return + +
+
+ + { room ? room.name : _t("Unknown room") } + +
+ { _t("Video call started") } +
+ { call + ? + : + } +
+ + { _t("Join") } + +
+ +
; +} diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts index 1178d35bec..4bac0a5423 100644 --- a/test/Notifier-test.ts +++ b/test/Notifier-test.ts @@ -14,28 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import BasePlatform from "../src/BasePlatform"; +import { ElementCall } from "../src/models/Call"; import Notifier from "../src/Notifier"; +import SettingsStore from "../src/settings/SettingsStore"; +import ToastStore from "../src/stores/ToastStore"; import { getLocalNotificationAccountDataEventType } from "../src/utils/notifications"; import { getMockClientWithEventEmitter, mkEvent, mkRoom, mockPlatformPeg } from "./test-utils"; +import { IncomingCallToast } from "../src/toasts/IncomingCallToast"; describe("Notifier", () => { - let MockPlatform; - let accountDataStore = {}; - - const mockClient = getMockClientWithEventEmitter({ - getUserId: jest.fn().mockReturnValue("@bob:example.org"), - isGuest: jest.fn().mockReturnValue(false), - getAccountData: jest.fn().mockImplementation(eventType => accountDataStore[eventType]), - setAccountData: jest.fn().mockImplementation((eventType, content) => { - accountDataStore[eventType] = new MatrixEvent({ - type: eventType, - content, - }); - }), - }); - const accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId); const roomId = "!room1:server"; const testEvent = mkEvent({ event: true, @@ -44,10 +37,33 @@ describe("Notifier", () => { room: roomId, content: {}, }); - const testRoom = mkRoom(mockClient, roomId); + + let MockPlatform: MockedObject; + let mockClient: MockedObject; + let testRoom: MockedObject; + let accountDataEventKey: string; + let accountDataStore = {}; beforeEach(() => { accountDataStore = {}; + mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue("@bob:example.org"), + isGuest: jest.fn().mockReturnValue(false), + getAccountData: jest.fn().mockImplementation(eventType => accountDataStore[eventType]), + setAccountData: jest.fn().mockImplementation((eventType, content) => { + accountDataStore[eventType] = new MatrixEvent({ + type: eventType, + content, + }); + }), + decryptEventIfNeeded: jest.fn(), + getRoom: jest.fn(), + getPushActionsForEvent: jest.fn(), + }); + accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId); + + testRoom = mkRoom(mockClient, roomId); + MockPlatform = mockPlatformPeg({ supportsNotifications: jest.fn().mockReturnValue(true), maySendNotifications: jest.fn().mockReturnValue(true), @@ -55,6 +71,8 @@ describe("Notifier", () => { }); Notifier.isBodyEnabled = jest.fn().mockReturnValue(true); + + mockClient.getRoom.mockReturnValue(testRoom); }); describe("_displayPopupNotification", () => { @@ -82,4 +100,73 @@ describe("Notifier", () => { expect(Notifier.getSoundForRoom).toHaveBeenCalledTimes(count); }); }); + + describe("group call notifications", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + + mockClient.getPushActionsForEvent.mockReturnValue({ + notify: true, + tweaks: {}, + }); + + Notifier.onSyncStateChange("SYNCING"); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const callOnEvent = (type?: string) => { + const callEvent = { + getContent: () => { }, + getRoomId: () => roomId, + isBeingDecrypted: () => false, + isDecryptionFailure: () => false, + getSender: () => "@alice:foo", + getType: () => type ?? ElementCall.CALL_EVENT_TYPE.name, + getStateKey: () => "state_key", + } as unknown as MatrixEvent; + + Notifier.onEvent(callEvent); + return callEvent; + }; + + const setGroupCallsEnabled = (val: boolean) => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_group_calls") return val; + }); + }; + + it("should show toast when group calls are supported", () => { + setGroupCallsEnabled(true); + + const callEvent = callOnEvent(); + + expect(ToastStore.sharedInstance().addOrReplaceToast).toHaveBeenCalledWith(expect.objectContaining({ + key: `call_${callEvent.getStateKey()}`, + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { callEvent }, + })); + }); + + it("should not show toast when group calls are not supported", () => { + setGroupCallsEnabled(false); + + callOnEvent(); + + expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled(); + }); + + it("should not show toast when calling with non-group call event", () => { + setGroupCallsEnabled(true); + + callOnEvent("event_type"); + + expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled(); + }); + }); }); diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts index c99fb56571..27f3090d3e 100644 --- a/test/TextForEvent-test.ts +++ b/test/TextForEvent-test.ts @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import TestRenderer from 'react-test-renderer'; import { ReactElement } from "react"; +import { mocked } from "jest-mock"; import { getSenderName, textForEvent } from "../src/TextForEvent"; import SettingsStore from "../src/settings/SettingsStore"; -import { createTestClient } from './test-utils'; +import { createTestClient, stubClient } from './test-utils'; import { MatrixClientPeg } from '../src/MatrixClientPeg'; import UserIdentifierCustomisations from '../src/customisations/UserIdentifier'; +import { ElementCall } from "../src/models/Call"; jest.mock("../src/settings/SettingsStore"); jest.mock('../src/customisations/UserIdentifier', () => ({ @@ -444,4 +446,42 @@ describe('TextForEvent', () => { expect(textForEvent(messageEvent)).toEqual('@a: test message'); }); }); + + describe("textForCallEvent()", () => { + let mockClient: MatrixClient; + let callEvent: MatrixEvent; + + beforeEach(() => { + stubClient(); + mockClient = MatrixClientPeg.get(); + + mocked(mockClient.getRoom).mockReturnValue({ + name: "Test room", + } as unknown as Room); + + callEvent = { + getRoomId: jest.fn(), + getType: jest.fn(), + isState: jest.fn().mockReturnValue(true), + } as unknown as MatrixEvent; + }); + + describe.each(ElementCall.CALL_EVENT_TYPE.names)("eventType=%s", (eventType: string) => { + beforeEach(() => { + mocked(callEvent).getType.mockReturnValue(eventType); + }); + + it("returns correct message for call event when supported", () => { + expect(textForEvent(callEvent)).toEqual('Video call started in Test room.'); + }); + + it("returns correct message for call event when supported", () => { + mocked(mockClient).supportsVoip.mockReturnValue(false); + + expect(textForEvent(callEvent)).toEqual( + 'Video call started in Test room. (not supported by this browser)', + ); + }); + }); + }); }); diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx index 38c646cfe8..10017376bb 100644 --- a/test/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/components/views/context_menus/MessageContextMenu-test.tsx @@ -27,7 +27,7 @@ import { EventType, } from 'matrix-js-sdk/src/matrix'; import { ExtensibleEvent, MessageEvent, M_POLL_KIND_DISCLOSED, PollStartEvent } from 'matrix-events-sdk'; -import { Thread } from "matrix-js-sdk/src/models/thread"; +import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread"; import { mocked } from "jest-mock"; import { act } from '@testing-library/react'; @@ -469,7 +469,7 @@ describe('MessageContextMenu', () => { const eventContent = MessageEvent.from("hello"); const mxEvent = new MatrixEvent(eventContent.serialize()); - Thread.hasServerSideSupport = true; + Thread.hasServerSideSupport = FeatureSupport.Stable; const context = { canSendMessages: true, }; diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx index d4dcb1a2df..670d39ec64 100644 --- a/test/components/views/messages/MessageActionBar-test.tsx +++ b/test/components/views/messages/MessageActionBar-test.tsx @@ -25,7 +25,7 @@ import { MsgType, Room, } from 'matrix-js-sdk/src/matrix'; -import { Thread } from 'matrix-js-sdk/src/models/thread'; +import { FeatureSupport, Thread } from 'matrix-js-sdk/src/models/thread'; import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar'; import { @@ -388,13 +388,13 @@ describe('', () => { describe('thread button', () => { beforeEach(() => { - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Stable); }); describe('when threads feature is not enabled', () => { it('does not render thread button when threads does not have server support', () => { jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); - Thread.setServerSideSupport(false, false); + Thread.setServerSideSupport(FeatureSupport.None); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); expect(queryByLabelText('Reply in thread')).toBeFalsy(); }); diff --git a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap index 05c0ca8c98..96ec4a13bd 100644 --- a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap @@ -115,10 +115,14 @@ exports[` renders device panel with devices 1`] = ` class="mx_DeviceTypeIcon" >