Merge branch 'develop' into improved-forwarding-ui
This commit is contained in:
commit
88e0e9b9fb
7 changed files with 426 additions and 278 deletions
|
@ -61,6 +61,39 @@ limitations under the License.
|
||||||
.mx_RoomDirectory_tableWrapper {
|
.mx_RoomDirectory_tableWrapper {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|
||||||
|
.mx_RoomDirectory_footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 40px auto 60px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-20px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
max-width: 464px; // easier reading
|
||||||
|
}
|
||||||
|
|
||||||
|
> hr {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_newRoom {
|
||||||
|
margin: 24px auto 0;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_table {
|
.mx_RoomDirectory_table {
|
||||||
|
@ -138,11 +171,6 @@ limitations under the License.
|
||||||
color: $settings-grey-fg-color;
|
color: $settings-grey-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_table tr {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomDirectory .mx_RoomView_MessageList {
|
.mx_RoomDirectory .mx_RoomView_MessageList {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -665,7 +665,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this.createRoom(payload.public);
|
this.createRoom(payload.public, payload.defaultName);
|
||||||
break;
|
break;
|
||||||
case 'view_create_group': {
|
case 'view_create_group': {
|
||||||
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
||||||
|
@ -1011,7 +1011,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createRoom(defaultPublic = false) {
|
private async createRoom(defaultPublic = false, defaultName?: string) {
|
||||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
if (communityId) {
|
if (communityId) {
|
||||||
// double check the user will have permission to associate this room with the community
|
// double check the user will have permission to associate this room with the community
|
||||||
|
@ -1025,7 +1025,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
|
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||||
|
defaultPublic,
|
||||||
|
defaultName,
|
||||||
|
});
|
||||||
|
|
||||||
const [shouldCreate, opts] = await modal.finished;
|
const [shouldCreate, opts] = await modal.finished;
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,39 +15,90 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
|
||||||
import * as sdk from "../../index";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import SdkConfig from '../../SdkConfig';
|
import SdkConfig from '../../SdkConfig';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
import {ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols} from "../views/directory/NetworkDropdown";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||||
import GroupStore from "../../stores/GroupStore";
|
import GroupStore from "../../stores/GroupStore";
|
||||||
import FlairStore from "../../stores/FlairStore";
|
import FlairStore from "../../stores/FlairStore";
|
||||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
|
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||||
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||||
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
|
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
||||||
|
import NetworkDropdown from "../views/directory/NetworkDropdown";
|
||||||
|
import ScrollPanel from "./ScrollPanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 800;
|
const MAX_TOPIC_LENGTH = 800;
|
||||||
|
|
||||||
function track(action) {
|
function track(action: string) {
|
||||||
Analytics.trackEvent('RoomDirectory', action);
|
Analytics.trackEvent('RoomDirectory', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
initialText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
publicRooms: IRoom[];
|
||||||
|
loading: boolean;
|
||||||
|
protocolsLoading: boolean;
|
||||||
|
error?: string;
|
||||||
|
instanceId: string | symbol;
|
||||||
|
roomServer: string;
|
||||||
|
filterString: string;
|
||||||
|
selectedCommunityId?: string;
|
||||||
|
communityName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
interface IRoom {
|
||||||
|
room_id: string;
|
||||||
|
name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
topic?: string;
|
||||||
|
canonical_alias?: string;
|
||||||
|
aliases?: string[];
|
||||||
|
world_readable: boolean;
|
||||||
|
guest_can_join: boolean;
|
||||||
|
num_joined_members: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPublicRoomsRequest {
|
||||||
|
limit?: number;
|
||||||
|
since?: string;
|
||||||
|
server?: string;
|
||||||
|
filter?: object;
|
||||||
|
include_all_networks?: boolean;
|
||||||
|
third_party_instance_id?: string;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomDirectory")
|
@replaceableComponent("structures.RoomDirectory")
|
||||||
export default class RoomDirectory extends React.Component {
|
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private readonly startTime: number;
|
||||||
initialText: PropTypes.string,
|
private unmounted = false
|
||||||
onFinished: PropTypes.func.isRequired,
|
private nextBatch: string = null;
|
||||||
};
|
private filterTimeout: NodeJS.Timeout;
|
||||||
|
private protocols: Protocols;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -56,41 +106,21 @@ export default class RoomDirectory extends React.Component {
|
||||||
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
||||||
this.startTime = CountlyAnalytics.getTimestamp();
|
this.startTime = CountlyAnalytics.getTimestamp();
|
||||||
|
|
||||||
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
|
const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||||
this.state = {
|
? GroupFilterOrderStore.getSelectedTags()[0]
|
||||||
publicRooms: [],
|
: null;
|
||||||
loading: true,
|
|
||||||
protocolsLoading: true,
|
|
||||||
error: null,
|
|
||||||
instanceId: undefined,
|
|
||||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
|
||||||
filterString: this.props.initialText || "",
|
|
||||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
|
||||||
? selectedCommunityId
|
|
||||||
: null,
|
|
||||||
communityName: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._unmounted = false;
|
let protocolsLoading = true;
|
||||||
this.nextBatch = null;
|
|
||||||
this.filterTimeout = null;
|
|
||||||
this.scrollPanel = null;
|
|
||||||
this.protocols = null;
|
|
||||||
|
|
||||||
this.state.protocolsLoading = true;
|
|
||||||
if (!MatrixClientPeg.get()) {
|
if (!MatrixClientPeg.get()) {
|
||||||
// We may not have a client yet when invoked from welcome page
|
// We may not have a client yet when invoked from welcome page
|
||||||
this.state.protocolsLoading = false;
|
protocolsLoading = false;
|
||||||
return;
|
} else if (!selectedCommunityId) {
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.selectedCommunityId) {
|
|
||||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||||
this.protocols = response;
|
this.protocols = response;
|
||||||
this.setState({protocolsLoading: false});
|
this.setState({ protocolsLoading: false });
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.warn(`error loading third party protocols: ${err}`);
|
console.warn(`error loading third party protocols: ${err}`);
|
||||||
this.setState({protocolsLoading: false});
|
this.setState({ protocolsLoading: false });
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// Guests currently aren't allowed to use this API, so
|
// Guests currently aren't allowed to use this API, so
|
||||||
// ignore this as otherwise this error is literally the
|
// ignore this as otherwise this error is literally the
|
||||||
|
@ -103,19 +133,31 @@ export default class RoomDirectory extends React.Component {
|
||||||
error: _t(
|
error: _t(
|
||||||
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
||||||
'The homeserver may be too old to support third party networks.',
|
'The homeserver may be too old to support third party networks.',
|
||||||
{brand},
|
{ brand },
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// We don't use the protocols in the communities v2 prototype experience
|
// We don't use the protocols in the communities v2 prototype experience
|
||||||
this.state.protocolsLoading = false;
|
protocolsLoading = false;
|
||||||
|
|
||||||
// Grab the profile info async
|
// Grab the profile info async
|
||||||
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
||||||
this.setState({communityName: profile.name});
|
this.setState({ communityName: profile.name });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
publicRooms: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
instanceId: undefined,
|
||||||
|
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||||
|
filterString: this.props.initialText || "",
|
||||||
|
selectedCommunityId,
|
||||||
|
communityName: null,
|
||||||
|
protocolsLoading,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
clearTimeout(this.filterTimeout);
|
clearTimeout(this.filterTimeout);
|
||||||
}
|
}
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRoomList = () => {
|
private refreshRoomList = () => {
|
||||||
if (this.state.selectedCommunityId) {
|
if (this.state.selectedCommunityId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
||||||
|
@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
this.getMoreRooms();
|
this.getMoreRooms();
|
||||||
};
|
};
|
||||||
|
|
||||||
getMoreRooms() {
|
private getMoreRooms() {
|
||||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||||
|
|
||||||
|
@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component {
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const my_filter_string = this.state.filterString;
|
const filterString = this.state.filterString;
|
||||||
const my_server = this.state.roomServer;
|
const roomServer = this.state.roomServer;
|
||||||
// remember the next batch token when we sent the request
|
// remember the next batch token when we sent the request
|
||||||
// too. If it's changed, appending to the list will corrupt it.
|
// too. If it's changed, appending to the list will corrupt it.
|
||||||
const my_next_batch = this.nextBatch;
|
const nextBatch = this.nextBatch;
|
||||||
const opts = {limit: 20};
|
const opts: IPublicRoomsRequest = { limit: 20 };
|
||||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||||
opts.server = my_server;
|
opts.server = roomServer;
|
||||||
}
|
}
|
||||||
if (this.state.instanceId === ALL_ROOMS) {
|
if (this.state.instanceId === ALL_ROOMS) {
|
||||||
opts.include_all_networks = true;
|
opts.include_all_networks = true;
|
||||||
} else if (this.state.instanceId) {
|
} else if (this.state.instanceId) {
|
||||||
opts.third_party_instance_id = this.state.instanceId;
|
opts.third_party_instance_id = this.state.instanceId as string;
|
||||||
}
|
}
|
||||||
if (this.nextBatch) opts.since = this.nextBatch;
|
if (this.nextBatch) opts.since = this.nextBatch;
|
||||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
if (filterString) opts.filter = { generic_search_term: filterString };
|
||||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.state.filterString ||
|
filterString != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
roomServer != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch) {
|
nextBatch != this.nextBatch) {
|
||||||
// if the filter or server has changed since this request was sent,
|
// if the filter or server has changed since this request was sent,
|
||||||
// throw away the result (don't even clear the busy flag
|
// throw away the result (don't even clear the busy flag
|
||||||
// since we must still have a request in flight)
|
// since we must still have a request in flight)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
// if we've been unmounted, we don't care either.
|
// if we've been unmounted, we don't care either.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nextBatch = data.next_batch;
|
this.nextBatch = data.next_batch;
|
||||||
this.setState((s) => {
|
this.setState((s) => ({
|
||||||
s.publicRooms.push(...(data.chunk || []));
|
...s,
|
||||||
s.loading = false;
|
publicRooms: [...s.publicRooms, ...(data.chunk || [])],
|
||||||
return s;
|
loading: false,
|
||||||
});
|
}));
|
||||||
return Boolean(data.next_batch);
|
return Boolean(data.next_batch);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.state.filterString ||
|
filterString != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
roomServer != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch) {
|
nextBatch != this.nextBatch) {
|
||||||
// as above: we don't care about errors for old
|
// as above: we don't care about errors for old
|
||||||
// requests either
|
// requests either
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
// if we've been unmounted, we don't care either.
|
// if we've been unmounted, we don't care either.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
* HS admins to do this through the RoomSettings interface, but
|
* HS admins to do this through the RoomSettings interface, but
|
||||||
* this needs SPEC-417.
|
* this needs SPEC-417.
|
||||||
*/
|
*/
|
||||||
removeFromDirectory(room) {
|
private removeFromDirectory(room: IRoom) {
|
||||||
const alias = get_display_alias_for_room(room);
|
const alias = getDisplayAliasForRoom(room);
|
||||||
const name = room.name || alias || _t('Unnamed room');
|
const name = room.name || alias || _t('Unnamed room');
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
|
|
||||||
let desc;
|
let desc;
|
||||||
if (alias) {
|
if (alias) {
|
||||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||||
|
@ -269,11 +308,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
||||||
title: _t('Remove from Directory'),
|
title: _t('Remove from Directory'),
|
||||||
description: desc,
|
description: desc,
|
||||||
onFinished: (should_delete) => {
|
onFinished: (shouldDelete: boolean) => {
|
||||||
if (!should_delete) return;
|
if (!shouldDelete) return;
|
||||||
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const modal = Modal.createDialog(Spinner);
|
||||||
const modal = Modal.createDialog(Loader);
|
|
||||||
let step = _t('remove %(name)s from the directory.', {name: name});
|
let step = _t('remove %(name)s from the directory.', {name: name});
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||||
|
@ -289,14 +327,16 @@ export default class RoomDirectory extends React.Component {
|
||||||
console.error("Failed to " + step + ": " + err);
|
console.error("Failed to " + step + ": " + err);
|
||||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
|
description: (err && err.message)
|
||||||
|
? err.message
|
||||||
|
: _t('The server may be unavailable or overloaded'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomClicked = (room, ev) => {
|
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.removeFromDirectory(room);
|
this.removeFromDirectory(room);
|
||||||
|
@ -305,7 +345,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onOptionChange = (server, instanceId) => {
|
private onOptionChange = (server: string, instanceId?: string | symbol) => {
|
||||||
// clear next batch so we don't try to load more rooms
|
// clear next batch so we don't try to load more rooms
|
||||||
this.nextBatch = null;
|
this.nextBatch = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component {
|
||||||
// Easiest to just blow away the state & re-fetch.
|
// Easiest to just blow away the state & re-fetch.
|
||||||
};
|
};
|
||||||
|
|
||||||
onFillRequest = (backwards) => {
|
private onFillRequest = (backwards: boolean) => {
|
||||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||||
|
|
||||||
return this.getMoreRooms();
|
return this.getMoreRooms();
|
||||||
};
|
};
|
||||||
|
|
||||||
onFilterChange = (alias) => {
|
private onFilterChange = (alias: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: alias || null,
|
filterString: alias || null,
|
||||||
});
|
});
|
||||||
|
@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}, 700);
|
}, 700);
|
||||||
};
|
};
|
||||||
|
|
||||||
onFilterClear = () => {
|
private onFilterClear = () => {
|
||||||
// update immediately
|
// update immediately
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: null,
|
filterString: null,
|
||||||
|
@ -360,7 +400,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onJoinFromSearchClick = (alias) => {
|
private onJoinFromSearchClick = (alias: string) => {
|
||||||
// If we don't have a particular instance id selected, just show that rooms alias
|
// 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 (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
// If the user specified an alias without a domain, add on whichever server is selected
|
// If the user specified an alias without a domain, add on whichever server is selected
|
||||||
|
@ -373,9 +413,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
// This is a 3rd party protocol. Let's see if we can join it
|
// This is a 3rd party protocol. Let's see if we can join it
|
||||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
const fields = protocolName
|
||||||
|
? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance)
|
||||||
|
: null;
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||||
title: _t('Unable to join network'),
|
title: _t('Unable to join network'),
|
||||||
|
@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component {
|
||||||
if (resp.length > 0 && resp[0].alias) {
|
if (resp.length > 0 && resp[0].alias) {
|
||||||
this.showRoomAlias(resp[0].alias, true);
|
this.showRoomAlias(resp[0].alias, true);
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||||
title: _t('Room not found'),
|
title: _t('Room not found'),
|
||||||
description: _t('Couldn\'t find a matching Matrix room'),
|
description: _t('Couldn\'t find a matching Matrix room'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
||||||
title: _t('Fetching third party location failed'),
|
title: _t('Fetching third party location failed'),
|
||||||
description: _t('Unable to look up room ID from server'),
|
description: _t('Unable to look up room ID from server'),
|
||||||
|
@ -403,36 +442,37 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onPreviewClick = (ev, room) => {
|
private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room, null, false, true);
|
this.showRoom(room, null, false, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewClick = (ev, room) => {
|
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room);
|
this.showRoom(room);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onJoinClick = (ev, room) => {
|
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room, null, true);
|
this.showRoom(room, null, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onCreateRoomClick = room => {
|
private onCreateRoomClick = () => {
|
||||||
this.onFinished();
|
this.onFinished();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_create_room',
|
action: 'view_create_room',
|
||||||
public: true,
|
public: true,
|
||||||
|
defaultName: this.state.filterString.trim(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
showRoomAlias(alias, autoJoin=false) {
|
private showRoomAlias(alias: string, autoJoin = false) {
|
||||||
this.showRoom(null, alias, autoJoin);
|
this.showRoom(null, alias, autoJoin);
|
||||||
}
|
}
|
||||||
|
|
||||||
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
|
private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||||
this.onFinished();
|
this.onFinished();
|
||||||
const payload = {
|
const payload: ActionPayload = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
auto_join: autoJoin,
|
auto_join: autoJoin,
|
||||||
should_peek: shouldPeek,
|
should_peek: shouldPeek,
|
||||||
|
@ -449,15 +489,15 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room_alias) {
|
if (!roomAlias) {
|
||||||
room_alias = get_display_alias_for_room(room);
|
roomAlias = getDisplayAliasForRoom(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.oob_data = {
|
payload.oob_data = {
|
||||||
avatarUrl: room.avatar_url,
|
avatarUrl: room.avatar_url,
|
||||||
// XXX: This logic is duplicated from the JS SDK which
|
// XXX: This logic is duplicated from the JS SDK which
|
||||||
// would normally decide what the name is.
|
// would normally decide what the name is.
|
||||||
name: room.name || room_alias || _t('Unnamed room'),
|
name: room.name || roomAlias || _t('Unnamed room'),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.roomServer) {
|
if (this.state.roomServer) {
|
||||||
|
@ -471,21 +511,19 @@ export default class RoomDirectory extends React.Component {
|
||||||
// which servers to start querying. However, there's no other way to join rooms in
|
// 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
|
// this list without aliases at present, so if roomAlias isn't set here we have no
|
||||||
// choice but to supply the ID.
|
// choice but to supply the ID.
|
||||||
if (room_alias) {
|
if (roomAlias) {
|
||||||
payload.room_alias = room_alias;
|
payload.room_alias = roomAlias;
|
||||||
} else {
|
} else {
|
||||||
payload.room_id = room.room_id;
|
payload.room_id = room.room_id;
|
||||||
}
|
}
|
||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoomCells(room) {
|
private createRoomCells(room: IRoom) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const clientRoom = client.getRoom(room.room_id);
|
const clientRoom = client.getRoom(room.room_id);
|
||||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||||
const isGuest = client.isGuest();
|
const isGuest = client.isGuest();
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
let previewButton;
|
let previewButton;
|
||||||
let joinOrViewButton;
|
let joinOrViewButton;
|
||||||
|
|
||||||
|
@ -495,20 +533,26 @@ export default class RoomDirectory extends React.Component {
|
||||||
// it is readable, the preview appears as normal.
|
// it is readable, the preview appears as normal.
|
||||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||||
previewButton = (
|
previewButton = (
|
||||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>
|
||||||
|
{ _t("Preview") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (hasJoinedRoom) {
|
if (hasJoinedRoom) {
|
||||||
joinOrViewButton = (
|
joinOrViewButton = (
|
||||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>
|
||||||
|
{ _t("View") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else if (!isGuest) {
|
} else if (!isGuest) {
|
||||||
joinOrViewButton = (
|
joinOrViewButton = (
|
||||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>
|
||||||
|
{ _t("Join") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
|
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||||
if (name.length > MAX_NAME_LENGTH) {
|
if (name.length > MAX_NAME_LENGTH) {
|
||||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||||
}
|
}
|
||||||
|
@ -531,9 +575,13 @@ export default class RoomDirectory extends React.Component {
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||||
className="mx_RoomDirectory_roomAvatar"
|
className="mx_RoomDirectory_roomAvatar"
|
||||||
>
|
>
|
||||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
<BaseAvatar
|
||||||
name={ name } idName={ name }
|
width={32}
|
||||||
url={ avatarUrl }
|
height={32}
|
||||||
|
resizeMethod='crop'
|
||||||
|
name={name}
|
||||||
|
idName={name}
|
||||||
|
url={avatarUrl}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_description` }
|
<div key={ `${room.room_id}_description` }
|
||||||
|
@ -547,7 +595,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||||
dangerouslySetInnerHTML={{ __html: topic }}
|
dangerouslySetInnerHTML={{ __html: topic }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_memberCount` }
|
<div key={ `${room.room_id}_memberCount` }
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
@ -576,20 +624,16 @@ export default class RoomDirectory extends React.Component {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
collectScrollPanel = (element) => {
|
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||||
this.scrollPanel = element;
|
|
||||||
};
|
|
||||||
|
|
||||||
_stringLooksLikeId(s, field_type) {
|
|
||||||
let pat = /^#[^\s]+:[^\s]/;
|
let pat = /^#[^\s]+:[^\s]/;
|
||||||
if (field_type && field_type.regexp) {
|
if (fieldType && fieldType.regexp) {
|
||||||
pat = new RegExp(field_type.regexp);
|
pat = new RegExp(fieldType.regexp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pat.test(s);
|
return pat.test(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getFieldsForThirdPartyLocation(userInput, protocol, instance) {
|
private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) {
|
||||||
// make an object with the fields specified by that protocol. We
|
// make an object with the fields specified by that protocol. We
|
||||||
// require that the values of all but the last field come from the
|
// require that the values of all but the last field come from the
|
||||||
// instance. The last is the user input.
|
// instance. The last is the user input.
|
||||||
|
@ -605,71 +649,73 @@ export default class RoomDirectory extends React.Component {
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private onFinished = () => {
|
||||||
* called by the parent component when PageUp/Down/etc is pressed.
|
|
||||||
*
|
|
||||||
* We pass it down to the scroll panel.
|
|
||||||
*/
|
|
||||||
handleScrollKey = ev => {
|
|
||||||
if (this.scrollPanel) {
|
|
||||||
this.scrollPanel.handleScrollKey(ev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onFinished = () => {
|
|
||||||
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
||||||
this.props.onFinished();
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
content = this.state.error;
|
content = this.state.error;
|
||||||
} else if (this.state.protocolsLoading) {
|
} else if (this.state.protocolsLoading) {
|
||||||
content = <Loader />;
|
content = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const cells = (this.state.publicRooms || [])
|
const cells = (this.state.publicRooms || [])
|
||||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
|
||||||
// we still show the scrollpanel, at least for now, because
|
// we still show the scrollpanel, at least for now, because
|
||||||
// otherwise we don't fetch more because we don't get a fill
|
// otherwise we don't fetch more because we don't get a fill
|
||||||
// request from the scrollpanel because there isn't one
|
// request from the scrollpanel because there isn't one
|
||||||
|
|
||||||
let spinner;
|
let spinner;
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
spinner = <Loader />;
|
spinner = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollpanel_content;
|
const createNewButton = <>
|
||||||
|
<hr />
|
||||||
|
<AccessibleButton kind="primary" onClick={this.onCreateRoomClick} className="mx_RoomDirectory_newRoom">
|
||||||
|
{ _t("Create new room") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
let scrollPanelContent;
|
||||||
|
let footer;
|
||||||
if (cells.length === 0 && !this.state.loading) {
|
if (cells.length === 0 && !this.state.loading) {
|
||||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
footer = <>
|
||||||
|
<h5>{ _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }</h5>
|
||||||
|
<p>
|
||||||
|
{ _t("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.") }
|
||||||
|
</p>
|
||||||
|
{ createNewButton }
|
||||||
|
</>;
|
||||||
} else {
|
} else {
|
||||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
scrollPanelContent = <div className="mx_RoomDirectory_table">
|
||||||
{ cells }
|
{ cells }
|
||||||
</div>;
|
</div>;
|
||||||
|
if (!this.state.loading && !this.nextBatch) {
|
||||||
|
footer = createNewButton;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
content = <ScrollPanel
|
||||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
|
||||||
className="mx_RoomDirectory_tableWrapper"
|
className="mx_RoomDirectory_tableWrapper"
|
||||||
onFillRequest={ this.onFillRequest }
|
onFillRequest={this.onFillRequest}
|
||||||
stickyBottom={false}
|
stickyBottom={false}
|
||||||
startAtBottom={false}
|
startAtBottom={false}
|
||||||
>
|
>
|
||||||
{ scrollpanel_content }
|
{ scrollPanelContent }
|
||||||
{ spinner }
|
{ spinner }
|
||||||
|
{ footer && <div className="mx_RoomDirectory_footer">
|
||||||
|
{ footer }
|
||||||
|
</div> }
|
||||||
</ScrollPanel>;
|
</ScrollPanel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listHeader;
|
let listHeader;
|
||||||
if (!this.state.protocolsLoading) {
|
if (!this.state.protocolsLoading) {
|
||||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
|
||||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
|
||||||
|
|
||||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||||
let instance_expected_field_type;
|
let instanceExpectedFieldType;
|
||||||
if (
|
if (
|
||||||
protocolName &&
|
protocolName &&
|
||||||
this.protocols &&
|
this.protocols &&
|
||||||
|
@ -677,21 +723,27 @@ export default class RoomDirectory extends React.Component {
|
||||||
this.protocols[protocolName].location_fields.length > 0 &&
|
this.protocols[protocolName].location_fields.length > 0 &&
|
||||||
this.protocols[protocolName].field_types
|
this.protocols[protocolName].field_types
|
||||||
) {
|
) {
|
||||||
const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
|
const lastField = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField];
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholder = _t('Find a room…');
|
let placeholder = _t('Find a room…');
|
||||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
|
||||||
} else if (instance_expected_field_type) {
|
exampleRoom: "#example:" + this.state.roomServer,
|
||||||
placeholder = instance_expected_field_type.placeholder;
|
});
|
||||||
|
} else if (instanceExpectedFieldType) {
|
||||||
|
placeholder = instanceExpectedFieldType.placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
|
let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType);
|
||||||
if (protocolName) {
|
if (protocolName) {
|
||||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||||
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
|
if (this.getFieldsForThirdPartyLocation(
|
||||||
|
this.state.filterString,
|
||||||
|
this.protocols[protocolName],
|
||||||
|
instance,
|
||||||
|
) === null) {
|
||||||
showJoinButton = false;
|
showJoinButton = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -723,12 +775,11 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
const explanation =
|
const explanation =
|
||||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||||
{a: sub => {
|
{a: sub => (
|
||||||
return (<AccessibleButton
|
<AccessibleButton kind="secondary" onClick={this.onCreateRoomClick}>
|
||||||
kind="secondary"
|
{ sub }
|
||||||
onClick={this.onCreateRoomClick}
|
</AccessibleButton>
|
||||||
>{sub}</AccessibleButton>);
|
)},
|
||||||
}},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = this.state.selectedCommunityId
|
const title = this.state.selectedCommunityId
|
||||||
|
@ -756,6 +807,6 @@ export default class RoomDirectory extends React.Component {
|
||||||
|
|
||||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||||
// but works with the objects we get from the public room list
|
// but works with the objects we get from the public room list
|
||||||
function get_display_alias_for_room(room) {
|
function getDisplayAliasForRoom(room: IRoom) {
|
||||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
return room.canonical_alias || room.aliases?.[0] || "";
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,27 +15,46 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation, {IFieldState} from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import {_t} from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
defaultPublic?: boolean;
|
||||||
|
defaultName?: string;
|
||||||
|
parentSpace?: Room;
|
||||||
|
onFinished(proceed: boolean, opts?: IOpts): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isPublic: boolean;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
name: string;
|
||||||
|
topic: string;
|
||||||
|
alias: string;
|
||||||
|
detailsOpen: boolean;
|
||||||
|
noFederate: boolean;
|
||||||
|
nameIsValid: boolean;
|
||||||
|
canChangeEncryption: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
||||||
export default class CreateRoomDialog extends React.Component {
|
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private nameField = createRef<Field>();
|
||||||
onFinished: PropTypes.func.isRequired,
|
private aliasField = createRef<RoomAliasField>();
|
||||||
defaultPublic: PropTypes.bool,
|
|
||||||
parentSpace: PropTypes.instanceOf(Room),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -44,7 +63,7 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublic: this.props.defaultPublic || false,
|
isPublic: this.props.defaultPublic || false,
|
||||||
isEncrypted: privateShouldBeEncrypted(),
|
isEncrypted: privateShouldBeEncrypted(),
|
||||||
name: "",
|
name: this.props.defaultName || "",
|
||||||
topic: "",
|
topic: "",
|
||||||
alias: "",
|
alias: "",
|
||||||
detailsOpen: false,
|
detailsOpen: false,
|
||||||
|
@ -54,26 +73,25 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
||||||
.then(isForced => this.setState({canChangeEncryption: !isForced}));
|
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||||
}
|
}
|
||||||
|
|
||||||
_roomCreateOptions() {
|
private roomCreateOptions() {
|
||||||
const opts = {};
|
const opts: IOpts = {};
|
||||||
const createOpts = opts.createOpts = {};
|
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||||
createOpts.name = this.state.name;
|
createOpts.name = this.state.name;
|
||||||
if (this.state.isPublic) {
|
if (this.state.isPublic) {
|
||||||
createOpts.visibility = "public";
|
createOpts.visibility = Visibility.Public;
|
||||||
createOpts.preset = "public_chat";
|
createOpts.preset = Preset.PublicChat;
|
||||||
opts.guestAccess = false;
|
opts.guestAccess = false;
|
||||||
const {alias} = this.state;
|
const { alias } = this.state;
|
||||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
createOpts['room_alias_name'] = localPart;
|
|
||||||
}
|
}
|
||||||
if (this.state.topic) {
|
if (this.state.topic) {
|
||||||
createOpts.topic = this.state.topic;
|
createOpts.topic = this.state.topic;
|
||||||
}
|
}
|
||||||
if (this.state.noFederate) {
|
if (this.state.noFederate) {
|
||||||
createOpts.creation_content = {'m.federate': false};
|
createOpts.creation_content = { 'm.federate': false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.isPublic) {
|
if (!this.state.isPublic) {
|
||||||
|
@ -98,16 +116,14 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
|
||||||
// move focus to first field when showing dialog
|
// move focus to first field when showing dialog
|
||||||
this._nameFieldRef.focus();
|
this.nameField.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown = event => {
|
private onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === Key.ENTER) {
|
if (event.key === Key.ENTER) {
|
||||||
this.onOk();
|
this.onOk();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -115,26 +131,26 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onOk = async () => {
|
private onOk = async () => {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.blur();
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
await this._nameFieldRef.validate({allowEmpty: false});
|
await this.nameField.current.validate({allowEmpty: false});
|
||||||
if (this._aliasFieldRef) {
|
if (this.aliasField.current) {
|
||||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
await this.aliasField.current.validate({allowEmpty: false});
|
||||||
}
|
}
|
||||||
// Validation and state updates are async, so we need to wait for them to complete
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
// first. Queue a `setState` callback and wait for it to resolve.
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
await new Promise(resolve => this.setState({}, resolve));
|
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) {
|
||||||
this.props.onFinished(true, this._roomCreateOptions());
|
this.props.onFinished(true, this.roomCreateOptions());
|
||||||
} else {
|
} else {
|
||||||
let field;
|
let field;
|
||||||
if (!this.state.nameIsValid) {
|
if (!this.state.nameIsValid) {
|
||||||
field = this._nameFieldRef;
|
field = this.nameField.current;
|
||||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
|
||||||
field = this._aliasFieldRef;
|
field = this.aliasField.current;
|
||||||
}
|
}
|
||||||
if (field) {
|
if (field) {
|
||||||
field.focus();
|
field.focus();
|
||||||
|
@ -143,49 +159,45 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = () => {
|
private onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
onNameChange = ev => {
|
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({name: ev.target.value});
|
this.setState({ name: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTopicChange = ev => {
|
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({topic: ev.target.value});
|
this.setState({ topic: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onPublicChange = isPublic => {
|
private onPublicChange = (isPublic: boolean) => {
|
||||||
this.setState({isPublic});
|
this.setState({ isPublic });
|
||||||
};
|
};
|
||||||
|
|
||||||
onEncryptedChange = isEncrypted => {
|
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||||
this.setState({isEncrypted});
|
this.setState({ isEncrypted });
|
||||||
};
|
};
|
||||||
|
|
||||||
onAliasChange = alias => {
|
private onAliasChange = (alias: string) => {
|
||||||
this.setState({alias});
|
this.setState({ alias });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDetailsToggled = ev => {
|
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>) => {
|
||||||
this.setState({detailsOpen: ev.target.open});
|
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
||||||
};
|
};
|
||||||
|
|
||||||
onNoFederateChange = noFederate => {
|
private onNoFederateChange = (noFederate: boolean) => {
|
||||||
this.setState({noFederate});
|
this.setState({ noFederate });
|
||||||
};
|
};
|
||||||
|
|
||||||
collectDetailsRef = ref => {
|
private onNameValidate = async (fieldState: IFieldState) => {
|
||||||
this._detailsRef = ref;
|
const result = await CreateRoomDialog.validateRoomName(fieldState);
|
||||||
};
|
|
||||||
|
|
||||||
onNameValidate = async fieldState => {
|
|
||||||
const result = await CreateRoomDialog._validateRoomName(fieldState);
|
|
||||||
this.setState({nameIsValid: result.valid});
|
this.setState({nameIsValid: result.valid});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
static _validateRoomName = withValidation({
|
private static validateRoomName = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -196,18 +208,17 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const Field = sdk.getComponent('views.elements.Field');
|
|
||||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
|
||||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
|
||||||
|
|
||||||
let aliasField;
|
let aliasField;
|
||||||
if (this.state.isPublic) {
|
if (this.state.isPublic) {
|
||||||
const domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
aliasField = (
|
aliasField = (
|
||||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||||
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
<RoomAliasField
|
||||||
|
ref={this.aliasField}
|
||||||
|
onChange={this.onAliasChange}
|
||||||
|
domain={domain}
|
||||||
|
value={this.state.alias}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -270,16 +281,34 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
<Field
|
||||||
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
|
ref={this.nameField}
|
||||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
label={_t('Name')}
|
||||||
|
onChange={this.onNameChange}
|
||||||
|
onValidate={this.onNameValidate}
|
||||||
|
value={this.state.name}
|
||||||
|
className="mx_CreateRoomDialog_name"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={_t('Topic (optional)')}
|
||||||
|
onChange={this.onTopicChange}
|
||||||
|
value={this.state.topic}
|
||||||
|
className="mx_CreateRoomDialog_topic"
|
||||||
|
/>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
label={_t("Make this room public")}
|
||||||
|
onChange={this.onPublicChange}
|
||||||
|
value={this.state.isPublic}
|
||||||
|
/>
|
||||||
{ publicPrivateLabel }
|
{ publicPrivateLabel }
|
||||||
{ e2eeSection }
|
{ e2eeSection }
|
||||||
{ aliasField }
|
{ aliasField }
|
||||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
<summary className="mx_CreateRoomDialog_details_summary">
|
||||||
|
{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }
|
||||||
|
</summary>
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
label={_t(
|
label={_t(
|
||||||
"Block anyone not part of %(serverName)s from ever joining this room.",
|
"Block anyone not part of %(serverName)s from ever joining this room.",
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2016, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,39 +15,42 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
useContextMenu,
|
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
MenuItemRadio,
|
|
||||||
MenuItem,
|
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
|
MenuItem,
|
||||||
|
MenuItemRadio,
|
||||||
|
useContextMenu,
|
||||||
} from "../../structures/ContextMenu";
|
} from "../../structures/ContextMenu";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import {useSettingValue} from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import withValidation from "../elements/Validation";
|
import withValidation from "../elements/Validation";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
|
||||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||||
|
|
||||||
const SETTING_NAME = "room_directory_servers";
|
const SETTING_NAME = "room_directory_servers";
|
||||||
|
|
||||||
const inPlaceOf = (elementRect) => ({
|
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||||
right: window.innerWidth - elementRect.right,
|
right: window.innerWidth - elementRect.right,
|
||||||
top: elementRect.top,
|
top: elementRect.top,
|
||||||
chevronOffset: 0,
|
chevronOffset: 0,
|
||||||
chevronFace: "none",
|
chevronFace: ChevronFace.None,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validServer = withValidation({
|
const validServer = withValidation<undefined, { error?: MatrixError }>({
|
||||||
deriveData: async ({ value }) => {
|
deriveData: async ({ value }) => {
|
||||||
try {
|
try {
|
||||||
// check if we can successfully load this server's room directory
|
// check if we can successfully load this server's room directory
|
||||||
|
@ -78,15 +80,49 @@ const validServer = withValidation({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IFieldType {
|
||||||
|
regexp: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstance {
|
||||||
|
desc: string;
|
||||||
|
icon?: string;
|
||||||
|
fields: object;
|
||||||
|
network_id: string;
|
||||||
|
// XXX: this is undocumented but we rely on it.
|
||||||
|
// we inject a fake entry with a symbolic instance_id.
|
||||||
|
instance_id: string | symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProtocol {
|
||||||
|
user_fields: string[];
|
||||||
|
location_fields: string[];
|
||||||
|
icon: string;
|
||||||
|
field_types: Record<string, IFieldType>;
|
||||||
|
instances: IInstance[];
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
export type Protocols = Record<string, IProtocol>;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
protocols: Protocols;
|
||||||
|
selectedServerName: string;
|
||||||
|
selectedInstanceId: string | symbol;
|
||||||
|
onOptionChange(server: string, instanceId?: string | symbol): void;
|
||||||
|
}
|
||||||
|
|
||||||
// This dropdown sources homeservers from three places:
|
// This dropdown sources homeservers from three places:
|
||||||
// + your currently connected homeserver
|
// + your currently connected homeserver
|
||||||
// + homeservers in config.json["roomDirectory"]
|
// + homeservers in config.json["roomDirectory"]
|
||||||
// + homeservers in SettingsStore["room_directory_servers"]
|
// + homeservers in SettingsStore["room_directory_servers"]
|
||||||
// if a server exists in multiple, only keep the top-most entry.
|
// if a server exists in multiple, only keep the top-most entry.
|
||||||
|
|
||||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, selectedInstanceId }: IProps) => {
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
const _userDefinedServers: string[] = useSettingValue(SETTING_NAME);
|
||||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||||
|
|
||||||
const handlerFactory = (server, instanceId) => {
|
const handlerFactory = (server, instanceId) => {
|
||||||
|
@ -98,7 +134,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
|
|
||||||
const setUserDefinedServers = servers => {
|
const setUserDefinedServers = servers => {
|
||||||
_setUserDefinedServers(servers);
|
_setUserDefinedServers(servers);
|
||||||
SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, servers);
|
||||||
};
|
};
|
||||||
// keep local echo up to date with external changes
|
// keep local echo up to date with external changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -112,7 +148,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
const roomDirectory = config.roomDirectory || {};
|
const roomDirectory = config.roomDirectory || {};
|
||||||
|
|
||||||
const hsName = MatrixClientPeg.getHomeserverName();
|
const hsName = MatrixClientPeg.getHomeserverName();
|
||||||
const configServers = new Set(roomDirectory.servers);
|
const configServers = new Set<string>(roomDirectory.servers);
|
||||||
|
|
||||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||||
|
@ -136,9 +172,15 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
// add a fake protocol with the ALL_ROOMS symbol
|
// add a fake protocol with the ALL_ROOMS symbol
|
||||||
protocolsList.push({
|
protocolsList.push({
|
||||||
instances: [{
|
instances: [{
|
||||||
|
fields: [],
|
||||||
|
network_id: "",
|
||||||
instance_id: ALL_ROOMS,
|
instance_id: ALL_ROOMS,
|
||||||
desc: _t("All rooms"),
|
desc: _t("All rooms"),
|
||||||
}],
|
}],
|
||||||
|
location_fields: [],
|
||||||
|
user_fields: [],
|
||||||
|
field_types: {},
|
||||||
|
icon: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +214,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
if (removableServers.has(server)) {
|
if (removableServers.has(server)) {
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||||
title: _t("Are you sure?"),
|
title: _t("Are you sure?"),
|
||||||
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||||
|
@ -191,7 +232,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
setUserDefinedServers(servers.filter(s => s !== server));
|
setUserDefinedServers(servers.filter(s => s !== server));
|
||||||
|
|
||||||
// the selected server is being removed, reset to our HS
|
// the selected server is being removed, reset to our HS
|
||||||
if (serverSelected === server) {
|
if (serverSelected) {
|
||||||
onOptionChange(hsName, undefined);
|
onOptionChange(hsName, undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -223,7 +264,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
|
||||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||||
title: _t("Add a new server"),
|
title: _t("Add a new server"),
|
||||||
description: _t("Enter the name of a new server you want to explore."),
|
description: _t("Enter the name of a new server you want to explore."),
|
||||||
|
@ -284,9 +324,4 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkDropdown.propTypes = {
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
protocols: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkDropdown;
|
export default NetworkDropdown;
|
|
@ -39,7 +39,7 @@ import { makeSpaceParentEvent } from "./utils/space";
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
|
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
|
||||||
enum Visibility {
|
export enum Visibility {
|
||||||
Public = "public",
|
Public = "public",
|
||||||
Private = "private",
|
Private = "private",
|
||||||
}
|
}
|
||||||
|
|
|
@ -2651,6 +2651,8 @@
|
||||||
"Unable to look up room ID from server": "Unable to look up room ID from server",
|
"Unable to look up room ID from server": "Unable to look up room ID from server",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
"View": "View",
|
"View": "View",
|
||||||
|
"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.",
|
||||||
"Find a room…": "Find a room…",
|
"Find a room…": "Find a room…",
|
||||||
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
||||||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||||
|
|
Loading…
Reference in a new issue