diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index db553615415..a40711a9ca1 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -20,7 +20,7 @@ jobs: # from creeping in. They take a long time to run and consume 4 concurrent runners. if: github.event.workflow_run.event == 'merge_group' - uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@66854039a33ed6cfe1fc635ff2daa8bb261c0b56 + uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.83.0-rc.1 permissions: actions: read issues: read diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index df24b5ed270..e79becf410f 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -39,7 +39,7 @@ jobs: if: github.event.action == 'opened' steps: - name: Check membership - uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2 + uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3 id: teams with: username: ${{ github.event.pull_request.user.login }} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000000..e19198e62d0 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,8 @@ +# Summary + +- [Introduction](../README.md) + +# Deep dive + +- [Storage notes](storage-notes.md) +- [Unverified devices](warning-on-unverified-devices.md) diff --git a/docs/warning-on-unverified-devices.txt b/docs/warning-on-unverified-devices.md similarity index 56% rename from docs/warning-on-unverified-devices.txt rename to docs/warning-on-unverified-devices.md index e3a6c567d31..f7431379c56 100644 --- a/docs/warning-on-unverified-devices.txt +++ b/docs/warning-on-unverified-devices.md @@ -1,31 +1,29 @@ Random notes from Matthew on the two possible approaches for warning users about unexpected unverified devices popping up in their rooms.... -Original idea... -================ +# Original idea... Warn when an existing user adds an unknown device to a room. Warn when a user joins the room with unverified or unknown devices. Warn when you initial sync if the room has any unverified devices in it. - ^ this is good enough if we're doing local storage. - OR, better: +^ this is good enough if we're doing local storage. +OR, better: Warn when you initial sync if the room has any new undefined devices since you were last there. - => This means persisting the rooms that devices are in, across initial syncs. +=> This means persisting the rooms that devices are in, across initial syncs. - -Updated idea... -=============== +# Updated idea... Warn when the user tries to send a message: - - If the room has unverified devices which the user has not yet been told about in the context of this room - ...or in the context of this user? currently all verification is per-user, not per-room. + +- If the room has unverified devices which the user has not yet been told about in the context of this room + ...or in the context of this user? currently all verification is per-user, not per-room. ...this should be good enough. - - so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned. +- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned. throw an error when trying to encrypt if there are pure unverified devices there app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway? - - or megolm could warn which devices are causing the problems. + - or megolm could warn which devices are causing the problems. -Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources? \ No newline at end of file +Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources? diff --git a/package.json b/package.json index 092a26a7534..67cf1311e76 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^48.0.0", "exorcist": "^2.0.0", - "fake-indexeddb": "^4.0.0", + "fake-indexeddb": "^5.0.0", "fetch-mock-jest": "^1.5.1", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0.0", diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index efb2404f6a6..bcc2b42a254 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -14,10 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from "jest-mock"; + import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { emitPromise } from "../../test-utils/test-utils"; import { Crypto, IEventDecryptionResult } from "../../../src/crypto"; -import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src"; +import { + IAnnotatedPushRule, + MatrixClient, + PushRuleActionName, + Room, + THREAD_RELATION_TYPE, + TweakName, +} from "../../../src"; describe("MatrixEvent", () => { it("should create copies of itself", () => { @@ -77,17 +86,98 @@ describe("MatrixEvent", () => { expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().ciphertext).toBe("xyz"); + const mockClient = {} as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); const redaction = new MatrixEvent({ type: "m.room.redaction", redacts: ev.getId(), }); - ev.makeRedacted(redaction); + ev.makeRedacted(redaction, room); expect(ev.getContent().body).toBeUndefined(); expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().ciphertext).toBeUndefined(); }); + it("should remain in the main timeline when redacted", async () => { + // Given an event in the main timeline + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + body: "Test", + }, + event_id: "$event1:server", + }); + + await room.addLiveEvents([ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it remains in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + }); + + it("should move into the main timeline when redacted", async () => { + // Given an event in a thread + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = new MatrixEvent({ + type: "m.room.message", + content: { + body: "threadRoot", + }, + event_id: "$threadroot:server", + }); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + "body": "Test", + "m.relates_to": { + rel_type: THREAD_RELATION_TYPE.name, + event_id: "$threadroot:server", + }, + }, + event_id: "$event1:server", + }); + + await room.addLiveEvents([threadRoot, ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toEqual("$threadroot:server"); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server"]); + expect(threadLiveEventIds(room, 0)).toEqual(["$threadroot:server", "$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it disappears from the thread and appears in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server", "$event1:server"]); + expect(threadLiveEventIds(room, 0)).not.toContain("$event1:server"); + }); + describe("applyVisibilityEvent", () => { it("should emit VisibilityChange if a change was made", async () => { const ev = new MatrixEvent({ @@ -330,3 +420,19 @@ describe("MatrixEvent", () => { expect(stateEvent.threadRootId).toBeUndefined(); }); }); + +function mainTimelineLiveEventIds(room: Room): Array { + return room + .getLiveTimeline() + .getEvents() + .map((e) => e.getId()!); +} + +function threadLiveEventIds(room: Room, threadIndex: number): Array { + return room + .getThreads() + [threadIndex].getUnfilteredTimelineSet() + .getLiveTimeline() + .getEvents() + .map((e) => e.getId()!); +} diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 8b451b48847..05bd12200e6 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -675,6 +675,69 @@ describe("Thread", () => { }); }); }); + + describe("addEvent", () => { + describe("Given server support for threads", () => { + let previousThreadHasServerSideSupport: FeatureSupport; + + beforeAll(() => { + previousThreadHasServerSideSupport = Thread.hasServerSideSupport; + Thread.hasServerSideSupport = FeatureSupport.Stable; + }); + + afterAll(() => { + Thread.hasServerSideSupport = previousThreadHasServerSideSupport; + }); + + it("Adds events even if they appear out of order", async () => { + // Given a thread exists + const client = createClient(); + const user = "@alice:matrix.org"; + const room = "!room:z"; + const thread = await createThread(client, user, room); + const prevNumEvents = thread.timeline.length; + + // When two messages come in but the later one has an older timestamp + const message1 = createThreadMessage(thread.id, user, room, "message1"); + const message2 = createThreadMessage(thread.id, user, room, "message2"); + message2.localTimestamp -= 10000; + + await thread.addEvent(message1, false); + await thread.addEvent(message2, false); + + // Then both events end up in the timeline + expect(thread.timeline.length - prevNumEvents).toEqual(2); + const lastEvent = thread.timeline.at(-1)!; + const secondLastEvent = thread.timeline.at(-2)!; + expect(lastEvent).toBe(message2); + expect(secondLastEvent).toBe(message1); + }); + + it("Adds events to start even if they appear out of order", async () => { + // Given a thread exists + const client = createClient(); + const user = "@alice:matrix.org"; + const room = "!room:z"; + const thread = await createThread(client, user, room); + const prevNumEvents = thread.timeline.length; + + // When two messages come in but the later one has an older timestamp + const message1 = createThreadMessage(thread.id, user, room, "message1"); + const message2 = createThreadMessage(thread.id, user, room, "message2"); + message2.localTimestamp -= 10000; + + await thread.addEvent(message1, false); + await thread.addEvent(message2, true); + + // Then both events end up in the timeline + expect(thread.timeline.length - prevNumEvents).toEqual(2); + const lastEvent = thread.timeline.at(-1)!; + const firstEvent = thread.timeline.at(0)!; + expect(lastEvent).toBe(message1); + expect(firstEvent).toBe(message2); + }); + }); + }); }); /** diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts index 435d2e33ddc..6c60d08f908 100644 --- a/spec/unit/room-state.spec.ts +++ b/spec/unit/room-state.spec.ts @@ -27,6 +27,7 @@ import { M_BEACON } from "../../src/@types/beacon"; import { MatrixClient } from "../../src/client"; import { DecryptionError } from "../../src/crypto/algorithms"; import { defer } from "../../src/utils"; +import { Room } from "../../src/models/room"; describe("RoomState", function () { const roomId = "!foo:bar"; @@ -362,9 +363,11 @@ describe("RoomState", function () { }); it("does not add redacted beacon info events to state", () => { + const mockClient = {} as unknown as MockedObject; const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); const redactionEvent = new MatrixEvent({ type: "m.room.redaction" }); - redactedBeaconEvent.makeRedacted(redactionEvent); + const room = new Room(roomId, mockClient, userA); + redactedBeaconEvent.makeRedacted(redactionEvent, room); const emitSpy = jest.spyOn(state, "emit"); state.setStateEvents([redactedBeaconEvent]); @@ -394,11 +397,13 @@ describe("RoomState", function () { }); it("destroys and removes redacted beacon events", () => { + const mockClient = {} as unknown as MockedObject; const beaconId = "$beacon1"; const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const redactionEvent = new MatrixEvent({ type: "m.room.redaction", redacts: beaconEvent.getId() }); - redactedBeaconEvent.makeRedacted(redactionEvent); + const room = new Room(roomId, mockClient, userA); + redactedBeaconEvent.makeRedacted(redactionEvent, room); state.setStateEvents([beaconEvent]); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index de4e24b1ef2..b1db43f5dcd 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3564,7 +3564,7 @@ describe("Room", function () { expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy(); const redactedEvent = new MatrixEvent({ type: "m.room.redaction" }); - pollStartEvent.makeRedacted(redactedEvent); + pollStartEvent.makeRedacted(redactedEvent, room); await flushPromises(); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index c1efe498999..ff7128435d7 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -427,7 +427,6 @@ export class MatrixRTCSession extends TypedEventEmitter setTimeout(resolve, resendDelay)); await this.triggerCallMembershipEventUpdate(); } diff --git a/src/models/event.ts b/src/models/event.ts index dcd11b53e1c..b618a2c715e 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -45,6 +45,8 @@ import { DecryptionError } from "../crypto/algorithms"; import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { WITHHELD_MESSAGES } from "../crypto/OlmDevice"; import { IAnnotatedPushRule } from "../@types/PushRules"; +import { Room } from "./room"; +import { EventTimeline } from "./event-timeline"; export { EventStatus } from "./event-status"; @@ -414,9 +416,12 @@ export class MatrixEvent extends TypedEventEmitter { // if we know about this event, redact its contents now. const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { - redactedEvent.makeRedacted(event); + redactedEvent.makeRedacted(event, this); // If this is in the current state, replace it with the redacted version if (redactedEvent.isState()) { diff --git a/src/models/thread.ts b/src/models/thread.ts index 4003f810f0f..1da3fca558b 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -430,22 +430,19 @@ export class Thread extends ReadReceipt