Extract view/join room logic to room helper (#8329)

This commit is contained in:
Germain 2022-04-21 10:56:02 +01:00 committed by GitHub
parent d5e911d876
commit 5d6143aaa7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 122 deletions

View file

@ -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<IProps, IState> {
};
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
<PublicRoomTile
key={room.room_id}
room={room}
showRoom={this.showRoom}
showRoom={this.onRoomClick}
removeFromDirectory={this.removeFromDirectory}
/>,
);
@ -571,7 +498,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
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,

View file

@ -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<undefined, { error?: MatrixError }>({
],
});
export type Protocols = Record<string, IProtocol>;
interface IProps {
protocols: Protocols;
selectedServerName: string;

View file

@ -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.",

View file

@ -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<string, IProtocol>;
// Find a protocol 'instance' with a given instance_id
// in the supplied protocols dict

24
src/utils/error.ts Normal file
View file

@ -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);
}
}

View file

@ -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;
}