Show suggested rooms from the selected space
							parent
							
								
									ab4220b20d
								
							
						
					
					
						commit
						6a5efad142
					
				|  | @ -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<IProps, IState> { | |||
|     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<IProps, IState> { | |||
|         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<IProps, IState> { | |||
|         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<IProps, IState> { | |||
| 
 | ||||
|     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<IProps, IState> { | |||
|         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<IProps, IState> { | |||
|         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 = ( | ||||
|                 <RoomAvatar | ||||
|                     oobData={{ | ||||
|                         name, | ||||
|                         avatarUrl: room.avatar_url, | ||||
|                     }} | ||||
|                     width={32} | ||||
|                     height={32} | ||||
|                     resizeMethod="crop" | ||||
|                 /> | ||||
|             ); | ||||
|             const viewRoom = () => { | ||||
|                 defaultDispatcher.dispatch({ | ||||
|                     action: "view_room", | ||||
|                     room_id: room.room_id, | ||||
|                 }); | ||||
|             }; | ||||
|             return ( | ||||
|                 <TemporaryTile | ||||
|                     isMinimized={this.props.isMinimized} | ||||
|                     isSelected={this.state.currentRoomId === room.room_id} | ||||
|                     displayName={name} | ||||
|                     avatar={avatar} | ||||
|                     onClick={viewRoom} | ||||
|                     key={`suggestedRoomTile_${room.room_id}`} | ||||
|                 /> | ||||
|             ); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     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<IProps, IState> { | |||
| 
 | ||||
|         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
 | ||||
|  |  | |||
|  | @ -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<IProps, IState> { | |||
|             'mx_RoomTile_minimized': this.props.isMinimized, | ||||
|         }); | ||||
| 
 | ||||
|         const badge = ( | ||||
|             <NotificationBadge | ||||
|                 notification={this.props.notificationState} | ||||
|                 forceCount={false} | ||||
|             /> | ||||
|         ); | ||||
|         let badge; | ||||
|         if (this.props.notificationState) { | ||||
|             badge = ( | ||||
|                 <NotificationBadge | ||||
|                     notification={this.props.notificationState} | ||||
|                     forceCount={false} | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         let name = this.props.displayName; | ||||
|         if (typeof name !== 'string') name = ''; | ||||
|  | @ -76,7 +79,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         const nameClasses = classNames({ | ||||
|             "mx_RoomTile_name": true, | ||||
|             "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread, | ||||
|             "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread, | ||||
|         }); | ||||
| 
 | ||||
|         let nameContainer = ( | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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<IState> { | |||
|     private spaceFilteredRooms = new Map<string | symbol, Set<string>>(); | ||||
|     // 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<IState> { | |||
|         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<IState> { | |||
|         } 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) { | ||||
|  |  | |||
|  | @ -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, | ||||
| ]; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski