Merge branch 'develop' into element
This commit is contained in:
@@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
|
||||
@@ -64,7 +64,6 @@ export default function AccessibleButton({
|
||||
className,
|
||||
...restProps
|
||||
}: IProps) {
|
||||
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
if (!disabled) {
|
||||
newProps.onClick = onClick;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
@@ -284,6 +285,9 @@ export default createReactClass({
|
||||
_getTransition: function(e) {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return 'invite_withdrawal';
|
||||
}
|
||||
return 'invited';
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,16 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { Dispatcher } from "flux";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist2 from "./RoomSublist2";
|
||||
@@ -35,6 +38,9 @@ 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";
|
||||
|
||||
// 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
|
||||
@@ -64,8 +70,6 @@ interface IState {
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
// -- Community Invites Placeholder --
|
||||
|
||||
DefaultTagID.Invite,
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
@@ -77,7 +81,6 @@ const TAG_ORDER: TagID[] = [
|
||||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
@@ -141,6 +144,7 @@ const TAG_AESTHETICS: {
|
||||
|
||||
export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
private dispatcherRef;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -149,6 +153,8 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
sublists: {},
|
||||
layouts: new Map<TagID, ListLayout>(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
@@ -173,8 +179,50 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
|
||||
public componentWillUnmount() {
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoomDelta) {
|
||||
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
||||
const currentRoomId = RoomViewStore.getRoomId();
|
||||
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
|
||||
if (room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
||||
const lists = RoomListStore.instance.orderedLists;
|
||||
let rooms: Room = [];
|
||||
TAG_ORDER.forEach(t => {
|
||||
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;
|
||||
});
|
||||
notificationStates.forEach(state => state.destroy());
|
||||
}
|
||||
|
||||
rooms.push(...listRooms);
|
||||
});
|
||||
|
||||
const currentIndex = rooms.findIndex(r => r.roomId === roomId);
|
||||
// use slice to account for looping around the start
|
||||
const [room] = rooms.slice((currentIndex + delta) % rooms.length);
|
||||
return room;
|
||||
};
|
||||
|
||||
private updateLists = () => {
|
||||
const newLists = RoomListStore.instance.orderedLists;
|
||||
console.log("new lists", newLists);
|
||||
@@ -227,17 +275,15 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
const components: React.ReactElement[] = [];
|
||||
|
||||
for (const orderedTagId of TAG_ORDER) {
|
||||
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate community invites if we have the chance
|
||||
// TODO: Community invites: https://github.com/vector-im/riot-web/issues/14179
|
||||
}
|
||||
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate custom tags if needed
|
||||
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
|
||||
}
|
||||
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
||||
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
continue; // skip tag - not needed
|
||||
}
|
||||
|
||||
@@ -245,7 +291,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
components.push(
|
||||
<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
@@ -280,9 +325,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
className="mx_RoomList2"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
>{sublists}</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
|
||||
@@ -20,24 +20,30 @@ import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||
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 { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuButton,
|
||||
StyledMenuItemCheckbox,
|
||||
StyledMenuItemRadio,
|
||||
} from "../../structures/ContextMenu";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
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
|
||||
@@ -52,6 +58,7 @@ import { Key } from "../../../Keyboard";
|
||||
|
||||
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;
|
||||
|
||||
@@ -63,7 +70,7 @@ interface IProps {
|
||||
onAddRoom?: () => void;
|
||||
addRoomLabel: string;
|
||||
isInvite: boolean;
|
||||
layout: ListLayout;
|
||||
layout?: ListLayout;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
@@ -86,6 +93,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);
|
||||
@@ -96,6 +104,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 {
|
||||
@@ -114,8 +123,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();
|
||||
@@ -137,16 +167,28 @@ 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.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.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
// focus will flow to the show more button here
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: InputEvent) => {
|
||||
private focusRoomTile = (index: number) => {
|
||||
if (!this.sublistRef.current) return;
|
||||
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile2");
|
||||
const element = elements && elements[index];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
@@ -204,6 +246,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -217,7 +260,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 {
|
||||
@@ -280,10 +327,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
}
|
||||
|
||||
if (this.props.rooms) {
|
||||
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||
for (const room of visibleRooms) {
|
||||
@@ -299,6 +342,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
}
|
||||
|
||||
// We only have to do this because of the extra tiles. We do it conditionally
|
||||
// to avoid spending cycles on slicing. It's generally fine to do this though
|
||||
// as users are unlikely to have more than a handful of tiles when the extra
|
||||
@@ -311,18 +358,45 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderMenu(): React.ReactElement {
|
||||
// TODO: Get a proper invite context menu, or take invites out of the room list.
|
||||
if (this.props.tagId === DefaultTagID.Invite) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
let otherSections = null;
|
||||
if (this.props.tagId !== DefaultTagID.Invite) {
|
||||
otherSections = (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
chevronFace={ChevronFace.None}
|
||||
left={this.state.contextMenuPosition.left}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
@@ -330,41 +404,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
<div className="mx_RoomSublist2_contextMenu">
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
||||
<StyledRadioButton
|
||||
<StyledMenuItemRadio
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||
checked={!isAlphabetical}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("Activity")}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
</StyledMenuItemRadio>
|
||||
<StyledMenuItemRadio
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
||||
checked={isAlphabetical}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("A-Z")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<StyledCheckbox
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledCheckbox
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledCheckbox>
|
||||
</StyledMenuItemRadio>
|
||||
</div>
|
||||
{otherSections}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
@@ -385,16 +442,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
|
||||
private renderHeader(): React.ReactElement {
|
||||
return (
|
||||
<RovingTabIndexWrapper>
|
||||
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||
{({onFocus, isActive, ref}) => {
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
let ariaLabel = _t("Jump to first unread room.");
|
||||
if (this.props.tagId === DefaultTagID.Invite) {
|
||||
ariaLabel = _t("Jump to first invite.");
|
||||
}
|
||||
|
||||
const badge = (
|
||||
<NotificationBadge
|
||||
forceCount={true}
|
||||
notification={this.state.notificationState}
|
||||
onClick={this.onBadgeClick}
|
||||
tabIndex={tabIndex}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -428,14 +491,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO: a11y (see old component): https://github.com/vector-im/riot-web/issues/14180
|
||||
// Note: the addRoomButton conditionally gets moved around
|
||||
// the DOM depending on whether or not the list is minimized.
|
||||
// If we're minimized, we want it below the header so it
|
||||
// doesn't become sticky.
|
||||
// The same applies to the notification badge.
|
||||
return (
|
||||
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
|
||||
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
|
||||
<div className="mx_RoomSublist2_stickable">
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
@@ -443,6 +505,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-level={1}
|
||||
onClick={this.onHeaderClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
@@ -498,12 +561,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
);
|
||||
if (this.props.isMinimized) showMoreText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
||||
<RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{showMoreText}
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
||||
// we have all tiles visible - add a button to show less
|
||||
@@ -514,12 +577,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
);
|
||||
if (this.props.isMinimized) showLessText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
||||
<RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{showLessText}
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -26,17 +26,33 @@ import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
MenuItemCheckbox,
|
||||
MenuItem,
|
||||
} from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||
import {
|
||||
getRoomNotifsState,
|
||||
setRoomNotifsState,
|
||||
ALL_MESSAGES,
|
||||
ALL_MESSAGES_LOUD,
|
||||
MENTIONS_ONLY,
|
||||
MUTE,
|
||||
} from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { setRoomNotifsState } from "../../../RoomNotifs";
|
||||
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 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
|
||||
@@ -68,11 +84,13 @@ interface IState {
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
|
||||
|
||||
const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||
// align the context menu's icons with the icon which opened the context menu
|
||||
const left = elementRect.left + window.pageXOffset - 9;
|
||||
const top = elementRect.bottom + window.pageYOffset + 17;
|
||||
const chevronFace = "none";
|
||||
const chevronFace = ChevronFace.None;
|
||||
return {left, top, chevronFace};
|
||||
};
|
||||
|
||||
@@ -103,6 +121,8 @@ 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) {
|
||||
@@ -117,18 +137,47 @@ 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 {
|
||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||
}
|
||||
|
||||
private get showMessagePreview(): boolean {
|
||||
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});
|
||||
};
|
||||
@@ -142,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)),
|
||||
@@ -153,7 +201,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
this.setState({selected: isActive});
|
||||
};
|
||||
|
||||
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
|
||||
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
@@ -164,7 +212,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
this.setState({notificationsMenuPosition: null});
|
||||
};
|
||||
|
||||
private onGeneralMenuOpenClick = (ev: InputEvent) => {
|
||||
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
@@ -195,6 +243,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
|
||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
||||
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
||||
|
||||
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
}
|
||||
};
|
||||
|
||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
||||
@@ -219,11 +272,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
};
|
||||
|
||||
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
|
||||
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// get key before we go async and React discards the nativeEvent
|
||||
const key = (ev as React.KeyboardEvent).key;
|
||||
try {
|
||||
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
||||
await setRoomNotifsState(this.props.room.roomId, newState);
|
||||
@@ -233,7 +288,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.setState({notificationsMenuPosition: null}); // Close the context menu
|
||||
if (key === Key.ENTER) {
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||
}
|
||||
}
|
||||
|
||||
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
||||
@@ -322,20 +380,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
||||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
||||
<div className="mx_IconizedContextMenu_optionList">
|
||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
|
||||
<MenuItemCheckbox
|
||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
|
||||
label={_t("Favourite")}
|
||||
>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
||||
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
||||
</MenuItemCheckbox>
|
||||
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
|
||||
<AccessibleButton onClick={this.onLeaveRoomClick}>
|
||||
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
|
||||
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
@@ -357,7 +419,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Invites: https://github.com/vector-im/riot-web/issues/14198
|
||||
// TODO: a11y proper: https://github.com/vector-im/riot-web/issues/14180
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile2': true,
|
||||
@@ -375,8 +436,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
|
||||
let badge: React.ReactNode;
|
||||
if (!this.props.isMinimized) {
|
||||
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||
badge = (
|
||||
<div className="mx_RoomTile2_badgeContainer">
|
||||
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
|
||||
<NotificationBadge
|
||||
notification={this.state.notificationState}
|
||||
forceCount={false}
|
||||
@@ -392,24 +454,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let messagePreview = null;
|
||||
if (this.props.showMessagePreview && !this.props.isMinimized) {
|
||||
if (this.showMessagePreview) {
|
||||
// The preview store heavily caches this info, so should be safe to hammer.
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
|
||||
// Only show the preview if there is one to show.
|
||||
if (text) {
|
||||
messagePreview = (
|
||||
<div className="mx_RoomTile2_messagePreview">
|
||||
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const notificationColor = this.state.notificationState.color;
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile2_name": true,
|
||||
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
@@ -422,9 +485,30 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
);
|
||||
if (this.props.isMinimized) nameContainer = null;
|
||||
|
||||
let ariaLabel = name;
|
||||
// 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) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||
count: this.state.notificationState.count,
|
||||
});
|
||||
} else if (notificationColor >= NotificationColor.Grey) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||
count: this.state.notificationState.count,
|
||||
});
|
||||
} else if (notificationColor >= NotificationColor.Bold) {
|
||||
ariaLabel += " " + _t("Unread messages.");
|
||||
}
|
||||
|
||||
let ariaDescribedBy: string;
|
||||
if (this.showMessagePreview) {
|
||||
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RovingTabIndexWrapper>
|
||||
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
@@ -434,8 +518,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
onMouseEnter={this.onTileMouseEnter}
|
||||
onMouseLeave={this.onTileMouseLeave}
|
||||
onClick={this.onTileClick}
|
||||
role="treeitem"
|
||||
onContextMenu={this.onContextMenu}
|
||||
role="treeitem"
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
>
|
||||
{roomAvatar}
|
||||
{nameContainer}
|
||||
|
||||
Reference in New Issue
Block a user