diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml new file mode 100644 index 0000000000..2e08418cf6 --- /dev/null +++ b/.github/workflows/typecheck.yaml @@ -0,0 +1,24 @@ +name: Type Check +on: + pull_request: + branches: [develop] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: c-hive/gha-yarn-cache@v2 + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + - name: Typecheck + run: "yarn run lint:types" + - name: Switch js-sdk to release mode + run: | + scripts/ci/js-sdk-to-release.js + cd node_modules/matrix-js-sdk + yarn install + yarn run build:compile + yarn run build:types + - name: Typecheck (release mode) + run: "yarn run lint:types" + diff --git a/scripts/ci/js-sdk-to-release.js b/scripts/ci/js-sdk-to-release.js new file mode 100755 index 0000000000..e1fecfde03 --- /dev/null +++ b/scripts/ci/js-sdk-to-release.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +const fsProm = require('fs/promises'); + +const PKGJSON = 'node_modules/matrix-js-sdk/package.json'; + +async function main() { + const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8')); + for (const field of ['main', 'typings']) { + if (pkgJson["matrix_lib_"+field] !== undefined) { + pkgJson[field] = pkgJson["matrix_lib_"+field]; + } + } + await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2)); +} + +main(); diff --git a/scripts/ci/js-sdk-to-release.sh b/scripts/ci/js-sdk-to-release.sh deleted file mode 100755 index a03165bd82..0000000000 --- a/scripts/ci/js-sdk-to-release.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -# This changes the js-sdk into 'release mode', that is: -# * The entry point for the library is the babel-compiled lib/index.js rather than src/index.ts -# * There's a 'typings' entry referencing the types output by tsc -# We do this so we can test that each PR still builds / type checks correctly when built -# against the released js-sdk, because if you do things like `import { User } from 'matrix-js-sdk';` -# rather than `import { User } from 'matrix-js-sdk/src/models/user';` it will work fine with the -# js-sdk in development mode but then break at release time. -# We can't use the last release of the js-sdk though: it might not be up to date enough. - -cd node_modules/matrix-js-sdk -for i in main typings -do - lib_value=$(jq -r ".matrix_lib_$i" package.json) - if [ "$lib_value" != "null" ]; then - jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json - fi -done -yarn run build:compile -yarn run build:types diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index d955271249..9a2ebd45e2 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -399,7 +399,9 @@ export default class LeftPanel extends React.Component { mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, })} onClick={this.onExplore} - title={_t("Explore rooms")} + title={this.state.activeSpace + ? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name }) + : _t("Explore rooms")} /> ); diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 67634c63d2..5b12e542bd 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -269,7 +269,7 @@ export default class RightPanel extends React.Component { case RightPanelPhases.EncryptionPanel: panel = { defaultDispatcher.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.SpaceMemberList, - refireParams: { space: space }, + refireParams: { space }, }); onFinished(); }; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index a426dce5c7..a73f0a595b 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -79,7 +79,10 @@ export default class RoomSettingsDialog extends React.Component { ROOM_SECURITY_TAB, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", - , + this.props.onFinished(true)} + />, )); tabs.push(new Tab( ROOM_ROLES_TAB, diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index fe836ebc5c..bf73a2edb1 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -60,7 +60,7 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin SpaceSettingsTab.Visibility, _td("Visibility"), "mx_SpaceSettingsDialog_visibilityIcon", - , + , ), SettingsStore.getValue(UIFeature.AdvancedSettings) ? new Tab( diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index b1c8d427bf..8beb089b38 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -57,7 +57,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { // state to show a spinner immediately after clicking "start verification", // before we have a request const [isRequesting, setRequesting] = useState(false); - const [phase, setPhase] = useState(request && request.phase); + const [phase, setPhase] = useState(request?.phase); useEffect(() => { setRequest(verificationRequest); if (verificationRequest) { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d15f349d62..f90643f1df 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1278,7 +1278,9 @@ const BasicUserInfo: React.FC<{ // hide the Roles section for DMs as it doesn't make sense there if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { memberDetails =
-

{ _t("Role") }

+

{ _t("Role in ", {}, { + RoomName: () => { room.name }, + }) }

= ({ // We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { previousPhase = RightPanelPhases.RoomMemberInfo; - refireParams = { member: member }; + refireParams = { member }; + } else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) { + previousPhase = previousPhase = RightPanelPhases.SpaceMemberList; + refireParams = { space: room }; } else if (room) { - previousPhase = previousPhase = SpaceStore.spacesEnabled && room.isSpaceRoom() - ? RightPanelPhases.SpaceMemberList - : RightPanelPhases.RoomMemberList; + previousPhase = RightPanelPhases.RoomMemberList; } const onEncryptionPanelClose = () => { diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 395bdc21e0..a29bdea90b 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -29,43 +29,27 @@ import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import E2EIcon from "../rooms/E2EIcon"; -import { - PHASE_READY, - PHASE_DONE, - PHASE_STARTED, - PHASE_CANCELLED, -} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import Spinner from "../elements/Spinner"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleButton from "../elements/AccessibleButton"; import VerificationShowSas from "../verification/VerificationShowSas"; -// XXX: Should be defined in matrix-js-sdk -enum VerificationPhase { - PHASE_UNSENT, - PHASE_REQUESTED, - PHASE_READY, - PHASE_DONE, - PHASE_STARTED, - PHASE_CANCELLED, -} - interface IProps { layout: string; request: VerificationRequest; member: RoomMember | User; - phase: VerificationPhase; + phase: Phase; onClose: () => void; isRoomEncrypted: boolean; inDialog: boolean; - key: number; } interface IState { - sasEvent?: SAS; + sasEvent?: SAS["sasEvent"]; emojiButtonClicked?: boolean; reciprocateButtonClicked?: boolean; - reciprocateQREvent?: ReciprocateQRCode; + reciprocateQREvent?: ReciprocateQRCode["reciprocateQREvent"]; } @replaceableComponent("views.right_panel.VerificationPanel") @@ -321,9 +305,9 @@ export default class VerificationPanel extends React.PureComponent { const { request } = this.props; - const { sasEvent, reciprocateQREvent } = request.verifier; + const sasEvent = (request.verifier as SAS).sasEvent; + const reciprocateQREvent = (request.verifier as ReciprocateQRCode).reciprocateQREvent; request.verifier.off('show_sas', this.updateVerifierState); request.verifier.off('show_reciprocate_qr', this.updateVerifierState); this.setState({ sasEvent, reciprocateQREvent }); @@ -402,7 +387,8 @@ export default class VerificationPanel extends React.PureComponent void; @@ -522,20 +523,23 @@ export default class RoomList extends React.PureComponent { } else if ( this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join" ) { + const spaceName = this.props.activeSpace.name; explorePrompt =
{ _t("Quick actions") }
- { this.props.activeSpace.canInvite(userId) && { _t("Invite people") } - } - { this.props.activeSpace.getMyMembership() === "join" && } + { this.props.activeSpace.getMyMembership() === "join" && { _t("Explore rooms") } - } + }
; } else if (Object.values(this.state.sublists).some(list => list.length > 0)) { const unfilteredLists = RoomListStore.instance.unfilteredLists; diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index 1ff7eb83f6..2f457f2916 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -31,15 +31,18 @@ import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog"; import { upgradeRoom } from "../../../utils/RoomUpgrade"; import { arrayHasDiff } from "../../../utils/arrays"; import { useLocalEcho } from "../../../hooks/useLocalEcho"; +import dis from "../../../dispatcher/dispatcher"; +import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; interface IProps { room: Room; promptUpgrade?: boolean; + closeSettingsFn(): void; onError(error: Error): void; beforeChange?(joinRule: JoinRule): Promise; // if returns false then aborts the change } -const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange }: IProps) => { +const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSettingsFn }: IProps) => { const cli = room.client; const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport; @@ -208,9 +211,20 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange }: IProps targetVersion, description: _t("This upgrade will allow members of selected spaces " + "access to this room without an invite."), - onFinished: (resp) => { + onFinished: async (resp) => { if (!resp?.continue) return; - upgradeRoom(room, targetVersion, resp.invite); + const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true); + closeSettingsFn(); + // switch to the new room in the background + dis.dispatch({ + action: "view_room", + room_id: roomId, + }); + // open new settings on this tab + dis.dispatch({ + action: "open_room_settings", + initial_tab_id: ROOM_SECURITY_TAB, + }); }, }); return; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 40b15cfc78..5ebfa92acf 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -38,6 +38,7 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; interface IProps { roomId: string; + closeSettingsFn: () => void; } interface IState { @@ -276,6 +277,7 @@ export default class SecurityRoomSettingsTab extends React.Component
; diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index 83587f54e6..2e9ec77345 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -31,9 +31,10 @@ import JoinRuleSettings from "../settings/JoinRuleSettings"; interface IProps { matrixClient: MatrixClient; space: Room; + closeSettingsFn(): void; } -const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { +const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space, closeSettingsFn }: IProps) => { const [error, setError] = useState(""); const userId = cli.getUserId(); @@ -119,6 +120,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { setError(_t("Failed to update the visibility of this space"))} + closeSettingsFn={closeSettingsFn} /> diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 71a947df49..2b9ea5da96 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS"; +import { IGeneratedSas } from "matrix-js-sdk/src/crypto/verification/SAS"; import { DeviceInfo } from "matrix-js-sdk/src//crypto/deviceinfo"; import { _t, _td } from '../../../languageHandler'; import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; @@ -30,7 +30,7 @@ interface IProps { device?: DeviceInfo; onDone: () => void; onCancel: () => void; - sas: SAS.sas; + sas: IGeneratedSas; isSelf?: boolean; inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c53e76253d..a381f7ff72 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1642,6 +1642,7 @@ "Start a new chat": "Start a new chat", "Explore all public rooms": "Explore all public rooms", "Quick actions": "Quick actions", + "Explore %(spaceName)s": "Explore %(spaceName)s", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "%(count)s results in all spaces|other": "%(count)s results in all spaces", "%(count)s results in all spaces|one": "%(count)s result in all spaces", @@ -1865,7 +1866,7 @@ "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "Deactivate user": "Deactivate user", "Failed to deactivate user": "Failed to deactivate user", - "Role": "Role", + "Role in ": "Role in ", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Edit devices": "Edit devices", "Security": "Security", diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index e632ec6345..366f49d892 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -22,6 +22,7 @@ import Modal from "../Modal"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import SpaceStore from "../stores/SpaceStore"; +import Spinner from "../components/views/elements/Spinner"; export async function upgradeRoom( room: Room, @@ -29,8 +30,10 @@ export async function upgradeRoom( inviteUsers = false, handleError = true, updateSpaces = true, + awaitRoom = false, ): Promise { const cli = room.client; + const spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); let newRoomId: string; try { @@ -46,27 +49,36 @@ export async function upgradeRoom( throw e; } - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (inviteUsers) { - const checkForUpgradeFn = async (newRoom: Room): Promise => { - // The upgradePromise should be done by the time we await it here. - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); + if (awaitRoom || inviteUsers) { + await new Promise(resolve => { + // already have the room + if (room.client.getRoom(newRoomId)) { + resolve(); + return; } - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); + // We have to wait for the js-sdk to give us the room back so + // we can more effectively abuse the MultiInviter behaviour + // which heavily relies on the Room object being available. + const checkForRoomFn = (newRoom: Room) => { + if (newRoom.roomId !== newRoomId) return; + resolve(); + cli.off("Room", checkForRoomFn); + }; + cli.on("Room", checkForRoomFn); + }); + } + + if (inviteUsers) { + const toInvite = [ + ...room.getMembersWithMembership("join"), + ...room.getMembersWithMembership("invite"), + ].map(m => m.userId).filter(m => m !== cli.getUserId()); + + if (toInvite.length > 0) { + // Errors are handled internally to this function + await inviteUsersToRoom(newRoomId, toInvite); + } } if (updateSpaces) { @@ -89,5 +101,6 @@ export async function upgradeRoom( } } + spinnerModal.close(); return newRoomId; }