diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 2a07c3f81b5..5577bc29e70 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -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 { * 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 { }); }, }); - } - - 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 { } }; - 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 { 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 { 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 = ( - this.onPreviewClick(ev, room)}> - { _t("Preview") } - - ); - } - if (hasJoinedRoom) { - joinOrViewButton = ( - this.onViewClick(ev, room)}> - { _t("View") } - - ); - } else if (!isGuest) { - joinOrViewButton = ( - this.onJoinClick(ev, room)}> - { _t("Join") } - - ); - } - - 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
-
this.onRoomClicked(room, ev)} - className="mx_RoomDirectory_roomAvatar" - > - -
-
this.onRoomClicked(room, ev)} - className="mx_RoomDirectory_roomDescription" - > -
- { name } -
  -
-
- { getDisplayAliasForRoom(room) } -
-
-
this.onRoomClicked(room, ev)} - className="mx_RoomDirectory_roomMemberCount" - > - { room.num_joined_members } -
-
this.onRoomClicked(room, ev)} - className="mx_RoomDirectory_preview" - > - { previewButton } -
-
this.onRoomClicked(room, ev)} - className="mx_RoomDirectory_join" - > - { joinOrViewButton } -
-
; - } - + }; 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 { content = ; } else { const cells = (this.state.publicRooms || []) - .reduce((cells, room) => cells.concat(this.createRoomCells(room)), []); + .map(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 diff --git a/src/components/views/rooms/PublicRoomTile.tsx b/src/components/views/rooms/PublicRoomTile.tsx new file mode 100644 index 00000000000..a0f3b89fae2 --- /dev/null +++ b/src/components/views/rooms/PublicRoomTile.tsx @@ -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(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 = ( + + { _t("Preview") } + + ); + } + if (hasJoinedRoom) { + joinOrViewButton = ( + + { _t("View") } + + ); + } else if (!isGuest) { + joinOrViewButton = ( + + { _t("Join") } + + ); + } + + return
+
+ +
+
+
+ { name } +
  +
+
+ { getDisplayAliasForRoom(room) } +
+
+
+ { room.num_joined_members } +
+
+ { previewButton } +
+
+ { joinOrViewButton } +
+
; +}; diff --git a/src/components/views/rooms/RoomDetailList.tsx b/src/components/views/rooms/RoomDetailList.tsx deleted file mode 100644 index eb909d16592..00000000000 --- a/src/components/views/rooms/RoomDetailList.tsx +++ /dev/null @@ -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 { - private getRows(): JSX.Element[] { - if (!this.props.rooms) return []; - return this.props.rooms.map((room, index) => { - return ; - }); - } - - private onDetailsClick = (ev: React.MouseEvent, room: Room): void => { - dis.dispatch({ - 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 = { _t('No rooms to show') }; - } else { - rooms = - - { this.getRows() } - -
; - } - return
- { rooms } -
; - } -} diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js deleted file mode 100644 index 47071adf850..00000000000 --- a/src/components/views/rooms/RoomDetailRow.js +++ /dev/null @@ -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 ? ( -
{ _t('World readable') }
- ) :
; - const guestJoin = room.guestCanJoin ? ( -
{ _t('Guests can join') }
- ) :
; - - const perms = (guestRead || guestJoin) ? (
- { guestRead }  - { guestJoin } -
) :
; - - let avatarUrl = null; - if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24); - - return - - - - -
{ name }
  - { perms } -
- { room.topic } -
-
{ getDisplayAliasForRoom(room) }
- - - { room.numJoinedMembers } - - ; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ee8cbedd6ec..445c90d6c00 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue 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.",