diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 50d376a66f..7a5d69fc8a 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -21,6 +21,10 @@ limitations under the License. margin-bottom: 4px; padding: 4px; + // allow scrollIntoView to ignore the sticky headers, must match combined height of .mx_RoomSublist2_headerContainer + scroll-margin-top: 32px; + scroll-margin-bottom: 32px; + // The tile is also a flexbox row itself display: flex; @@ -165,6 +169,11 @@ limitations under the License. } } +// do not apply scroll-margin-bottom to the sublist which will not have a sticky header below it +.mx_RoomSublist2:last-child .mx_RoomTile2 { + scroll-margin-bottom: 0; +} + // We use these both in context menus and the room tiles .mx_RoomTile2_iconBell::before { mask-image: url('$(res)/img/feather-customised/bell.svg'); diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index bb61769f60..a45631553b 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -42,6 +42,8 @@ import { ListNotificationState } from "../../../stores/notifications/ListNotific import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { Key } from "../../../Keyboard"; import StyledCheckbox from "../elements/StyledCheckbox"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {ActionPayload} from "../../../dispatcher/payloads"; // 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 @@ -90,6 +92,7 @@ interface IState { export default class RoomSublist2 extends React.Component<IProps, IState> { private headerButton = createRef<HTMLDivElement>(); private sublistRef = createRef<HTMLDivElement>(); + private dispatcherRef: string; constructor(props: IProps) { super(props); @@ -100,6 +103,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { isResizing: false, }; this.state.notificationState.setRooms(this.props.rooms); + this.dispatcherRef = defaultDispatcher.register(this.onAction); } private get numTiles(): number { @@ -118,8 +122,29 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { public componentWillUnmount() { this.state.notificationState.destroy(); + defaultDispatcher.unregister(this.dispatcherRef); } + private onAction = (payload: ActionPayload) => { + if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) { + // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change + // where we lose the room we are changing from temporarily and then it comes back in an update right after. + setImmediate(() => { + const isCollapsed = this.props.layout.isCollapsed; + const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id); + + if (isCollapsed && roomIndex > -1) { + this.toggleCollapsed(); + } + // extend the visible section to include the room if it is entirely invisible + if (roomIndex >= this.numVisibleTiles) { + this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT); + this.forceUpdate(); // because the layout doesn't trigger a re-render + } + }); + } + }; + private onAddRoom = (e) => { e.stopPropagation(); if (this.props.onAddRoom) this.props.onAddRoom(); diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 35167e3cf5..bba8ab15ba 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {createRef} from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; @@ -51,6 +51,8 @@ import { INotificationState } from "../../../stores/notifications/INotificationS import NotificationBadge from "./NotificationBadge"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { Volume } from "../../../RoomNotifsTypes"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {ActionPayload} from "../../../dispatcher/payloads"; // 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 @@ -119,6 +121,10 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam }; export default class RoomTile2 extends React.Component<IProps, IState> { + private dispatcherRef: string; + private roomTileRef = createRef<HTMLDivElement>(); + // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180 + constructor(props: IProps) { super(props); @@ -131,6 +137,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { }; ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); + this.dispatcherRef = defaultDispatcher.register(this.onAction); } private get showContextMenu(): boolean { @@ -141,12 +148,36 @@ export default class RoomTile2 extends React.Component<IProps, IState> { return !this.props.isMinimized && this.props.showMessagePreview; } + public componentDidMount() { + // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active + if (this.state.selected) { + this.scrollIntoView(); + } + } + public componentWillUnmount() { if (this.props.room) { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); } + defaultDispatcher.unregister(this.dispatcherRef); } + private onAction = (payload: ActionPayload) => { + if (payload.action === "view_room" && payload.room_id === this.props.room.roomId && payload.show_room_tile) { + setImmediate(() => { + this.scrollIntoView(); + }); + } + }; + + private scrollIntoView = () => { + if (!this.roomTileRef.current) return; + this.roomTileRef.current.scrollIntoView({ + block: "nearest", + behavior: "auto", + }); + }; + private onTileMouseEnter = () => { this.setState({hover: true}); }; @@ -160,7 +191,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> { ev.stopPropagation(); dis.dispatch({ action: 'view_room', - // TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233 show_room_tile: true, // make sure the room is visible in the list room_id: this.props.room.roomId, clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), @@ -478,7 +508,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { return ( <React.Fragment> - <RovingTabIndexWrapper> + <RovingTabIndexWrapper inputRef={this.roomTileRef}> {({onFocus, isActive, ref}) => <AccessibleButton onFocus={onFocus}