diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index ca8b68b5bb..1da1bfe1d6 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -50,6 +50,7 @@ import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog" import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; import { createThumbnail } from "./utils/image-media"; import { attachRelation } from "./components/views/rooms/SendMessageComposer"; +import { doMaybeLocalRoomAction } from "./utils/local-room"; // scraped out of a macOS hidpi (5660ppm) screenshot png // 5669 px (x-axis) , 5669 px (y-axis) , per metre @@ -351,11 +352,14 @@ export default class ContentMessages { text: string, matrixClient: MatrixClient, ): Promise { - const prom = matrixClient.sendStickerMessage(roomId, threadId, url, info, text).catch((e) => { + return doMaybeLocalRoomAction( + roomId, + (actualRoomId: string) => matrixClient.sendStickerMessage(actualRoomId, threadId, url, info, text), + matrixClient, + ).catch((e) => { logger.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); throw e; }); - return prom; } public getUploadLimit(): number | null { @@ -412,6 +416,8 @@ export default class ContentMessages { let promBefore: Promise = Promise.resolve(); for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; + const loopPromiseBefore = promBefore; + if (!uploadAll) { const { finished } = Modal.createDialog<[boolean, boolean]>(UploadConfirmDialog, { file, @@ -425,7 +431,17 @@ export default class ContentMessages { } } - promBefore = this.sendContentToRoom(file, roomId, relation, matrixClient, replyToEvent, promBefore); + promBefore = doMaybeLocalRoomAction( + roomId, + (actualRoomId) => this.sendContentToRoom( + file, + actualRoomId, + relation, + matrixClient, + replyToEvent, + loopPromiseBefore, + ), + ); } if (replyToEvent) { diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index b7a117d493..3d4cc9826f 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -35,6 +35,7 @@ import { arrayFastClone, arraySeed } from "../../../utils/arrays"; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; +import { doMaybeLocalRoomAction } from "../../../utils/local-room"; interface IProps extends IDialogProps { room: Room; @@ -163,11 +164,15 @@ export default class PollCreateDialog extends ScrollableBaseModal this.matrixClient.sendEvent( + actualRoomId, + this.props.threadId, + pollEvent.type, + pollEvent.content, + ), + this.matrixClient, ).then( () => this.props.onFinished(true), ).catch(e => { diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index a3a9a79ee9..0ff3098501 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -26,6 +26,7 @@ import Modal from "../../../Modal"; import QuestionDialog from "../dialogs/QuestionDialog"; import SdkConfig from "../../../SdkConfig"; import { OwnBeaconStore } from "../../../stores/OwnBeaconStore"; +import { doMaybeLocalRoomAction } from "../../../utils/local-room"; export enum LocationShareType { Own = 'Own', @@ -95,10 +96,11 @@ export const shareLocation = ( try { const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; const assetType = shareType === LocationShareType.Pin ? LocationAssetType.Pin : LocationAssetType.Self; - await client.sendMessage( + const content = makeLocationContent(undefined, uri, timestamp, undefined, assetType); + await doMaybeLocalRoomAction( roomId, - threadId, - makeLocationContent(undefined, uri, timestamp, undefined, assetType), + (actualRoomId: string) => client.sendMessage(actualRoomId, threadId, content), + client, ); } catch (error) { handleShareError(error, openMenu, shareType); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 281666b56f..a8e087e451 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -58,6 +58,7 @@ import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } fr import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { PosthogAnalytics } from "../../../PosthogAnalytics"; import { addReplyToMessageContent } from '../../../utils/Reply'; +import { doMaybeLocalRoomAction } from '../../../utils/local-room'; // Merges favouring the given relation export function attachRelation(content: IContent, relation?: IEventRelation): void { @@ -401,7 +402,11 @@ export class SendMessageComposer extends React.Component this.props.mxClient.sendMessage(actualRoomId, threadId, content), + this.props.mxClient, + ); if (replyToEvent) { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index ec23e3c77f..11b5274ee3 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -37,6 +37,7 @@ import { StaticNotificationState } from "../../../stores/notifications/StaticNot import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import InlineSpinner from "../elements/InlineSpinner"; import { PlaybackManager } from "../../../audio/PlaybackManager"; +import { doMaybeLocalRoomAction } from "../../../utils/local-room"; interface IProps { room: Room; @@ -103,7 +104,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint - }); + }; + + doMaybeLocalRoomAction( + this.props.room.roomId, + (actualRoomId: string) => MatrixClientPeg.get().sendMessage(actualRoomId, content), + ); } catch (e) { logger.error("Error sending voice message:", e); diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index c0c442aaaf..f3184153f2 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -44,6 +44,7 @@ import { watchPosition, } from "../utils/beacon"; import { getCurrentPosition } from "../utils/beacon"; +import { doMaybeLocalRoomAction } from "../utils/local-room"; const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId; @@ -399,9 +400,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient { await Promise.all(existingLiveBeaconIdsForRoom.map(beaconId => this.stopBeacon(beaconId))); // eslint-disable-next-line camelcase - const { event_id } = await this.matrixClient.unstable_createLiveBeacon( + const { event_id } = await doMaybeLocalRoomAction( roomId, - beaconInfoContent, + (actualRoomId: string) => this.matrixClient.unstable_createLiveBeacon(actualRoomId, beaconInfoContent), + this.matrixClient, ); storeLocallyCreateBeaconEventId(event_id); diff --git a/test/ContentMessages-test.ts b/test/ContentMessages-test.ts new file mode 100644 index 0000000000..8572fbe879 --- /dev/null +++ b/test/ContentMessages-test.ts @@ -0,0 +1,68 @@ +/* +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 { IImageInfo, ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; + +import ContentMessages from "../src/ContentMessages"; +import { doMaybeLocalRoomAction } from "../src/utils/local-room"; + +jest.mock("../src/utils/local-room", () => ({ + doMaybeLocalRoomAction: jest.fn(), +})); + +describe("ContentMessages", () => { + const stickerUrl = "https://example.com/sticker"; + const roomId = "!room:example.com"; + const imageInfo = {} as unknown as IImageInfo; + const text = "test sticker"; + let client: MatrixClient; + let contentMessages: ContentMessages; + let prom: Promise; + + beforeEach(() => { + client = { + sendStickerMessage: jest.fn(), + } as unknown as MatrixClient; + contentMessages = new ContentMessages(); + prom = Promise.resolve(null); + }); + + describe("sendStickerContentToRoom", () => { + beforeEach(() => { + mocked(client.sendStickerMessage).mockReturnValue(prom); + mocked(doMaybeLocalRoomAction).mockImplementation(( + roomId: string, + fn: (actualRoomId: string) => Promise, + client?: MatrixClient, + ) => { + return fn(roomId); + }); + }); + + it("should forward the call to doMaybeLocalRoomAction", async () => { + await contentMessages.sendStickerContentToRoom( + stickerUrl, + roomId, + null, + imageInfo, + text, + client, + ); + expect(client.sendStickerMessage).toHaveBeenCalledWith(roomId, null, stickerUrl, imageInfo, text); + }); + }); +}); diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 13a298d5a5..9b7700410d 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -28,8 +28,8 @@ describe("MatrixClientPeg", () => { it("setJustRegisteredUserId", () => { stubClient(); (peg as any).matrixClient = peg.get(); - peg.setJustRegisteredUserId("@userId:matrix.rog"); - expect(peg.get().credentials.userId).toBe("@userId:matrix.rog"); + peg.setJustRegisteredUserId("@userId:matrix.org"); + expect(peg.get().credentials.userId).toBe("@userId:matrix.org"); expect(peg.currentUserIsJustRegistered()).toBe(true); expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(true); diff --git a/test/components/views/location/shareLocation-test.ts b/test/components/views/location/shareLocation-test.ts new file mode 100644 index 0000000000..c96d74a80f --- /dev/null +++ b/test/components/views/location/shareLocation-test.ts @@ -0,0 +1,65 @@ +/* +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 { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { makeLocationContent } from "matrix-js-sdk/src/content-helpers"; +import { LegacyLocationEventContent, MLocationEventContent } from "matrix-js-sdk/src/@types/location"; + +import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room"; +import { + LocationShareType, + shareLocation, + ShareLocationFn, +} from "../../../../src/components/views/location/shareLocation"; + +jest.mock("../../../../src/utils/local-room", () => ({ + doMaybeLocalRoomAction: jest.fn(), +})); + +jest.mock("matrix-js-sdk/src/content-helpers", () => ({ + makeLocationContent: jest.fn(), +})); + +describe("shareLocation", () => { + const roomId = "!room:example.com"; + const shareType = LocationShareType.Pin; + const content = { test: "location content" } as unknown as LegacyLocationEventContent & MLocationEventContent; + let client: MatrixClient; + let shareLocationFn: ShareLocationFn; + + beforeEach(() => { + client = { + sendMessage: jest.fn(), + } as unknown as MatrixClient; + + mocked(makeLocationContent).mockReturnValue(content); + mocked(doMaybeLocalRoomAction).mockImplementation(( + roomId: string, + fn: (actualRoomId: string) => Promise, + client?: MatrixClient, + ) => { + return fn(roomId); + }); + + shareLocationFn = shareLocation(client, roomId, shareType, null, () => {}); + }); + + it("should forward the call to doMaybeLocalRoomAction", () => { + shareLocationFn({ uri: "https://example.com/" }); + expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, content); + }); +}); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index 014f5af66e..c46a76852e 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -17,8 +17,9 @@ limitations under the License. import React from "react"; import { act } from "react-dom/test-utils"; import { sleep } from "matrix-js-sdk/src/utils"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { ISendEventResponse, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; import { mount } from 'enzyme'; +import { mocked } from "jest-mock"; import SendMessageComposer, { createMessageContent, @@ -37,6 +38,11 @@ import { Layout } from '../../../../src/settings/enums/Layout'; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { mockPlatformPeg } from "../../../test-utils/platform"; +import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room"; + +jest.mock("../../../../src/utils/local-room", () => ({ + doMaybeLocalRoomAction: jest.fn(), +})); const WrapWithProviders: React.FC<{ roomContext: IRoomState; @@ -306,6 +312,34 @@ describe('', () => { const key = instance.editorStateKey; expect(key).toEqual('mx_cider_state_myfakeroom_myFakeThreadId'); }); + + it("correctly sends a message", () => { + mocked(doMaybeLocalRoomAction).mockImplementation(( + roomId: string, + fn: (actualRoomId: string) => Promise, + _client?: MatrixClient, + ) => { + return fn(roomId); + }); + + mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); + const wrapper = getComponent(); + + addTextToComposer(wrapper, "test message"); + act(() => { + wrapper.find(".mx_SendMessageComposer").simulate("keydown", { key: "Enter" }); + wrapper.update(); + }); + + expect(mockClient.sendMessage).toHaveBeenCalledWith( + "myfakeroom", + null, + { + "body": "test message", + "msgtype": MsgType.Text, + }, + ); + }); }); describe("isQuickReaction", () => { diff --git a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx new file mode 100644 index 0000000000..a1645db097 --- /dev/null +++ b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx @@ -0,0 +1,104 @@ +/* +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 { mount, ReactWrapper } from "enzyme"; +import { ISendEventResponse, MatrixClient, MsgType, Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import VoiceRecordComposerTile from "../../../../src/components/views/rooms/VoiceRecordComposerTile"; +import { IUpload, VoiceRecording } from "../../../../src/audio/VoiceRecording"; +import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; + +jest.mock("../../../../src/utils/local-room", () => ({ + doMaybeLocalRoomAction: jest.fn(), +})); + +describe("", () => { + let voiceRecordComposerTile: ReactWrapper; + let mockRecorder: VoiceRecording; + let mockUpload: IUpload; + let mockClient: MatrixClient; + const roomId = "!room:example.com"; + + beforeEach(() => { + mockClient = { + sendMessage: jest.fn(), + } as unknown as MatrixClient; + MatrixClientPeg.get = () => mockClient; + + const props = { + room: { + roomId, + } as unknown as Room, + }; + mockUpload = { + mxc: "mxc://example.com/voice", + }; + mockRecorder = { + stop: jest.fn(), + upload: () => Promise.resolve(mockUpload), + durationSeconds: 1337, + contentType: "audio/ogg", + getPlayback: () => ({ + thumbnailWaveform: [], + }), + } as unknown as VoiceRecording; + voiceRecordComposerTile = mount(); + voiceRecordComposerTile.setState({ + recorder: mockRecorder, + }); + + mocked(doMaybeLocalRoomAction).mockImplementation(( + roomId: string, + fn: (actualRoomId: string) => Promise, + _client?: MatrixClient, + ) => { + return fn(roomId); + }); + }); + + describe("send", () => { + it("should send the voice recording", async () => { + await (voiceRecordComposerTile.instance() as VoiceRecordComposerTile).send(); + expect(mockClient.sendMessage).toHaveBeenCalledWith(roomId, { + "body": "Voice message", + "file": undefined, + "info": { + "duration": 1337000, + "mimetype": "audio/ogg", + "size": undefined, + }, + "msgtype": MsgType.Audio, + "org.matrix.msc1767.audio": { + "duration": 1337000, + "waveform": [], + }, + "org.matrix.msc1767.file": { + "file": undefined, + "mimetype": "audio/ogg", + "name": "Voice message.ogg", + "size": undefined, + "url": "mxc://example.com/voice", + }, + "org.matrix.msc1767.text": "Voice message", + "org.matrix.msc3245.voice": {}, + "url": "mxc://example.com/voice", + }); + }); + }); +}); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 892142b98e..7a27a1165e 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -71,20 +71,23 @@ export function stubClient() { */ export function createTestClient(): MatrixClient { const eventEmitter = new EventEmitter(); + let txnId = 1; return { getHomeserverUrl: jest.fn(), getIdentityServerUrl: jest.fn(), - getDomain: jest.fn().mockReturnValue("matrix.rog"), - getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"), + getDomain: jest.fn().mockReturnValue("matrix.org"), + getUserId: jest.fn().mockReturnValue("@userId:matrix.org"), getUser: jest.fn().mockReturnValue({ on: jest.fn() }), getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), - credentials: { userId: "@userId:matrix.rog" }, + credentials: { userId: "@userId:matrix.org" }, store: { getPendingEvents: jest.fn().mockResolvedValue([]), setPendingEvents: jest.fn().mockResolvedValue(undefined), + storeRoom: jest.fn(), + removeRoom: jest.fn(), }, getPushActionsForEvent: jest.fn(), @@ -124,7 +127,7 @@ export function createTestClient(): MatrixClient { setRoomAccountData: jest.fn(), setRoomTopic: jest.fn(), sendTyping: jest.fn().mockResolvedValue({}), - sendMessage: () => jest.fn().mockResolvedValue({}), + sendMessage: jest.fn().mockResolvedValue({}), sendStateEvent: jest.fn().mockResolvedValue(undefined), getSyncState: () => "SYNCING", generateClientSecret: () => "t35tcl1Ent5ECr3T", @@ -157,6 +160,7 @@ export function createTestClient(): MatrixClient { isInitialSyncComplete: jest.fn().mockReturnValue(true), downloadKeys: jest.fn(), fetchRoomEvent: jest.fn(), + makeTxnId: jest.fn().mockImplementation(() => `t${txnId++}`), } as unknown as MatrixClient; }