From fa1bff67cffe5a306bfe81991e1b546293f94554 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 20 Jul 2022 09:26:25 +0200 Subject: [PATCH] Wire local room logic (#9078) * Wire local room logic * Migrate to testling-lib; update test descriptions --- src/Avatar.ts | 8 +- src/components/structures/MatrixChat.tsx | 8 +- .../dialogs/spotlight/SpotlightDialog.tsx | 6 +- .../views/messages/EncryptionEvent.tsx | 6 +- src/i18n/strings/en_EN.json | 1 + src/stores/TypingStore.ts | 4 + .../room-list/filters/VisibilityProvider.ts | 4 +- src/utils/localRoom/isLocalRoom.ts | 26 ++++ test/Avatar-test.ts | 102 ++++++++++++++ .../views/dialogs/SpotlightDialog-test.tsx | 68 ++++++++- .../views/messages/EncryptionEvent-test.tsx | 131 ++++++++++++++++++ test/stores/TypingStore-test.ts | 88 ++++++++++++ test/test-utils/test-utils.ts | 2 + test/utils/localRoom/isLocalRoom-test.ts | 52 +++++++ 14 files changed, 494 insertions(+), 12 deletions(-) create mode 100644 src/utils/localRoom/isLocalRoom.ts create mode 100644 test/Avatar-test.ts create mode 100644 test/components/views/messages/EncryptionEvent-test.tsx create mode 100644 test/stores/TypingStore-test.ts create mode 100644 test/utils/localRoom/isLocalRoom-test.ts diff --git a/src/Avatar.ts b/src/Avatar.ts index 86560713ae..0472e00b0d 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -22,6 +22,7 @@ import { split } from "lodash"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; +import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( @@ -142,7 +143,12 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi if (room.isSpaceRoom()) return null; // If the room is not a DM don't fallback to a member avatar - if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null; + if ( + !DMRoomMap.shared().getUserIdForRoomId(room.roomId) + && !(isLocalRoom(room)) + ) { + return null; + } // If there are only two members in the DM use the avatar of the other member const otherMember = room.getAvatarFallbackMember(); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index eb979c5798..534d82036c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -132,6 +132,7 @@ import VideoChannelStore from "../../stores/VideoChannelStore"; import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators"; import { UseCaseSelection } from '../views/elements/UseCaseSelection'; import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig'; +import { isLocalRoom } from '../../utils/localRoom/isLocalRoom'; // legacy export export { default as Views } from "../../Views"; @@ -890,7 +891,12 @@ export default class MatrixChat extends React.PureComponent { } // If we are redirecting to a Room Alias and it is for the room we already showing then replace history item - const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId; + let replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId; + + if (isLocalRoom(this.state.currentRoomId)) { + // Replace local room history items + replaceLast = true; + } if (roomInfo.room_id === this.state.currentRoomId) { // if we are re-viewing the same room then copy any state we already know diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 4bf7b57239..dbe55972c5 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -91,6 +91,7 @@ import { PublicRoomResultDetails } from "./PublicRoomResultDetails"; import { RoomResultContextMenus } from "./RoomResultContextMenus"; import { RoomContextDetails } from "../../rooms/RoomContextDetails"; import { TooltipOption } from "./TooltipOption"; +import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons @@ -243,6 +244,9 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via const findVisibleRooms = (cli: MatrixClient) => { return cli.getVisibleRooms().filter(room => { + // Do not show local rooms + if (isLocalRoom(room)) return false; + // TODO we may want to put invites in their own list return room.getMyMembership() === "join" || room.getMyMembership() == "invite"; }); @@ -395,7 +399,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n possibleResults.forEach(entry => { if (isRoomResult(entry)) { - if (!entry.room.normalizedName.includes(normalizedQuery) && + if (!entry.room.normalizedName?.includes(normalizedQuery) && !entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) && !entry.query?.some(q => q.includes(lcQuery)) ) return; // bail, does not match query diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 809cf75f76..dc4daaaf7f 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -24,6 +24,7 @@ import EventTileBubble from "./EventTileBubble"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { objectHasDiff } from "../../../utils/objects"; +import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom'; interface IProps { mxEvent: MatrixEvent; @@ -46,12 +47,15 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp if (content.algorithm === ALGORITHM && isRoomEncrypted) { let subtitle: string; const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + const room = cli?.getRoom(roomId); if (prevContent.algorithm === ALGORITHM) { subtitle = _t("Some encryption parameters have been changed."); } else if (dmPartner) { - const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner; + const displayName = room.getMember(dmPartner)?.rawDisplayName || dmPartner; subtitle = _t("Messages here are end-to-end encrypted. " + "Verify %(displayName)s in their profile - tap on their avatar.", { displayName }); + } else if (isLocalRoom(room)) { + subtitle = _t("Messages in this chat will be end-to-end encrypted."); } else { subtitle = _t("Messages in this room are end-to-end encrypted. " + "When people join, you can verify them in their profile, just tap on their avatar."); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 875516e0bb..a2568949d9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2101,6 +2101,7 @@ "View Source": "View Source", "Some encryption parameters have been changed.": "Some encryption parameters have been changed.", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", + "Messages in this chat will be end-to-end encrypted.": "Messages in this chat will be end-to-end encrypted.", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.", "Encryption enabled": "Encryption enabled", "Ignored attempt to disable encryption": "Ignored attempt to disable encryption", diff --git a/src/stores/TypingStore.ts b/src/stores/TypingStore.ts index cae8529bbd..d642f3fea7 100644 --- a/src/stores/TypingStore.ts +++ b/src/stores/TypingStore.ts @@ -16,6 +16,7 @@ limitations under the License. import { MatrixClientPeg } from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; +import { isLocalRoom } from "../utils/localRoom/isLocalRoom"; import Timer from "../utils/Timer"; const TYPING_USER_TIMEOUT = 10000; @@ -64,6 +65,9 @@ export default class TypingStore { * @param {boolean} isTyping Whether the user is typing or not. */ public setSelfTyping(roomId: string, threadId: string | null, isTyping: boolean): void { + // No typing notifications for local rooms + if (isLocalRoom(roomId)) return; + if (!SettingsStore.getValue('sendTypingNotifications')) return; if (SettingsStore.getValue('lowBandwidth')) return; // Disable typing notification for threads for the initial launch diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 26bfcd78ea..ca37733106 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import CallHandler from "../../../CallHandler"; import { RoomListCustomisations } from "../../../customisations/RoomList"; -import { LocalRoom } from "../../../models/LocalRoom"; +import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import VoipUserMapper from "../../../VoipUserMapper"; export class VisibilityProvider { @@ -55,7 +55,7 @@ export class VisibilityProvider { return false; } - if (room instanceof LocalRoom) { + if (isLocalRoom(room)) { // local rooms shouldn't show up anywhere return false; } diff --git a/src/utils/localRoom/isLocalRoom.ts b/src/utils/localRoom/isLocalRoom.ts new file mode 100644 index 0000000000..a31774ea5e --- /dev/null +++ b/src/utils/localRoom/isLocalRoom.ts @@ -0,0 +1,26 @@ +/* +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 { Room } from "matrix-js-sdk/src/matrix"; + +import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom"; + +export function isLocalRoom(roomOrID: Room|string): boolean { + if (typeof roomOrID === "string") { + return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX); + } + return roomOrID instanceof LocalRoom; +} diff --git a/test/Avatar-test.ts b/test/Avatar-test.ts new file mode 100644 index 0000000000..214ada9486 --- /dev/null +++ b/test/Avatar-test.ts @@ -0,0 +1,102 @@ +/* +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 { mocked } from "jest-mock"; +import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; + +import { avatarUrlForRoom } from "../src/Avatar"; +import { Media, mediaFromMxc } from "../src/customisations/Media"; +import DMRoomMap from "../src/utils/DMRoomMap"; + +jest.mock("../src/customisations/Media", () => ({ + mediaFromMxc: jest.fn(), +})); + +const roomId = "!room:example.com"; +const avatarUrl1 = "https://example.com/avatar1"; +const avatarUrl2 = "https://example.com/avatar2"; + +describe("avatarUrlForRoom", () => { + let getThumbnailOfSourceHttp: jest.Mock; + let room: Room; + let roomMember: RoomMember; + let dmRoomMap: DMRoomMap; + + beforeEach(() => { + getThumbnailOfSourceHttp = jest.fn(); + mocked(mediaFromMxc).mockImplementation((): Media => { + return { + getThumbnailOfSourceHttp, + } as unknown as Media; + }); + room = { + roomId, + getMxcAvatarUrl: jest.fn(), + isSpaceRoom: jest.fn(), + getAvatarFallbackMember: jest.fn(), + } as unknown as Room; + dmRoomMap = { + getUserIdForRoomId: jest.fn(), + } as unknown as DMRoomMap; + DMRoomMap.setShared(dmRoomMap); + roomMember = { + getMxcAvatarUrl: jest.fn(), + } as unknown as RoomMember; + }); + + it("should return null for a null room", () => { + expect(avatarUrlForRoom(null, 128, 128)).toBeNull(); + }); + + it("should return the HTTP source if the room provides a MXC url", () => { + mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); + getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); + expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); + }); + + it("should return null for a space room", () => { + mocked(room.isSpaceRoom).mockReturnValue(true); + expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); + }); + + it("should return null if the room is not a DM", () => { + mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue(null); + expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); + expect(dmRoomMap.getUserIdForRoomId).toHaveBeenCalledWith(roomId); + }); + + it("should return null if there is no other member in the room", () => { + mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + mocked(room.getAvatarFallbackMember).mockReturnValue(null); + expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); + }); + + it("should return null if the other member has no avatar URL", () => { + mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); + expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); + }); + + it("should return the other member's avatar URL", () => { + mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); + mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); + getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); + expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); + }); +}); diff --git a/test/components/views/dialogs/SpotlightDialog-test.tsx b/test/components/views/dialogs/SpotlightDialog-test.tsx index 7c8bbc6bc7..c2445139a7 100644 --- a/test/components/views/dialogs/SpotlightDialog-test.tsx +++ b/test/components/views/dialogs/SpotlightDialog-test.tsx @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mount } from "enzyme"; -import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; +import { mount, ReactWrapper } from "enzyme"; +import { mocked } from "jest-mock"; +import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; import React from "react"; import { act } from "react-dom/test-utils"; @@ -23,7 +24,15 @@ import sanitizeHtml from "sanitize-html"; import SpotlightDialog, { Filter } from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { stubClient } from "../../../test-utils"; +import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import { mkRoom, stubClient } from "../../../test-utils"; + +jest.mock("../../../../src/utils/direct-messages", () => ({ + // @ts-ignore + ...jest.requireActual("../../../../src/utils/direct-messages"), + startDmOnFirstMessage: jest.fn(), +})); interface IUserChunkMember { user_id: string; @@ -110,10 +119,23 @@ describe("Spotlight Dialog", () => { guest_can_join: false, }; - beforeEach(() => { - mockClient({ rooms: [testPublicRoom], users: [testPerson] }); - }); + let testRoom: Room; + let testLocalRoom: LocalRoom; + let mockedClient: MatrixClient; + + beforeEach(() => { + mockedClient = mockClient({ rooms: [testPublicRoom], users: [testPerson] }); + testRoom = mkRoom(mockedClient, "!test23:example.com"); + mocked(testRoom.getMyMembership).mockReturnValue("join"); + testLocalRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test23", mockedClient, mockedClient.getUserId()); + testLocalRoom.updateMyMembership("join"); + mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom]); + + jest.spyOn(DMRoomMap, "shared").mockReturnValue({ + getUserIdForRoomId: jest.fn(), + } as unknown as DMRoomMap); + }); describe("should apply filters supplied via props", () => { it("without filter", async () => { const wrapper = mount( @@ -289,4 +311,38 @@ describe("Spotlight Dialog", () => { wrapper.unmount(); }); }); + + describe("searching for rooms", () => { + let wrapper: ReactWrapper; + let options: ReactWrapper; + + beforeAll(async () => { + wrapper = mount( + null} />, + ); + await act(async () => { + await sleep(200); + }); + wrapper.update(); + + const content = wrapper.find("#mx_SpotlightDialog_content"); + options = content.find("div.mx_SpotlightDialog_option"); + }); + + afterAll(() => { + wrapper.unmount(); + }); + + it("should find Rooms", () => { + expect(options.length).toBe(3); + expect(options.first().text()).toContain(testRoom.name); + }); + + it("should not find LocalRooms", () => { + expect(options.length).toBe(3); + expect(options.first().text()).not.toContain(testLocalRoom.name); + }); + }); }); diff --git a/test/components/views/messages/EncryptionEvent-test.tsx b/test/components/views/messages/EncryptionEvent-test.tsx new file mode 100644 index 0000000000..7b70fa4f6f --- /dev/null +++ b/test/components/views/messages/EncryptionEvent-test.tsx @@ -0,0 +1,131 @@ +/* +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 from 'react'; +import { mocked } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, screen } from '@testing-library/react'; + +import EncryptionEvent from "../../../../src/components/views/messages/EncryptionEvent"; +import { createTestClient, mkMessage } from "../../../test-utils"; +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; +import { LocalRoom } from '../../../../src/models/LocalRoom'; +import DMRoomMap from '../../../../src/utils/DMRoomMap'; +import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; + +const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => { + render( + + ); +}; + +const checkTexts = (title: string, subTitle: string) => { + screen.getByText(title); + screen.getByText(subTitle); +}; + +describe("EncryptionEvent", () => { + const roomId = "!room:example.com"; + const algorithm = "m.megolm.v1.aes-sha2"; + let client: MatrixClient; + let event: MatrixEvent; + + beforeEach(() => { + jest.clearAllMocks(); + client = createTestClient(); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); + event = mkMessage({ + event: true, + room: roomId, + user: client.getUserId(), + }); + jest.spyOn(DMRoomMap, "shared").mockReturnValue({ + getUserIdForRoomId: jest.fn(), + } as unknown as DMRoomMap); + }); + + describe("for an encrypted room", () => { + beforeEach(() => { + event.event.content.algorithm = algorithm; + mocked(client.isRoomEncrypted).mockReturnValue(true); + const room = new Room(roomId, client, client.getUserId()); + mocked(client.getRoom).mockReturnValue(room); + }); + + it("should show the expected texts", () => { + renderEncryptionEvent(client, event); + checkTexts( + "Encryption enabled", + "Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their avatar.", + ); + }); + + describe("with same previous algorithm", () => { + beforeEach(() => { + jest.spyOn(event, "getPrevContent").mockReturnValue({ + algorithm: algorithm, + }); + }); + + it("should show the expected texts", () => { + renderEncryptionEvent(client, event); + checkTexts( + "Encryption enabled", + "Some encryption parameters have been changed.", + ); + }); + }); + + describe("with unknown algorithm", () => { + beforeEach(() => { + event.event.content.algorithm = "unknown"; + }); + + it("should show the expected texts", () => { + renderEncryptionEvent(client, event); + checkTexts("Encryption enabled", "Ignored attempt to disable encryption"); + }); + }); + }); + + describe("for an unencrypted room", () => { + beforeEach(() => { + mocked(client.isRoomEncrypted).mockReturnValue(false); + renderEncryptionEvent(client, event); + }); + + it("should show the expected texts", () => { + expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); + checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."); + }); + }); + + describe("for an encrypted local room", () => { + beforeEach(() => { + event.event.content.algorithm = algorithm; + mocked(client.isRoomEncrypted).mockReturnValue(true); + const localRoom = new LocalRoom(roomId, client, client.getUserId()); + mocked(client.getRoom).mockReturnValue(localRoom); + renderEncryptionEvent(client, event); + }); + + it("should show the expected texts", () => { + expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); + checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted."); + }); + }); +}); diff --git a/test/stores/TypingStore-test.ts b/test/stores/TypingStore-test.ts new file mode 100644 index 0000000000..98ddfca3c4 --- /dev/null +++ b/test/stores/TypingStore-test.ts @@ -0,0 +1,88 @@ +/* +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 { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import TypingStore from "../../src/stores/TypingStore"; +import { LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom"; +import SettingsStore from "../../src/settings/SettingsStore"; + +jest.mock("../../src/settings/SettingsStore", () => ({ + getValue: jest.fn(), +})); + +describe("TypingStore", () => { + let typingStore: TypingStore; + let mockClient: MatrixClient; + const settings = { + "sendTypingNotifications": true, + "feature_thread": false, + }; + const roomId = "!test:example.com"; + const localRoomId = LOCAL_ROOM_ID_PREFIX + "test"; + + beforeEach(() => { + typingStore = new TypingStore(); + mockClient = { + sendTyping: jest.fn(), + } as unknown as MatrixClient; + MatrixClientPeg.get = () => mockClient; + mocked(SettingsStore.getValue).mockImplementation((setting: string) => { + return settings[setting]; + }); + }); + + describe("setSelfTyping", () => { + it("shouldn't do anything for a local room", () => { + typingStore.setSelfTyping(localRoomId, null, true); + expect(mockClient.sendTyping).not.toHaveBeenCalled(); + }); + + describe("in typing state true", () => { + beforeEach(() => { + typingStore.setSelfTyping(roomId, null, true); + }); + + it("should change to false when setting false", () => { + typingStore.setSelfTyping(roomId, null, false); + expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, false, 30000); + }); + + it("should change to true when setting true", () => { + typingStore.setSelfTyping(roomId, null, true); + expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000); + }); + }); + + describe("in typing state false", () => { + beforeEach(() => { + typingStore.setSelfTyping(roomId, null, false); + }); + + it("shouldn't change when setting false", () => { + typingStore.setSelfTyping(roomId, null, false); + expect(mockClient.sendTyping).not.toHaveBeenCalled(); + }); + + it("should change to true when setting true", () => { + typingStore.setSelfTyping(roomId, null, true); + expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000); + }); + }); + }); +}); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 62ef22f92f..aea6e591cb 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -31,6 +31,7 @@ import { IEventRelation, IUnsigned, } from 'matrix-js-sdk/src/matrix'; +import { normalize } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg'; import dis from '../../src/dispatcher/dispatcher'; @@ -389,6 +390,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl removeListener: jest.fn(), getDMInviter: jest.fn(), name, + normalizedName: normalize(name || ""), getAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', isSpaceRoom: jest.fn().mockReturnValue(false), diff --git a/test/utils/localRoom/isLocalRoom-test.ts b/test/utils/localRoom/isLocalRoom-test.ts new file mode 100644 index 0000000000..c94fd0608a --- /dev/null +++ b/test/utils/localRoom/isLocalRoom-test.ts @@ -0,0 +1,52 @@ +/* +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 { Room } from "matrix-js-sdk/src/matrix"; + +import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom"; +import { isLocalRoom } from "../../../src/utils/localRoom/isLocalRoom"; +import { createTestClient } from "../../test-utils"; + +describe("isLocalRoom", () => { + let room: Room; + let localRoom: LocalRoom; + + beforeEach(() => { + const client = createTestClient(); + room = new Room("!room:example.com", client, client.getUserId()); + localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, client.getUserId()); + }); + + it("should return false for null", () => { + expect(isLocalRoom(null)).toBe(false); + }); + + it("should return false for a Room", () => { + expect(isLocalRoom(room)).toBe(false); + }); + + it("should return false for a non-local room ID", () => { + expect(isLocalRoom(room.roomId)).toBe(false); + }); + + it("should return true for LocalRoom", () => { + expect(isLocalRoom(localRoom)).toBe(true); + }); + + it("should return true for local room ID", () => { + expect(isLocalRoom(LOCAL_ROOM_ID_PREFIX + "test")).toBe(true); + }); +});