diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index f4a861ab7cd..bef1cd0393c 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -250,7 +250,7 @@ describe("Timeline", () => { cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area - cy.get(".mx_EventTile .mx_ViewSourceEvent") + cy.get(".mx_EventTile:not(:first-child) .mx_ViewSourceEvent") .should("exist") .realHover() .within(() => { diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 9c28678e7dc..95e41395ac3 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -22,6 +22,7 @@ import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall"; +import SettingsStore from "../settings/SettingsStore"; import EditorStateTransfer from "../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks"; import LegacyCallEventGrouper from "../components/structures/LegacyCallEventGrouper"; @@ -91,6 +92,7 @@ const HiddenEventFactory: Factory = (ref, props) => ; export const JSONEventFactory: Factory = (ref, props) => ; +export const RoomCreateEventFactory: Factory = (ref, props) => ; const EVENT_TILE_TYPES = new Map([ [EventType.RoomMessage, MessageEventFactory], // note that verification requests are handled in pickFactory() @@ -105,7 +107,7 @@ const EVENT_TILE_TYPES = new Map([ const STATE_EVENT_TILE_TYPES = new Map([ [EventType.RoomEncryption, (ref, props) => ], [EventType.RoomCanonicalAlias, TextualEventFactory], - [EventType.RoomCreate, (_ref, props) => ], + [EventType.RoomCreate, RoomCreateEventFactory], [EventType.RoomMember, TextualEventFactory], [EventType.RoomName, TextualEventFactory], [EventType.RoomAvatar, (ref, props) => ], @@ -213,6 +215,14 @@ export function pickFactory( } } + if (evType === EventType.RoomCreate) { + const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors"); + const predecessor = cli.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled); + if (!predecessor) { + return noEventFactoryFactory(); + } + } + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) if (evType === "im.vector.modular.widgets") { let type = mxEvent.getContent()["type"]; @@ -415,12 +425,15 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo // No tile for replacement events since they update the original tile if (mxEvent.isRelation(RelationType.Replace)) return false; - const handler = pickFactory(mxEvent, MatrixClientPeg.get(), showHiddenEvents); + const cli = MatrixClientPeg.get(); + const handler = pickFactory(mxEvent, cli, showHiddenEvents); if (!handler) return false; if (handler === TextualEventFactory) { return hasText(mxEvent, showHiddenEvents); } else if (handler === STATE_EVENT_TILE_TYPES.get(EventType.RoomCreate)) { - return Boolean(mxEvent.getContent()["predecessor"]); + const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors"); + const predecessor = cli.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled); + return Boolean(predecessor); } else if ( ElementCall.CALL_EVENT_TYPE.names.some((eventType) => handler === STATE_EVENT_TILE_TYPES.get(eventType)) ) { diff --git a/test/components/structures/MessagePanel-test.tsx b/test/components/structures/MessagePanel-test.tsx index d3659ae302a..e05dc038fdd 100644 --- a/test/components/structures/MessagePanel-test.tsx +++ b/test/components/structures/MessagePanel-test.tsx @@ -37,6 +37,7 @@ import { } from "../../test-utils"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../src/components/structures/RoomView"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; jest.mock("../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -58,6 +59,7 @@ describe("MessagePanel", function () { getRoom: jest.fn(), getClientWellKnown: jest.fn().mockReturnValue({}), }); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); const room = new Room(roomId, client, userId); @@ -464,11 +466,12 @@ describe("MessagePanel", function () { it("should collapse creation events", function () { const events = mkCreationEvents(); - TestUtilsMatrix.upsertRoomStateEvents(room, events); - const { container } = render(getComponent({ events })); - const createEvent = events.find((event) => event.getType() === "m.room.create"); const encryptionEvent = events.find((event) => event.getType() === "m.room.encryption"); + client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null)); + TestUtilsMatrix.upsertRoomStateEvents(room, events); + + const { container } = render(getComponent({ events })); // we expect that // - the room creation event, the room encryption event, and Alice inviting Bob, @@ -508,6 +511,8 @@ describe("MessagePanel", function () { it("should hide read-marker at the end of creation event summary", function () { const events = mkCreationEvents(); + const createEvent = events.find((event) => event.getType() === "m.room.create"); + client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null)); TestUtilsMatrix.upsertRoomStateEvents(room, events); const { container } = render( diff --git a/test/events/EventTileFactory-test.ts b/test/events/EventTileFactory-test.ts index 911abb71b4c..b54bedfb17c 100644 --- a/test/events/EventTileFactory-test.ts +++ b/test/events/EventTileFactory-test.ts @@ -18,8 +18,10 @@ import { JSONEventFactory, MessageEventFactory, pickFactory, + RoomCreateEventFactory, TextualEventFactory, } from "../../src/events/EventTileFactory"; +import SettingsStore from "../../src/settings/SettingsStore"; import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast"; import { createTestClient, mkEvent } from "../test-utils"; import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; @@ -27,23 +29,64 @@ import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-ut const roomId = "!room:example.com"; describe("pickFactory", () => { + let client: MatrixClient; + let room: Room; + + let createEventWithPredecessor: MatrixEvent; + let createEventWithoutPredecessor: MatrixEvent; + let dynamicPredecessorEvent: MatrixEvent; + let voiceBroadcastStartedEvent: MatrixEvent; let voiceBroadcastStoppedEvent: MatrixEvent; let voiceBroadcastChunkEvent: MatrixEvent; let utdEvent: MatrixEvent; let utdBroadcastChunkEvent: MatrixEvent; let audioMessageEvent: MatrixEvent; - let client: MatrixClient; beforeAll(() => { client = createTestClient(); - const room = new Room(roomId, client, client.getSafeUserId()); + room = new Room(roomId, client, client.getSafeUserId()); mocked(client.getRoom).mockImplementation((getRoomId: string): Room | null => { if (getRoomId === room.roomId) return room; return null; }); + createEventWithoutPredecessor = mkEvent({ + event: true, + type: EventType.RoomCreate, + user: client.getUserId()!, + room: roomId, + content: { + creator: client.getUserId()!, + room_version: "9", + }, + }); + createEventWithPredecessor = mkEvent({ + event: true, + type: EventType.RoomCreate, + user: client.getUserId()!, + room: roomId, + content: { + creator: client.getUserId()!, + room_version: "9", + predecessor: { + room_id: "roomid1", + event_id: null, + }, + }, + }); + dynamicPredecessorEvent = mkEvent({ + event: true, + type: EventType.RoomPredecessor, + user: client.getUserId()!, + room: roomId, + skey: "", + content: { + predecessor_room_id: "roomid2", + last_known_event_id: null, + }, + }); voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Started, @@ -117,6 +160,15 @@ describe("pickFactory", () => { expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory); }); + it("should return a JSONEventFactory for a room create event without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory); + }); + it("should return a TextualEventFactory for a voice broadcast stopped event", () => { expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory); }); @@ -131,6 +183,80 @@ describe("pickFactory", () => { }); describe("when not showing hidden events", () => { + describe("without dynamic predecessor support", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReset(); + }); + + it("should return undefined for a room without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + + it("should return a RoomCreateFactory for a room with fixed predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithPredecessor.getStateKey()!, createEventWithPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + + it("should return undefined for a room with dynamic predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set( + EventType.RoomPredecessor, + new Map([[dynamicPredecessorEvent.getStateKey()!, dynamicPredecessorEvent]]), + ); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + }); + + describe("with dynamic predecessor support", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue") + .mockReset() + .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors"); + }); + + it("should return undefined for a room without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + + it("should return a RoomCreateFactory for a room with fixed predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithPredecessor.getStateKey()!, createEventWithPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + + it("should return a RoomCreateFactory for a room with dynamic predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set( + EventType.RoomPredecessor, + new Map([[dynamicPredecessorEvent.getStateKey()!, dynamicPredecessorEvent]]), + ); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + }); + it("should return undefined for a voice broadcast event", () => { expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined(); });