Merge branch 'develop' into element

This commit is contained in:
Bruno Windels 2020-07-14 14:31:31 +02:00
commit 4fe4788c2e
39 changed files with 357 additions and 339 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

@ -48,15 +48,15 @@ limitations under the License.
} }
&.mx_NotificationBadge_2char { &.mx_NotificationBadge_2char {
width: 16px; width: $font-16px;
height: 16px; height: $font-16px;
border-radius: 16px; border-radius: $font-16px;
} }
&.mx_NotificationBadge_3char { &.mx_NotificationBadge_3char {
width: 26px; width: $font-26px;
height: 16px; height: $font-16px;
border-radius: 16px; border-radius: $font-16px;
} }
// The following is the floating badge // The following is the floating badge

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
@ -48,12 +48,6 @@ limitations under the License.
height: 24px; height: 24px;
color: $roomlist2-header-color; color: $roomlist2-header-color;
// Hide the header container if the contained element is stickied.
// We don't use display:none as that causes the header to go away too.
&.mx_RoomSublist2_headerContainer_hasSticky {
height: 0;
}
.mx_RoomSublist2_stickable { .mx_RoomSublist2_stickable {
flex: 1; flex: 1;
max-width: 100%; max-width: 100%;
@ -182,6 +176,15 @@ limitations under the License.
} }
} }
// In the general case, we leave height of headers alone even if sticky, so
// that the sublists below them do not jump. However, that leaves a gap
// when scrolled to the top above the first sublist (whose header can only
// ever stick to top), so we force height to 0 for only that first header.
// See also https://github.com/vector-im/riot-web/issues/14429.
&:first-child .mx_RoomSublist2_headerContainer {
height: 0;
}
.mx_RoomSublist2_resizeBox { .mx_RoomSublist2_resizeBox {
position: relative; position: relative;
@ -198,6 +201,8 @@ limitations under the License.
// as the box model should be top aligned. Happens in both FF and Chromium // as the box model should be top aligned. Happens in both FF and Chromium
display: flex; display: flex;
flex-direction: column; flex-direction: column;
mask-image: linear-gradient(0deg, transparent, black 3px);
} }
.mx_RoomSublist2_resizerHandles_showNButton { .mx_RoomSublist2_resizerHandles_showNButton {

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,16 +35,7 @@ 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -111,6 +102,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
const newVal = BreadcrumbsStore.instance.visible; const newVal = BreadcrumbsStore.instance.visible;
if (newVal !== this.state.showBreadcrumbs) { if (newVal !== this.state.showBreadcrumbs) {
this.setState({showBreadcrumbs: newVal}); this.setState({showBreadcrumbs: newVal});
// Update the sticky headers too as the breadcrumbs will be popping in or out.
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
this.handleStickyHeaders(this.listContainerRef.current);
} }
}; };
@ -170,7 +165,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// layout updates. // layout updates.
for (const header of targetStyles.keys()) { for (const header of targetStyles.keys()) {
const style = targetStyles.get(header); const style = targetStyles.get(header);
const headerContainer = header.parentElement; // .mx_RoomSublist2_headerContainer
if (style.makeInvisible) { if (style.makeInvisible) {
// we will have already removed the 'display: none', so add it back. // we will have already removed the 'display: none', so add it back.
@ -187,19 +181,29 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (header.style.top !== newTop) { if (header.style.top !== newTop) {
header.style.top = newTop; header.style.top = newTop;
} }
} else if (style.stickyBottom) { } else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
}
if (header.style.top) {
header.style.removeProperty('top');
}
}
if (style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) { if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
} }
} else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
}
} }
if (style.stickyTop || style.stickyBottom) { if (style.stickyTop || style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_sticky");
} }
if (!headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
headerContainer.classList.add("mx_RoomSublist2_headerContainer_hasSticky");
}
const newWidth = `${headerStickyWidth}px`; const newWidth = `${headerStickyWidth}px`;
if (header.style.width !== newWidth) { if (header.style.width !== newWidth) {
@ -209,21 +213,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky"); header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
} }
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
}
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
}
if (headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
headerContainer.classList.remove("mx_RoomSublist2_headerContainer_hasSticky");
}
if (header.style.width) { if (header.style.width) {
header.style.removeProperty('width'); header.style.removeProperty('width');
} }
if (header.style.top) {
header.style.removeProperty('top');
}
} }
} }
@ -242,7 +234,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
} }
// TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => { private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
const list = ev.target as HTMLDivElement; const list = ev.target as HTMLDivElement;
this.handleStickyHeaders(list); this.handleStickyHeaders(list);
@ -274,6 +265,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 +345,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"
@ -374,8 +374,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
onResize={this.onResize} onResize={this.onResize}
/>; />;
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
const containerClasses = classNames({ const containerClasses = classNames({
"mx_LeftPanel2": true, "mx_LeftPanel2": true,
"mx_LeftPanel2_hasTagPanel": !!tagPanel, "mx_LeftPanel2_hasTagPanel": !!tagPanel,

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,20 +25,11 @@ 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
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps { 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 +106,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

@ -170,6 +170,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
ev.stopPropagation(); ev.stopPropagation();
// TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038 // TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038
// Note: You'll need to uncomment the button too.
console.log("TODO: Show archived rooms"); console.log("TODO: Show archived rooms");
}; };

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

@ -18,8 +18,6 @@ import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils"; import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common"; import { XOR } from "../../../@types/common";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState"; import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";

View File

@ -27,16 +27,7 @@ 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps { interface IProps {
} }

View File

@ -41,16 +41,7 @@ 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
@ -231,6 +222,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
private renderCommunityInvites(): React.ReactElement[] { private renderCommunityInvites(): React.ReactElement[] {
// TODO: Put community invites in a more sensible place (not in the room list) // TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/riot-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => { return MatrixClientPeg.get().getGroups().filter(g => {
if (g.myMembership !== 'invite') return false; if (g.myMembership !== 'invite') return false;
return !this.searchFilter || this.searchFilter.matches(g.name || ""); return !this.searchFilter || this.searchFilter.matches(g.name || "");

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} 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,16 +48,7 @@ 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -137,9 +128,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
let padding = RESIZE_HANDLE_HEIGHT; let padding = RESIZE_HANDLE_HEIGHT;
// this is used for calculating the max height of the whole container, // this is used for calculating the max height of the whole container,
// and takes into account whether there should be room reserved for the show less button // and takes into account whether there should be room reserved for the show less button
// when fully expanded. Note that the show more button might still be shown when not fully expanded, // when fully expanded. We cannot check against the layout's defaultVisible tile count
// but in this case it will take the space of a tile and we don't need to reserve space for it. // because there are conditions in which we need to know that the 'show more' button
if (this.numTiles > this.layout.defaultVisibleTiles) { // is present while well under the default tile limit.
if (this.numTiles > this.numVisibleTiles) {
padding += SHOW_N_BUTTON_HEIGHT; padding += SHOW_N_BUTTON_HEIGHT;
} }
return padding; return padding;
@ -236,10 +228,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}; };
private onShowAllClick = () => { private onShowAllClick = () => {
// read number of visible tiles before we mutate it
const numVisibleTiles = this.numVisibleTiles;
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding); const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
this.applyHeightChange(newHeight); this.applyHeightChange(newHeight);
this.setState({height: newHeight}, () => { this.setState({height: newHeight}, () => {
this.focusRoomTile(this.numTiles - 1); // focus the top-most new room
this.focusRoomTile(numVisibleTiles);
}); });
}; };
@ -321,25 +316,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,9 +594,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
); );
} }
public render(): React.ReactElement { private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 // 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 {
const visibleTiles = this.renderVisibleTiles(); const visibleTiles = this.renderVisibleTiles();
const classes = classNames({ const classes = classNames({
'mx_RoomSublist2': true, 'mx_RoomSublist2': true,
@ -613,11 +616,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const showMoreAtMinHeight = minTiles < this.numTiles; const showMoreAtMinHeight = minTiles < this.numTiles;
const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0); const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding); const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding); let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({ const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true, 'mx_RoomSublist2_showNButton': true,
}); });
if (this.numTiles > this.layout.defaultVisibleTiles) {
maxTilesPx += SHOW_N_BUTTON_HEIGHT;
}
// If we're hiding rooms, show a 'show more' button to the user. This button // If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present. If the user has all // floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'. // tiles visible, it becomes 'show less'.
@ -704,7 +711,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,16 +55,7 @@ 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps { interface IProps {
room: Room; room: Room;
@ -124,7 +115,6 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
export default class RoomTile2 extends React.Component<IProps, IState> { export default class RoomTile2 extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private roomTileRef = createRef<HTMLDivElement>(); private roomTileRef = createRef<HTMLDivElement>();
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -276,6 +266,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();
@ -299,7 +300,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
await setRoomNotifsState(this.props.room.roomId, newState); await setRoomNotifsState(this.props.room.roomId, newState);
} catch (error) { } catch (error) {
// TODO: some form of error notification to the user to inform them that their state change failed. // TODO: some form of error notification to the user to inform them that their state change failed.
// https://github.com/vector-im/riot-web/issues/14281 // See https://github.com/vector-im/riot-web/issues/14281
console.error(error); console.error(error);
} }
@ -315,7 +316,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;
} }
@ -387,8 +388,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
private renderGeneralMenu(): React.ReactElement { private renderGeneralMenu(): React.ReactElement {
if (!this.showContextMenu) return null; // no menu to show if (!this.showContextMenu) return null; // no menu to show
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room); const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
const isFavorite = roomTags.includes(DefaultTagID.Favourite); const isFavorite = roomTags.includes(DefaultTagID.Favourite);
@ -397,7 +396,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">
@ -441,8 +453,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
} }
public render(): React.ReactElement { public render(): React.ReactElement {
// TODO: Invites: https://github.com/vector-im/riot-web/issues/14198
const classes = classNames({ const classes = classNames({
'mx_RoomTile2': true, 'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected, 'mx_RoomTile2_selected': this.state.selected,
@ -471,7 +481,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
); );
} }
// TODO: the original RoomTile uses state for the room name. Do we need to?
let name = this.props.room.name; let name = this.props.room.name;
if (typeof name !== 'string') name = ''; if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon

View File

@ -34,6 +34,7 @@ interface IState {
hover: boolean; hover: boolean;
} }
// TODO: Remove with community invites in the room list: https://github.com/vector-im/riot-web/issues/14456
export default class TemporaryTile extends React.Component<IProps, IState> { export default class TemporaryTile extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);

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

@ -21,6 +21,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher"; import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays"; import { arrayHasDiff } from "../utils/arrays";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
const MAX_ROOMS = 20; // arbitrary const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
@ -51,13 +52,17 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
public get visible(): boolean { public get visible(): boolean {
return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20; return this.state.enabled && this.meetsRoomRequirement;
}
private get meetsRoomRequirement(): boolean {
return this.matrixClient.getVisibleRooms().length >= 20;
} }
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 +85,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 +96,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);
@ -99,8 +104,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
private onMyMembership = async (room: Room) => { private onMyMembership = async (room: Room) => {
// We turn on breadcrumbs by default once the user has at least 1 room to show. // Only turn on breadcrumbs is the user hasn't explicitly turned it off again.
if (!this.state.enabled) { const settingValueRaw = SettingsStore.getValue("breadcrumbs", null, /*excludeDefault=*/true);
if (this.meetsRoomRequirement && isNullOrUndefined(settingValueRaw)) {
await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true); await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
} }
}; };

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

@ -17,7 +17,7 @@ limitations under the License.
import { NotificationColor } from "./NotificationColor"; import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable"; import { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import { EffectiveMembership, getEffectiveMembership } from "../room-list/membership"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";

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

@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import TagOrderStore from "../TagOrderStore"; import TagOrderStore from "../TagOrderStore";
import { AsyncStore } from "../AsyncStore";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
@ -29,11 +28,11 @@ import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
import { TagWatcher } from "./TagWatcher"; import { TagWatcher } from "./TagWatcher";
import RoomViewStore from "../RoomViewStore"; import RoomViewStore from "../RoomViewStore";
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
import { EffectiveMembership, getEffectiveMembership } from "./membership"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { ListLayout } from "./ListLayout";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import RoomListLayoutStore from "./RoomListLayoutStore"; import RoomListLayoutStore from "./RoomListLayoutStore";
import { MarkedExecution } from "../../utils/MarkedExecution"; import { MarkedExecution } from "../../utils/MarkedExecution";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
interface IState { interface IState {
tagsEnabled?: boolean; tagsEnabled?: boolean;
@ -45,8 +44,13 @@ 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 AsyncStoreWithClient<ActionPayload> {
private _matrixClient: MatrixClient; /**
* 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 initialListsGenerated = false; private initialListsGenerated = false;
private enabled = false; private enabled = false;
private algorithm = new Algorithm(); private algorithm = new Algorithm();
@ -74,12 +78,51 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
} }
public get matrixClient(): MatrixClient { public get matrixClient(): MatrixClient {
return this._matrixClient; return super.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.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);
// Reset state without causing updates as the client will have been destroyed
// and downstream code will throw NPE errors.
await this.reset(null, true);
}
// Public for test usage. Do not call this.
public async makeReady(forcedClient?: MatrixClient) {
if (forcedClient) {
super.matrixClient = forcedClient;
}
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
this.checkEnabled();
if (!this.enabled) return;
// 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 +142,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,48 +165,32 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
if (trigger) this.updateFn.trigger(); if (trigger) this.updateFn.trigger();
} }
protected onDispatch(payload: ActionPayload) { protected async onReady(): Promise<any> {
await this.makeReady();
}
protected async onNotReady(): Promise<any> {
await this.resetStore();
}
protected async onAction(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));
} }
protected async onDispatchAsync(payload: ActionPayload) { protected async onDispatchAsync(payload: ActionPayload) {
if (payload.action === 'MatrixActions.sync') {
// Filter out anything that isn't the first PREPARED sync.
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
return;
}
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14231
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
}
// TODO: Remove this once the RoomListStore becomes default // TODO: Remove this once the RoomListStore becomes default
if (!this.enabled) return; if (!this.enabled) return;
if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { // Everything here requires a MatrixClient or some sort of logical readiness.
// Reset state without causing updates as the client will have been destroyed
// and downstream code will throw NPE errors.
await this.reset(null, true);
this._matrixClient = null;
this.initialListsGenerated = false; // we'll want to regenerate them
}
// Everything below here requires a MatrixClient or some sort of logical readiness.
const logicallyReady = this.matrixClient && this.initialListsGenerated; const logicallyReady = this.matrixClient && this.initialListsGenerated;
if (!logicallyReady) return; if (!logicallyReady) return;
@ -390,7 +417,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
// logic must match calculateListOrder // logic must match calculateListOrder
private calculateTagSorting(tagId: TagID): SortAlgorithm { private calculateTagSorting(tagId: TagID): SortAlgorithm {
const defaultSort = SortAlgorithm.Alphabetic; const isDefaultRecent = tagId === DefaultTagID.Invite || tagId === DefaultTagID.DM;
const defaultSort = isDefaultRecent ? SortAlgorithm.Recent : SortAlgorithm.Alphabetic;
const settingAlphabetical = SettingsStore.getValue("RoomList.orderAlphabetically", null, true); const settingAlphabetical = SettingsStore.getValue("RoomList.orderAlphabetically", null, true);
const definedSort = this.getTagSorting(tagId); const definedSort = this.getTagSorting(tagId);
const storedSort = this.getStoredTagSorting(tagId); const storedSort = this.getStoredTagSorting(tagId);
@ -496,10 +524,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

@ -30,12 +30,10 @@ import {
SortAlgorithm SortAlgorithm
} from "./models"; } from "./models";
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../membership"; import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering"; import { getListAlgorithmInstance } from "./list-ordering";
// TODO: Add locking support to avoid concurrent writes? https://github.com/vector-im/riot-web/issues/14235
/** /**
* Fired when the Algorithm has determined a list has been updated. * Fired when the Algorithm has determined a list has been updated.
*/ */
@ -698,8 +696,8 @@ export class Algorithm extends EventEmitter {
} }
} }
let didTagChange = false;
if (cause === RoomUpdateCause.PossibleTagChange) { if (cause === RoomUpdateCause.PossibleTagChange) {
let didTagChange = false;
const oldTags = this.roomIdsToTags[room.roomId] || []; const oldTags = this.roomIdsToTags[room.roomId] || [];
const newTags = this.getTagsForRoom(room); const newTags = this.getTagsForRoom(room);
const diff = arrayDiff(oldTags, newTags); const diff = arrayDiff(oldTags, newTags);
@ -713,6 +711,11 @@ export class Algorithm extends EventEmitter {
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
this.cachedRooms[rmTag] = algorithm.orderedRooms; this.cachedRooms[rmTag] = algorithm.orderedRooms;
// Later on we won't update the filtered rooms or sticky room for removed
// tags, so do so now.
this.recalculateFilteredRoomsForTag(rmTag);
this.recalculateStickyRoom(rmTag);
} }
for (const addTag of diff.added) { for (const addTag of diff.added) {
if (!window.mx_QuietRoomListLogging) { if (!window.mx_QuietRoomListLogging) {
@ -812,7 +815,7 @@ export class Algorithm extends EventEmitter {
return false; return false;
} }
let changed = false; let changed = didTagChange;
for (const tag of tags) { for (const tag of tags) {
const algorithm: OrderingAlgorithm = this.algorithms[tag]; const algorithm: OrderingAlgorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`); if (!algorithm) throw new Error(`No algorithm for ${tag}`);

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

@ -55,7 +55,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
} }
} }
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14035 // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14457
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);

View File

@ -17,8 +17,6 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models"; import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm"; import { IAlgorithm } from "./IAlgorithm";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import * as Unread from "../../../../Unread";
/** /**
* Sorts rooms according to the browser's determination of alphabetic. * Sorts rooms according to the browser's determination of alphabetic.

View File

@ -19,7 +19,7 @@ import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm"; import { IAlgorithm } from "./IAlgorithm";
import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import * as Unread from "../../../../Unread"; import * as Unread from "../../../../Unread";
import { EffectiveMembership, getEffectiveMembership } from "../../membership"; import { EffectiveMembership, getEffectiveMembership } from "../../../../utils/membership";
/** /**
* Sorts rooms according to the last event's timestamp in each room that seems * Sorts rooms according to the last event's timestamp in each room that seems
@ -33,12 +33,17 @@ export class RecentAlgorithm implements IAlgorithm {
// of the rooms to each other. // of the rooms to each other.
// TODO: We could probably improve the sorting algorithm here by finding changes. // TODO: We could probably improve the sorting algorithm here by finding changes.
// See https://github.com/vector-im/riot-web/issues/14035 // See https://github.com/vector-im/riot-web/issues/14459
// For example, if we spent a little bit of time to determine which elements have // For example, if we spent a little bit of time to determine which elements have
// actually changed (probably needs to be done higher up?) then we could do an // actually changed (probably needs to be done higher up?) then we could do an
// insertion sort or similar on the limited set of changes. // insertion sort or similar on the limited set of changes.
const myUserId = MatrixClientPeg.get().getUserId(); // TODO: Don't assume we're using the same client as the peg
// See https://github.com/vector-im/riot-web/issues/14458
let myUserId = '';
if (MatrixClientPeg.get()) {
myUserId = MatrixClientPeg.get().getUserId();
}
const tsCache: { [roomId: string]: number } = {}; const tsCache: { [roomId: string]: number } = {};
const getLastTs = (r: Room) => { const getLastTs = (r: Room) => {
@ -68,7 +73,6 @@ export class RecentAlgorithm implements IAlgorithm {
const ev = r.timeline[i]; const ev = r.timeline[i];
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
// TODO: Don't assume we're using the same client as the peg
if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) { if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) {
return ev.getTs(); return ev.getTs();
} }

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};