Merge branches 'develop' and 't3chguy/room-list/6' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/6

 Conflicts:
	src/components/views/rooms/RoomSublist2.tsx
	src/components/views/rooms/RoomTile2.tsx
This commit is contained in:
Michael Telatynski 2020-07-03 00:02:37 +01:00
commit 9cf162e267
20 changed files with 118 additions and 92 deletions

View File

@ -55,7 +55,7 @@ limitations under the License.
border-radius: 4px;
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
background-color: $menu-bg-color;
z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs
z-index: 6000; // Higher than context menu so tooltips can be used everywhere
padding: 10px;
pointer-events: none;
line-height: $font-14px;

View File

@ -51,3 +51,18 @@ limitations under the License.
height: 32px;
}
}
.mx_RoomBreadcrumbs2_Tooltip {
margin-left: -42px;
margin-top: -42px;
&.mx_Tooltip {
background-color: $tagpanel-bg-color;
color: $accent-fg-color;
border: 0;
.mx_Tooltip_chevron {
display: none;
}
}
}

View File

@ -390,3 +390,7 @@ limitations under the License.
margin-top: 8px;
}
}
.mx_RoomSublist2_addRoomTooltip {
margin-top: -3px;
}

View File

@ -140,6 +140,13 @@ export class ContextMenu extends React.Component {
e.stopPropagation();
};
// Prevent clicks on the background from going through to the component which opened the menu.
_onFinished = (ev: InputEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (this.props.onFinished) this.props.onFinished();
};
_onMoveFocus = (element, up) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
@ -326,7 +333,7 @@ export class ContextMenu extends React.Component {
let background;
if (hasBackground) {
background = (
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={this._onFinished} onContextMenu={this.onContextMenu} />
);
}

View File

@ -37,6 +37,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
import classNames from "classnames";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
interface IProps {
isMinimized: boolean;
@ -230,7 +231,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
{MatrixClientPeg.get().getUserId()}
</span>
</div>
<div
<AccessibleTooltipButton
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
@ -240,7 +241,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
alt={_t("Switch theme")}
width={16}
/>
</div>
</AccessibleTooltipButton>
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">

View File

@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
interface IProps extends React.InputHTMLAttributes<Element> {
export interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>;
element?: string;
// The kind of button, similar to how Bootstrap works.

View File

@ -16,21 +16,28 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import * as sdk from "../../../index";
import {IProps} from "./AccessibleButton";
import Tooltip from './Tooltip';
export default class AccessibleTooltipButton extends React.PureComponent {
static propTypes = {
...AccessibleButton.propTypes,
// The tooltip to render on hover
title: PropTypes.string.isRequired,
};
interface ITooltipProps extends IProps {
title: string;
tooltipClassName?: string;
}
state = {
hover: false,
};
interface IState {
hover: boolean;
}
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({
@ -45,14 +52,15 @@ export default class AccessibleTooltipButton extends React.PureComponent {
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {title, children, ...props} = this.props;
const tooltipClassName = classnames(
"mx_AccessibleTooltipButton_tooltip",
this.props.tooltipClassName,
);
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
tooltipClassName={tooltipClassName}
label={title}
/> : <div />;
return (

View File

@ -26,6 +26,7 @@ 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";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// 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
@ -98,11 +99,13 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return (
<AccessibleButton
<AccessibleTooltipButton
className="mx_RoomBreadcrumbs2_crumb"
key={r.roomId}
onClick={() => this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})}
title={r.name}
tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"}
>
<DecoratedRoomAvatar
room={r}
@ -111,7 +114,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
displayBadge={true}
forceCount={true}
/>
</AccessibleButton>
</AccessibleTooltipButton>
);
});

View File

@ -166,19 +166,26 @@ export default class RoomList2 extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
const newLists = store.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});
});
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.updateLists(); // trigger the first update
}
public componentWillUnmount() {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
}
private updateLists = () => {
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});
};
private renderCommunityInvites(): React.ReactElement[] {
// TODO: Put community invites in a more sensible place (not in the room list)
return MatrixClientPeg.get().getGroups().filter(g => {

View File

@ -35,6 +35,8 @@ 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";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
@ -397,11 +399,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
let addRoomButton = null;
if (!!this.props.onAddRoom) {
addRoomButton = (
<AccessibleButton
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist2_auxButton"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"}
/>
);
}

View File

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from "react";
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@ -30,7 +30,6 @@ import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/
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";
import { setRoomNotifsState } from "../../../RoomNotifs";
@ -120,6 +119,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
private get showContextMenu(): boolean {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@ -169,6 +172,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
};
private onContextMenu = (ev: React.MouseEvent) => {
// If we don't have a context menu to show, ignore the action.
if (!this.showContextMenu) return;
ev.preventDefault();
ev.stopPropagation();
this.setState({
@ -236,7 +242,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
private onClickMute = ev => this.saveNotifState(ev, MUTE);
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) {
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
// the menu makes no sense in these cases so do not show one
return null;
}
@ -281,12 +287,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const classes = classNames("mx_RoomTile2_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === MENTIONS_ONLY,
mx_RoomTile2_iconBell: state === state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile2_iconBellCrossed: state === MUTE,
// XXX: RoomNotifs assumes ALL_MESSAGES is default, this is wrong,
// but cannot be fixed until FTUE Notifications lands.
mx_RoomTile2_notificationsButton_show: state !== ALL_MESSAGES,
// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile2_notificationsButton_show: state === MUTE,
});
return (
@ -304,12 +312,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
}
private renderGeneralMenu(): React.ReactElement {
if (this.props.isMinimized) return null; // no menu when minimized
if (!this.showContextMenu) return null; // no menu to show
// TODO: Get a proper invite context menu, or take invites out of the room list.
if (this.props.tag === DefaultTagID.Invite) {
return null;
}
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
let contextMenu = null;
if (this.state.generalMenuPosition) {

View File

@ -150,7 +150,7 @@ export const SETTINGS = {
isFeature: true,
displayName: _td("Use the improved room list (will refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
default: true,
controller: new ReloadOnChangeController(),
},
"feature_custom_themes": {

View File

@ -37,6 +37,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
this.room.on("Room.timeline", this.handleRoomEventUpdate);
this.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
this.updateNotificationState();
}
@ -62,6 +63,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
}
}
@ -78,10 +80,21 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
this.updateNotificationState();
};
private handleAccountDataUpdate = (ev: MatrixEvent) => {
if (ev.getType() === "m.push_rules") {
this.updateNotificationState();
}
};
private updateNotificationState() {
const before = {count: this.count, symbol: this.symbol, color: this.color};
if (this.roomIsInvite) {
if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
// When muted we suppress all notification states, even if we have context on them.
this._color = NotificationColor.None;
this._symbol = null;
this._count = 0;
} else if (this.roomIsInvite) {
this._color = NotificationColor.Red;
this._symbol = "!";
this._count = 1; // not used, technically

View File

@ -101,8 +101,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
this.algorithm.stickyRoom = null;
} else if (activeRoom !== this.algorithm.stickyRoom) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Changing sticky room to ${activeRoomId}`);
this.algorithm.stickyRoom = activeRoom;
}
}
@ -299,8 +297,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
if (shouldUpdate) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
this.emit(LISTS_UPDATE_EVENT, this);
}
}
@ -367,8 +363,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
private onAlgorithmListUpdated = () => {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Underlying algorithm has triggered a list update - refiring");
this.emit(LISTS_UPDATE_EVENT, this);
};
@ -408,8 +402,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
public addFilter(filter: IFilterCondition): void {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Adding filter condition:", filter);
this.filterConditions.push(filter);
if (this.algorithm) {
this.algorithm.addFilterCondition(filter);
@ -417,8 +409,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
public removeFilter(filter: IFilterCondition): void {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Removing filter condition:", filter);
const idx = this.filterConditions.indexOf(filter);
if (idx >= 0) {
this.filterConditions.splice(idx, 1);

View File

@ -272,9 +272,6 @@ export class Algorithm extends EventEmitter {
}
}
newMap[tagId] = allowedRoomsInThisTag;
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
}
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
@ -310,9 +307,6 @@ export class Algorithm extends EventEmitter {
if (filteredRooms.length > 0) {
this.filteredRooms[tagId] = filteredRooms;
}
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
}
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
@ -351,8 +345,6 @@ export class Algorithm extends EventEmitter {
}
if (!this._cachedStickyRooms || !updatedTag) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Generating clone of cached rooms for sticky room handling`);
const stickiedTagMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) {
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
@ -363,8 +355,6 @@ export class Algorithm extends EventEmitter {
if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date.
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Replacing cached sticky rooms for ${updatedTag}`);
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
}
@ -373,8 +363,6 @@ export class Algorithm extends EventEmitter {
// we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
}
@ -466,13 +454,9 @@ export class Algorithm extends EventEmitter {
// Split out the easy rooms first (leave and invite)
const memberships = splitRoomsByMembership(rooms);
for (const room of memberships[EffectiveMembership.Invite]) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
newTags[DefaultTagID.Invite].push(room);
}
for (const room of memberships[EffectiveMembership.Leave]) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
newTags[DefaultTagID.Archived].push(room);
}
@ -483,11 +467,7 @@ export class Algorithm extends EventEmitter {
let inTag = false;
if (tags.length > 0) {
for (const tag of tags) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
if (!isNullOrUndefined(newTags[tag])) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
newTags[tag].push(room);
inTag = true;
}
@ -497,9 +477,6 @@ export class Algorithm extends EventEmitter {
if (!inTag) {
// TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236
newTags[DefaultTagID.Untagged].push(room);
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
}
}

View File

@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
}
// noinspection JSMethodCanBeStatic

View File

@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
}
public async setRooms(rooms: Room[]): Promise<any> {

View File

@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
const beforeRoomIds = this.roomIds;
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Updating filter for group: ", this.community.groupId);
this.emit(FILTER_CHANGED);
}
};

View File

@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
public set search(val: string) {
this._search = val;
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Updating filter for room name search:", this._search);
this.emit(FILTER_CHANGED);
}

View File

@ -30,6 +30,8 @@ export class MessageEventPreview implements IPreview {
eventContent = event.getContent()['m.new_content'];
}
if (!eventContent || !eventContent['body']) return null; // invalid for our purposes
let body = (eventContent['body'] || '').trim();
const msgtype = eventContent['msgtype'];
if (!body || !msgtype) return null; // invalid event, no preview