diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 75897bbb0a..75254afb3c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -46,7 +46,7 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; -import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership"; +import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; import SdkConfig from "./SdkConfig"; import SettingsStore from "./settings/SettingsStore"; import { UIComponent, UIFeature } from "./settings/UIFeature"; @@ -66,6 +66,7 @@ import { XOR } from "./@types/common"; import { PosthogAnalytics } from "./PosthogAnalytics"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import VoipUserMapper from './VoipUserMapper'; +import { leaveRoomBehaviour } from "./utils/leave-behaviour"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c0b307156b..82a832d85e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -83,7 +83,6 @@ import { UPDATE_STATUS_INDICATOR, } from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; -import { leaveRoomBehaviour } from "../../utils/membership"; import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; import { UIFeature } from "../../settings/UIFeature"; import DialPadModal from "../views/voip/DialPadModal"; @@ -129,6 +128,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import InfoDialog from '../views/dialogs/InfoDialog'; +import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; // legacy export export { default as Views } from "../../Views"; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index aab270e2e8..d9286c618b 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -22,7 +22,6 @@ import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { _t } from "../../../languageHandler"; import { - leaveSpace, shouldShowSpaceSettings, showCreateNewRoom, showCreateNewSubspace, @@ -30,6 +29,7 @@ import { showSpacePreferences, showSpaceSettings, } from "../../../utils/space"; +import { leaveSpace } from "../../../utils/leave-behaviour"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { ButtonEvent } from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index 4752c0f6db..b572122da1 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -26,7 +26,7 @@ import SpaceBasicSettings from "./SpaceBasicSettings"; import { avatarUrlForRoom } from "../../../Avatar"; import { IDialogProps } from "../dialogs/IDialogProps"; import { getTopic } from "../elements/RoomTopic"; -import { leaveSpace } from "../../../utils/space"; +import { leaveSpace } from "../../../utils/leave-behaviour"; interface IProps extends IDialogProps { matrixClient: MatrixClient; diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts new file mode 100644 index 0000000000..2d87e4d4dd --- /dev/null +++ b/src/utils/leave-behaviour.ts @@ -0,0 +1,158 @@ +/* +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 { sleep } from "matrix-js-sdk/src/utils"; +import React from "react"; +import { EventStatus } from "matrix-js-sdk/src/models/event-status"; +import { MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import Modal, { IHandle } from "../Modal"; +import Spinner from "../components/views/elements/Spinner"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { _t } from "../languageHandler"; +import ErrorDialog from "../components/views/dialogs/ErrorDialog"; +import { isMetaSpace } from "../stores/spaces"; +import SpaceStore from "../stores/spaces/SpaceStore"; +import { RoomViewStore } from "../stores/RoomViewStore"; +import dis from "../dispatcher/dispatcher"; +import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; +import { Action } from "../dispatcher/actions"; +import { ViewHomePagePayload } from "../dispatcher/payloads/ViewHomePagePayload"; +import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog"; +import { AfterLeaveRoomPayload } from "../dispatcher/payloads/AfterLeaveRoomPayload"; +import { bulkSpaceBehaviour } from "./space"; + +export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true) { + let spinnerModal: IHandle; + if (spinner) { + spinnerModal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); + } + + const cli = MatrixClientPeg.get(); + let leavingAllVersions = true; + const history = cli.getRoomUpgradeHistory(roomId); + if (history && history.length > 0) { + const currentRoom = history[history.length - 1]; + if (currentRoom.roomId !== roomId) { + // The user is trying to leave an older version of the room. Let them through + // without making them leave the current version of the room. + leavingAllVersions = false; + } + } + + const room = cli.getRoom(roomId); + // await any queued messages being sent so that they do not fail + await Promise.all(room.getPendingEvents().filter(ev => { + return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status); + }).map(ev => new Promise((resolve, reject) => { + const handler = () => { + if (ev.status === EventStatus.NOT_SENT) { + spinnerModal?.close(); + reject(ev.error); + } + + if (!ev.status || ev.status === EventStatus.SENT) { + ev.off(MatrixEventEvent.Status, handler); + resolve(); + } + }; + + ev.on(MatrixEventEvent.Status, handler); + }))); + + let results: { [roomId: string]: Error & { errcode?: string, message: string, data?: Record } } = {}; + if (!leavingAllVersions) { + try { + await cli.leave(roomId); + } catch (e) { + if (e?.data?.errcode) { + const message = e.data.error || _t("Unexpected server error trying to leave the room"); + results[roomId] = Object.assign(new Error(message), { errcode: e.data.errcode, data: e.data }); + } else { + results[roomId] = e || new Error("Failed to leave room for unknown causes"); + } + } + } else { + results = await cli.leaveRoomChain(roomId, retry); + } + + if (retry) { + const limitExceededError = Object.values(results).find(e => e?.errcode === "M_LIMIT_EXCEEDED"); + if (limitExceededError) { + await sleep(limitExceededError.data.retry_after_ms ?? 100); + return leaveRoomBehaviour(roomId, false, false); + } + } + + spinnerModal?.close(); + + const errors = Object.entries(results).filter(r => !!r[1]); + if (errors.length > 0) { + const messages = []; + for (const roomErr of errors) { + const err = roomErr[1]; // [0] is the roomId + let message = _t("Unexpected server error trying to leave the room"); + if (err.errcode && err.message) { + if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { + Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, { + title: _t("Can't leave Server Notices room"), + description: _t( + "This room is used for important messages from the Homeserver, " + + "so you cannot leave it.", + ), + }); + return; + } + message = results[roomId].message; + } + messages.push(message, React.createElement('BR')); // createElement to avoid using a tsx file in utils + } + Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, { + title: _t("Error leaving room"), + description: messages, + }); + return; + } + + if (!isMetaSpace(SpaceStore.instance.activeSpace) && + SpaceStore.instance.activeSpace !== roomId && + RoomViewStore.instance.getRoomId() === roomId + ) { + dis.dispatch({ + action: Action.ViewRoom, + room_id: SpaceStore.instance.activeSpace, + metricsTrigger: undefined, // other + }); + } else { + dis.dispatch({ action: Action.ViewHomePage }); + } +} + +export const leaveSpace = (space: Room) => { + Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, { + space, + onFinished: async (leave: boolean, rooms: Room[]) => { + if (!leave) return; + await bulkSpaceBehaviour(space, rooms, room => leaveRoomBehaviour(room.roomId)); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: space.roomId, + }); + }, + }, "mx_LeaveSpaceDialog_wrapper"); +}; diff --git a/src/utils/membership.ts b/src/utils/membership.ts index 93cf0fc981..394db19cc9 100644 --- a/src/utils/membership.ts +++ b/src/utils/membership.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 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. @@ -15,22 +15,6 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { sleep } from "matrix-js-sdk/src/utils"; -import { EventStatus, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; -import React from "react"; - -import { MatrixClientPeg } from "../MatrixClientPeg"; -import { _t } from "../languageHandler"; -import Modal, { IHandle } from "../Modal"; -import ErrorDialog from "../components/views/dialogs/ErrorDialog"; -import dis from "../dispatcher/dispatcher"; -import { RoomViewStore } from "../stores/RoomViewStore"; -import Spinner from "../components/views/elements/Spinner"; -import { isMetaSpace } from "../stores/spaces"; -import SpaceStore from "../stores/spaces/SpaceStore"; -import { Action } from "../dispatcher/actions"; -import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; -import { ViewHomePagePayload } from "../dispatcher/payloads/ViewHomePagePayload"; /** * Approximation of a membership status for a given room. @@ -91,109 +75,3 @@ export function isJoinedOrNearlyJoined(membership: string): boolean { const effective = getEffectiveMembership(membership); return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite; } - -export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true) { - let spinnerModal: IHandle; - if (spinner) { - spinnerModal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); - } - - const cli = MatrixClientPeg.get(); - let leavingAllVersions = true; - const history = cli.getRoomUpgradeHistory(roomId); - if (history && history.length > 0) { - const currentRoom = history[history.length - 1]; - if (currentRoom.roomId !== roomId) { - // The user is trying to leave an older version of the room. Let them through - // without making them leave the current version of the room. - leavingAllVersions = false; - } - } - - const room = cli.getRoom(roomId); - // await any queued messages being sent so that they do not fail - await Promise.all(room.getPendingEvents().filter(ev => { - return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status); - }).map(ev => new Promise((resolve, reject) => { - const handler = () => { - if (ev.status === EventStatus.NOT_SENT) { - spinnerModal?.close(); - reject(ev.error); - } - - if (!ev.status || ev.status === EventStatus.SENT) { - ev.off(MatrixEventEvent.Status, handler); - resolve(); - } - }; - - ev.on(MatrixEventEvent.Status, handler); - }))); - - let results: { [roomId: string]: Error & { errcode?: string, message: string, data?: Record } } = {}; - if (!leavingAllVersions) { - try { - await cli.leave(roomId); - } catch (e) { - if (e?.data?.errcode) { - const message = e.data.error || _t("Unexpected server error trying to leave the room"); - results[roomId] = Object.assign(new Error(message), { errcode: e.data.errcode, data: e.data }); - } else { - results[roomId] = e || new Error("Failed to leave room for unknown causes"); - } - } - } else { - results = await cli.leaveRoomChain(roomId, retry); - } - - if (retry) { - const limitExceededError = Object.values(results).find(e => e?.errcode === "M_LIMIT_EXCEEDED"); - if (limitExceededError) { - await sleep(limitExceededError.data.retry_after_ms ?? 100); - return leaveRoomBehaviour(roomId, false, false); - } - } - - spinnerModal?.close(); - - const errors = Object.entries(results).filter(r => !!r[1]); - if (errors.length > 0) { - const messages = []; - for (const roomErr of errors) { - const err = roomErr[1]; // [0] is the roomId - let message = _t("Unexpected server error trying to leave the room"); - if (err.errcode && err.message) { - if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { - Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, { - title: _t("Can't leave Server Notices room"), - description: _t( - "This room is used for important messages from the Homeserver, " + - "so you cannot leave it.", - ), - }); - return; - } - message = results[roomId].message; - } - messages.push(message, React.createElement('BR')); // createElement to avoid using a tsx file in utils - } - Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, { - title: _t("Error leaving room"), - description: messages, - }); - return; - } - - if (!isMetaSpace(SpaceStore.instance.activeSpace) && - SpaceStore.instance.activeSpace !== roomId && - RoomViewStore.instance.getRoomId() === roomId - ) { - dis.dispatch({ - action: Action.ViewRoom, - room_id: SpaceStore.instance.activeSpace, - metricsTrigger: undefined, // other - }); - } else { - dis.dispatch({ action: Action.ViewHomePage }); - } -} diff --git a/src/utils/space.tsx b/src/utils/space.tsx index e2b05c8ddf..8fb737440f 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -32,16 +32,12 @@ import { showRoomInviteDialog } from "../RoomInvite"; import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog"; import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog"; import defaultDispatcher from "../dispatcher/dispatcher"; -import dis from "../dispatcher/dispatcher"; import { RoomViewStore } from "../stores/RoomViewStore"; import { Action } from "../dispatcher/actions"; -import { leaveRoomBehaviour } from "./membership"; import Spinner from "../components/views/elements/Spinner"; -import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog"; import SpacePreferencesDialog, { SpacePreferenceTab } from "../components/views/dialogs/SpacePreferencesDialog"; import PosthogTrackers from "../PosthogTrackers"; import { ButtonEvent } from "../components/views/elements/AccessibleButton"; -import { AfterLeaveRoomPayload } from "../dispatcher/payloads/AfterLeaveRoomPayload"; import { shouldShowComponent } from "../customisations/helpers/UIComponents"; import { UIComponent } from "../settings/UIFeature"; @@ -185,21 +181,6 @@ export const bulkSpaceBehaviour = async ( } }; -export const leaveSpace = (space: Room) => { - Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, { - space, - onFinished: async (leave: boolean, rooms: Room[]) => { - if (!leave) return; - await bulkSpaceBehaviour(space, rooms, room => leaveRoomBehaviour(room.roomId)); - - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: space.roomId, - }); - }, - }, "mx_LeaveSpaceDialog_wrapper"); -}; - export const showSpacePreferences = (space: Room, initialTabId?: SpacePreferenceTab): Promise => { return Modal.createTrackedDialog("Space preferences", "", SpacePreferencesDialog, { initialTabId, diff --git a/test/components/views/context_menus/SpaceContextMenu-test.tsx b/test/components/views/context_menus/SpaceContextMenu-test.tsx index 5288c57ec6..53b762ef86 100644 --- a/test/components/views/context_menus/SpaceContextMenu-test.tsx +++ b/test/components/views/context_menus/SpaceContextMenu-test.tsx @@ -24,13 +24,13 @@ import SpaceContextMenu from '../../../../src/components/views/context_menus/Spa import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; import { findByTestId } from '../../../test-utils'; import { - leaveSpace, shouldShowSpaceSettings, showCreateNewRoom, showCreateNewSubspace, showSpaceInvite, showSpaceSettings, } from '../../../../src/utils/space'; +import { leaveSpace } from "../../../../src/utils/leave-behaviour"; import { shouldShowComponent } from '../../../../src/customisations/helpers/UIComponents'; import { UIComponent } from '../../../../src/settings/UIFeature'; @@ -39,7 +39,6 @@ jest.mock('../../../../src/customisations/helpers/UIComponents', () => ({ })); jest.mock('../../../../src/utils/space', () => ({ - leaveSpace: jest.fn(), shouldShowSpaceSettings: jest.fn(), showCreateNewRoom: jest.fn(), showCreateNewSubspace: jest.fn(), @@ -48,6 +47,10 @@ jest.mock('../../../../src/utils/space', () => ({ showSpaceSettings: jest.fn(), })); +jest.mock('../../../../src/utils/leave-behaviour', () => ({ + leaveSpace: jest.fn(), +})); + describe('', () => { const userId = '@test:server'; const mockClient = {