diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 5577bc29e7..99aeb6f547 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from "react"; -import { IFieldType, IInstance, IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client"; +import { IFieldType, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client"; import { Visibility } from "matrix-js-sdk/src/@types/partials"; import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests"; import { logger } from "matrix-js-sdk/src/logger"; @@ -28,7 +28,7 @@ import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown"; +import NetworkDropdown from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import { IDialogProps } from "../views/dialogs/IDialogProps"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; @@ -39,10 +39,11 @@ import DirectorySearchBox from "../views/elements/DirectorySearchBox"; import ScrollPanel from "./ScrollPanel"; import Spinner from "../views/elements/Spinner"; import { getDisplayAliasForAliasSet } from "../../Rooms"; -import { Action } from "../../dispatcher/actions"; import PosthogTrackers from "../../PosthogTrackers"; -import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { PublicRoomTile } from "../views/rooms/PublicRoomTile"; +import { getFieldsForThirdPartyLocation, joinRoomByAlias, showRoom } from "../../utils/rooms"; +import { GenericError } from "../../utils/error"; +import { ALL_ROOMS, Protocols } from "../../utils/DirectoryUtils"; const LAST_SERVER_KEY = "mx_last_room_directory_server"; const LAST_INSTANCE_KEY = "mx_last_room_directory_instance"; @@ -350,44 +351,23 @@ export default class RoomDirectory extends React.Component { }; private onJoinFromSearchClick = (alias: string) => { - // If we don't have a particular instance id selected, just show that rooms alias - if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { - // If the user specified an alias without a domain, add on whichever server is selected - // in the dropdown - if (alias.indexOf(':') == -1) { - alias = alias + ':' + this.state.roomServer; - } - this.showRoomAlias(alias, true); - } else { - // This is a 3rd party protocol. Let's see if we can join it - const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); - const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - const fields = protocolName - ? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) - : null; - if (!fields) { - const brand = SdkConfig.get().brand; - Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { - title: _t('Unable to join network'), - description: _t('%(brand)s does not know how to join a room on this network', { brand }), - }); - return; - } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { - if (resp.length > 0 && resp[0].alias) { - this.showRoomAlias(resp[0].alias, true); - } else { - Modal.createTrackedDialog('Room not found', '', ErrorDialog, { - title: _t('Room not found'), - description: _t('Couldn\'t find a matching Matrix room'), - }); - } - }, (e) => { - Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, { - title: _t('Fetching third party location failed'), - description: _t('Unable to look up room ID from server'), - }); + const cli = MatrixClientPeg.get(); + try { + joinRoomByAlias(cli, alias, { + instanceId: this.state.instanceId, + roomServer: this.state.roomServer, + protocols: this.protocols, + metricsTrigger: "RoomDirectory", }); + } catch (e) { + if (e instanceof GenericError) { + Modal.createTrackedDialog(e.message, '', ErrorDialog, { + title: e.message, + description: e.description, + }); + } else { + throw e; + } } }; @@ -401,55 +381,18 @@ export default class RoomDirectory extends React.Component { PosthogTrackers.trackInteraction("WebRoomDirectoryCreateRoomButton", ev); }; - private showRoomAlias(alias: string, autoJoin = false) { - this.showRoom(null, alias, autoJoin); - } - - private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => { + private onRoomClick = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => { this.onFinished(); - const payload: ViewRoomPayload = { - action: Action.ViewRoom, - auto_join: autoJoin, - should_peek: shouldPeek, + const cli = MatrixClientPeg.get(); + showRoom(cli, room, { + roomAlias, + autoJoin, + shouldPeek, + roomServer: this.state.roomServer, metricsTrigger: "RoomDirectory", - }; - if (room) { - // Don't let the user view a room they won't be able to either - // peek or join: fail earlier so they don't have to click back - // to the directory. - if (MatrixClientPeg.get().isGuest()) { - if (!room.world_readable && !room.guest_can_join) { - dis.dispatch({ action: 'require_registration' }); - return; - } - } - - if (!roomAlias) { - roomAlias = getDisplayAliasForRoom(room); - } - - payload.oob_data = { - avatarUrl: room.avatar_url, - // XXX: This logic is duplicated from the JS SDK which - // would normally decide what the name is. - name: room.name || roomAlias || _t('Unnamed room'), - }; - - if (this.state.roomServer) { - payload.via_servers = [this.state.roomServer]; - } - } - // It's not really possible to join Matrix rooms by ID because the HS has no way to know - // which servers to start querying. However, there's no other way to join rooms in - // this list without aliases at present, so if roomAlias isn't set here we have no - // choice but to supply the ID. - if (roomAlias) { - payload.room_alias = roomAlias; - } else { - payload.room_id = room.room_id; - } - dis.dispatch(payload); + }); }; + private stringLooksLikeId(s: string, fieldType: IFieldType) { let pat = /^#[^\s]+:[^\s]/; if (fieldType && fieldType.regexp) { @@ -459,27 +402,11 @@ export default class RoomDirectory extends React.Component { return pat.test(s); } - private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) { - // make an object with the fields specified by that protocol. We - // require that the values of all but the last field come from the - // instance. The last is the user input. - const requiredFields = protocol.location_fields; - if (!requiredFields) return null; - const fields = {}; - for (let i = 0; i < requiredFields.length - 1; ++i) { - const thisField = requiredFields[i]; - if (instance.fields[thisField] === undefined) return null; - fields[thisField] = instance.fields[thisField]; - } - fields[requiredFields[requiredFields.length - 1]] = userInput; - return fields; - } - private onFinished = () => { this.props.onFinished(false); }; - render() { + public render() { let content; if (this.state.error) { content = this.state.error; @@ -491,7 +418,7 @@ export default class RoomDirectory extends React.Component { , ); @@ -571,7 +498,7 @@ export default class RoomDirectory extends React.Component { let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType); if (protocolName) { const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - if (this.getFieldsForThirdPartyLocation( + if (getFieldsForThirdPartyLocation( this.state.filterString, this.protocols[protocolName], instance, diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index cc7a559db0..5a9e4100e1 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { useEffect, useState } from "react"; import { MatrixError } from "matrix-js-sdk/src/http-api"; -import { IProtocol } from "matrix-js-sdk/src/client"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { instanceForInstanceId } from '../../../utils/DirectoryUtils'; @@ -42,9 +41,7 @@ import UIStore from "../../../stores/UIStore"; import { compare } from "../../../utils/strings"; import { SnakedObject } from "../../../utils/SnakedObject"; import { IConfigOptions } from "../../../IConfigOptions"; - -// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage -export const ALL_ROOMS = "ALL_ROOMS"; +import { ALL_ROOMS, Protocols } from "../../../utils/DirectoryUtils"; const SETTING_NAME = "room_directory_servers"; @@ -85,8 +82,6 @@ const validServer = withValidation({ ], }); -export type Protocols = Record; - interface IProps { protocols: Protocols; selectedServerName: string; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5204401e67..a4889b74cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -733,6 +733,13 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Unnamed room": "Unnamed room", + "Unable to join network": "Unable to join network", + "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", + "Room not found": "Room not found", + "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", + "Fetching third party location failed": "Fetching third party location failed", + "Unable to look up room ID from server": "Unable to look up room ID from server", "Error upgrading room": "Error upgrading room", "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", @@ -1753,7 +1760,6 @@ "Idle": "Idle", "Offline": "Offline", "Unknown": "Unknown", - "Unnamed room": "Unnamed room", "Preview": "Preview", "View": "View", "Join": "Join", @@ -3054,12 +3060,6 @@ "remove %(name)s from the directory.": "remove %(name)s from the directory.", "delete the address.": "delete the address.", "The server may be unavailable or overloaded": "The server may be unavailable or overloaded", - "Unable to join network": "Unable to join network", - "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", - "Room not found": "Room not found", - "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", - "Fetching third party location failed": "Fetching third party location failed", - "Unable to look up room ID from server": "Unable to look up room ID from server", "Create new room": "Create new room", "No results for \"%(query)s\"": "No results for \"%(query)s\"", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.", diff --git a/src/utils/DirectoryUtils.ts b/src/utils/DirectoryUtils.ts index 89e719f320..f8b9e858d1 100644 --- a/src/utils/DirectoryUtils.ts +++ b/src/utils/DirectoryUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 The Matrix.org Foundation C.I.C. +Copyright 2018, 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. @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IInstance } from "matrix-js-sdk/src/client"; +import { IInstance, IProtocol } from "matrix-js-sdk/src/client"; -import { Protocols } from "../components/views/directory/NetworkDropdown"; +// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage +export const ALL_ROOMS = "ALL_ROOMS"; + +export type Protocols = Record; // Find a protocol 'instance' with a given instance_id // in the supplied protocols dict diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000000..8dec29e7f0 --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,24 @@ +/* +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. +*/ + +export class GenericError extends Error { + constructor( + public readonly message: string, + public readonly description?: string | undefined, + ) { + super(message); + } +} diff --git a/src/utils/rooms.ts b/src/utils/rooms.ts index bd6fcf9d97..d9aa310ea4 100644 --- a/src/utils/rooms.ts +++ b/src/utils/rooms.ts @@ -14,7 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { IInstance, IProtocol, IPublicRoomsChunkRoom, MatrixClient } from "matrix-js-sdk/src/client"; +import { ViewRoom as ViewRoomEvent } from "matrix-analytics-events/types/typescript/ViewRoom"; + +import { Action } from "../dispatcher/actions"; +import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { getE2EEWellKnown } from "./WellKnownUtils"; +import dis from "../dispatcher/dispatcher"; +import { getDisplayAliasForAliasSet } from "../Rooms"; +import { _t } from "../languageHandler"; +import { instanceForInstanceId, protocolNameForInstanceId } from "./DirectoryUtils"; +import SdkConfig from "../SdkConfig"; +import { GenericError } from "./error"; +import { ALL_ROOMS, Protocols } from "./DirectoryUtils"; export function privateShouldBeEncrypted(): boolean { const e2eeWellKnown = getE2EEWellKnown(); @@ -24,3 +36,146 @@ export function privateShouldBeEncrypted(): boolean { } return true; } + +interface IShowRoomOpts { + roomAlias?: string; + autoJoin?: boolean; + shouldPeek?: boolean; + roomServer?: string; + metricsTrigger: ViewRoomEvent["trigger"]; +} + +export const showRoom = ( + client: MatrixClient, + room: IPublicRoomsChunkRoom | null, + { + roomAlias, + autoJoin = false, + shouldPeek = false, + roomServer, + }: IShowRoomOpts, +): void => { + const payload: ViewRoomPayload = { + action: Action.ViewRoom, + auto_join: autoJoin, + should_peek: shouldPeek, + metricsTrigger: "RoomDirectory", + }; + if (room) { + // Don't let the user view a room they won't be able to either + // peek or join: fail earlier so they don't have to click back + // to the directory. + if (client.isGuest()) { + if (!room.world_readable && !room.guest_can_join) { + dis.dispatch({ action: 'require_registration' }); + return; + } + } + + if (!roomAlias) { + roomAlias = getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); + } + + payload.oob_data = { + avatarUrl: room.avatar_url, + // XXX: This logic is duplicated from the JS SDK which + // would normally decide what the name is. + name: room.name || roomAlias || _t('Unnamed room'), + }; + + if (roomServer) { + payload.via_servers = [roomServer]; + } + } + // It's not really possible to join Matrix rooms by ID because the HS has no way to know + // which servers to start querying. However, there's no other way to join rooms in + // this list without aliases at present, so if roomAlias isn't set here we have no + // choice but to supply the ID. + if (roomAlias) { + payload.room_alias = roomAlias; + } else { + payload.room_id = room.room_id; + } + dis.dispatch(payload); +}; + +interface IJoinRoomByAliasOpts { + instanceId?: string; + roomServer?: string; + protocols: Protocols; + metricsTrigger: ViewRoomEvent["trigger"]; +} + +export function joinRoomByAlias(cli: MatrixClient, alias: string, { + instanceId, + roomServer, + protocols, + metricsTrigger, +}: IJoinRoomByAliasOpts): void { + // If we don't have a particular instance id selected, just show that rooms alias + if (!instanceId || instanceId === ALL_ROOMS) { + // If the user specified an alias without a domain, add on whichever server is selected + // in the dropdown + if (!alias.includes(':')) { + alias = alias + ':' + roomServer; + } + showRoom(cli, null, { + roomAlias: alias, + autoJoin: true, + metricsTrigger, + }); + } else { + // This is a 3rd party protocol. Let's see if we can join it + const protocolName = protocolNameForInstanceId(protocols, instanceId); + const instance = instanceForInstanceId(protocols, instanceId); + const fields = protocolName + ? getFieldsForThirdPartyLocation(alias, protocols[protocolName], instance) + : null; + if (!fields) { + const brand = SdkConfig.get().brand; + throw new GenericError( + _t('Unable to join network'), + _t('%(brand)s does not know how to join a room on this network', { brand }), + ); + } + cli.getThirdpartyLocation(protocolName, fields).then((resp) => { + if (resp.length > 0 && resp[0].alias) { + showRoom(cli, null, { + roomAlias: resp[0].alias, + autoJoin: true, + metricsTrigger, + }); + } else { + throw new GenericError( + _t('Room not found'), + _t('Couldn\'t find a matching Matrix room'), + ); + } + }, (e) => { + throw new GenericError( + _t('Fetching third party location failed'), + _t('Unable to look up room ID from server'), + ); + }); + } +} + +export function getFieldsForThirdPartyLocation( + userInput: string, + protocol: IProtocol, + instance: IInstance, +): { searchFields?: string[] } | null { + // make an object with the fields specified by that protocol. We + // require that the values of all but the last field come from the + // instance. The last is the user input. + const requiredFields = protocol.location_fields; + if (!requiredFields) return null; + const fields = {}; + for (let i = 0; i < requiredFields.length - 1; ++i) { + const thisField = requiredFields[i]; + if (instance.fields[thisField] === undefined) return null; + fields[thisField] = instance.fields[thisField]; + } + fields[requiredFields[requiredFields.length - 1]] = userInput; + return fields; +}