Integrate searching public rooms and people into the new search experience (#8707)

* Implement searching for public rooms and users in new search experience
* Implement loading indicator for spotlight results
* Moved spotlight dialog into own subfolder
* Extract search result avatar into separate component
* Build generic new dropdown menu component
* Build new network menu based on new network dropdown component
* Switch roomdirectory to use new network dropdown
* Replace old networkdropdown with new networkdropdown
* Added component for public room result details
* Extract hooks and subcomponents from SpotlightDialog
* Create new hook to get profile info based for an mxid
* Add hook to automatically re-request search results
* Add hook to prevent out-of-order search results
* Extract member sort algorithm from InviteDialog
* Keep sorting for non-room results stable
* Sort people suggestions using sort algorithm from InviteDialog
* Add copy/copied tooltip for invite link option in spotlight
* Clamp length of topic for public room results
* Add unit test for useDebouncedSearch
* Add unit test for useProfileInfo
* Create cypress test cases for spotlight dialog
* Add test for useLatestResult to prevent out-of-order results
This commit is contained in:
Janne Mareike Koschinski 2022-06-15 16:14:05 +02:00 committed by GitHub
parent 37298d7b1b
commit 5096e7b992
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 3520 additions and 1397 deletions

View file

@ -27,9 +27,9 @@ import Modal from "../../Modal";
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId, ALL_ROOMS, Protocols } from '../../utils/DirectoryUtils';
import NetworkDropdown from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import { IPublicRoomDirectoryConfig, NetworkDropdown } from "../views/directory/NetworkDropdown";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import QuestionDialog from "../views/dialogs/QuestionDialog";
@ -54,16 +54,15 @@ interface IState {
publicRooms: IPublicRoomsChunkRoom[];
loading: boolean;
protocolsLoading: boolean;
error?: string;
instanceId: string;
roomServer: string;
error?: string | null;
serverConfig: IPublicRoomDirectoryConfig | null;
filterString: string;
}
export default class RoomDirectory extends React.Component<IProps, IState> {
private unmounted = false;
private nextBatch: string = null;
private filterTimeout: number;
private nextBatch: string | null = null;
private filterTimeout: number | null;
private protocols: Protocols;
constructor(props) {
@ -77,10 +76,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
const myHomeserver = MatrixClientPeg.getHomeserverName();
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY);
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY);
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY) ?? undefined;
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY) ?? undefined;
let roomServer = myHomeserver;
let roomServer: string | undefined = myHomeserver;
if (
SdkConfig.getObject("room_directory")?.get("servers")?.includes(lsRoomServer) ||
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
@ -88,7 +87,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
roomServer = lsRoomServer;
}
let instanceId: string = null;
let instanceId: string | undefined = undefined;
if (roomServer === myHomeserver && (
lsInstanceId === ALL_ROOMS ||
Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId))
@ -97,11 +96,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
// Refresh the room list only if validation failed and we had to change these
if (this.state.instanceId !== instanceId || this.state.roomServer !== roomServer) {
if (this.state.serverConfig?.instanceId !== instanceId ||
this.state.serverConfig?.roomServer !== roomServer) {
this.setState({
protocolsLoading: false,
instanceId,
roomServer,
serverConfig: roomServer ? { instanceId, roomServer } : null,
});
this.refreshRoomList();
return;
@ -127,12 +126,20 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
}
let serverConfig: IPublicRoomDirectoryConfig | null = null;
const roomServer = localStorage.getItem(LAST_SERVER_KEY);
if (roomServer) {
serverConfig = {
roomServer,
instanceId: localStorage.getItem(LAST_INSTANCE_KEY) ?? undefined,
};
}
this.state = {
publicRooms: [],
loading: true,
error: null,
instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
roomServer: localStorage.getItem(LAST_SERVER_KEY),
serverConfig,
filterString: this.props.initialText || "",
protocolsLoading,
};
@ -166,7 +173,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
const filterString = this.state.filterString;
const roomServer = this.state.roomServer;
const roomServer = this.state.serverConfig?.roomServer;
// remember the next batch token when we sent the request
// too. If it's changed, appending to the list will corrupt it.
const nextBatch = this.nextBatch;
@ -174,17 +181,17 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
if (roomServer != MatrixClientPeg.getHomeserverName()) {
opts.server = roomServer;
}
if (this.state.instanceId === ALL_ROOMS) {
if (this.state.serverConfig?.instanceId === ALL_ROOMS) {
opts.include_all_networks = true;
} else if (this.state.instanceId) {
opts.third_party_instance_id = this.state.instanceId as string;
} else if (this.state.serverConfig?.instanceId) {
opts.third_party_instance_id = this.state.serverConfig?.instanceId as string;
}
if (this.nextBatch) opts.since = this.nextBatch;
if (filterString) opts.filter = { generic_search_term: filterString };
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
if (
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
roomServer != this.state.serverConfig?.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
@ -197,7 +204,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
return false;
}
this.nextBatch = data.next_batch;
this.nextBatch = data.next_batch ?? null;
this.setState((s) => ({
...s,
publicRooms: [...s.publicRooms, ...(data.chunk || [])],
@ -207,7 +214,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}, (err) => {
if (
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
roomServer != this.state.serverConfig?.roomServer ||
nextBatch != this.nextBatch) {
// as above: we don't care about errors for old requests either
return false;
@ -227,6 +234,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')
),
});
return false;
});
}
@ -279,7 +287,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
};
private onOptionChange = (server: string, instanceId?: string) => {
private onOptionChange = (serverConfig: IPublicRoomDirectoryConfig) => {
// clear next batch so we don't try to load more rooms
this.nextBatch = null;
this.setState({
@ -287,8 +295,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// spend time filtering lots of rooms when we're about to
// to clear the list anyway.
publicRooms: [],
roomServer: server,
instanceId: instanceId,
serverConfig,
error: null,
}, this.refreshRoomList);
// We also refresh the room list each time even though this
@ -299,9 +306,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// Easiest to just blow away the state & re-fetch.
// We have to be careful here so that we don't set instanceId = "undefined"
localStorage.setItem(LAST_SERVER_KEY, server);
if (instanceId) {
localStorage.setItem(LAST_INSTANCE_KEY, instanceId);
localStorage.setItem(LAST_SERVER_KEY, serverConfig.roomServer);
if (serverConfig.instanceId) {
localStorage.setItem(LAST_INSTANCE_KEY, serverConfig.instanceId);
} else {
localStorage.removeItem(LAST_INSTANCE_KEY);
}
@ -346,8 +353,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
const cli = MatrixClientPeg.get();
try {
joinRoomByAlias(cli, alias, {
instanceId: this.state.instanceId,
roomServer: this.state.roomServer,
instanceId: this.state.serverConfig?.instanceId,
roomServer: this.state.serverConfig?.roomServer,
protocols: this.protocols,
metricsTrigger: "RoomDirectory",
});
@ -380,7 +387,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
roomAlias,
autoJoin,
shouldPeek,
roomServer: this.state.roomServer,
roomServer: this.state.serverConfig?.roomServer,
metricsTrigger: "RoomDirectory",
});
};
@ -465,7 +472,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
let listHeader;
if (!this.state.protocolsLoading) {
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
const protocolName = protocolNameForInstanceId(this.protocols, this.state.serverConfig?.instanceId);
let instanceExpectedFieldType;
if (
protocolName &&
@ -479,9 +486,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
let placeholder = _t('Find a room…');
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
if (!this.state.serverConfig?.instanceId || this.state.serverConfig?.instanceId === ALL_ROOMS) {
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
exampleRoom: "#example:" + this.state.roomServer,
exampleRoom: "#example:" + this.state.serverConfig?.roomServer,
});
} else if (instanceExpectedFieldType) {
placeholder = instanceExpectedFieldType.placeholder;
@ -489,8 +496,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType);
if (protocolName) {
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
if (getFieldsForThirdPartyLocation(
const instance = instanceForInstanceId(this.protocols, this.state.serverConfig?.instanceId);
if (!instance || getFieldsForThirdPartyLocation(
this.state.filterString,
this.protocols[protocolName],
instance,
@ -511,14 +518,13 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
/>
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
config={this.state.serverConfig}
setConfig={this.onOptionChange}
/>
</div>;
}
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>.", {},
{ a: sub => (
<AccessibleButton kind="link_inline" onClick={this.onCreateRoomClick}>
{ sub }