Merge pull request #4862 from matrix-org/joriks/room-list-breadcrumbs
Implement breadcrumb notifications and scrollingpull/21833/head
						commit
						e98f6ca656
					
				|  | @ -49,6 +49,7 @@ | |||
| @import "./views/auth/_ServerTypeSelector.scss"; | ||||
| @import "./views/auth/_Welcome.scss"; | ||||
| @import "./views/avatars/_BaseAvatar.scss"; | ||||
| @import "./views/avatars/_DecoratedRoomAvatar.scss"; | ||||
| @import "./views/avatars/_MemberStatusMessageAvatar.scss"; | ||||
| @import "./views/context_menus/_MessageContextMenu.scss"; | ||||
| @import "./views/context_menus/_RoomTileContextMenu.scss"; | ||||
|  |  | |||
|  | @ -70,7 +70,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations | |||
| 
 | ||||
|             .mx_LeftPanel2_breadcrumbsContainer { | ||||
|                 width: 100%; | ||||
|                 overflow: hidden; | ||||
|                 overflow-y: hidden; | ||||
|                 overflow-x: scroll; | ||||
|                 margin-top: 8px; | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -0,0 +1,33 @@ | |||
| /* | ||||
| Copyright 2020 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_DecoratedRoomAvatar { | ||||
|     position: relative; | ||||
| 
 | ||||
|     .mx_RoomTileIcon { | ||||
|         position: absolute; | ||||
|         bottom: 0; | ||||
|         right: 0; | ||||
|     } | ||||
| 
 | ||||
|     .mx_NotificationBadge { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         height: 18px; | ||||
|         width: 18px; | ||||
|     } | ||||
| } | ||||
|  | @ -29,15 +29,8 @@ limitations under the License. | |||
|         border-radius: 32px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomTile2_avatarContainer { | ||||
|     .mx_DecoratedRoomAvatar { | ||||
|         margin-right: 8px; | ||||
|         position: relative; | ||||
| 
 | ||||
|         .mx_RoomTileIcon { | ||||
|             position: absolute; | ||||
|             bottom: 0; | ||||
|             right: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_RoomTile2_nameContainer { | ||||
|  | @ -155,16 +148,9 @@ limitations under the License. | |||
|         align-items: center; | ||||
|         position: relative; | ||||
| 
 | ||||
|         .mx_RoomTile2_avatarContainer { | ||||
|         .mx_DecoratedRoomAvatar { | ||||
|             margin-right: 0; | ||||
|         } | ||||
| 
 | ||||
|         .mx_RoomTile2_badgeContainer { | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             right: 0; | ||||
|             height: 18px; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; | |||
| import { UPDATE_EVENT } from "../../stores/AsyncStore"; | ||||
| import ResizeNotifier from "../../utils/ResizeNotifier"; | ||||
| import SettingsStore from "../../settings/SettingsStore"; | ||||
| import RoomListStore, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; | ||||
| 
 | ||||
| // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
 | ||||
| // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
 | ||||
|  | @ -69,6 +70,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|         }; | ||||
| 
 | ||||
|         BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); | ||||
|         RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); | ||||
|         this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { | ||||
|             this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); | ||||
|         }); | ||||
|  | @ -81,6 +83,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|     public componentWillUnmount() { | ||||
|         SettingsStore.unwatchSetting(this.tagPanelWatcherRef); | ||||
|         BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); | ||||
|         RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); | ||||
|         this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); | ||||
|     } | ||||
| 
 | ||||
|  | @ -151,7 +154,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { | |||
|         let breadcrumbs; | ||||
|         if (this.state.showBreadcrumbs) { | ||||
|             breadcrumbs = ( | ||||
|                 <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer"> | ||||
|                 <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"> | ||||
|                     {this.props.isMinimized ? null : <RoomBreadcrumbs2 />} | ||||
|                 </div> | ||||
|             ); | ||||
|  |  | |||
|  | @ -0,0 +1,63 @@ | |||
| /* | ||||
| Copyright 2020 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 from 'react'; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| 
 | ||||
| import { TagID } from '../../../stores/room-list/models'; | ||||
| import RoomAvatar from "./RoomAvatar"; | ||||
| import RoomTileIcon from "../rooms/RoomTileIcon"; | ||||
| import NotificationBadge, { INotificationState, TagSpecificNotificationState } from '../rooms/NotificationBadge'; | ||||
| 
 | ||||
| interface IProps { | ||||
|     room: Room; | ||||
|     avatarSize: number; | ||||
|     tag: TagID; | ||||
|     displayBadge?: boolean; | ||||
|     forceCount?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     notificationState?: INotificationState; | ||||
| } | ||||
| 
 | ||||
| export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> { | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public render(): React.ReactNode { | ||||
|         let badge: React.ReactNode; | ||||
|         if (this.props.displayBadge) { | ||||
|             badge = <NotificationBadge | ||||
|                 notification={this.state.notificationState} | ||||
|                 forceCount={this.props.forceCount} | ||||
|                 roomId={this.props.room.roomId} | ||||
|             />; | ||||
|         } | ||||
| 
 | ||||
|         return <div className="mx_DecoratedRoomAvatar"> | ||||
|             <RoomAvatar room={this.props.room} width={this.props.avatarSize} height={this.props.avatarSize} /> | ||||
|             <RoomTileIcon room={this.props.room} tag={this.props.tag} /> | ||||
|             {badge} | ||||
|         </div>; | ||||
|     } | ||||
| } | ||||
|  | @ -17,13 +17,15 @@ limitations under the License. | |||
| import React from "react"; | ||||
| import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; | ||||
| import AccessibleButton from "../elements/AccessibleButton"; | ||||
| import RoomAvatar from "../avatars/RoomAvatar"; | ||||
| import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; | ||||
| import { _t } from "../../../languageHandler"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import defaultDispatcher from "../../../dispatcher/dispatcher"; | ||||
| import Analytics from "../../../Analytics"; | ||||
| import { UPDATE_EVENT } from "../../../stores/AsyncStore"; | ||||
| import { CSSTransition } from "react-transition-group"; | ||||
| import RoomListStore from "../../../stores/room-list/RoomListStore2"; | ||||
| import { DefaultTagID } from "../../../stores/room-list/models"; | ||||
| 
 | ||||
| // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
 | ||||
| // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
 | ||||
|  | @ -93,6 +95,8 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState | |||
|         // TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
 | ||||
|         // TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
 | ||||
|         const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { | ||||
|             const roomTags = RoomListStore.instance.getTagsForRoom(r); | ||||
|             const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; | ||||
|             return ( | ||||
|                 <AccessibleButton | ||||
|                     className="mx_RoomBreadcrumbs2_crumb" | ||||
|  | @ -100,7 +104,13 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState | |||
|                     onClick={() => this.viewRoom(r, i)} | ||||
|                     aria-label={_t("Room %(name)s", {name: r.name})} | ||||
|                 > | ||||
|                     <RoomAvatar room={r} width={32} height={32}/> | ||||
|                     <DecoratedRoomAvatar | ||||
|                         room={r} | ||||
|                         avatarSize={32} | ||||
|                         tag={roomTag} | ||||
|                         displayBadge={true} | ||||
|                         forceCount={true} | ||||
|                     /> | ||||
|                 </AccessibleButton> | ||||
|             ); | ||||
|         }); | ||||
|  |  | |||
|  | @ -22,7 +22,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; | |||
| import classNames from "classnames"; | ||||
| import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; | ||||
| import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; | ||||
| import RoomAvatar from "../../views/avatars/RoomAvatar"; | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import { Key } from "../../../Keyboard"; | ||||
| import ActiveRoomObserver from "../../../ActiveRoomObserver"; | ||||
|  | @ -35,6 +34,7 @@ import { _t } from "../../../languageHandler"; | |||
| import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu"; | ||||
| import { DefaultTagID, TagID } from "../../../stores/room-list/models"; | ||||
| import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; | ||||
| import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; | ||||
| import RoomTileIcon from "./RoomTileIcon"; | ||||
| import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
|  | @ -361,13 +361,21 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             'mx_RoomTile2_minimized': this.props.isMinimized, | ||||
|         }); | ||||
| 
 | ||||
|         const badge = ( | ||||
|             <NotificationBadge | ||||
|         const roomAvatar = <DecoratedRoomAvatar | ||||
|             room={this.props.room} | ||||
|             avatarSize={32} | ||||
|             tag={this.props.tag} | ||||
|             displayBadge={this.props.isMinimized} | ||||
|         />; | ||||
| 
 | ||||
|         let badge: React.ReactNode; | ||||
|         if (!this.props.isMinimized) { | ||||
|         badge = <NotificationBadge | ||||
|                 notification={this.state.notificationState} | ||||
|                 forceCount={false} | ||||
|                 roomId={this.props.room.roomId} | ||||
|             /> | ||||
|         ); | ||||
|             />; | ||||
|         } | ||||
| 
 | ||||
|         // TODO: the original RoomTile uses state for the room name. Do we need to?
 | ||||
|         let name = this.props.room.name; | ||||
|  | @ -405,7 +413,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         ); | ||||
|         if (this.props.isMinimized) nameContainer = null; | ||||
| 
 | ||||
|         const avatarSize = 32; | ||||
|         return ( | ||||
|             <React.Fragment> | ||||
|                 <RovingTabIndexWrapper> | ||||
|  | @ -421,10 +428,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|                             role="treeitem" | ||||
|                             onContextMenu={this.onContextMenu} | ||||
|                         > | ||||
|                             <div className="mx_RoomTile2_avatarContainer"> | ||||
|                                 <RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} /> | ||||
|                                 <RoomTileIcon room={this.props.room} tag={this.props.tag} /> | ||||
|                             </div> | ||||
|                             {roomAvatar} | ||||
|                             {nameContainer} | ||||
|                             <div className="mx_RoomTile2_badgeContainer"> | ||||
|                                 {badge} | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> { | |||
|     } | ||||
| 
 | ||||
|     public get visible(): boolean { | ||||
|         return this.state.enabled; | ||||
|         return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20; | ||||
|     } | ||||
| 
 | ||||
|     protected async onAction(payload: ActionPayload) { | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ limitations under the License. | |||
| 
 | ||||
| import { MatrixClient } from "matrix-js-sdk/src/client"; | ||||
| import SettingsStore from "../../settings/SettingsStore"; | ||||
| import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; | ||||
| import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; | ||||
| import TagOrderStore from "../TagOrderStore"; | ||||
| import { AsyncStore } from "../AsyncStore"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
|  | @ -186,7 +186,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|             const room = this.matrixClient.getRoom(roomId); | ||||
|             const tryUpdate = async (updatedRoom: Room) => { | ||||
|                 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
 | ||||
|                 console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`); | ||||
|                 console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + | ||||
|                     ` in ${updatedRoom.roomId}`); | ||||
|                 if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { | ||||
|                     // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
 | ||||
|                     console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); | ||||
|  | @ -427,6 +428,19 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the tags for a room identified by the store. The returned set | ||||
|      * should never be empty, and will contain DefaultTagID.Untagged if | ||||
|      * the store is not aware of any tags. | ||||
|      * @param room The room to get the tags for. | ||||
|      * @returns The tags for the room. | ||||
|      */ | ||||
|     public getTagsForRoom(room: Room): TagID[] { | ||||
|         const algorithmTags = this.algorithm.getTagsForRoom(room); | ||||
|         if (!algorithmTags) return [DefaultTagID.Untagged]; | ||||
|         return algorithmTags; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class RoomListStore { | ||||
|  |  | |||
|  | @ -524,7 +524,7 @@ export class Algorithm extends EventEmitter { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private getTagsForRoom(room: Room): TagID[] { | ||||
|     public getTagsForRoom(room: Room): TagID[] { | ||||
|         // XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
 | ||||
|         // different use case and therefore different performance curve
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston