Merge remote-tracking branch 'origin/develop' into element

This commit is contained in:
J. Ryan Stinnett 2020-07-13 18:49:55 +01:00
commit 995a7879a1
29 changed files with 265 additions and 200 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
$tagPanelWidth: 56px; // only applies in this file, used for calculations $tagPanelWidth: 56px; // only applies in this file, used for calculations

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
.mx_RoomBreadcrumbs2 { .mx_RoomBreadcrumbs2 {
width: 100%; width: 100%;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
.mx_RoomSublist2 { .mx_RoomSublist2 {
// The sublist is a column of rows, essentially // The sublist is a column of rows, essentially

View file

@ -14,17 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// Note: the room tile expects to be in a flexbox column container // Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 { .mx_RoomTile2 {
margin-bottom: 4px; margin-bottom: 4px;
padding: 4px; padding: 4px;
// allow scrollIntoView to ignore the sticky headers, must match combined height of .mx_RoomSublist2_headerContainer
scroll-margin-top: 32px;
scroll-margin-bottom: 32px;
// The tile is also a flexbox row itself // The tile is also a flexbox row itself
display: flex; display: flex;
@ -168,11 +164,6 @@ limitations under the License.
} }
} }
// do not apply scroll-margin-bottom to the sublist which will not have a sticky header below it
.mx_RoomSublist2:last-child .mx_RoomTile2 {
scroll-margin-bottom: 0;
}
// We use these both in context menus and the room tiles // We use these both in context menus and the room tiles
.mx_RoomTile2_iconBell::before { .mx_RoomTile2_iconBell::before {
mask-image: url('$(res)/img/element-icons/notifications.svg'); mask-image: url('$(res)/img/element-icons/notifications.svg');

View file

@ -660,7 +660,7 @@ export const Commands = [
if (args) { if (args) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const matches = args.match(/^(\S+)$/); const matches = args.match(/^(@[^:]+:\S+)$/);
if (matches) { if (matches) {
const userId = matches[1]; const userId = matches[1];
const ignoredUsers = cli.getIgnoredUsers(); const ignoredUsers = cli.getIgnoredUsers();
@ -690,7 +690,7 @@ export const Commands = [
if (args) { if (args) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const matches = args.match(/^(\S+)$/); const matches = args.match(/(^@[^:]+:\S+$)/);
if (matches) { if (matches) {
const userId = matches[1]; const userId = matches[1];
const ignoredUsers = cli.getIgnoredUsers(); const ignoredUsers = cli.getIgnoredUsers();

View file

@ -35,8 +35,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomLi
import {Key} from "../../Keyboard"; import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import IndicatorScrollbar from "../structures/IndicatorScrollbar";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // 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/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -274,6 +274,14 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
}; };
private onEnter = () => {
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2");
if (firstRoom) {
firstRoom.click();
this.onSearch(""); // clear the search field
}
};
private onMoveFocus = (up: boolean) => { private onMoveFocus = (up: boolean) => {
let element = this.focusedElement; let element = this.focusedElement;
@ -346,6 +354,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
onQueryUpdate={this.onSearch} onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown} onVerticalArrow={this.onKeyDown}
onEnter={this.onEnter}
/> />
<AccessibleButton <AccessibleButton
className="mx_LeftPanel2_exploreButton" className="mx_LeftPanel2_exploreButton"

View file

@ -668,8 +668,7 @@ class LoggedInView extends React.Component<IProps, IState> {
disabled={this.props.leftDisabled} disabled={this.props.leftDisabled}
/> />
); );
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { if (SettingsStore.getValue("feature_new_room_list")) {
// TODO: Supply props like collapsed and disabled to LeftPanel2
leftPanel = ( leftPanel = (
<LeftPanel2 <LeftPanel2
isMinimized={this.props.collapseLhs || false} isMinimized={this.props.collapseLhs || false}

View file

@ -50,7 +50,7 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages'; import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import { _t, getCurrentLanguage } from '../../languageHandler'; import {_t, _td, getCurrentLanguage} from '../../languageHandler';
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController"; import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration.js"; import { startAnyRegistrationFlow } from "../../Registration.js";
@ -74,6 +74,7 @@ import {
} from "../../toasts/AnalyticsToast"; } from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -460,7 +461,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onAction = (payload) => { onAction = (payload) => {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`); // console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// Start the onboarding process for certain actions // Start the onboarding process for certain actions
@ -554,6 +554,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'leave_room': case 'leave_room':
this.leaveRoom(payload.room_id); this.leaveRoom(payload.room_id);
break; break;
case 'forget_room':
this.forgetRoom(payload.room_id);
break;
case 'reject_invite': case 'reject_invite':
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
title: _t('Reject invitation'), title: _t('Reject invitation'),
@ -1060,7 +1063,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoom(roomId: string) { private leaveRoom(roomId: string) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId); const warnings = this.leaveRoomWarnings(roomId);
@ -1124,6 +1126,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
private forgetRoom(roomId: string) {
MatrixClientPeg.get().forget(roomId).then(() => {
// Switch to another room view if we're currently viewing the historical room
if (this.state.currentRoomId === roomId) {
dis.dispatch({ action: "view_next_room" });
}
}).catch((err) => {
const errCode = err.errcode || _td("unknown error code");
Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, {
title: _t("Failed to forget room %(errCode)s", {errCode}),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
}
/** /**
* Starts a chat with the welcome user, if the user doesn't already have one * Starts a chat with the welcome user, if the user doesn't already have one
* @returns {string} The room ID of the new room, or null if no room was created * @returns {string} The room ID of the new room, or null if no room was created
@ -1372,7 +1389,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return; return;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Signed out', '', ErrorDialog, { Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
title: _t('Signed Out'), title: _t('Signed Out'),
description: _t('For security, this session has been signed out. Please sign in again.'), description: _t('For security, this session has been signed out. Please sign in again.'),
@ -1442,7 +1458,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
}); });
cli.on("crypto.warning", (type) => { cli.on("crypto.warning", (type) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
switch (type) { switch (type) {
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;

View file

@ -25,7 +25,7 @@ import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -39,6 +39,7 @@ interface IProps {
onQueryUpdate: (newQuery: string) => void; onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean; isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent); onVerticalArrow(ev: React.KeyboardEvent);
onEnter(ev: React.KeyboardEvent);
} }
interface IState { interface IState {
@ -115,6 +116,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
defaultDispatcher.fire(Action.FocusComposer); defaultDispatcher.fire(Action.FocusComposer);
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { } else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
this.props.onVerticalArrow(ev); this.props.onVerticalArrow(ev);
} else if (ev.key === Key.ENTER) {
this.props.onEnter(ev);
} }
}; };

View file

@ -1380,15 +1380,9 @@ export default createReactClass({
}, },
onForgetClick: function() { onForgetClick: function() {
this.context.forget(this.state.room.roomId).then(function() { dis.dispatch({
dis.dispatch({ action: 'view_next_room' }); action: 'forget_room',
}, function(err) { room_id: this.state.room.roomId,
const errCode = err.errcode || _t("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t("Error"),
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
});
}); });
}, },

View file

@ -99,6 +99,7 @@ const BaseAvatar = (props: IProps) => {
defaultToInitialLetter = true, defaultToInitialLetter = true,
onClick, onClick,
inputRef, inputRef,
className,
...otherProps ...otherProps
} = props; } = props;
@ -138,7 +139,7 @@ const BaseAvatar = (props: IProps) => {
<AccessibleButton <AccessibleButton
{...otherProps} {...otherProps}
element="span" element="span"
className="mx_BaseAvatar" className={classNames("mx_BaseAvatar", className)}
onClick={onClick} onClick={onClick}
inputRef={inputRef} inputRef={inputRef}
> >
@ -149,7 +150,7 @@ const BaseAvatar = (props: IProps) => {
} else { } else {
return ( return (
<span <span
className="mx_BaseAvatar" className={classNames("mx_BaseAvatar", className)}
ref={inputRef} ref={inputRef}
{...otherProps} {...otherProps}
role="presentation" role="presentation"
@ -164,7 +165,7 @@ const BaseAvatar = (props: IProps) => {
if (onClick !== null) { if (onClick !== null) {
return ( return (
<AccessibleButton <AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image" className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
element='img' element='img'
src={imageUrl} src={imageUrl}
onClick={onClick} onClick={onClick}
@ -180,7 +181,7 @@ const BaseAvatar = (props: IProps) => {
} else { } else {
return ( return (
<img <img
className="mx_BaseAvatar mx_BaseAvatar_image" className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
src={imageUrl} src={imageUrl}
onError={onError} onError={onError}
style={{ style={{

View file

@ -126,16 +126,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}; };
public render() { public render() {
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
const roomName = room ? room.name : oobData.name; const roomName = room ? room.name : oobData.name;
return ( return (
<BaseAvatar {...otherProps} name={roomName} <BaseAvatar {...otherProps}
name={roomName}
idName={room ? room.roomId : null} idName={room ? room.roomId : null}
urls={this.state.urls} urls={this.state.urls}
onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null} onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null}
/> />
); );
} }

View file

@ -27,8 +27,8 @@ import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { DefaultTagID } from "../../../stores/room-list/models"; import { DefaultTagID } from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // 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/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *

View file

@ -41,8 +41,8 @@ import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // 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/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *

View file

@ -17,7 +17,7 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import {createRef, UIEventHandler} from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames'; import classNames from 'classnames';
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@ -48,8 +48,8 @@ import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // 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/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -321,25 +321,29 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
}; };
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => { private onHeaderClick = () => {
let target = ev.target as HTMLDivElement; const possibleSticky = this.headerButton.current.parentElement;
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
// If we don't have the headerText class, the user clicked the span in the headerText.
target = target.parentElement as HTMLDivElement;
}
const possibleSticky = target.parentElement;
const sublist = possibleSticky.parentElement.parentElement; const sublist = possibleSticky.parentElement.parentElement;
const list = sublist.parentElement.parentElement; const list = sublist.parentElement.parentElement;
// the scrollTop is capped at the height of the header in LeftPanel2 // the scrollTop is capped at the height of the header in LeftPanel2, the top header is always sticky
const isAtTop = list.scrollTop <= HEADER_HEIGHT; const isAtTop = list.scrollTop <= HEADER_HEIGHT;
const isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky'); const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
if (isSticky && !isAtTop) { const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyBottom');
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
// is sticky - jump to list // is sticky - jump to list
sublist.scrollIntoView({behavior: 'smooth'}); sublist.scrollIntoView({behavior: 'smooth'});
} else { } else {
// on screen - toggle collapse // on screen - toggle collapse
const isExpanded = this.state.isExpanded;
this.toggleCollapsed(); this.toggleCollapsed();
// if the bottom list is collapsed then scroll it in so it doesn't expand off screen
if (!isExpanded && isStickyBottom) {
setImmediate(() => {
sublist.scrollIntoView({behavior: 'smooth'});
});
}
} }
}; };
@ -595,6 +599,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
); );
} }
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
// this fixes https://github.com/vector-im/riot-web/issues/14413
(e.target as HTMLDivElement).scrollTop = 0;
}
public render(): React.ReactElement { public render(): React.ReactElement {
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
@ -704,7 +714,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
className="mx_RoomSublist2_resizeBox" className="mx_RoomSublist2_resizeBox"
enable={handles} enable={handles}
> >
<div className="mx_RoomSublist2_tiles"> <div className="mx_RoomSublist2_tiles" onScroll={this.onScrollPrevent}>
{visibleTiles} {visibleTiles}
</div> </div>
{showNButton} {showNButton}

View file

@ -55,8 +55,8 @@ import {ActionPayload} from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState"; import { NotificationState } from "../../../stores/notifications/NotificationState";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // 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/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -276,6 +276,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({generalMenuPosition: null}); // hide the menu this.setState({generalMenuPosition: null}); // hide the menu
}; };
private onForgetRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'forget_room',
room_id: this.props.room.roomId,
});
this.setState({generalMenuPosition: null}); // hide the menu
};
private onOpenRoomSettings = (ev: ButtonEvent) => { private onOpenRoomSettings = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -315,7 +326,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
private onClickMute = ev => this.saveNotifState(ev, MUTE); private onClickMute = ev => this.saveNotifState(ev, MUTE);
private renderNotificationsMenu(isActive: boolean): React.ReactElement { private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) { if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived || !this.showContextMenu) {
// the menu makes no sense in these cases so do not show one // the menu makes no sense in these cases so do not show one
return null; return null;
} }
@ -397,7 +408,20 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite"); const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
let contextMenu = null; let contextMenu = null;
if (this.state.generalMenuPosition) { if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<MenuItem onClick={this.onForgetRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Forget Room")}</span>
</MenuItem>
</div>
</div>
</ContextMenu>
);
} else if (this.state.generalMenuPosition) {
contextMenu = ( contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}> <ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">

View file

@ -32,12 +32,12 @@ export default class PreferencesUserSettingsTab extends React.Component {
'breadcrumbs', 'breadcrumbs',
]; ];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static ROOM_LIST_2_SETTINGS = [ static ROOM_LIST_2_SETTINGS = [
'breadcrumbs', 'breadcrumbs',
]; ];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static eligibleRoomListSettings = () => { static eligibleRoomListSettings = () => {
if (RoomListStoreTempProxy.isUsingNewStore()) { if (RoomListStoreTempProxy.isUsingNewStore()) {
return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS; return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS;

View file

@ -1239,6 +1239,7 @@
"Favourited": "Favourited", "Favourited": "Favourited",
"Favourite": "Favourite", "Favourite": "Favourite",
"Leave Room": "Leave Room", "Leave Room": "Leave Room",
"Forget Room": "Forget Room",
"Room options": "Room options", "Room options": "Room options",
"Add a topic": "Add a topic", "Add a topic": "Add a topic",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",

View file

@ -147,7 +147,8 @@ export const SETTINGS = {
default: false, default: false,
}, },
"feature_new_room_list": { "feature_new_room_list": {
isFeature: true, // TODO: Remove setting: https://github.com/vector-im/riot-web/issues/14367
// XXX: We shouldn't have non-features appear like features.
displayName: _td("Use the improved room list (will refresh to apply changes)"), displayName: _td("Use the improved room list (will refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: true, default: true,

View file

@ -57,7 +57,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return; if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return; if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'setting_updated') { if (payload.action === 'setting_updated') {
@ -80,7 +80,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
protected async onReady() { protected async onReady() {
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return; if (!RoomListStoreTempProxy.isUsingNewStore()) return;
await this.updateRooms(); await this.updateRooms();
@ -91,7 +91,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
protected async onNotReady() { protected async onNotReady() {
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return; if (!RoomListStoreTempProxy.isUsingNewStore()) return;
this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); this.matrixClient.removeListener("Room.myMembership", this.onMyMembership);

View file

@ -99,7 +99,7 @@ class RoomListStore extends Store {
} }
_checkDisabled() { _checkDisabled() {
this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); this.disabled = SettingsStore.getValue("feature_new_room_list");
if (this.disabled) { if (this.disabled) {
console.warn("👋 legacy room list store has been disabled"); console.warn("👋 legacy room list store has been disabled");
} }

View file

@ -192,7 +192,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return; if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return; if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {

View file

@ -46,6 +46,12 @@ interface IState {
export const LISTS_UPDATE_EVENT = "lists_update"; export const LISTS_UPDATE_EVENT = "lists_update";
export class RoomListStore2 extends AsyncStore<ActionPayload> { export class RoomListStore2 extends AsyncStore<ActionPayload> {
/**
* Set to true if you're running tests on the store. Should not be touched in
* any other environment.
*/
public static TEST_MODE = false;
private _matrixClient: MatrixClient; private _matrixClient: MatrixClient;
private initialListsGenerated = false; private initialListsGenerated = false;
private enabled = false; private enabled = false;
@ -77,9 +83,43 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
return this._matrixClient; return this._matrixClient;
} }
// TODO: Remove enabled flag with the old RoomListStore: https://github.com/vector-im/riot-web/issues/14231 // Intended for test usage
public async resetStore() {
await this.reset();
this.tagWatcher = new TagWatcher(this);
this.filterConditions = [];
this.initialListsGenerated = false;
this._matrixClient = null;
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
this.algorithm = new Algorithm();
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmListUpdated);
}
// Public for test usage. Do not call this.
public async makeReady(client: MatrixClient) {
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
this.checkEnabled();
if (!this.enabled) return;
this._matrixClient = client;
// Update any settings here, as some may have happened before we were logically ready.
// Update any settings here, as some may have happened before we were logically ready.
console.log("Regenerating room lists: Startup");
await this.readAndCacheSettingsFromStore();
await this.regenerateAllLists({trigger: false});
await this.handleRVSUpdate({trigger: false}); // fake an RVS update to adjust sticky room, if needed
this.updateFn.mark(); // we almost certainly want to trigger an update.
this.updateFn.trigger();
}
// TODO: Remove enabled flag with the old RoomListStore: https://github.com/vector-im/riot-web/issues/14367
private checkEnabled() { private checkEnabled() {
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); this.enabled = SettingsStore.getValue("feature_new_room_list");
if (this.enabled) { if (this.enabled) {
console.log("⚡ new room list store engaged"); console.log("⚡ new room list store engaged");
} }
@ -99,7 +139,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
* be used if the calling code will manually trigger the update. * be used if the calling code will manually trigger the update.
*/ */
private async handleRVSUpdate({trigger = true}) { private async handleRVSUpdate({trigger = true}) {
if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14231 if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
const activeRoomId = RoomViewStore.getRoomId(); const activeRoomId = RoomViewStore.getRoomId();
@ -122,7 +162,14 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
if (trigger) this.updateFn.trigger(); if (trigger) this.updateFn.trigger();
} }
protected onDispatch(payload: ActionPayload) { protected async onDispatch(payload: ActionPayload) {
// When we're running tests we can't reliably use setImmediate out of timing concerns.
// As such, we use a more synchronous model.
if (RoomListStore2.TEST_MODE) {
await this.onDispatchAsync(payload);
return;
}
// We do this to intentionally break out of the current event loop task, allowing // We do this to intentionally break out of the current event loop task, allowing
// us to instead wait for a more convenient time to run our updates. // us to instead wait for a more convenient time to run our updates.
setImmediate(() => this.onDispatchAsync(payload)); setImmediate(() => this.onDispatchAsync(payload));
@ -135,19 +182,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
return; return;
} }
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14231 await this.makeReady(payload.matrixClient);
this.checkEnabled();
if (!this.enabled) return;
this._matrixClient = payload.matrixClient;
// Update any settings here, as some may have happened before we were logically ready.
console.log("Regenerating room lists: Startup");
await this.readAndCacheSettingsFromStore();
await this.regenerateAllLists({trigger: false});
await this.handleRVSUpdate({trigger: false}); // fake an RVS update to adjust sticky room, if needed
this.updateFn.trigger();
return; // no point in running the next conditions - they won't match return; // no point in running the next conditions - they won't match
} }
@ -496,10 +531,13 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
/** /**
* Regenerates the room whole room list, discarding any previous results. * Regenerates the room whole room list, discarding any previous results.
*
* Note: This is only exposed externally for the tests. Do not call this from within
* the app.
* @param trigger Set to false to prevent a list update from being sent. Should only * @param trigger Set to false to prevent a list update from being sent. Should only
* be used if the calling code will manually trigger the update. * be used if the calling code will manually trigger the update.
*/ */
private async regenerateAllLists({trigger = true}) { public async regenerateAllLists({trigger = true}) {
console.warn("Regenerating all room lists"); console.warn("Regenerating all room lists");
const sorts: ITagSortingMap = {}; const sorts: ITagSortingMap = {};

View file

@ -24,11 +24,11 @@ import { ITagMap } from "./algorithms/models";
* Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when
* it is available to everyone. * it is available to everyone.
* *
* TODO: Delete this: https://github.com/vector-im/riot-web/issues/14231 * TODO: Delete this: https://github.com/vector-im/riot-web/issues/14367
*/ */
export class RoomListStoreTempProxy { export class RoomListStoreTempProxy {
public static isUsingNewStore(): boolean { public static isUsingNewStore(): boolean {
return SettingsStore.isFeatureEnabled("feature_new_room_list"); return SettingsStore.getValue("feature_new_room_list");
} }
public static addListener(handler: () => void): RoomListStoreTempToken { public static addListener(handler: () => void): RoomListStoreTempToken {

View file

@ -19,47 +19,29 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomUpdateCause, TagID } from "../../models"; import { RoomUpdateCause, TagID } from "../../models";
import { SortAlgorithm } from "../models"; import { SortAlgorithm } from "../models";
import { sortRoomsWithAlgorithm } from "../tag-sorting"; import { sortRoomsWithAlgorithm } from "../tag-sorting";
import * as Unread from '../../../../Unread';
import { OrderingAlgorithm } from "./OrderingAlgorithm"; import { OrderingAlgorithm } from "./OrderingAlgorithm";
import { NotificationColor } from "../../../notifications/NotificationColor";
/** import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";
* The determined category of a room.
*/
export enum Category {
/**
* The room has unread mentions within.
*/
Red = "RED",
/**
* The room has unread notifications within. Note that these are not unread
* mentions - they are simply messages which the user has asked to cause a
* badge count update or push notification.
*/
Grey = "GREY",
/**
* The room has unread messages within (grey without the badge).
*/
Bold = "BOLD",
/**
* The room has no relevant unread messages within.
*/
Idle = "IDLE",
}
interface ICategorizedRoomMap { interface ICategorizedRoomMap {
// @ts-ignore - TS wants this to be a string, but we know better // @ts-ignore - TS wants this to be a string, but we know better
[category: Category]: Room[]; [category: NotificationColor]: Room[];
} }
interface ICategoryIndex { interface ICategoryIndex {
// @ts-ignore - TS wants this to be a string, but we know better // @ts-ignore - TS wants this to be a string, but we know better
[category: Category]: number; // integer [category: NotificationColor]: number; // integer
} }
// Caution: changing this means you'll need to update a bunch of assumptions and // Caution: changing this means you'll need to update a bunch of assumptions and
// comments! Check the usage of Category carefully to figure out what needs changing // comments! Check the usage of Category carefully to figure out what needs changing
// if you're going to change this array's order. // if you're going to change this array's order.
const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idle]; const CATEGORY_ORDER = [
NotificationColor.Red,
NotificationColor.Grey,
NotificationColor.Bold,
NotificationColor.None, // idle
];
/** /**
* An implementation of the "importance" algorithm for room list sorting. Where * An implementation of the "importance" algorithm for room list sorting. Where
@ -92,10 +74,10 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
const map: ICategorizedRoomMap = { const map: ICategorizedRoomMap = {
[Category.Red]: [], [NotificationColor.Red]: [],
[Category.Grey]: [], [NotificationColor.Grey]: [],
[Category.Bold]: [], [NotificationColor.Bold]: [],
[Category.Idle]: [], [NotificationColor.None]: [],
}; };
for (const room of rooms) { for (const room of rooms) {
const category = this.getRoomCategory(room); const category = this.getRoomCategory(room);
@ -105,25 +87,11 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
private getRoomCategory(room: Room): Category { private getRoomCategory(room: Room): NotificationColor {
// Function implementation borrowed from old RoomListStore // It's fine for us to call this a lot because it's cached, and we shouldn't be
// wasting anything by doing so as the store holds single references
const mentions = room.getUnreadNotificationCount('highlight') > 0; const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
if (mentions) { return state.color;
return Category.Red;
}
let unread = room.getUnreadNotificationCount() > 0;
if (unread) {
return Category.Grey;
}
unread = Unread.doesRoomHaveUnreadMessages(room);
if (unread) {
return Category.Bold;
}
return Category.Idle;
} }
public async setRooms(rooms: Room[]): Promise<any> { public async setRooms(rooms: Room[]): Promise<any> {
@ -217,7 +185,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
} }
private async sortCategory(category: Category) { private async sortCategory(category: NotificationColor) {
// This should be relatively quick because the room is usually inserted at the top of the // This should be relatively quick because the room is usually inserted at the top of the
// category, and most popular sorting algorithms will deal with trying to keep the active // category, and most popular sorting algorithms will deal with trying to keep the active
// room at the top/start of the category. For the few algorithms that will have to move the // room at the top/start of the category. For the few algorithms that will have to move the
@ -234,7 +202,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category { private getCategoryFromIndices(index: number, indices: ICategoryIndex): NotificationColor {
for (let i = 0; i < CATEGORY_ORDER.length; i++) { for (let i = 0; i < CATEGORY_ORDER.length; i++) {
const category = CATEGORY_ORDER[i]; const category = CATEGORY_ORDER[i];
const isLast = i === (CATEGORY_ORDER.length - 1); const isLast = i === (CATEGORY_ORDER.length - 1);
@ -250,7 +218,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) { private moveRoomIndexes(nRooms: number, fromCategory: NotificationColor, toCategory: NotificationColor, indices: ICategoryIndex) {
// We have to update the index of the category *after* the from/toCategory variables // We have to update the index of the category *after* the from/toCategory variables
// in order to update the indices correctly. Because the room is moving from/to those // in order to update the indices correctly. Because the room is moving from/to those
// categories, the next category's index will change - not the category we're modifying. // categories, the next category's index will change - not the category we're modifying.
@ -261,7 +229,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
this.alterCategoryPositionBy(toCategory, +nRooms, indices); this.alterCategoryPositionBy(toCategory, +nRooms, indices);
} }
private alterCategoryPositionBy(category: Category, n: number, indices: ICategoryIndex) { private alterCategoryPositionBy(category: NotificationColor, n: number, indices: ICategoryIndex) {
// Note: when we alter a category's index, we actually have to modify the ones following // Note: when we alter a category's index, we actually have to modify the ones following
// the target and not the target itself. // the target and not the target itself.

View file

@ -111,7 +111,7 @@ export function pillifyLinks(nodes, mxEvent, pills) {
type={Pill.TYPE_AT_ROOM_MENTION} type={Pill.TYPE_AT_ROOM_MENTION}
inMessage={true} inMessage={true}
room={room} room={room}
shouldShowPillAvatar={true} shouldShowPillAvatar={shouldShowPillAvatar}
/>; />;
ReactDOM.render(pill, pillContainer); ReactDOM.render(pill, pillContainer);

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import ReactTestUtils from 'react-dom/test-utils'; import ReactTestUtils from 'react-dom/test-utils';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import lolex from 'lolex';
import * as TestUtils from '../../../test-utils'; import * as TestUtils from '../../../test-utils';
@ -15,11 +14,18 @@ import GroupStore from '../../../../src/stores/GroupStore.js';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import {DefaultTagID} from "../../../../src/stores/room-list/models"; import {DefaultTagID} from "../../../../src/stores/room-list/models";
import RoomListStore, {LISTS_UPDATE_EVENT, RoomListStore2} from "../../../../src/stores/room-list/RoomListStore2";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
function generateRoomId() { function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain'; return '!' + Math.random().toString().slice(2, 10) + ':domain';
} }
function waitForRoomListStoreUpdate() {
return new Promise((resolve) => {
RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve());
});
}
describe('RoomList', () => { describe('RoomList', () => {
function createRoom(opts) { function createRoom(opts) {
@ -34,7 +40,6 @@ describe('RoomList', () => {
let client = null; let client = null;
let root = null; let root = null;
const myUserId = '@me:domain'; const myUserId = '@me:domain';
let clock = null;
const movingRoomId = '!someroomid'; const movingRoomId = '!someroomid';
let movingRoom; let movingRoom;
@ -43,25 +48,25 @@ describe('RoomList', () => {
let myMember; let myMember;
let myOtherMember; let myOtherMember;
beforeEach(function() { beforeEach(async function(done) {
RoomListStore2.TEST_MODE = true;
TestUtils.stubClient(); TestUtils.stubClient();
client = MatrixClientPeg.get(); client = MatrixClientPeg.get();
client.credentials = {userId: myUserId}; client.credentials = {userId: myUserId};
//revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value //revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value
client.getUserId = MatrixClient.prototype.getUserId; client.getUserId = MatrixClient.prototype.getUserId;
clock = lolex.install();
DMRoomMap.makeShared(); DMRoomMap.makeShared();
parentDiv = document.createElement('div'); parentDiv = document.createElement('div');
document.body.appendChild(parentDiv); document.body.appendChild(parentDiv);
const RoomList = sdk.getComponent('views.rooms.RoomList'); const RoomList = sdk.getComponent('views.rooms.RoomList2');
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render( root = ReactDOM.render(
<DragDropContext> <DragDropContext>
<WrappedRoomList searchFilter="" /> <WrappedRoomList searchFilter="" onResize={() => {}} />
</DragDropContext> </DragDropContext>
, parentDiv); , parentDiv);
ReactTestUtils.findRenderedComponentWithType(root, RoomList); ReactTestUtils.findRenderedComponentWithType(root, RoomList);
@ -102,23 +107,29 @@ describe('RoomList', () => {
}); });
client.getRoom = (roomId) => roomMap[roomId]; client.getRoom = (roomId) => roomMap[roomId];
// Now that everything has been set up, prepare and update the store
await RoomListStore.instance.makeReady(client);
done();
}); });
afterEach((done) => { afterEach(async (done) => {
if (parentDiv) { if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv); ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove(); parentDiv.remove();
parentDiv = null; parentDiv = null;
} }
clock.uninstall(); await RoomListLayoutStore.instance.resetLayouts();
await RoomListStore.instance.resetStore();
done(); done();
}); });
function expectRoomInSubList(room, subListTest) { function expectRoomInSubList(room, subListTest) {
const RoomSubList = sdk.getComponent('structures.RoomSubList'); const RoomSubList = sdk.getComponent('views.rooms.RoomSublist2');
const RoomTile = sdk.getComponent('views.rooms.RoomTile'); const RoomTile = sdk.getComponent('views.rooms.RoomTile2');
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList); const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList);
const containingSubList = subLists.find(subListTest); const containingSubList = subLists.find(subListTest);
@ -140,20 +151,20 @@ describe('RoomList', () => {
expect(expectedRoomTile.props.room).toBe(room); expect(expectedRoomTile.props.room).toBe(room);
} }
function expectCorrectMove(oldTag, newTag) { function expectCorrectMove(oldTagId, newTagId) {
const getTagSubListTest = (tag) => { const getTagSubListTest = (tagId) => {
if (tag === undefined) return (s) => s.props.label.endsWith('Rooms'); return (s) => s.props.tagId === tagId;
return (s) => s.props.tagName === tag;
}; };
// Default to finding the destination sublist with newTag // Default to finding the destination sublist with newTag
const destSubListTest = getTagSubListTest(newTag); const destSubListTest = getTagSubListTest(newTagId);
const srcSubListTest = getTagSubListTest(oldTag); const srcSubListTest = getTagSubListTest(oldTagId);
// Set up the room that will be moved such that it has the correct state for a room in // Set up the room that will be moved such that it has the correct state for a room in
// the section for oldTag // the section for oldTagId
if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}}; if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) {
if (oldTag === DefaultTagID.DM) { movingRoom.tags = {[oldTagId]: {}};
} else if (oldTagId === DefaultTagID.DM) {
// Mock inverse m.direct // Mock inverse m.direct
DMRoomMap.shared().roomToUser = { DMRoomMap.shared().roomToUser = {
[movingRoom.roomId]: '@someotheruser:domain', [movingRoom.roomId]: '@someotheruser:domain',
@ -162,17 +173,12 @@ describe('RoomList', () => {
dis.dispatch({action: 'MatrixActions.sync', prevState: null, state: 'PREPARED', matrixClient: client}); dis.dispatch({action: 'MatrixActions.sync', prevState: null, state: 'PREPARED', matrixClient: client});
clock.runAll();
expectRoomInSubList(movingRoom, srcSubListTest); expectRoomInSubList(movingRoom, srcSubListTest);
dis.dispatch({action: 'RoomListActions.tagRoom.pending', request: { dis.dispatch({action: 'RoomListActions.tagRoom.pending', request: {
oldTag, newTag, room: movingRoom, oldTagId, newTagId, room: movingRoom,
}}); }});
// Run all setTimeouts for dispatches and room list rate limiting
clock.runAll();
expectRoomInSubList(movingRoom, destSubListTest); expectRoomInSubList(movingRoom, destSubListTest);
} }
@ -269,6 +275,12 @@ describe('RoomList', () => {
}; };
GroupStore._notifyListeners(); GroupStore._notifyListeners();
// We also have to mock the client's getGroup function for the room list to filter it.
// It's not smart enough to tell the difference between a real group and a template though.
client.getGroup = (groupId) => {
return {groupId};
};
// Select tag // Select tag
dis.dispatch({action: 'select_tag', tag: '+group:domain'}, true); dis.dispatch({action: 'select_tag', tag: '+group:domain'}, true);
} }
@ -277,17 +289,14 @@ describe('RoomList', () => {
setupSelectedTag(); setupSelectedTag();
}); });
it('displays the correct rooms when the groups rooms are changed', () => { it('displays the correct rooms when the groups rooms are changed', async () => {
GroupStore.getGroupRooms = (groupId) => { GroupStore.getGroupRooms = (groupId) => {
return [movingRoom, otherRoom]; return [movingRoom, otherRoom];
}; };
GroupStore._notifyListeners(); GroupStore._notifyListeners();
// Run through RoomList debouncing await waitForRoomListStoreUpdate();
clock.runAll(); expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
// By default, the test will
expectRoomInSubList(otherRoom, (s) => s.props.label.endsWith('Rooms'));
}); });
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();

View file

@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
const {findSublist} = require("./create-room");
module.exports = async function acceptInvite(session, name) { module.exports = async function acceptInvite(session, name) {
session.log.step(`accepts "${name}" invite`); session.log.step(`accepts "${name}" invite`);
//TODO: brittle selector const inviteSublist = await findSublist(session, "invites");
const invitesHandles = await session.queryAll('.mx_RoomTile_name.mx_RoomTile_invite'); const invitesHandles = await inviteSublist.$$(".mx_RoomTile2_name");
const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => {
const text = await session.innerText(inviteHandle); const text = await session.innerText(inviteHandle);
return {inviteHandle, text}; return {inviteHandle, text};

View file

@ -16,21 +16,27 @@ limitations under the License.
*/ */
async function openRoomDirectory(session) { async function openRoomDirectory(session) {
const roomDirectoryButton = await session.query('.mx_LeftPanel_explore .mx_AccessibleButton'); const roomDirectoryButton = await session.query('.mx_LeftPanel2_exploreButton');
await roomDirectoryButton.click(); await roomDirectoryButton.click();
} }
async function findSublist(session, name) {
const sublists = await session.queryAll('.mx_RoomSublist2');
for (const sublist of sublists) {
const header = await sublist.$('.mx_RoomSublist2_headerText');
const headerText = await session.innerText(header);
if (headerText.toLowerCase().includes(name.toLowerCase())) {
return sublist;
}
}
throw new Error(`could not find room list section that contains '${name}' in header`);
}
async function createRoom(session, roomName, encrypted=false) { async function createRoom(session, roomName, encrypted=false) {
session.log.step(`creates room "${roomName}"`); session.log.step(`creates room "${roomName}"`);
const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); const roomsSublist = await findSublist(session, "rooms");
const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); const addRoomButton = await roomsSublist.$(".mx_RoomSublist2_auxButton");
const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms"));
if (roomsIndex === -1) {
throw new Error("could not find room list section that contains 'rooms' in header");
}
const roomsHeader = roomListHeaders[roomsIndex];
const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom");
await addRoomButton.click(); await addRoomButton.click();
const roomNameInput = await session.query('.mx_CreateRoomDialog_name input'); const roomNameInput = await session.query('.mx_CreateRoomDialog_name input');
@ -51,14 +57,8 @@ async function createRoom(session, roomName, encrypted=false) {
async function createDm(session, invitees) { async function createDm(session, invitees) {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`); session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); const dmsSublist = await findSublist(session, "people");
const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); const startChatButton = await dmsSublist.$(".mx_RoomSublist2_auxButton");
const dmsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes('direct messages'));
if (dmsIndex === -1) {
throw new Error("could not find room list section that contains 'direct messages' in header");
}
const dmsHeader = roomListHeaders[dmsIndex];
const startChatButton = await dmsHeader.$(".mx_RoomSubList_addRoom");
await startChatButton.click(); await startChatButton.click();
const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea'); const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea');
@ -83,4 +83,4 @@ async function createDm(session, invitees) {
session.log.done(); session.log.done();
} }
module.exports = {openRoomDirectory, createRoom, createDm}; module.exports = {openRoomDirectory, findSublist, createRoom, createDm};