From 4e93452275ed7ed8224d14a62c8711a85fdc6e6f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 2 Mar 2021 14:02:03 +0000 Subject: [PATCH 1/6] Tweak resizer collapse distributor behaviour to work with the expanding space panel --- src/components/structures/LoggedInView.tsx | 12 +++++++++++- src/resizer/distributors/collapse.ts | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 1694b4bcf5..4e768bd9e5 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -223,7 +223,14 @@ class LoggedInView extends React.Component { let size; let collapsed; const collapseConfig: ICollapseConfig = { - toggleSize: 260 - 50, + // TODO: the space panel currently does not have a fixed width, + // just the headers at each level have a max-width of 150px + // Taking 222px for the space panel for now, + // so this will look slightly off for now, + // depending on the depth of your space tree. + // To fix this, we'll need to turn toggleSize + // into a callback so it can be measured when starting the resize operation + toggleSize: 222 + 68, onCollapsed: (_collapsed) => { collapsed = _collapsed; if (_collapsed) { @@ -244,6 +251,9 @@ class LoggedInView extends React.Component { if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size); this.props.resizeNotifier.stopResizing(); }, + isItemCollapsed: domNode => { + return domNode.classList.contains("mx_LeftPanel_minimized"); + }, }; const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig); resizer.setClassNames({ diff --git a/src/resizer/distributors/collapse.ts b/src/resizer/distributors/collapse.ts index ddf3bd687e..f8db0be52c 100644 --- a/src/resizer/distributors/collapse.ts +++ b/src/resizer/distributors/collapse.ts @@ -22,6 +22,7 @@ import Sizer from "../sizer"; export interface ICollapseConfig extends IConfig { toggleSize: number; onCollapsed?(collapsed: boolean, id: string, element: HTMLElement): void; + isItemCollapsed(element: HTMLElement): boolean; } class CollapseItem extends ResizeItem { @@ -31,6 +32,11 @@ class CollapseItem extends ResizeItem { callback(collapsed, this.id, this.domNode); } } + + get isCollapsed() { + const isItemCollapsed = this.resizer.config.isItemCollapsed; + return isItemCollapsed(this.domNode); + } } export default class CollapseDistributor extends FixedDistributor { @@ -39,11 +45,12 @@ export default class CollapseDistributor extends FixedDistributor Date: Tue, 2 Mar 2021 14:11:38 +0000 Subject: [PATCH 2/6] Initial Space room directory view --- res/css/_components.scss | 1 + res/css/structures/_SpaceRoomDirectory.scss | 231 +++++++ src/components/structures/MatrixChat.tsx | 17 +- .../structures/SpaceRoomDirectory.tsx | 572 ++++++++++++++++++ src/i18n/strings/en_EN.json | 10 + 5 files changed, 827 insertions(+), 4 deletions(-) create mode 100644 res/css/structures/_SpaceRoomDirectory.scss create mode 100644 src/components/structures/SpaceRoomDirectory.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 8569f62de9..daa7016623 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -28,6 +28,7 @@ @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; @import "./structures/_SpacePanel.scss"; +@import "./structures/_SpaceRoomDirectory.scss"; @import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss new file mode 100644 index 0000000000..5cb91820cf --- /dev/null +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -0,0 +1,231 @@ +/* +Copyright 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. +*/ + +.mx_SpaceRoomDirectory_dialogWrapper > .mx_Dialog { + max-width: 960px; + height: 100%; +} + +.mx_SpaceRoomDirectory { + height: 100%; + margin-bottom: 12px; + color: $primary-fg-color; + word-break: break-word; + display: flex; + flex-direction: column; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar { + margin-right: 16px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + > div { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + } + + .mx_Dialog_content { + // TODO fix scrollbar + //display: flex; + //flex-direction: column; + //height: calc(100% - 80px); + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_SearchBox { + margin: 24px 0 28px; + } + + .mx_SpaceRoomDirectory_listHeader { + display: flex; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_FormButton { + margin-bottom: 8px; + } + + > span { + margin: auto 0 0 auto; + } + } + } +} + +.mx_SpaceRoomDirectory_list { + margin-top: 8px; + + .mx_SpaceRoomDirectory_roomCount { + > h3 { + display: inline; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + } + + > span { + margin-left: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_subspace { + margin-top: 8px; + + .mx_SpaceRoomDirectory_subspace_info { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + color: $secondary-fg-color; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + + .mx_BaseAvatar { + margin-right: 12px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + .mx_SpaceRoomDirectory_actions { + text-align: right; + height: min-content; + margin-left: auto; + margin-right: 16px; + } + } + + .mx_SpaceRoomDirectory_subspace_children { + margin-left: 12px; + border-left: 2px solid $space-button-outline-color; + padding-left: 24px; + } + } + + .mx_SpaceRoomDirectory_roomTile { + padding: 16px; + border-radius: 8px; + border: 1px solid $space-button-outline-color; + margin: 8px 0 16px; + display: flex; + min-height: 76px; + box-sizing: border-box; + + &.mx_AccessibleButton:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + .mx_BaseAvatar { + margin-right: 16px; + margin-top: 6px; + } + + .mx_SpaceRoomDirectory_roomTile_info { + display: inline-block; + font-size: $font-15px; + flex-grow: 1; + height: min-content; + margin: auto 0; + + .mx_SpaceRoomDirectory_roomTile_name { + font-weight: $font-semi-bold; + line-height: $font-18px; + } + .mx_SpaceRoomDirectory_roomTile_topic { + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_roomTile_memberCount { + position: relative; + margin: auto 0 auto 24px; + padding: 0 0 0 28px; + line-height: $font-24px; + display: inline-block; + width: 32px; + + &::before { + position: absolute; + content: ''; + width: 24px; + height: 24px; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + background-color: $secondary-fg-color; + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + + .mx_SpaceRoomDirectory_actions { + width: 180px; + text-align: right; + height: min-content; + margin: auto 0 auto 28px; + + .mx_AccessibleButton { + vertical-align: middle; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } + + .mx_SpaceRoomDirectory_actions { + .mx_SpaceRoomDirectory_actionsText { + font-weight: normal; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + + .mx_Checkbox { + display: inline-block; + } + } +} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 83b3565738..1700b627db 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -82,6 +82,8 @@ import {UIFeature} from "../../settings/UIFeature"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; +import SpaceStore from "../../stores/SpaceStore"; +import SpaceRoomDirectory from "./SpaceRoomDirectory"; /** constants for MatrixChat.state.view */ export enum Views { @@ -691,10 +693,17 @@ export default class MatrixChat extends React.PureComponent { break; } case Action.ViewRoomDirectory: { - const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); - Modal.createTrackedDialog('Room directory', '', RoomDirectory, { - initialText: payload.initialText, - }, 'mx_RoomDirectory_dialogWrapper', false, true); + if (SpaceStore.instance.activeSpace) { + Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, { + space: SpaceStore.instance.activeSpace, + initialText: payload.initialText, + }, "mx_SpaceRoomDirectory_dialogWrapper", false, true); + } else { + const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); + Modal.createTrackedDialog('Room directory', '', RoomDirectory, { + initialText: payload.initialText, + }, 'mx_RoomDirectory_dialogWrapper', false, true); + } // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx new file mode 100644 index 0000000000..7f7b9dbb99 --- /dev/null +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -0,0 +1,572 @@ +/* +Copyright 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, {useMemo, useRef, useState} from "react"; +import Room from "matrix-js-sdk/src/models/room"; +import MatrixEvent from "matrix-js-sdk/src/models/event"; +import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; + +import {MatrixClientPeg} from "../../MatrixClientPeg"; +import dis from "../../dispatcher/dispatcher"; +import {_t} from "../../languageHandler"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import BaseDialog from "../views/dialogs/BaseDialog"; +import FormButton from "../views/elements/FormButton"; +import SearchBox from "./SearchBox"; +import RoomAvatar from "../views/avatars/RoomAvatar"; +import RoomName from "../views/elements/RoomName"; +import {useAsyncMemo} from "../../hooks/useAsyncMemo"; +import {shouldShowSpaceSettings} from "../../utils/space"; +import {EnhancedMap} from "../../utils/maps"; +import StyledCheckbox from "../views/elements/StyledCheckbox"; +import AutoHideScrollbar from "./AutoHideScrollbar"; +import BaseAvatar from "../views/avatars/BaseAvatar"; + +interface IProps { + space: Room; + initialText?: string; + onFinished(): void; +} + +/* eslint-disable camelcase */ +export interface ISpaceSummaryRoom { + canonical_alias?: string; + aliases: string[]; + avatar_url?: string; + guest_can_join: boolean; + name?: string; + num_joined_members: number + room_id: string; + topic?: string; + world_readable: boolean; + num_refs: number; + room_type: string; +} + +export interface ISpaceSummaryEvent { + room_id: string; + event_id: string; + origin_server_ts: number; + type: string; + state_key: string; + content: { + order?: string; + auto_join?: boolean; + via?: string; + }; +} +/* eslint-enable camelcase */ + +interface ISubspaceProps { + space: ISpaceSummaryRoom; + event?: MatrixEvent; + editing?: boolean; + onPreviewClick?(): void; + queueAction?(action: IAction): void; + onJoinClick?(): void; +} + +const SubSpace: React.FC = ({ + space, + editing, + event, + queueAction, + onJoinClick, + onPreviewClick, + children, +}) => { + const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); + + const evContent = event?.getContent(); + const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); + const [removed, _setRemoved] = useState(!evContent?.via); + + const cli = MatrixClientPeg.get(); + const cliRoom = cli.getRoom(space.room_id); + const myMembership = cliRoom?.getMyMembership(); + + // TODO DRY code + let actions; + if (editing && queueAction) { + if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { + const setAutoJoin = () => { + _setAutoJoin(v => { + queueAction({ + event, + removed, + autoJoin: !v, + }); + return !v; + }); + }; + + const setRemoved = () => { + _setRemoved(v => { + queueAction({ + event, + removed: !v, + autoJoin, + }); + return !v; + }); + }; + + if (removed) { + actions = + + ; + } else { + actions = + + + ; + } + } else { + actions = + { _t("No permissions")} + ; + } + // TODO confirm remove from space click behaviour here + } else { + if (myMembership === "join") { + actions = + { _t("You're in this space")} + ; + } else if (onJoinClick) { + actions = + + { _t("Preview") } + + + + } + } + + let url: string; + if (space.avatar_url) { + url = MatrixClientPeg.get().mxcUrlToHttp(space.avatar_url, + Math.floor(24 * window.devicePixelRatio), + Math.floor(24 * window.devicePixelRatio), + "crop"); + } + + return
+
+ + { name } + +
+ { actions } +
+
+
+ { children } +
+
+}; + +interface IAction { + event: MatrixEvent; + removed: boolean; + autoJoin: boolean; +} + +interface IRoomTileProps { + room: ISpaceSummaryRoom; + event?: MatrixEvent; + editing?: boolean; + onPreviewClick(): void; + queueAction?(action: IAction): void; + onJoinClick?(): void; +} + +const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => { + const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room"); + + const evContent = event?.getContent(); + const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); + const [removed, _setRemoved] = useState(!evContent?.via); + + const cli = MatrixClientPeg.get(); + const cliRoom = cli.getRoom(room.room_id); + const myMembership = cliRoom?.getMyMembership(); + + let actions; + if (editing && queueAction) { + if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { + const setAutoJoin = () => { + _setAutoJoin(v => { + queueAction({ + event, + removed, + autoJoin: !v, + }); + return !v; + }); + }; + + const setRemoved = () => { + _setRemoved(v => { + queueAction({ + event, + removed: !v, + autoJoin, + }); + return !v; + }); + }; + + if (removed) { + actions = + + ; + } else { + actions = + + + ; + } + } else { + actions = + { _t("No permissions")} + ; + } + // TODO confirm remove from space click behaviour here + } else { + if (myMembership === "join") { + actions = + { _t("You're in this room")} + ; + } else if (onJoinClick) { + actions = + + { _t("Preview") } + + + + } + } + + let url: string; + if (room.avatar_url) { + url = cli.mxcUrlToHttp(room.avatar_url, + Math.floor(32 * window.devicePixelRatio), + Math.floor(32 * window.devicePixelRatio), + "crop"); + } + + const content = + + +
+
+ { name } +
+
+ { room.topic } +
+
+
+ { room.num_joined_members } +
+ +
+ { actions } +
+
; + + if (editing) { + return
+ { content } +
+ } + + return + { content } + ; +}; + +export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { + // Don't let the user view a room they won't be able to either peek or join: + // fail earlier so they don't have to click back to the directory. + if (MatrixClientPeg.get().isGuest()) { + if (!room.world_readable && !room.guest_can_join) { + dis.dispatch({ action: "require_registration" }); + return; + } + } + + const roomAlias = getDisplayAliasForRoom(room) || undefined; + dis.dispatch({ + action: "view_room", + auto_join: autoJoin, + should_peek: true, + _type: "room_directory", // instrumentation + room_alias: roomAlias, + room_id: room.room_id, + via_servers: viaServers, + 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 || roomAlias || _t("Unnamed room"), + }, + }); +}; + +interface IHierarchyLevelProps { + spaceId: string; + rooms: Map; + editing?: boolean; + relations: EnhancedMap; + parents: Set; + queueAction?(action: IAction): void; + onPreviewClick(roomId: string): void; + onRemoveFromSpaceClick?(roomId: string): void; + onJoinClick?(roomId: string): void; +} + +export const HierarchyLevel = ({ + spaceId, + rooms, + editing, + relations, + parents, + onPreviewClick, + onJoinClick, + queueAction, +}: IHierarchyLevelProps) => { + const cli = MatrixClientPeg.get(); + const space = cli.getRoom(spaceId); + // TODO respect order + const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => { + if (!rooms.has(roomId)) return result; // TODO wat + result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); + return result; + }, [[], []]) || [[], []]; + + // Don't render this subspace if it has no rooms we can show + // TODO this is broken - as a space may have subspaces we still need to show + // if (!childRooms.length) return null; + + const userId = cli.getUserId(); + + const newParents = new Set(parents).add(spaceId); + return + { + childRooms.map(roomId => ( + { + onPreviewClick(roomId); + }} + onJoinClick={onJoinClick ? () => { + onJoinClick(roomId); + } : undefined} + /> + )) + } + + { + subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( + { + onPreviewClick(roomId); + }} + onJoinClick={() => { + onJoinClick(roomId); + }} + > + + + )) + } + +}; + +const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { + // TODO pagination + const cli = MatrixClientPeg.get(); + const [query, setQuery] = useState(initialText); + const [isEditing, setIsEditing] = useState(false); + + const onCreateRoomClick = () => { + dis.dispatch({ + action: 'view_create_room', + public: true, + }); + onFinished(); + }; + + // stored within a ref as we don't need to re-render when it changes + const pendingActions = useRef(new Map()); + + let adminButton; + if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test + const onManageButtonClicked = () => { + setIsEditing(true); + }; + + const onSaveButtonClicked = () => { + // TODO setBusy + pendingActions.current.forEach(({event, autoJoin, removed}) => { + const content = { + ...event.getContent(), + auto_join: autoJoin, + }; + + if (removed) { + delete content["via"]; + } + + cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey()); + }); + setIsEditing(false); + }; + + if (isEditing) { + adminButton = + + { _t("All users join by default") } + ; + } else { + adminButton = ; + } + } + + const [rooms, relations, viaMap] = useAsyncMemo(async () => { + try { + const data = await cli.getSpaceSummary(space.roomId); + + const parentChildRelations = new EnhancedMap(); + const viaMap = new EnhancedMap>(); + data.events.map((ev: ISpaceSummaryEvent) => { + if (ev.type === EventType.SpaceChild) { + parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + } + if (Array.isArray(ev.content["via"])) { + const set = viaMap.getOrCreate(ev.state_key, new Set()); + ev.content["via"].forEach(via => set.add(via)); + } + }); + + return [data.rooms, parentChildRelations, viaMap]; + } catch (e) { + console.error(e); // TODO + } + + return []; + }, [space], []); + + const roomsMap = useMemo(() => { + if (!rooms) return null; + const lcQuery = query.toLowerCase(); + + const filteredRooms = rooms.filter(r => { + return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms + || r.name?.toLowerCase().includes(lcQuery) + || r.topic?.toLowerCase().includes(lcQuery); + }); + + return new Map(filteredRooms.map(r => [r.room_id, r])); + // const root = rooms.get(space.roomId); + }, [rooms, query]); + + const title = + +
+

{ _t("Explore rooms") }

+
+
+
; + const explanation = + _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, + {a: sub => { + return {sub}; + }}, + ); + + let content; + if (roomsMap) { + content = + { + pendingActions.current.set(action.event.room_id, action); + }} + onPreviewClick={roomId => { + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false); + onFinished(); + }} + onJoinClick={(roomId) => { + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true); + onFinished(); + }} + /> + ; + } + + // TODO loading state/error state + return ( + +
+ { explanation } + + + +
+ { adminButton } +
+ { content } +
+
+ ); +}; + +export default SpaceRoomDirectory; + +// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom +// but works with the objects we get from the public room list +function getDisplayAliasForRoom(room: ISpaceSummaryRoom) { + return room.canonical_alias || (room.aliases ? room.aliases[0] : ""); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bb800b2af2..04a6d63272 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2577,6 +2577,16 @@ "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", + "Unnamed Space": "Unnamed Space", + "Undo": "Undo", + "Remove from Space": "Remove from Space", + "No permissions": "No permissions", + "You're in this space": "You're in this space", + "You're in this room": "You're in this room", + "Save changes": "Save changes", + "All users join by default": "All users join by default", + "Manage rooms": "Manage rooms", + "Find a room...": "Find a room...", "Accept Invite": "Accept Invite", "Invite people": "Invite people", "Add existing rooms & spaces": "Add existing rooms & spaces", From ca1bd78921e0fb05bbaaae185f3a29402b61a38e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 2 Mar 2021 14:19:40 +0000 Subject: [PATCH 3/6] Add space specific variant of the dropdown on "Rooms +" sublist --- res/css/views/rooms/_RoomList.scss | 5 +- .../element-icons/roomlist/hash-circle.svg | 7 +++ .../element-icons/roomlist/plus-circle.svg | 3 ++ src/components/views/rooms/RoomList.tsx | 47 +++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 res/img/element-icons/roomlist/hash-circle.svg create mode 100644 res/img/element-icons/roomlist/plus-circle.svg diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 66e1b827d0..d49ed4b736 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -19,7 +19,10 @@ limitations under the License. } .mx_RoomList_iconPlus::before { - mask-image: url('$(res)/img/element-icons/roomlist/plus.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg'); +} +.mx_RoomList_iconHash::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); } .mx_RoomList_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); diff --git a/res/img/element-icons/roomlist/hash-circle.svg b/res/img/element-icons/roomlist/hash-circle.svg new file mode 100644 index 0000000000..924b22cf32 --- /dev/null +++ b/res/img/element-icons/roomlist/hash-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/roomlist/plus-circle.svg b/res/img/element-icons/roomlist/plus-circle.svg new file mode 100644 index 0000000000..251ded225c --- /dev/null +++ b/res/img/element-icons/roomlist/plus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 45db15df7c..f7da6571da 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -47,6 +47,9 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; +import SpaceStore from "../../../stores/SpaceStore"; +import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; +import { EventType } from "matrix-js-sdk/src/@types/event"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -152,6 +155,50 @@ const TAG_AESTHETICS: ITagAestheticsMap = { defaultHidden: false, addRoomLabel: _td("Add room"), addRoomContextMenu: (onFinished: () => void) => { + if (SpaceStore.instance.activeSpace) { + const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild, + MatrixClientPeg.get().getUserId()); + + return + { + e.preventDefault(); + e.stopPropagation(); + onFinished(); + showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace); + }} + disabled={!canAddRooms} + tooltip={canAddRooms ? undefined + : _t("You do not have permissions to create new rooms in this space")} + /> + { + e.preventDefault(); + e.stopPropagation(); + onFinished(); + showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace); + }} + disabled={!canAddRooms} + tooltip={canAddRooms ? undefined + : _t("You do not have permissions to add rooms to this space")} + /> + { + e.preventDefault(); + e.stopPropagation(); + onFinished(); + defaultDispatcher.fire(Action.ViewRoomDirectory); + }} + /> + ; + } + return Date: Tue, 2 Mar 2021 14:34:47 +0000 Subject: [PATCH 4/6] Add context menu to spaces in the space panel --- res/css/structures/_SpacePanel.scss | 75 ++++++ src/accessibility/context_menu/MenuItem.tsx | 11 +- src/components/views/dialogs/InfoDialog.js | 6 +- .../views/spaces/SpaceTreeLevel.tsx | 216 ++++++++++++++++++ src/i18n/strings/en_EN.json | 16 +- 5 files changed, 314 insertions(+), 10 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 24d2243912..9937117086 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -212,6 +212,30 @@ $activeBorderColor: $secondary-fg-color; border-radius: 8px; } } + + .mx_SpaceButton_menuButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin-top: auto; + margin-bottom: auto; + position: relative; + display: none; + + &::before { + top: 2px; + left: 2px; + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/context-menu.svg'); + background: $primary-fg-color; + } + } } .mx_SpacePanel_badgeContainer { @@ -254,6 +278,10 @@ $activeBorderColor: $secondary-fg-color; height: 0; display: none; } + + .mx_SpaceButton_menuButton { + display: block; + } } } @@ -272,3 +300,50 @@ $activeBorderColor: $secondary-fg-color; } } } + +.mx_SpacePanel_contextMenu { + .mx_SpacePanel_contextMenu_header { + margin: 12px 16px 12px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + } + + .mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton { + color: $accent-color; + + .mx_SpacePanel_iconInvite::before { + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + .mx_SpacePanel_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpacePanel_iconLeave::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); + } + + .mx_SpacePanel_iconHome::before { + mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); + } + + .mx_SpacePanel_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpacePanel_iconPlus::before { + mask-image: url('$(res)/img/element-icons/plus.svg'); + } + + .mx_SpacePanel_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } +} + + +.mx_SpacePanel_sharePublicSpace { + margin: 0; +} diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 0bb169abf8..9a7c1d1f0a 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -19,14 +19,23 @@ limitations under the License. import React from "react"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; interface IProps extends React.ComponentProps { label?: string; + tooltip?: string; } // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({children, label, ...props}) => { +export const MenuItem: React.FC = ({children, label, tooltip, ...props}) => { const ariaLabel = props["aria-label"] || label; + + if (tooltip) { + return + { children } + ; + } + return ( { children } diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 97ae968ff3..6dc9fc01b0 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component { className: PropTypes.string, title: PropTypes.string, description: PropTypes.node, - button: PropTypes.string, + button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool), onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, onKeyDown: PropTypes.func, @@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
{ this.props.description }
- - + } ); } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f94798433f..04d6c02208 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -23,7 +23,27 @@ import SpaceStore from "../../../stores/SpaceStore"; import NotificationBadge from "../rooms/NotificationBadge"; import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../context_menus/IconizedContextMenu"; +import {_t} from "../../../languageHandler"; +import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import {toRightOf} from "../../structures/ContextMenu"; +import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {ButtonEvent} from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import Modal from "../../../Modal"; +import SpacePublicShare from "./SpacePublicShare"; +import {Action} from "../../../dispatcher/actions"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import {showRoomInviteDialog} from "../../../RoomInvite"; +import InfoDialog from "../dialogs/InfoDialog"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory"; interface IItemProps { space?: Room; @@ -78,6 +98,200 @@ export class SpaceItem extends React.PureComponent { SpaceStore.instance.setActiveSpace(this.props.space); }; + private onMenuOpenClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.target as HTMLButtonElement; + this.setState({contextMenuPosition: target.getBoundingClientRect()}); + }; + + private onMenuClose = () => { + this.setState({contextMenuPosition: null}); + }; + + private onHomeClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: this.props.space.roomId, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.props.space.getJoinRule() === "public") { + const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { + title: _t("Invite members"), + description: + { _t("Share your public space") } + modal.close()} /> + , + fixedWidth: false, + button: false, + className: "mx_SpacePanel_sharePublicSpace", + hasCloseButton: true, + }); + } else { + showRoomInviteDialog(this.props.space.roomId); + } + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(this.context, this.props.space); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "leave_room", + room_id: this.props.space.roomId, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(this.context, this.props.space); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: this.props.space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space: this.props.space }, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, { + space: this.props.space, + }, "mx_SpaceRoomDirectory_dialogWrapper", false, true); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private renderContextMenu(): React.ReactElement { + let contextMenu = null; + if (this.state.contextMenuPosition) { + const userId = this.context.getUserId(); + + let inviteOption; + if (this.props.space.canInvite(userId)) { + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(this.context, this.props.space)) { + settingsOption = ( + + ); + } else { + leaveSection = + + ; + } + + let newRoomOption; + if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + newRoomOption = ( + + ); + } + + contextMenu = +
+ { this.props.space.name } +
+ + { inviteOption } + + + { settingsOption } + + { newRoomOption } + + { leaveSection } +
; + } + + return ( + + + { contextMenu } + + ); + } + render() { const {space, activeSpaces, isNested} = this.props; @@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent {
{ notifBadge } + { this.renderContextMenu() }
); @@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent { { space.name } { notifBadge } + { this.renderContextMenu() } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6d2f41ceae..19324e1540 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1003,6 +1003,16 @@ "Failed to copy": "Failed to copy", "Share invite link": "Share invite link", "Invite by email or username": "Invite by email or username", + "Invite members": "Invite members", + "Share your public space": "Share your public space", + "Invite people": "Invite people", + "Settings": "Settings", + "Leave space": "Leave space", + "New room": "New room", + "Space Home": "Space Home", + "Members": "Members", + "Explore rooms": "Explore rooms", + "Space options": "Space options", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1583,7 +1593,6 @@ "Favourited": "Favourited", "Favourite": "Favourite", "Low Priority": "Low Priority", - "Settings": "Settings", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -1672,7 +1681,6 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", - "Members": "Members", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -2510,13 +2518,11 @@ "Explore Public Rooms": "Explore Public Rooms", "Create a Group Chat": "Create a Group Chat", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", - "Explore rooms": "Explore rooms", "Failed to reject invitation": "Failed to reject invitation", "Cannot create rooms in this community": "Cannot create rooms in this community", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", "This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", - "Leave space": "Leave space", "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", @@ -2592,7 +2598,6 @@ "Manage rooms": "Manage rooms", "Find a room...": "Find a room...", "Accept Invite": "Accept Invite", - "Invite people": "Invite people", "Add existing rooms & spaces": "Add existing rooms & spaces", "%(count)s members|other": "%(count)s members", "%(count)s members|one": "%(count)s member", @@ -2607,7 +2612,6 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "Share your public space": "Share your public space", "At the moment only you can see it.": "At the moment only you can see it.", "Finish": "Finish", "Who are you working with?": "Who are you working with?", From 43cc7deedae82893a96f22493a0e360399f12968 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 2 Mar 2021 14:37:28 +0000 Subject: [PATCH 5/6] Show hierarchy of auto_join rooms in the space view --- res/css/structures/_SpaceRoomView.scss | 8 +++ src/components/structures/SpaceRoomView.tsx | 59 ++++++++++++++++++++- src/i18n/strings/en_EN.json | 2 + 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index ee60389c59..38310d39a9 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -219,6 +219,14 @@ $SpaceRoomViewInnerWidth: 428px; } } } + + .mx_SpaceRoomDirectory_list { + max-width: 600px; + + .mx_SpaceRoomDirectory_roomTile_actions { + display: none; + } + } } .mx_SpaceRoomView_privateScope { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index f1a8a4d71b..5c91efc1c0 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, {RefObject, useContext, useRef, useState} from "react"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -24,6 +24,7 @@ import {_t} from "../../languageHandler"; import AccessibleButton from "../views/elements/AccessibleButton"; import RoomName from "../views/elements/RoomName"; import RoomTopic from "../views/elements/RoomTopic"; +import InlineSpinner from "../views/elements/InlineSpinner"; import FormButton from "../views/elements/FormButton"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {useRoomMembers} from "../../hooks/useRoomMembers"; @@ -47,7 +48,12 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel import {useStateArray} from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; +import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory"; +import {useAsyncMemo} from "../../hooks/useAsyncMemo"; +import {EnhancedMap} from "../../utils/maps"; +import AutoHideScrollbar from "./AutoHideScrollbar"; import MemberAvatar from "../views/avatars/MemberAvatar"; +import {useStateToggle} from "../../hooks/useStateToggle"; interface IProps { space: Room; @@ -121,13 +127,15 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + const [_, forceUpdate] = useStateToggle(false); // TODO + let addRoomButtons; if (canAddRooms) { addRoomButtons = { const [added] = await showAddExistingRooms(cli, space); if (added) { - // TODO update rooms shown once we show hierarchy here + forceUpdate(); } }}> { _t("Add existing rooms & spaces") } @@ -149,6 +157,51 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => ; } + const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => { + try { + const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); + + const parentChildRelations = new EnhancedMap(); + data.events.map((ev: ISpaceSummaryEvent) => { + if (ev.type === EventType.SpaceChild) { + parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + } + }); + + const roomsMap = new Map(data.rooms.map(r => [r.room_id, r])); + const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length; + return [false, roomsMap, parentChildRelations, numRooms]; + } catch (e) { + console.error(e); // TODO + } + + return [false]; + }, [space, _], [true]); + + let previewRooms; + if (roomsMap) { + previewRooms = +
+

{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}

+ { numRooms } +
+ { + showRoom(roomsMap.get(roomId), [], false); // TODO + }} + /> +
; + } else if (loading) { + previewRooms = ; + } else { + previewRooms =

{_t("Your server does not support showing space hierarchies.")}

; + } + return
@@ -213,6 +266,8 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { addRoomButtons } { settingsButton }
+ + { previewRooms }
; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 19324e1540..8609af6a71 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2599,6 +2599,8 @@ "Find a room...": "Find a room...", "Accept Invite": "Accept Invite", "Add existing rooms & spaces": "Add existing rooms & spaces", + "Default Rooms": "Default Rooms", + "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "%(count)s members|other": "%(count)s members", "%(count)s members|one": "%(count)s member", " invited you to ": " invited you to ", From 73411fa53dd9b8a2f3b9300779ee1bfe45c8c0f9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 3 Mar 2021 13:42:44 +0000 Subject: [PATCH 6/6] tidy code style --- src/components/structures/SpaceRoomDirectory.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 7f7b9dbb99..06df6a528e 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -157,10 +157,12 @@ const SubSpace: React.FC = ({ let url: string; if (space.avatar_url) { - url = MatrixClientPeg.get().mxcUrlToHttp(space.avatar_url, + url = MatrixClientPeg.get().mxcUrlToHttp( + space.avatar_url, Math.floor(24 * window.devicePixelRatio), Math.floor(24 * window.devicePixelRatio), - "crop"); + "crop", + ); } return
@@ -262,10 +264,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli let url: string; if (room.avatar_url) { - url = cli.mxcUrlToHttp(room.avatar_url, + url = cli.mxcUrlToHttp( + room.avatar_url, Math.floor(32 * window.devicePixelRatio), Math.floor(32 * window.devicePixelRatio), - "crop"); + "crop", + ); } const content =