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 {
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
|
@ -138,11 +171,6 @@ limitations under the License.
|
|||
color: $settings-grey-fg-color;
|
||||
}
|
||||
|
||||
.mx_RoomDirectory_table tr {
|
||||
padding-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomDirectory .mx_RoomView_MessageList {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -665,7 +665,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_create_room':
|
||||
this.createRoom(payload.public);
|
||||
this.createRoom(payload.public, payload.defaultName);
|
||||
break;
|
||||
case 'view_create_group': {
|
||||
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();
|
||||
if (communityId) {
|
||||
// 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 modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||
defaultPublic,
|
||||
defaultName,
|
||||
});
|
||||
|
||||
const [shouldCreate, opts] = await modal.finished;
|
||||
if (shouldCreate) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,18 +15,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
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 GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import GroupStore from "../../stores/GroupStore";
|
||||
|
@ -35,20 +33,72 @@ import FlairStore from "../../stores/FlairStore";
|
|||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
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_TOPIC_LENGTH = 800;
|
||||
|
||||
function track(action) {
|
||||
function track(action: string) {
|
||||
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")
|
||||
export default class RoomDirectory extends React.Component {
|
||||
static propTypes = {
|
||||
initialText: PropTypes.string,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||
private readonly startTime: number;
|
||||
private unmounted = false
|
||||
private nextBatch: string = null;
|
||||
private filterTimeout: NodeJS.Timeout;
|
||||
private protocols: Protocols;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -56,35 +106,15 @@ export default class RoomDirectory extends React.Component {
|
|||
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
||||
this.startTime = CountlyAnalytics.getTimestamp();
|
||||
|
||||
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
|
||||
this.state = {
|
||||
publicRooms: [],
|
||||
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,
|
||||
};
|
||||
const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||
? GroupFilterOrderStore.getSelectedTags()[0]
|
||||
: null;
|
||||
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
this.scrollPanel = null;
|
||||
this.protocols = null;
|
||||
|
||||
this.state.protocolsLoading = true;
|
||||
let protocolsLoading = true;
|
||||
if (!MatrixClientPeg.get()) {
|
||||
// We may not have a client yet when invoked from welcome page
|
||||
this.state.protocolsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.selectedCommunityId) {
|
||||
protocolsLoading = false;
|
||||
} else if (!selectedCommunityId) {
|
||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||
this.protocols = response;
|
||||
this.setState({ protocolsLoading: false });
|
||||
|
@ -109,13 +139,25 @@ export default class RoomDirectory extends React.Component {
|
|||
});
|
||||
} else {
|
||||
// We don't use the protocols in the communities v2 prototype experience
|
||||
this.state.protocolsLoading = false;
|
||||
protocolsLoading = false;
|
||||
|
||||
// Grab the profile info async
|
||||
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
||||
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() {
|
||||
|
@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component {
|
|||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
this._unmounted = true;
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
refreshRoomList = () => {
|
||||
private refreshRoomList = () => {
|
||||
if (this.state.selectedCommunityId) {
|
||||
this.setState({
|
||||
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
||||
|
@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component {
|
|||
this.getMoreRooms();
|
||||
};
|
||||
|
||||
getMoreRooms() {
|
||||
private getMoreRooms() {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
|
||||
|
@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component {
|
|||
loading: true,
|
||||
});
|
||||
|
||||
const my_filter_string = this.state.filterString;
|
||||
const my_server = this.state.roomServer;
|
||||
const filterString = this.state.filterString;
|
||||
const roomServer = this.state.roomServer;
|
||||
// remember the next batch token when we sent the request
|
||||
// too. If it's changed, appending to the list will corrupt it.
|
||||
const my_next_batch = this.nextBatch;
|
||||
const opts = {limit: 20};
|
||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = my_server;
|
||||
const nextBatch = this.nextBatch;
|
||||
const opts: IPublicRoomsRequest = { limit: 20 };
|
||||
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = roomServer;
|
||||
}
|
||||
if (this.state.instanceId === ALL_ROOMS) {
|
||||
opts.include_all_networks = true;
|
||||
} 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 (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) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch) {
|
||||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
|
||||
this.nextBatch = data.next_batch;
|
||||
this.setState((s) => {
|
||||
s.publicRooms.push(...(data.chunk || []));
|
||||
s.loading = false;
|
||||
return s;
|
||||
});
|
||||
this.setState((s) => ({
|
||||
...s,
|
||||
publicRooms: [...s.publicRooms, ...(data.chunk || [])],
|
||||
loading: false,
|
||||
}));
|
||||
return Boolean(data.next_batch);
|
||||
}, (err) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch) {
|
||||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// as above: we don't care about errors for old
|
||||
// requests either
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component {
|
|||
* HS admins to do this through the RoomSettings interface, but
|
||||
* this needs SPEC-417.
|
||||
*/
|
||||
removeFromDirectory(room) {
|
||||
const alias = get_display_alias_for_room(room);
|
||||
private removeFromDirectory(room: IRoom) {
|
||||
const alias = getDisplayAliasForRoom(room);
|
||||
const name = room.name || alias || _t('Unnamed room');
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
let desc;
|
||||
if (alias) {
|
||||
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, {
|
||||
title: _t('Remove from Directory'),
|
||||
description: desc,
|
||||
onFinished: (should_delete) => {
|
||||
if (!should_delete) return;
|
||||
onFinished: (shouldDelete: boolean) => {
|
||||
if (!shouldDelete) return;
|
||||
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader);
|
||||
const modal = Modal.createDialog(Spinner);
|
||||
let step = _t('remove %(name)s from the directory.', {name: name});
|
||||
|
||||
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);
|
||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||
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) {
|
||||
ev.preventDefault();
|
||||
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
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
|
@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component {
|
|||
// Easiest to just blow away the state & re-fetch.
|
||||
};
|
||||
|
||||
onFillRequest = (backwards) => {
|
||||
private onFillRequest = (backwards: boolean) => {
|
||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||
|
||||
return this.getMoreRooms();
|
||||
};
|
||||
|
||||
onFilterChange = (alias) => {
|
||||
private onFilterChange = (alias: string) => {
|
||||
this.setState({
|
||||
filterString: alias || null,
|
||||
});
|
||||
|
@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component {
|
|||
}, 700);
|
||||
};
|
||||
|
||||
onFilterClear = () => {
|
||||
private onFilterClear = () => {
|
||||
// update immediately
|
||||
this.setState({
|
||||
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 (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
// 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
|
||||
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;
|
||||
const fields = protocolName
|
||||
? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance)
|
||||
: null;
|
||||
if (!fields) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const brand = SdkConfig.get().brand;
|
||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||
title: _t('Unable to join network'),
|
||||
|
@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component {
|
|||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias, true);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
title: _t('Room not found'),
|
||||
description: _t('Couldn\'t find a matching Matrix room'),
|
||||
});
|
||||
}
|
||||
}, (e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
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'),
|
||||
|
@ -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);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onViewClick = (ev, room) => {
|
||||
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||
this.showRoom(room);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onJoinClick = (ev, room) => {
|
||||
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
|
||||
this.showRoom(room, null, true);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onCreateRoomClick = room => {
|
||||
private onCreateRoomClick = () => {
|
||||
this.onFinished();
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
public: true,
|
||||
defaultName: this.state.filterString.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
showRoomAlias(alias, autoJoin=false) {
|
||||
private showRoomAlias(alias: string, autoJoin = false) {
|
||||
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();
|
||||
const payload = {
|
||||
const payload: ActionPayload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
should_peek: shouldPeek,
|
||||
|
@ -449,15 +489,15 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (!room_alias) {
|
||||
room_alias = get_display_alias_for_room(room);
|
||||
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 || room_alias || _t('Unnamed room'),
|
||||
name: room.name || roomAlias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
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
|
||||
// this list without aliases at present, so if roomAlias isn't set here we have no
|
||||
// choice but to supply the ID.
|
||||
if (room_alias) {
|
||||
payload.room_alias = room_alias;
|
||||
if (roomAlias) {
|
||||
payload.room_alias = roomAlias;
|
||||
} else {
|
||||
payload.room_id = room.room_id;
|
||||
}
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
createRoomCells(room) {
|
||||
private createRoomCells(room: IRoom) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
const isGuest = client.isGuest();
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
|
@ -495,20 +533,26 @@ export default class RoomDirectory extends React.Component {
|
|||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||
}
|
||||
|
@ -531,8 +575,12 @@ export default class RoomDirectory extends React.Component {
|
|||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
<BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
name={name}
|
||||
idName={name}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
</div>,
|
||||
|
@ -547,7 +595,7 @@ export default class RoomDirectory extends React.Component {
|
|||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
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 key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
|
@ -576,20 +624,16 @@ export default class RoomDirectory extends React.Component {
|
|||
];
|
||||
}
|
||||
|
||||
collectScrollPanel = (element) => {
|
||||
this.scrollPanel = element;
|
||||
};
|
||||
|
||||
_stringLooksLikeId(s, field_type) {
|
||||
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||
let pat = /^#[^\s]+:[^\s]/;
|
||||
if (field_type && field_type.regexp) {
|
||||
pat = new RegExp(field_type.regexp);
|
||||
if (fieldType && fieldType.regexp) {
|
||||
pat = new RegExp(fieldType.regexp);
|
||||
}
|
||||
|
||||
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
|
||||
// require that the values of all but the last field come from the
|
||||
// instance. The last is the user input.
|
||||
|
@ -605,71 +649,73 @@ export default class RoomDirectory extends React.Component {
|
|||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = () => {
|
||||
private onFinished = () => {
|
||||
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = this.state.error;
|
||||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
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
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
||||
let spinner;
|
||||
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) {
|
||||
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 {
|
||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
||||
scrollPanelContent = <div className="mx_RoomDirectory_table">
|
||||
{ cells }
|
||||
</div>;
|
||||
if (!this.state.loading && !this.nextBatch) {
|
||||
footer = createNewButton;
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
}
|
||||
content = <ScrollPanel
|
||||
className="mx_RoomDirectory_tableWrapper"
|
||||
onFillRequest={this.onFillRequest}
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
{ scrollPanelContent }
|
||||
{ spinner }
|
||||
{ footer && <div className="mx_RoomDirectory_footer">
|
||||
{ footer }
|
||||
</div> }
|
||||
</ScrollPanel>;
|
||||
}
|
||||
|
||||
let listHeader;
|
||||
if (!this.state.protocolsLoading) {
|
||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
let instance_expected_field_type;
|
||||
let instanceExpectedFieldType;
|
||||
if (
|
||||
protocolName &&
|
||||
this.protocols &&
|
||||
|
@ -677,21 +723,27 @@ export default class RoomDirectory extends React.Component {
|
|||
this.protocols[protocolName].location_fields.length > 0 &&
|
||||
this.protocols[protocolName].field_types
|
||||
) {
|
||||
const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
||||
const lastField = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||
instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField];
|
||||
}
|
||||
|
||||
let placeholder = _t('Find a room…');
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
|
||||
exampleRoom: "#example:" + this.state.roomServer,
|
||||
});
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -723,12 +775,11 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
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,
|
||||
{a: sub => {
|
||||
return (<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={this.onCreateRoomClick}
|
||||
>{sub}</AccessibleButton>);
|
||||
}},
|
||||
{a: sub => (
|
||||
<AccessibleButton kind="secondary" onClick={this.onCreateRoomClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>
|
||||
)},
|
||||
);
|
||||
|
||||
const title = this.state.selectedCommunityId
|
||||
|
@ -756,6 +807,6 @@ export default class RoomDirectory extends React.Component {
|
|||
|
||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function get_display_alias_for_room(room) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
function getDisplayAliasForRoom(room: IRoom) {
|
||||
return room.canonical_alias || room.aliases?.[0] || "";
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, {IFieldState} from '../elements/Validation';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
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")
|
||||
export default class CreateRoomDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
parentSpace: PropTypes.instanceOf(Room),
|
||||
};
|
||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
private nameField = createRef<Field>();
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -44,7 +63,7 @@ export default class CreateRoomDialog extends React.Component {
|
|||
this.state = {
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: privateShouldBeEncrypted(),
|
||||
name: "",
|
||||
name: this.props.defaultName || "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
detailsOpen: false,
|
||||
|
@ -57,17 +76,16 @@ export default class CreateRoomDialog extends React.Component {
|
|||
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||
}
|
||||
|
||||
_roomCreateOptions() {
|
||||
const opts = {};
|
||||
const createOpts = opts.createOpts = {};
|
||||
private roomCreateOptions() {
|
||||
const opts: IOpts = {};
|
||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||
createOpts.name = this.state.name;
|
||||
if (this.state.isPublic) {
|
||||
createOpts.visibility = "public";
|
||||
createOpts.preset = "public_chat";
|
||||
createOpts.visibility = Visibility.Public;
|
||||
createOpts.preset = Preset.PublicChat;
|
||||
opts.guestAccess = false;
|
||||
const { alias } = this.state;
|
||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||
createOpts['room_alias_name'] = localPart;
|
||||
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||
}
|
||||
if (this.state.topic) {
|
||||
createOpts.topic = this.state.topic;
|
||||
|
@ -98,16 +116,14 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
||||
// move focus to first field when showing dialog
|
||||
this._nameFieldRef.focus();
|
||||
this.nameField.current.focus();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
||||
}
|
||||
|
||||
_onKeyDown = event => {
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === Key.ENTER) {
|
||||
this.onOk();
|
||||
event.preventDefault();
|
||||
|
@ -115,26 +131,26 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onOk = async () => {
|
||||
const activeElement = document.activeElement;
|
||||
private onOk = async () => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
await this._nameFieldRef.validate({allowEmpty: false});
|
||||
if (this._aliasFieldRef) {
|
||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
||||
await this.nameField.current.validate({allowEmpty: false});
|
||||
if (this.aliasField.current) {
|
||||
await this.aliasField.current.validate({allowEmpty: false});
|
||||
}
|
||||
// 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.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
||||
this.props.onFinished(true, this._roomCreateOptions());
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) {
|
||||
this.props.onFinished(true, this.roomCreateOptions());
|
||||
} else {
|
||||
let field;
|
||||
if (!this.state.nameIsValid) {
|
||||
field = this._nameFieldRef;
|
||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
||||
field = this._aliasFieldRef;
|
||||
field = this.nameField.current;
|
||||
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
|
||||
field = this.aliasField.current;
|
||||
}
|
||||
if (field) {
|
||||
field.focus();
|
||||
|
@ -143,49 +159,45 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
onNameChange = ev => {
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
|
||||
onTopicChange = ev => {
|
||||
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ topic: ev.target.value });
|
||||
};
|
||||
|
||||
onPublicChange = isPublic => {
|
||||
private onPublicChange = (isPublic: boolean) => {
|
||||
this.setState({ isPublic });
|
||||
};
|
||||
|
||||
onEncryptedChange = isEncrypted => {
|
||||
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||
this.setState({ isEncrypted });
|
||||
};
|
||||
|
||||
onAliasChange = alias => {
|
||||
private onAliasChange = (alias: string) => {
|
||||
this.setState({ alias });
|
||||
};
|
||||
|
||||
onDetailsToggled = ev => {
|
||||
this.setState({detailsOpen: ev.target.open});
|
||||
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>) => {
|
||||
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
||||
};
|
||||
|
||||
onNoFederateChange = noFederate => {
|
||||
private onNoFederateChange = (noFederate: boolean) => {
|
||||
this.setState({ noFederate });
|
||||
};
|
||||
|
||||
collectDetailsRef = ref => {
|
||||
this._detailsRef = ref;
|
||||
};
|
||||
|
||||
onNameValidate = async fieldState => {
|
||||
const result = await CreateRoomDialog._validateRoomName(fieldState);
|
||||
private onNameValidate = async (fieldState: IFieldState) => {
|
||||
const result = await CreateRoomDialog.validateRoomName(fieldState);
|
||||
this.setState({nameIsValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
static _validateRoomName = withValidation({
|
||||
private static validateRoomName = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -196,18 +208,17 @@ export default class CreateRoomDialog extends React.Component {
|
|||
});
|
||||
|
||||
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;
|
||||
if (this.state.isPublic) {
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -270,16 +281,34 @@ export default class CreateRoomDialog extends React.Component {
|
|||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||
<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 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} />
|
||||
<Field
|
||||
ref={this.nameField}
|
||||
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 }
|
||||
{ e2eeSection }
|
||||
{ aliasField }
|
||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">
|
||||
{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }
|
||||
</summary>
|
||||
<LabelledToggleSwitch
|
||||
label={_t(
|
||||
"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 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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
MenuItem,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
MenuItemRadio,
|
||||
useContextMenu,
|
||||
} from "../../structures/ContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
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");
|
||||
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
const inPlaceOf = (elementRect) => ({
|
||||
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||
right: window.innerWidth - elementRect.right,
|
||||
top: elementRect.top,
|
||||
chevronOffset: 0,
|
||||
chevronFace: "none",
|
||||
chevronFace: ChevronFace.None,
|
||||
});
|
||||
|
||||
const validServer = withValidation({
|
||||
const validServer = withValidation<undefined, { error?: MatrixError }>({
|
||||
deriveData: async ({ value }) => {
|
||||
try {
|
||||
// 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:
|
||||
// + your currently connected homeserver
|
||||
// + homeservers in config.json["roomDirectory"]
|
||||
// + homeservers in SettingsStore["room_directory_servers"]
|
||||
// if a server exists in multiple, only keep the top-most entry.
|
||||
|
||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
||||
const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, selectedInstanceId }: IProps) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
const _userDefinedServers: string[] = useSettingValue(SETTING_NAME);
|
||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||
|
||||
const handlerFactory = (server, instanceId) => {
|
||||
|
@ -98,7 +134,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
|
||||
const 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
|
||||
useEffect(() => {
|
||||
|
@ -112,7 +148,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
const roomDirectory = config.roomDirectory || {};
|
||||
|
||||
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.
|
||||
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
|
||||
protocolsList.push({
|
||||
instances: [{
|
||||
fields: [],
|
||||
network_id: "",
|
||||
instance_id: 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)) {
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||
title: _t("Are you sure?"),
|
||||
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));
|
||||
|
||||
// the selected server is being removed, reset to our HS
|
||||
if (serverSelected === server) {
|
||||
if (serverSelected) {
|
||||
onOptionChange(hsName, undefined);
|
||||
}
|
||||
};
|
||||
|
@ -223,7 +264,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||
title: _t("Add a new server"),
|
||||
description: _t("Enter the name of a new server you want to explore."),
|
||||
|
@ -284,9 +324,4 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
</div>;
|
||||
};
|
||||
|
||||
NetworkDropdown.propTypes = {
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
protocols: PropTypes.object,
|
||||
};
|
||||
|
||||
export default NetworkDropdown;
|
|
@ -39,7 +39,7 @@ import { makeSpaceParentEvent } from "./utils/space";
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
|
||||
enum Visibility {
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
|
|
@ -2651,6 +2651,8 @@
|
|||
"Unable to look up room ID from server": "Unable to look up room ID from server",
|
||||
"Preview": "Preview",
|
||||
"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… (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>.",
|
||||
|
|
Loading…
Reference in a new issue