Fix invite-specific join errors not being shown (#32621)

* Fix invite-specific join errors not being shown

* remove horrible non-jest function of getAccountData

* Use an Error

* Always dispatch an Error

* Throw a UserFriendlyError
This commit is contained in:
Will Hunt
2026-02-27 11:01:51 +00:00
committed by GitHub
parent 7461cdbf3f
commit 89fb388e57
6 changed files with 67 additions and 25 deletions
@@ -6,16 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type MatrixError } from "matrix-js-sdk/src/matrix"; import type { ActionPayload } from "../payloads";
import type { Action } from "../actions";
import { type ActionPayload } from "../payloads";
import { type Action } from "../actions";
export interface JoinRoomErrorPayload extends Pick<ActionPayload, "action"> { export interface JoinRoomErrorPayload extends Pick<ActionPayload, "action"> {
action: Action.JoinRoomError; action: Action.JoinRoomError;
roomId: string; roomId: string | null;
err: MatrixError; err: Error;
canAskToJoin?: boolean; canAskToJoin?: boolean;
} }
+1
View File
@@ -1990,6 +1990,7 @@
"error_join_incompatible_version_1": "Sorry, your homeserver is too old to participate here.", "error_join_incompatible_version_1": "Sorry, your homeserver is too old to participate here.",
"error_join_incompatible_version_2": "Please contact your homeserver administrator.", "error_join_incompatible_version_2": "Please contact your homeserver administrator.",
"error_join_title": "Failed to join", "error_join_title": "Failed to join",
"error_join_unknown": "An unknonw error occured.",
"error_jump_to_date": "Server returned %(statusCode)s with error code %(errorCode)s", "error_jump_to_date": "Server returned %(statusCode)s with error code %(errorCode)s",
"error_jump_to_date_connection": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", "error_jump_to_date_connection": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.",
"error_jump_to_date_details": "Error details", "error_jump_to_date_details": "Error details",
+13 -12
View File
@@ -24,7 +24,7 @@ import {
import { type MatrixDispatcher } from "../dispatcher/dispatcher"; import { type MatrixDispatcher } from "../dispatcher/dispatcher";
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";
import Modal from "../Modal"; import Modal from "../Modal";
import { _t } from "../languageHandler"; import { _t, UserFriendlyError } from "../languageHandler";
import { getCachedRoomIdForAlias, storeRoomAliasInCache } from "../RoomAliasCache"; import { getCachedRoomIdForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
import { Action } from "../dispatcher/actions"; import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise"; import { retry } from "../utils/promise";
@@ -569,10 +569,11 @@ export class RoomViewStore extends EventEmitter {
metricsTrigger: payload.metricsTrigger, metricsTrigger: payload.metricsTrigger,
}); });
} catch (err) { } catch (err) {
this.dis?.dispatch({ logger.error("Error thrown while handling joinRoom", err);
this.dis?.dispatch<JoinRoomErrorPayload>({
action: Action.JoinRoomError, action: Action.JoinRoomError,
roomId, roomId,
err, err: err instanceof Error ? err : new UserFriendlyError("room|error_join_unknown", { cause: err }),
canAskToJoin: payload.canAskToJoin, canAskToJoin: payload.canAskToJoin,
}); });
@@ -592,11 +593,11 @@ export class RoomViewStore extends EventEmitter {
} }
} }
public showJoinRoomError(err: MatrixError, roomId: string): void { public showJoinRoomError(err: unknown, roomId: string | null): void {
let description: ReactNode = err.message ? err.message : JSON.stringify(err); let description: ReactNode = err instanceof Error && err.message ? err.message : JSON.stringify(err);
logger.log("Failed to join room:", description); if (err instanceof MatrixError === false) {
// This isn't a MatrixError so just show the error verbatim.
if (err.name === "ConnectionError") { } else if (err.name === "ConnectionError") {
description = _t("room|error_join_connection"); description = _t("room|error_join_connection");
} else if (err.errcode === "M_INCOMPATIBLE_ROOM_VERSION") { } else if (err.errcode === "M_INCOMPATIBLE_ROOM_VERSION") {
description = ( description = (
@@ -607,7 +608,7 @@ export class RoomViewStore extends EventEmitter {
</div> </div>
); );
} else if (err.httpStatus === 404) { } else if (err.httpStatus === 404) {
const invitingUserId = this.getInvitingUserId(roomId); const invitingUserId = roomId && this.getInvitingUserId(roomId);
// provide a better error message for invites // provide a better error message for invites
if (invitingUserId) { if (invitingUserId) {
// if the inviting user is on the same HS, there can only be one cause: they left. // if the inviting user is on the same HS, there can only be one cause: they left.
@@ -617,10 +618,9 @@ export class RoomViewStore extends EventEmitter {
description = _t("room|error_join_404_invite"); description = _t("room|error_join_404_invite");
} }
} }
// provide a more detailed error than "No known servers" when attempting to // provide a more detailed error than "No known servers" when attempting to
// join using a room ID and no via servers // join using a room ID and no via servers
if (roomId === this.state.roomId && this.state.viaServers.length === 0) { else if (roomId === this.state.roomId && this.state.viaServers.length === 0) {
description = ( description = (
<div> <div>
{_t("room|error_join_404_1")} {_t("room|error_join_404_1")}
@@ -631,6 +631,7 @@ export class RoomViewStore extends EventEmitter {
); );
} }
} }
logger.log("Failed to join room:", description);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: _t("room|error_join_title"), title: _t("room|error_join_title"),
@@ -641,7 +642,7 @@ export class RoomViewStore extends EventEmitter {
private joinRoomError(payload: JoinRoomErrorPayload): void { private joinRoomError(payload: JoinRoomErrorPayload): void {
this.setState({ this.setState({
joining: false, joining: false,
joinError: payload.err, joinError: payload.err instanceof Error ? payload.err : null,
}); });
if (payload.err && !payload.canAskToJoin) { if (payload.err && !payload.canAskToJoin) {
this.showJoinRoomError(payload.err, payload.roomId); this.showJoinRoomError(payload.err, payload.roomId);
+1 -1
View File
@@ -688,7 +688,6 @@ export function mkStubRoom(
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()), fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
findEventById: jest.fn().mockReturnValue(undefined), findEventById: jest.fn().mockReturnValue(undefined),
findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }), findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }),
getAccountData: (_: EventType | string) => undefined as MatrixEvent | undefined,
getAltAliases: jest.fn().mockReturnValue([]), getAltAliases: jest.fn().mockReturnValue([]),
getAvatarUrl: () => "mxc://avatar.url/room.png", getAvatarUrl: () => "mxc://avatar.url/room.png",
getCanonicalAlias: jest.fn(), getCanonicalAlias: jest.fn(),
@@ -725,6 +724,7 @@ export function mkStubRoom(
getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0), getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0),
getVersion: jest.fn().mockReturnValue("1"), getVersion: jest.fn().mockReturnValue("1"),
getBumpStamp: jest.fn().mockReturnValue(0), getBumpStamp: jest.fn().mockReturnValue(0),
getAccountData: jest.fn(),
hasMembershipState: () => false, hasMembershipState: () => false,
isElementVideoRoom: jest.fn().mockReturnValue(false), isElementVideoRoom: jest.fn().mockReturnValue(false),
isSpaceRoom: jest.fn().mockReturnValue(false), isSpaceRoom: jest.fn().mockReturnValue(false),
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { MatrixError, Room } from "matrix-js-sdk/src/matrix"; import { KnownMembership, MatrixError, Room } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { import {
RoomViewLifecycle, RoomViewLifecycle,
@@ -20,6 +20,9 @@ import { Action } from "../../../src/dispatcher/actions";
import { import {
flushPromises, flushPromises,
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
mkEvent,
mkRoom,
mkRoomMember,
setupAsyncStoreWithClient, setupAsyncStoreWithClient,
untilDispatch, untilDispatch,
untilEmission, untilEmission,
@@ -126,6 +129,7 @@ describe("RoomViewStore", function () {
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId), getSafeUserId: jest.fn().mockReturnValue(userId),
getDeviceId: jest.fn().mockReturnValue("ABC123"), getDeviceId: jest.fn().mockReturnValue("ABC123"),
getDomain: jest.fn().mockReturnValue("server"),
sendStateEvent: jest.fn().mockResolvedValue({}), sendStateEvent: jest.fn().mockResolvedValue({}),
supportsThreads: jest.fn(), supportsThreads: jest.fn(),
isInitialSyncComplete: jest.fn().mockResolvedValue(false), isInitialSyncComplete: jest.fn().mockResolvedValue(false),
@@ -144,7 +148,7 @@ describe("RoomViewStore", function () {
} }
})(), })(),
}); });
const room = new Room(roomId, mockClient, userId); const room = mkRoom(mockClient, roomId);
const room2 = new Room(roomId2, mockClient, userId); const room2 = new Room(roomId2, mockClient, userId);
getRooms.mockReturnValue([room, room2]); getRooms.mockReturnValue([room, room2]);
@@ -439,6 +443,31 @@ describe("RoomViewStore", function () {
expect(mocked(Modal).createDialog.mock.calls[0][1]).toMatchSnapshot(); expect(mocked(Modal).createDialog.mock.calls[0][1]).toMatchSnapshot();
}); });
// The server bob is on will affect the message we send.
it.each(["server", "another-server"])(
"should display an invite-specific error message when the room is unreachable",
async (bobsServer) => {
room.getMyMembership.mockReturnValue(KnownMembership.Invite);
room.getMember.mockImplementationOnce((memberUserId) => {
if (userId === memberUserId) {
const member = mkRoomMember(roomId, userId, KnownMembership.Invite);
member.events.member!.getSender = () => `@bob:${bobsServer}`;
return member;
}
return null;
});
dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
await untilDispatch(Action.ActiveRoomChanged, dis);
// Generate error to display the expected error message
const error = new MatrixError(undefined, 404);
roomViewStore.showJoinRoomError(error, roomId);
// Check the modal props
expect(mocked(Modal).createDialog.mock.calls[0][1]).toMatchSnapshot();
},
);
it("should display the generic error message when the roomId doesnt match", async () => { it("should display the generic error message when the roomId doesnt match", async () => {
// When // When
// Generate error to display the expected error message // Generate error to display the expected error message
@@ -450,9 +479,9 @@ describe("RoomViewStore", function () {
}); });
it("clears the unread flag when viewing a room", async () => { it("clears the unread flag when viewing a room", async () => {
room.getAccountData = jest.fn().mockReturnValue({ room.getAccountData.mockReturnValue(
getContent: jest.fn().mockReturnValue({ unread: true }), mkEvent({ type: "m.marked_unread", user: "@anyone:example.org", content: { unread: true }, event: true }),
}); );
dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); dis.dispatch({ action: Action.ViewRoom, room_id: roomId });
await untilDispatch(Action.ActiveRoomChanged, dis); await untilDispatch(Action.ActiveRoomChanged, dis);
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(roomId, "m.marked_unread", { expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(roomId, "m.marked_unread", {
@@ -12,6 +12,20 @@ exports[`RoomViewStore should display an error message when the room is unreacha
} }
`; `;
exports[`RoomViewStore should display an invite-specific error message when the room is unreachable 1`] = `
{
"description": "The person who invited you has already left.",
"title": "Failed to join",
}
`;
exports[`RoomViewStore should display an invite-specific error message when the room is unreachable 2`] = `
{
"description": "The person who invited you has already left, or their server is offline.",
"title": "Failed to join",
}
`;
exports[`RoomViewStore should display the generic error message when the roomId doesnt match 1`] = ` exports[`RoomViewStore should display the generic error message when the roomId doesnt match 1`] = `
{ {
"description": "MatrixError: [404] my 404 error", "description": "MatrixError: [404] my 404 error",