From 6a5efad1424c7b4accbfb4f83ec0f37b728c5f0c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 8 Mar 2021 15:52:21 +0000 Subject: [PATCH] Show suggested rooms from the selected space --- src/components/views/rooms/RoomList.tsx | 74 +++++++++++++++++++- src/components/views/rooms/TemporaryTile.tsx | 19 ++--- src/i18n/strings/en_EN.json | 2 + src/stores/SpaceStore.tsx | 33 ++++++++- src/stores/room-list/models.ts | 2 + 5 files changed, 116 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index f7da6571da..beb85e50ce 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -19,6 +19,7 @@ limitations under the License. import * as React from "react"; import { Dispatcher } from "flux"; import { Room } from "matrix-js-sdk/src/models/room"; +import * as fbEmitter from "fbemitter"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; @@ -47,9 +48,11 @@ 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 SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore"; import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; +import RoomAvatar from "../avatars/RoomAvatar"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -63,6 +66,8 @@ interface IProps { interface IState { sublists: ITagMap; isNameFiltering: boolean; + currentRoomId?: string; + suggestedRooms: ISpaceSummaryRoom[]; } const TAG_ORDER: TagID[] = [ @@ -75,6 +80,7 @@ const TAG_ORDER: TagID[] = [ DefaultTagID.LowPriority, DefaultTagID.ServerNotice, + DefaultTagID.Suggested, DefaultTagID.Archived, ]; const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority; @@ -242,6 +248,12 @@ const TAG_AESTHETICS: ITagAestheticsMap = { isInvite: false, defaultHidden: true, }, + + [DefaultTagID.Suggested]: { + sectionLabel: _td("Suggested Rooms"), + isInvite: false, + defaultHidden: false, + }, }; function customTagAesthetics(tagId: TagID): ITagAesthetics { @@ -260,6 +272,7 @@ export default class RoomList extends React.PureComponent { private dispatcherRef; private customTagStoreRef; private tagAesthetics: ITagAestheticsMap; + private roomStoreToken: fbEmitter.EventSubscription; constructor(props: IProps) { super(props); @@ -267,6 +280,7 @@ export default class RoomList extends React.PureComponent { this.state = { sublists: {}, isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), + suggestedRooms: SpaceStore.instance.suggestedRooms, }; // shallow-copy from the template as we need to make modifications to it @@ -274,20 +288,30 @@ export default class RoomList extends React.PureComponent { this.updateDmAddRoomAction(); this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } public componentDidMount(): void { + SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); this.updateLists(); // trigger the first update } public componentWillUnmount() { + SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); defaultDispatcher.unregister(this.dispatcherRef); if (this.customTagStoreRef) this.customTagStoreRef.remove(); + if (this.roomStoreToken) this.roomStoreToken.remove(); } + private onRoomViewStoreUpdate = () => { + this.setState({ + currentRoomId: RoomViewStore.getRoomId(), + }); + }; + private updateDmAddRoomAction() { const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]); if (CallHandler.sharedInstance().getSupportsPstnProtocol()) { @@ -319,7 +343,7 @@ export default class RoomList extends React.PureComponent { private getRoomDelta = (roomId: string, delta: number, unread = false) => { const lists = RoomListStore.instance.orderedLists; - const rooms: Room = []; + const rooms: Room[] = []; TAG_ORDER.forEach(t => { let listRooms = lists[t]; @@ -340,6 +364,10 @@ export default class RoomList extends React.PureComponent { return room; }; + private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => { + this.setState({ suggestedRooms }); + }; + private updateLists = () => { const newLists = RoomListStore.instance.orderedLists; if (SettingsStore.getValue("advancedRoomListLogging")) { @@ -394,6 +422,39 @@ export default class RoomList extends React.PureComponent { dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); }; + private renderSuggestedRooms(): JSX.Element[] { + return this.state.suggestedRooms.map(room => { + const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); + const avatar = ( + + ); + const viewRoom = () => { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.room_id, + }); + }; + return ( + + ); + }); + } + private renderCommunityInvites(): TemporaryTile[] { // TODO: Put community invites in a more sensible place (not in the room list) // See https://github.com/vector-im/element-web/issues/14456 @@ -447,7 +508,14 @@ export default class RoomList extends React.PureComponent { for (const orderedTagId of tagOrder) { const orderedRooms = this.state.sublists[orderedTagId] || []; - const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null; + + let extraTiles = null; + if (orderedTagId === DefaultTagID.Invite) { + extraTiles = this.renderCommunityInvites(); + } else if (orderedTagId === DefaultTagID.Suggested) { + extraTiles = this.renderSuggestedRooms(); + } + const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0); if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) { continue; // skip tag - not needed diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx index eec3105880..31d2acbc61 100644 --- a/src/components/views/rooms/TemporaryTile.tsx +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -28,7 +28,7 @@ interface IProps { isSelected: boolean; displayName: string; avatar: React.ReactElement; - notificationState: NotificationState; + notificationState?: NotificationState; onClick: () => void; } @@ -63,12 +63,15 @@ export default class TemporaryTile extends React.Component { 'mx_RoomTile_minimized': this.props.isMinimized, }); - const badge = ( - - ); + let badge; + if (this.props.notificationState) { + badge = ( + + ); + } let name = this.props.displayName; if (typeof name !== 'string') name = ''; @@ -76,7 +79,7 @@ export default class TemporaryTile extends React.Component { const nameClasses = classNames({ "mx_RoomTile_name": true, - "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread, + "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread, }); let nameContainer = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7b680b6590..71aae7fecd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1529,7 +1529,9 @@ "Low priority": "Low priority", "System Alerts": "System Alerts", "Historical": "Historical", + "Suggested Rooms": "Suggested Rooms", "Custom Tag": "Custom Tag", + "Empty room": "Empty room", "Can't see what you’re looking for?": "Can't see what you’re looking for?", "Start a new chat": "Start a new chat", "Explore all public rooms": "Explore all public rooms", diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c334144d70..1ada5d6361 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {throttle, sortBy} from "lodash"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import {sortBy, throttle} from "lodash"; +import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; @@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; import {objectDiff} from "../utils/objects"; import {arrayHasDiff} from "../utils/arrays"; +import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; type SpaceKey = string | symbol; @@ -41,11 +42,14 @@ interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; export const HOME_SPACE = Symbol("home-space"); +export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change +const MAX_SUGGESTED_ROOMS = 20; + const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { result[room.isSpaceRoom() ? 0 : 1].push(room); @@ -85,6 +89,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private spaceFilteredRooms = new Map>(); // The space currently selected in the Space Panel - if null then `Home` is selected private _activeSpace?: Room = null; + private _suggestedRooms: ISpaceSummaryRoom[] = []; public get spacePanelSpaces(): Room[] { return this.rootSpaces; @@ -94,11 +99,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._activeSpace || null; } - public setActiveSpace(space: Room | null) { + public get suggestedRooms(): ISpaceSummaryRoom[] { + return this._suggestedRooms; + } + + public async setActiveSpace(space: Room | null) { if (space === this.activeSpace) return; this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); + this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); // persist space selected if (space) { @@ -106,6 +116,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } else { window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY); } + + if (space) { + try { + const data: { + rooms: ISpaceSummaryRoom[]; + events: ISpaceSummaryEvent[]; + } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, MAX_SUGGESTED_ROOMS); + if (this._activeSpace === space) { + this._suggestedRooms = data.rooms.filter(roomInfo => { + return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); + }); + this.emit(SUGGESTED_ROOMS, this._suggestedRooms); + } + } catch (e) { + console.error(e); + } + } } public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 7d3902f552..54d49ea18a 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -24,6 +24,7 @@ export enum DefaultTagID { Favourite = "m.favourite", DM = "im.vector.fake.direct", ServerNotice = "m.server_notice", + Suggested = "im.vector.fake.suggested", } export const OrderedDefaultTagIDs = [ @@ -33,6 +34,7 @@ export const OrderedDefaultTagIDs = [ DefaultTagID.Untagged, DefaultTagID.LowPriority, DefaultTagID.ServerNotice, + DefaultTagID.Suggested, DefaultTagID.Archived, ];