diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index b2ba6418d0c..58700c234e2 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -43,6 +43,7 @@ import { saveAndReload, sendMessageAsClient, } from "./read-receipts-utils"; +import { skipIfRustCrypto } from "../../support/util"; describe("Read receipts", () => { const roomAlpha = "Room Alpha"; @@ -321,6 +322,10 @@ describe("Read receipts", () => { assertUnreadThread("Root3"); }); it("After marking room as read, paging up to find old threads that were never read leaves the room read", () => { + // Flaky with rust crypto + // See https://github.com/vector-im/element-web/issues/26341 + skipIfRustCrypto(); + // Given lots of messages in threads that are unread but I marked as read on a main timeline message goTo(room1); receiveMessages(room2, [ diff --git a/src/components/structures/grouper/LateEventGrouper.ts b/src/components/structures/grouper/LateEventGrouper.ts index 5e7f0d19de7..67282e608c6 100644 --- a/src/components/structures/grouper/LateEventGrouper.ts +++ b/src/components/structures/grouper/LateEventGrouper.ts @@ -25,7 +25,7 @@ interface UnsignedLateEventInfo { /** * Milliseconds since epoch representing the time the event was received by the server */ - received_at: number; + received_ts: number; /** * An opaque identifier representing the group the server has put the late arriving event into */ diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index f39f804097c..08fe9a1d9ab 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { createRef, ReactNode, SyntheticEvent } from "react"; import classNames from "classnames"; -import { RoomMember, Room, MatrixError } from "matrix-js-sdk/src/matrix"; +import { RoomMember, Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { uniqBy } from "lodash"; @@ -368,26 +368,32 @@ export default class InviteDialog extends React.PureComponent alreadyInvited.add(m.userId)); - room.getMembersWithMembership("join").forEach((m) => alreadyInvited.add(m.userId)); + room.getMembersWithMembership("invite").forEach((m) => excludedIds.add(m.userId)); + room.getMembersWithMembership("join").forEach((m) => excludedIds.add(m.userId)); // add banned users, so we don't try to invite them - room.getMembersWithMembership("ban").forEach((m) => alreadyInvited.add(m.userId)); + room.getMembersWithMembership("ban").forEach((m) => excludedIds.add(m.userId)); + if (isFederated === false) { + // exclude users from external servers + const homeserver = props.roomId.split(":")[1]; + this.excludeExternals(homeserver, excludedIds); + } } this.state = { targets: [], // array of Member objects (see interface above) filterText: this.props.initialText || "", // Mutates alreadyInvited set so that buildSuggestions doesn't duplicate any users - recents: InviteDialog.buildRecents(alreadyInvited), + recents: InviteDialog.buildRecents(excludedIds), numRecentsShown: INITIAL_ROOMS_SHOWN, - suggestions: this.buildSuggestions(alreadyInvited), + suggestions: this.buildSuggestions(excludedIds), numSuggestionsShown: INITIAL_ROOMS_SHOWN, serverResultsMixin: [], threepidResultsMixin: [], @@ -418,6 +424,18 @@ export default class InviteDialog extends React.PureComponent): void { + const client = MatrixClientPeg.safeGet(); + // users with room membership + const members = Object.values(buildMemberScores(client)).map(({ member }) => member.userId); + // users with dm membership + const roomMembers = Object.keys(DMRoomMap.shared().getUniqueRoomsWithIndividuals()); + roomMembers.forEach((id) => members.push(id)); + // filter duplicates and user IDs from external servers + const externals = new Set(members.filter((id) => !id.includes(homeserver))); + externals.forEach((id) => excludedTargetIds.add(id)); + } + public static buildRecents(excludedTargetIds: Set): Result[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 45f42986acf..54d874d785e 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1127,7 +1127,7 @@ export class UnwrappedEventTile extends React.Component showRelative={this.context.timelineRenderingType === TimelineRenderingType.ThreadsList} showTwelveHour={this.props.isTwelveHour} ts={ts} - receivedTs={getLateEventInfo(this.props.mxEvent)?.received_at} + receivedTs={getLateEventInfo(this.props.mxEvent)?.received_ts} /> ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 50c1ad602a5..77648f0251b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1260,6 +1260,8 @@ "error_permissions_space": "You do not have permission to invite people to this space.", "error_profile_undisclosed": "User may or may not exist", "error_transfer_multiple_target": "A call can only be transferred to a single user.", + "error_unfederated_room": "This room is unfederated. You cannot invite people from external servers.", + "error_unfederated_space": "This space is unfederated. You cannot invite people from external servers.", "error_unknown": "Unknown server error", "error_user_not_found": "User does not exist", "error_version_unsupported_room": "The user's homeserver does not support the version of the room.", diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 1cf8ac6fc1d..de8ea1d7bcd 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -246,16 +246,26 @@ export default class MultiInviter { logger.error(err); - const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom(); + const room = this.roomId ? this.matrixClient.getRoom(this.roomId) : null; + const isSpace = room?.isSpaceRoom(); + const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[ + "m.federate" + ]; let errorText: string | undefined; let fatal = false; switch (err.errcode) { case "M_FORBIDDEN": if (isSpace) { - errorText = _t("invite|error_permissions_space"); + errorText = + isFederated === false + ? _t("invite|error_unfederated_space") + : _t("invite|error_permissions_space"); } else { - errorText = _t("invite|error_permissions_room"); + errorText = + isFederated === false + ? _t("invite|error_unfederated_room") + : _t("invite|error_permissions_room"); } fatal = true; break; diff --git a/test/components/views/dialogs/InviteDialog-test.tsx b/test/components/views/dialogs/InviteDialog-test.tsx index 4ca4fd82697..a7dd7439216 100644 --- a/test/components/views/dialogs/InviteDialog-test.tsx +++ b/test/components/views/dialogs/InviteDialog-test.tsx @@ -477,4 +477,20 @@ describe("InviteDialog", () => { ]); }); }); + + it("should not suggest users from other server when room has m.federate=false", async () => { + SdkConfig.add({ welcome_user_id: "@bot:example.org" }); + room.currentState.setStateEvents([mkRoomCreateEvent(bobId, roomId, { "m.federate": false })]); + + render( + , + ); + await flushPromises(); + expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument(); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index b54c0589f69..d92ebe89bba 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -291,13 +291,14 @@ type MakeEventProps = MakeEventPassThruProps & { unsigned?: IUnsigned; }; -export const mkRoomCreateEvent = (userId: string, roomId: string): MatrixEvent => { +export const mkRoomCreateEvent = (userId: string, roomId: string, content?: IContent): MatrixEvent => { return mkEvent({ event: true, type: EventType.RoomCreate, content: { creator: userId, room_version: KNOWN_SAFE_ROOM_VERSION, + ...content, }, skey: "", user: userId, diff --git a/test/utils/MultiInviter-test.ts b/test/utils/MultiInviter-test.ts index d92710bd2a0..55c40b34e9c 100644 --- a/test/utils/MultiInviter-test.ts +++ b/test/utils/MultiInviter-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { MatrixClient, MatrixError, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import Modal, { ComponentType, ComponentProps } from "../../src/Modal"; @@ -187,5 +187,56 @@ describe("MultiInviter", () => { }); expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1); }); + + it("should show sensible error when attempting to invite over federation with m.federate=false", async () => { + mocked(client.invite).mockRejectedValueOnce( + new MatrixError({ + errcode: "M_FORBIDDEN", + }), + ); + const room = new Room(ROOMID, client, client.getSafeUserId()); + room.currentState.setStateEvents([ + new MatrixEvent({ + type: EventType.RoomCreate, + state_key: "", + content: { + "m.federate": false, + }, + room_id: ROOMID, + }), + ]); + mocked(client.getRoom).mockReturnValue(room); + + await inviter.invite(["@user:other_server"]); + expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot( + `"This room is unfederated. You cannot invite people from external servers."`, + ); + }); + + it("should show sensible error when attempting to invite over federation with m.federate=false to space", async () => { + mocked(client.invite).mockRejectedValueOnce( + new MatrixError({ + errcode: "M_FORBIDDEN", + }), + ); + const room = new Room(ROOMID, client, client.getSafeUserId()); + room.currentState.setStateEvents([ + new MatrixEvent({ + type: EventType.RoomCreate, + state_key: "", + content: { + "m.federate": false, + "type": "m.space", + }, + room_id: ROOMID, + }), + ]); + mocked(client.getRoom).mockReturnValue(room); + + await inviter.invite(["@user:other_server"]); + expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot( + `"This space is unfederated. You cannot invite people from external servers."`, + ); + }); }); });