Merge branch 'develop' into element

This commit is contained in:
Bruno Windels 2020-07-09 17:59:56 +02:00
commit d90fc57469
29 changed files with 532 additions and 275 deletions

View file

@ -21,8 +21,8 @@ import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import RoomTileIcon from "../rooms/RoomTileIcon";
import NotificationBadge from '../rooms/NotificationBadge';
import { INotificationState } from "../../../stores/notifications/INotificationState";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
interface IProps {
room: Room;
@ -33,7 +33,7 @@ interface IProps {
}
interface IState {
notificationState?: INotificationState;
notificationState?: NotificationState;
}
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
@ -42,7 +42,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
super(props);
this.state = {
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
};
}

View file

@ -16,13 +16,18 @@ limitations under the License.
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames';
export default (props) => {
const className = classNames({
'mx_JumpToBottomButton': true,
'mx_JumpToBottomButton_highlight': props.highlight,
});
let badge;
if (props.numUnreadMessages) {
badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
}
return (<div className="mx_JumpToBottomButton">
return (<div className={className}>
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
title={_t("Scroll to most recent messages")}
onClick={props.onScrollToBottomClick}>

View file

@ -22,11 +22,10 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
interface IProps {
notification: INotificationState;
notification: NotificationState;
/**
* If true, the badge will show a count if at all possible. This is typically
@ -97,19 +96,17 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
const {notification, forceCount, roomId, onClick, ...props} = this.props;
// Don't show a badge if we don't need to
if (notification.color <= NotificationColor.None) return null;
if (notification.isIdle) return null;
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
// See git diff for what that boolean state looks like.
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
const hasNotif = notification.color >= NotificationColor.Red;
const hasCount = notification.color >= NotificationColor.Grey;
const hasAnySymbol = notification.symbol || notification.count > 0;
let isEmptyBadge = !hasAnySymbol || !hasCount;
let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount;
if (forceCount) {
isEmptyBadge = false;
if (!hasCount) return null; // Can't render a badge
if (!notification.hasUnreadCount) return null; // Can't render a badge
}
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
@ -117,8 +114,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
const classes = classNames({
'mx_NotificationBadge': true,
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount,
'mx_NotificationBadge_highlighted': hasNotif,
'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount,
'mx_NotificationBadge_highlighted': notification.hasMentions,
'mx_NotificationBadge_dot': isEmptyBadge,
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
'mx_NotificationBadge_3char': symbol.length > 2,

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import AccessibleButton from "../elements/AccessibleButton";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room";
@ -92,9 +91,6 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
};
public render(): React.ReactElement {
// TODO: Decorate crumbs with icons: https://github.com/vector-im/riot-web/issues/14040
// 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];

View file

@ -32,15 +32,14 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
import TemporaryTile from "./TemporaryTile";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
// 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
@ -66,7 +65,6 @@ interface IProps {
interface IState {
sublists: ITagMap;
layouts: Map<TagID, ListLayout>;
}
const TAG_ORDER: TagID[] = [
@ -151,7 +149,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
this.state = {
sublists: {},
layouts: new Map<TagID, ListLayout>(),
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
@ -204,14 +201,11 @@ export default class RoomList2 extends React.Component<IProps, IState> {
let listRooms = lists[t];
if (unread) {
// TODO Be smarter and not spin up a bunch of wasted listeners just to kill them 4 lines later
// https://github.com/vector-im/riot-web/issues/14035
const notificationStates = rooms.map(r => new TagSpecificNotificationState(r, t));
// filter to only notification rooms (and our current active room so we can index properly)
listRooms = notificationStates.filter(state => {
return state.room.roomId === roomId || state.color >= NotificationColor.Bold;
listRooms = listRooms.filter(r => {
const state = RoomNotificationStateStore.instance.getRoomState(r, t);
return state.room.roomId === roomId || state.isUnread;
});
notificationStates.forEach(state => state.destroy());
}
rooms.push(...listRooms);
@ -227,12 +221,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
const newLists = RoomListStore.instance.orderedLists;
console.log("new lists", newLists);
const layoutMap = new Map<TagID, ListLayout>();
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
this.setState({sublists: newLists, layouts: layoutMap}, () => {
this.setState({sublists: newLists}, () => {
this.props.onResize();
});
};
@ -301,8 +290,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}

View file

@ -45,6 +45,8 @@ 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/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@ -73,8 +75,6 @@ interface IProps {
label: string;
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout?: ListLayout;
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
@ -98,12 +98,15 @@ 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,
};
@ -116,8 +119,7 @@ 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);
}
@ -135,7 +137,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// 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 isCollapsed = this.layout.isCollapsed;
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
if (isCollapsed && roomIndex > -1) {
@ -143,7 +145,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
// 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.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
}
});
@ -170,10 +172,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// 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.props.layout.visibleTiles;
const heightInTiles = this.props.layout.pixelsToTiles(refToElement.clientHeight);
this.props.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
if (heightBefore === this.props.layout.visibleTiles) return; // no-op
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
};
@ -187,13 +189,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
};
@ -241,7 +243,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
};
@ -293,13 +295,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private toggleCollapsed = () => {
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.layout.isCollapsed = !this.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
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;
const isCollapsed = this.layout && this.layout.isCollapsed;
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
@ -339,7 +341,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private renderVisibleTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) {
if (this.layout && this.layout.isCollapsed) {
// don't waste time on rendering
return [];
}
@ -353,7 +355,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}
/>
@ -404,7 +406,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>
@ -496,7 +498,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.layout && this.layout.isCollapsed,
});
const classes = classNames({
@ -524,7 +526,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.layout.isCollapsed}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
@ -558,12 +560,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
@ -587,7 +587,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'>
@ -642,6 +642,14 @@ 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,
};

View file

@ -46,15 +46,14 @@ import {
MUTE,
} from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import RoomListActions from "../../../actions/RoomListActions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
// 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
@ -80,7 +79,7 @@ type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
interface IState {
hover: boolean;
notificationState: INotificationState;
notificationState: NotificationState;
selected: boolean;
notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect;
@ -132,7 +131,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.state = {
hover: false,
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
@ -492,11 +491,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
}
}
const notificationColor = this.state.notificationState.color;
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.isUnread,
});
let nameContainer = (
@ -513,15 +511,15 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (this.props.tag === DefaultTagID.Invite) {
// append nothing
} else if (notificationColor >= NotificationColor.Red) {
} else if (this.state.notificationState.hasMentions) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Grey) {
} else if (this.state.notificationState.hasUnreadCount) {
ariaLabel += " " + _t("%(count)s unread messages.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Bold) {
} else if (this.state.notificationState.isUnread) {
ariaLabel += " " + _t("Unread messages.");
}

View file

@ -18,16 +18,15 @@ import React from "react";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { NotificationState } from "../../../stores/notifications/NotificationState";
interface IProps {
isMinimized: boolean;
isSelected: boolean;
displayName: string;
avatar: React.ReactElement;
notificationState: INotificationState;
notificationState: NotificationState;
onClick: () => void;
}
@ -74,7 +73,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.isUnread,
});
let nameContainer = (

View file

@ -402,6 +402,12 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
useCheckbox={true}
disabled={this.state.useIRCLayout}
/>
<SettingsFlag
name="useIRCLayout"
level={SettingLevel.DEVICE}
useCheckbox={true}
onChange={(checked) => this.setState({useIRCLayout: checked})}
/>
<SettingsFlag
name="useSystemFont"
level={SettingLevel.DEVICE}
@ -440,7 +446,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</div>
{this.renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
{this.renderAdvancedSection()}
</div>
);