Merge branch 'develop' into travis/room-list/enable

This commit is contained in:
Travis Ralston 2020-07-09 22:28:16 -06:00
commit 75dbd5f1d4
67 changed files with 2347 additions and 823 deletions

View file

@ -24,9 +24,9 @@ import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibil
import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import {
ChevronFace,
ContextMenu,
ContextMenuButton,
StyledMenuItemCheckbox,
@ -40,7 +40,13 @@ import NotificationBadge from "./NotificationBadge";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
import StyledCheckbox from "../elements/StyledCheckbox";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads";
import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
@ -55,9 +61,13 @@ import StyledCheckbox from "../elements/StyledCheckbox";
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
export const HEADER_HEIGHT = 32; // As defined by CSS
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
// HACK: We really shouldn't have to do this.
polyfillTouchEvent();
interface IProps {
forRooms: boolean;
rooms?: Room[];
@ -65,11 +75,10 @@ interface IProps {
label: string;
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout?: ListLayout;
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
isFiltered: boolean;
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
// You should feel bad if you use this.
@ -84,21 +93,28 @@ interface IState {
notificationState: ListNotificationState;
contextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>();
private dispatcherRef: string;
private layout: ListLayout;
constructor(props: IProps) {
super(props);
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
this.state = {
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
contextMenuPosition: null,
isResizing: false,
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed
};
this.state.notificationState.setRooms(this.props.rooms);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}
private get numTiles(): number {
@ -106,28 +122,69 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
private get numVisibleTiles(): number {
if (!this.props.layout) return 0;
const nVisible = Math.floor(this.props.layout.visibleTiles);
const nVisible = Math.floor(this.layout.visibleTiles);
return Math.min(nVisible, this.numTiles);
}
public componentDidUpdate() {
public componentDidUpdate(prevProps: Readonly<IProps>) {
this.state.notificationState.setRooms(this.props.rooms);
if (prevProps.isFiltered !== this.props.isFiltered) {
if (this.props.isFiltered) {
this.setState({isExpanded: true});
} else {
this.setState({isExpanded: !this.layout.isCollapsed});
}
}
}
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 roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
if (!this.state.isExpanded && roomIndex > -1) {
this.toggleCollapsed();
}
// extend the visible section to include the room if it is entirely invisible
if (roomIndex >= this.numVisibleTiles) {
this.layout.visibleTiles = this.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();
};
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
const direction = e.movementY < 0 ? -1 : +1;
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles);
private onResize = (
e: MouseEvent | TouchEvent,
travelDirection: Direction,
refToElement: HTMLDivElement,
delta: { width: number, height: number }, // TODO: Use re-resizer's NumberSize when it is exposed as the type
) => {
// Do some sanity checks, but in reality we shouldn't need these.
if (travelDirection !== "bottom") return;
if (delta.height === 0) return; // something went wrong, so just ignore it.
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
// for our purposes. The delta provided by the library is also a change *from when
// resizing started*, meaning it is fairly useless for us. This is why we just use
// the client height and run with it.
const heightBefore = this.layout.visibleTiles;
const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
if (heightBefore === this.layout.visibleTiles) return; // no-op
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -141,13 +198,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private onShowAllClick = () => {
const numVisibleTiles = this.numVisibleTiles;
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
};
private onShowLessClick = () => {
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
this.layout.visibleTiles = this.layout.defaultVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
// focus will flow to the show more button here
};
@ -161,7 +218,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
};
private onOpenMenuClick = (ev: InputEvent) => {
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
@ -195,7 +252,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private onMessagePreviewChanged = () => {
this.props.layout.showPreviews = !this.props.layout.showPreviews;
this.layout.showPreviews = !this.layout.showPreviews;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -233,7 +290,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const possibleSticky = target.parentElement;
const sublist = possibleSticky.parentElement.parentElement;
if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) {
const list = sublist.parentElement.parentElement;
// the scrollTop is capped at the height of the header in LeftPanel2
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
const isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky');
if (isSticky && !isAtTop) {
// is sticky - jump to list
sublist.scrollIntoView({behavior: 'smooth'});
} else {
@ -243,24 +304,23 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private toggleCollapsed = () => {
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
this.layout.isCollapsed = this.state.isExpanded;
this.setState({isExpanded: !this.layout.isCollapsed});
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
if (!isCollapsed) {
if (this.state.isExpanded) {
// On ARROW_LEFT collapse the room sublist if it isn't already
this.toggleCollapsed();
}
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (isCollapsed) {
if (!this.state.isExpanded) {
// On ARROW_RIGHT expand the room sublist if it isn't already
this.toggleCollapsed();
} else if (this.sublistRef.current) {
@ -289,7 +349,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private renderVisibleTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) {
if (!this.state.isExpanded) {
// don't waste time on rendering
return [];
}
@ -303,7 +363,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<RoomTile2
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.props.layout.showPreviews}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
/>
@ -354,7 +414,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews}
checked={this.layout.showPreviews}
>
{_t("Message preview")}
</StyledMenuItemCheckbox>
@ -365,7 +425,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
contextMenu = (
<ContextMenu
chevronFace="none"
chevronFace={ChevronFace.None}
left={this.state.contextMenuPosition.left}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
onFinished={this.onCloseMenu}
@ -446,7 +506,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed,
'mx_RoomSublist2_collapseBtn_collapsed': !this.state.isExpanded,
});
const classes = classNames({
@ -474,7 +534,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
tabIndex={tabIndex}
className="mx_RoomSublist2_headerText"
role="treeitem"
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
aria-expanded={this.state.isExpanded}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
@ -508,12 +568,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
let content = null;
if (visibleTiles.length > 0) {
const layout = this.props.layout; // to shorten calls
const layout = this.layout; // to shorten calls
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true,
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
});
// If we're hiding rooms, show a 'show more' button to the user. This button
@ -537,7 +595,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText}
</RovingAccessibleButton>
);
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
@ -556,9 +614,19 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
// Figure out if we need a handle
let handles = ['s'];
const handles: Enable = {
bottom: true, // the only one we need, but the others must be explicitly false
bottomLeft: false,
bottomRight: false,
left: false,
right: false,
top: false,
topLeft: false,
topRight: false,
};
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
handles = []; // no handles, we're at a minimum
// we're at a minimum, don't have a bottom handle
handles.bottom = false;
}
// We have to account for padding so we can accommodate a 'show more' button and
@ -582,22 +650,33 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
// Now that we know our padding constraints, let's find out if we need to chop off the
// last rendered visible tile so it doesn't collide with the 'show more' button
let visibleUnpaddedTiles = Math.round(layout.visibleTiles - layout.pixelsToTiles(padding));
if (visibleUnpaddedTiles === visibleTiles.length - 1) {
const placeholder = <div className="mx_RoomSublist2_placeholder" key='placeholder' />;
visibleTiles.splice(visibleUnpaddedTiles, 1, placeholder);
}
const dimensions = {
height: tilesPx,
};
content = (
<ResizableBox
width={-1}
height={tilesPx}
axis="y"
minConstraints={[-1, minTilesPx]}
maxConstraints={[-1, maxTilesPx]}
resizeHandles={handles}
onResize={this.onResize}
className="mx_RoomSublist2_resizeBox"
<Resizable
size={dimensions as any}
minHeight={minTilesPx}
maxHeight={maxTilesPx}
onResizeStart={this.onResizeStart}
onResizeStop={this.onResizeStop}
onResize={this.onResize}
handleWrapperClass="mx_RoomSublist2_resizerHandles"
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
className="mx_RoomSublist2_resizeBox"
enable={handles}
>
{visibleTiles}
{showNButton}
</ResizableBox>
</Resizable>
);
}