Extract room directory results to its own component (#8252)

This commit is contained in:
Germain 2022-04-07 17:07:26 +01:00 committed by GitHub
parent 5fbb25c707
commit 233278546b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 336 deletions

View file

@ -24,17 +24,14 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
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";
@ -45,9 +42,7 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
import { Action } from "../../dispatcher/actions";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
import { PublicRoomTile } from "../views/rooms/PublicRoomTile";
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
@ -249,7 +244,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
private removeFromDirectory(room: IPublicRoomsChunkRoom) {
private removeFromDirectory = (room: IPublicRoomsChunkRoom) => {
const alias = getDisplayAliasForRoom(room);
const name = room.name || alias || _t('Unnamed room');
@ -289,14 +284,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
});
},
});
}
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey) {
ev.preventDefault();
this.removeFromDirectory(room);
}
};
private onOptionChange = (server: string, instanceId?: string) => {
@ -404,21 +391,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
};
private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, false, true);
ev.stopPropagation();
};
private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room);
ev.stopPropagation();
};
private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
this.showRoom(room, null, true);
ev.stopPropagation();
};
private onCreateRoomClick = (ev: ButtonEvent) => {
this.onFinished();
dis.dispatch({
@ -433,7 +405,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
this.showRoom(null, alias, autoJoin);
}
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => {
this.onFinished();
const payload: ViewRoomPayload = {
action: Action.ViewRoom,
@ -477,112 +449,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
payload.room_id = room.room_id;
}
dis.dispatch(payload);
}
private createRoomCells(room: IPublicRoomsChunkRoom) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
const isGuest = client.isGuest();
let previewButton;
let joinOrViewButton;
// Element Web currently does not allow guests to join rooms, so we
// instead show them preview buttons for all rooms. If the room is not
// world readable, a modal will appear asking you to register first. If
// 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>
);
}
if (hasJoinedRoom) {
joinOrViewButton = (
<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>
);
}
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = room.topic || '';
// Additional truncation based on line numbers is done via CSS,
// but to ensure that the DOM is not polluted with a huge string
// we give it a hard limit before rendering.
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
return <div
key={room.room_id}
role="listitem"
className="mx_RoomDirectory_listItem"
>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomAvatar"
>
<BaseAvatar
width={32}
height={32}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl}
/>
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomDescription"
>
<div className="mx_RoomDirectory_name">
{ name }
</div>&nbsp;
<div
className="mx_RoomDirectory_topic"
dangerouslySetInnerHTML={{ __html: topic }}
/>
<div className="mx_RoomDirectory_alias">
{ getDisplayAliasForRoom(room) }
</div>
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomMemberCount"
>
{ room.num_joined_members }
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_preview"
>
{ previewButton }
</div>
<div
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_join"
>
{ joinOrViewButton }
</div>
</div>;
}
};
private stringLooksLikeId(s: string, fieldType: IFieldType) {
let pat = /^#[^\s]+:[^\s]/;
if (fieldType && fieldType.regexp) {
@ -620,7 +487,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
content = <Spinner />;
} else {
const cells = (this.state.publicRooms || [])
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
.map(room =>
<PublicRoomTile
key={room.room_id}
room={room}
showRoom={this.showRoom}
removeFromDirectory={this.removeFromDirectory}
/>,
);
// 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

View file

@ -0,0 +1,179 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useContext, useEffect, useState } from "react";
import { IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client";
import BaseAvatar from "../avatars/BaseAvatar";
import { mediaFromMxc } from "../../../customisations/Media";
import { linkifyAndSanitizeHtml } from "../../../HtmlUtils";
import { getDisplayAliasForRoom } from "../../structures/RoomDirectory";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { _t } from "../../../languageHandler";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
interface IProps {
room: IPublicRoomsChunkRoom;
removeFromDirectory?: (room: IPublicRoomsChunkRoom) => void;
showRoom: (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin?: boolean, shouldPeek?: boolean) => void;
}
export const PublicRoomTile = ({
room,
showRoom,
removeFromDirectory,
}: IProps) => {
const client = useContext(MatrixClientContext);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [name, setName] = useState("");
const [topic, setTopic] = useState("");
const [hasJoinedRoom, setHasJoinedRoom] = useState(false);
const isGuest = client.isGuest();
useEffect(() => {
const clientRoom = client.getRoom(room.room_id);
setHasJoinedRoom(clientRoom?.getMyMembership() === "join");
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
setName(name);
let topic = room.topic || '';
// Additional truncation based on line numbers is done via CSS,
// but to ensure that the DOM is not polluted with a huge string
// we give it a hard limit before rendering.
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
setTopic(topic);
if (room.avatar_url) {
setAvatarUrl(mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32));
}
}, [room, client]);
const onRoomClicked = useCallback((ev: React.MouseEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey) {
ev.preventDefault();
removeFromDirectory?.(room);
}
}, [room, removeFromDirectory]);
const onPreviewClick = useCallback((ev: React.MouseEvent) => {
showRoom(room, null, false, true);
ev.stopPropagation();
}, [room, showRoom]);
const onViewClick = useCallback((ev: React.MouseEvent) => {
showRoom(room);
ev.stopPropagation();
}, [room, showRoom]);
const onJoinClick = useCallback((ev: React.MouseEvent) => {
showRoom(room, null, true);
ev.stopPropagation();
}, [room, showRoom]);
let previewButton;
let joinOrViewButton;
// Element Web currently does not allow guests to join rooms, so we
// instead show them preview buttons for all rooms. If the room is not
// world readable, a modal will appear asking you to register first. If
// it is readable, the preview appears as normal.
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
previewButton = (
<AccessibleButton kind="secondary" onClick={onPreviewClick}>
{ _t("Preview") }
</AccessibleButton>
);
}
if (hasJoinedRoom) {
joinOrViewButton = (
<AccessibleButton kind="secondary" onClick={onViewClick}>
{ _t("View") }
</AccessibleButton>
);
} else if (!isGuest) {
joinOrViewButton = (
<AccessibleButton kind="primary" onClick={onJoinClick}>
{ _t("Join") }
</AccessibleButton>
);
}
return <div
role="listitem"
className="mx_RoomDirectory_listItem"
>
<div
onMouseDown={onRoomClicked}
className="mx_RoomDirectory_roomAvatar"
>
<BaseAvatar
width={32}
height={32}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl}
/>
</div>
<div
onMouseDown={onRoomClicked}
className="mx_RoomDirectory_roomDescription"
>
<div className="mx_RoomDirectory_name">
{ name }
</div>&nbsp;
<div
className="mx_RoomDirectory_topic"
dangerouslySetInnerHTML={{ __html: topic }}
/>
<div className="mx_RoomDirectory_alias">
{ getDisplayAliasForRoom(room) }
</div>
</div>
<div
onMouseDown={onRoomClicked}
className="mx_RoomDirectory_roomMemberCount"
>
{ room.num_joined_members }
</div>
<div
onMouseDown={onRoomClicked}
className="mx_RoomDirectory_preview"
>
{ previewButton }
</div>
<div
onMouseDown={onRoomClicked}
className="mx_RoomDirectory_join"
>
{ joinOrViewButton }
</div>
</div>;
};

View file

@ -1,65 +0,0 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Room } from 'matrix-js-sdk/src/matrix';
import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { _t } from '../../../languageHandler';
import RoomDetailRow from "./RoomDetailRow";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
rooms?: Room[];
className?: string;
}
export default class RoomDetailList extends React.Component<IProps> {
private getRows(): JSX.Element[] {
if (!this.props.rooms) return [];
return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />;
});
}
private onDetailsClick = (ev: React.MouseEvent, room: Room): void => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
room_alias: room.getCanonicalAlias() || (room.getAltAliases() || [])[0],
metricsTrigger: undefined, // Deprecated groups
});
};
public render(): JSX.Element {
const rows = this.getRows();
let rooms;
if (rows.length === 0) {
rooms = <i>{ _t('No rooms to show') }</i>;
} else {
rooms = <table className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
</tbody>
</table>;
}
return <div className={classNames("mx_RoomDetailList", this.props.className)}>
{ rooms }
</div>;
}
}

View file

@ -1,126 +0,0 @@
/*
Copyright 2017-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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { linkifyElement } from '../../../HtmlUtils';
import { mediaFromMxc } from "../../../customisations/Media";
import { getDisplayAliasForAliasSet } from '../../../Rooms';
import BaseAvatar from "../avatars/BaseAvatar";
export function getDisplayAliasForRoom(room) {
return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
}
export const roomShape = PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
});
export default class RoomDetailRow extends React.Component {
static propTypes = {
room: roomShape,
// passes ev, room as args
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
};
constructor(props) {
super(props);
this._topic = createRef();
}
componentDidMount() {
this._linkifyTopic();
}
componentDidUpdate() {
this._linkifyTopic();
}
_linkifyTopic() {
if (this._topic.current) {
linkifyElement(this._topic.current);
}
}
onClick = (ev) => {
ev.preventDefault();
if (this.props.onClick) {
this.props.onClick(ev, this.props.room);
}
};
onTopicClick = (ev) => {
// When clicking a link in the topic, prevent the event being propagated
// to `onClick`.
ev.stopPropagation();
};
render() {
const room = this.props.room;
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
const guestRead = room.worldReadable ? (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
) : <div />;
const guestJoin = room.guestCanJoin ? (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
) : <div />;
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
{ guestRead }&nbsp;
{ guestJoin }
</div>) : <div />;
let avatarUrl = null;
if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24);
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar
width={24}
height={24}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl} />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic" ref={this._topic} onClick={this.onTopicClick}>
{ room.topic }
</div>
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ room.numJoinedMembers }
</td>
</tr>;
}
}

View file

@ -1732,6 +1732,10 @@
"Idle": "Idle",
"Offline": "Offline",
"Unknown": "Unknown",
"Unnamed room": "Unnamed room",
"Preview": "Preview",
"View": "View",
"Join": "Join",
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
"Recently viewed": "Recently viewed",
@ -1739,10 +1743,6 @@
"Room %(name)s": "Room %(name)s",
"Recently visited rooms": "Recently visited rooms",
"No recently visited rooms": "No recently visited rooms",
"No rooms to show": "No rooms to show",
"Unnamed room": "Unnamed room",
"World readable": "World readable",
"Guests can join": "Guests can join",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@ -2211,7 +2211,6 @@
"Application window": "Application window",
"Share content": "Share content",
"Backspace": "Backspace",
"Join": "Join",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"Something went wrong!": "Something went wrong!",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
@ -3021,8 +3020,6 @@
"Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room",
"Fetching third party location failed": "Fetching third party location failed",
"Unable to look up room ID from server": "Unable to look up room ID from server",
"Preview": "Preview",
"View": "View",
"Create new room": "Create new room",
"No results for \"%(query)s\"": "No results for \"%(query)s\"",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.",