diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 4199541fd6..2c5ef84322 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index d3b8ec1914..fd050cfd7c 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 { width: 100%; diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index dff69b49fa..c4638c583d 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 { // The sublist is a column of rows, essentially diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index df4182246a..9b967c09b8 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -14,17 +14,13 @@ See the License for the specific language governing permissions and 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 .mx_RoomTile2 { margin-bottom: 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 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 .mx_RoomTile2_iconBell::before { mask-image: url('$(res)/img/element-icons/notifications.svg'); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f667c47b3c..11c955749d 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -660,7 +660,7 @@ export const Commands = [ if (args) { const cli = MatrixClientPeg.get(); - const matches = args.match(/^(\S+)$/); + const matches = args.match(/^(@[^:]+:\S+)$/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); @@ -690,7 +690,7 @@ export const Commands = [ if (args) { const cli = MatrixClientPeg.get(); - const matches = args.match(/^(\S+)$/); + const matches = args.match(/(^@[^:]+:\S+$)/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 1f681c54c4..c03c839cdd 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -35,8 +35,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomLi import {Key} from "../../Keyboard"; 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/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/14367 /******************************************************************* * CAUTION * @@ -274,6 +274,14 @@ export default class LeftPanel2 extends React.Component { } }; + private onEnter = () => { + const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile2"); + if (firstRoom) { + firstRoom.click(); + this.onSearch(""); // clear the search field + } + }; + private onMoveFocus = (up: boolean) => { let element = this.focusedElement; @@ -346,6 +354,7 @@ export default class LeftPanel2 extends React.Component { onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} onVerticalArrow={this.onKeyDown} + onEnter={this.onEnter} /> { disabled={this.props.leftDisabled} /> ); - if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { - // TODO: Supply props like collapsed and disabled to LeftPanel2 + if (SettingsStore.getValue("feature_new_room_list")) { leftPanel = ( { onAction = (payload) => { // console.log(`MatrixClientPeg.onAction: ${payload.action}`); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // Start the onboarding process for certain actions @@ -554,6 +554,9 @@ export default class MatrixChat extends React.PureComponent { case 'leave_room': this.leaveRoom(payload.room_id); break; + case 'forget_room': + this.forgetRoom(payload.room_id); + break; case 'reject_invite': Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { title: _t('Reject invitation'), @@ -1060,7 +1063,6 @@ export default class MatrixChat extends React.PureComponent { private leaveRoom(roomId: string) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); @@ -1124,6 +1126,21 @@ export default class MatrixChat extends React.PureComponent { }); } + 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 * @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 { return; } - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Signed out', '', ErrorDialog, { title: _t('Signed Out'), 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 { } }); cli.on("crypto.warning", (type) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (type) { case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': const brand = SdkConfig.get().brand; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 8655db2e8c..231bd92ddf 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -25,7 +25,7 @@ import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; 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 * @@ -39,6 +39,7 @@ interface IProps { onQueryUpdate: (newQuery: string) => void; isMinimized: boolean; onVerticalArrow(ev: React.KeyboardEvent); + onEnter(ev: React.KeyboardEvent); } interface IState { @@ -115,6 +116,8 @@ export default class RoomSearch extends React.PureComponent { defaultDispatcher.fire(Action.FocusComposer); } else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { this.props.onVerticalArrow(ev); + } else if (ev.key === Key.ENTER) { + this.props.onEnter(ev); } }; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c493cdcace..7dc2d57ff0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1380,15 +1380,9 @@ export default createReactClass({ }, onForgetClick: function() { - this.context.forget(this.state.room.roomId).then(function() { - dis.dispatch({ action: 'view_next_room' }); - }, function(err) { - 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 }), - }); + dis.dispatch({ + action: 'forget_room', + room_id: this.state.room.roomId, }); }, diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index aa2c0ea954..7f30a7a377 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -99,6 +99,7 @@ const BaseAvatar = (props: IProps) => { defaultToInitialLetter = true, onClick, inputRef, + className, ...otherProps } = props; @@ -138,7 +139,7 @@ const BaseAvatar = (props: IProps) => { @@ -149,7 +150,7 @@ const BaseAvatar = (props: IProps) => { } else { return ( { if (onClick !== null) { return ( { } else { return ( { }; public render() { - /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; const roomName = room ? room.name : oobData.name; return ( - ); } diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 602c697d8a..7d0584ef66 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -27,8 +27,8 @@ import RoomListStore from "../../../stores/room-list/RoomListStore2"; import { DefaultTagID } from "../../../stores/room-list/models"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 +// 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/14367 /******************************************************************* * CAUTION * diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 440b0d5bb3..67787963a3 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -41,8 +41,8 @@ import { Action } from "../../../dispatcher/actions"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 +// 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/14367 /******************************************************************* * CAUTION * diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c351384be4..c22e6cd807 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -17,7 +17,7 @@ limitations under the License. */ import * as React from "react"; -import { createRef } from "react"; +import {createRef, UIEventHandler} from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; @@ -48,8 +48,8 @@ import { polyfillTouchEvent } from "../../../@types/polyfill"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; -// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 +// 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/14367 /******************************************************************* * CAUTION * @@ -321,25 +321,29 @@ export default class RoomSublist2 extends React.Component { } }; - private onHeaderClick = (ev: React.MouseEvent) => { - let target = ev.target as HTMLDivElement; - 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; + private onHeaderClick = () => { + const possibleSticky = this.headerButton.current.parentElement; const sublist = possibleSticky.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 isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky'); - if (isSticky && !isAtTop) { + const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight; + 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 sublist.scrollIntoView({behavior: 'smooth'}); } else { // on screen - toggle collapse + const isExpanded = this.state.isExpanded; 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 { ); } + private onScrollPrevent(e: React.UIEvent) { + // 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 { // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 @@ -704,7 +714,7 @@ export default class RoomSublist2 extends React.Component { className="mx_RoomSublist2_resizeBox" enable={handles} > -
+
{visibleTiles}
{showNButton} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index a7dc983fa6..ca2f8865f9 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -55,8 +55,8 @@ import {ActionPayload} from "../../../dispatcher/payloads"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; -// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 -// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 +// 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/14367 /******************************************************************* * CAUTION * @@ -276,6 +276,17 @@ export default class RoomTile2 extends React.Component { 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) => { ev.preventDefault(); ev.stopPropagation(); @@ -315,7 +326,7 @@ export default class RoomTile2 extends React.Component { private onClickMute = ev => this.saveNotifState(ev, MUTE); 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 return null; } @@ -397,7 +408,20 @@ export default class RoomTile2 extends React.Component { const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite"); let contextMenu = null; - if (this.state.generalMenuPosition) { + if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) { + contextMenu = ( + +
+
+ + + {_t("Forget Room")} + +
+
+
+ ); + } else if (this.state.generalMenuPosition) { contextMenu = (
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index 40b622cf37..abe6b48712 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -32,12 +32,12 @@ export default class PreferencesUserSettingsTab extends React.Component { '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 = [ '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 = () => { if (RoomListStoreTempProxy.isUsingNewStore()) { return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cefb34ece9..91896a0582 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1239,6 +1239,7 @@ "Favourited": "Favourited", "Favourite": "Favourite", "Leave Room": "Leave Room", + "Forget Room": "Forget Room", "Room options": "Room options", "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.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5f84b11247..49a7140acf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -147,7 +147,8 @@ export const SETTINGS = { default: false, }, "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)"), supportedLevels: LEVELS_FEATURE, default: true, diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index e51303b722..48ef75cb59 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -57,7 +57,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { 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 (payload.action === 'setting_updated') { @@ -80,7 +80,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } 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; await this.updateRooms(); @@ -91,7 +91,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } 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; this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index c19b2f8bc2..1861085a27 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -99,7 +99,7 @@ class RoomListStore extends Store { } _checkDisabled() { - this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + this.disabled = SettingsStore.getValue("feature_new_room_list"); if (this.disabled) { console.warn("👋 legacy room list store has been disabled"); } diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 01ddde2e17..ea7fa830cd 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -192,7 +192,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { 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 (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 5ba448b93c..8686a3a054 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -46,6 +46,12 @@ interface IState { export const LISTS_UPDATE_EVENT = "lists_update"; export class RoomListStore2 extends AsyncStore { + /** + * 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 initialListsGenerated = false; private enabled = false; @@ -77,9 +83,43 @@ export class RoomListStore2 extends AsyncStore { 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() { - this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + this.enabled = SettingsStore.getValue("feature_new_room_list"); if (this.enabled) { console.log("⚡ new room list store engaged"); } @@ -99,7 +139,7 @@ export class RoomListStore2 extends AsyncStore { * be used if the calling code will manually trigger the update. */ 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 const activeRoomId = RoomViewStore.getRoomId(); @@ -122,7 +162,14 @@ export class RoomListStore2 extends AsyncStore { 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 // us to instead wait for a more convenient time to run our updates. setImmediate(() => this.onDispatchAsync(payload)); @@ -135,19 +182,7 @@ export class RoomListStore2 extends AsyncStore { 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(); + await this.makeReady(payload.matrixClient); return; // no point in running the next conditions - they won't match } @@ -496,10 +531,13 @@ export class RoomListStore2 extends AsyncStore { /** * 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 * 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"); const sorts: ITagSortingMap = {}; diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 86aff178ee..2a5348ab6e 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -24,11 +24,11 @@ import { ITagMap } from "./algorithms/models"; * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when * 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 { public static isUsingNewStore(): boolean { - return SettingsStore.isFeatureEnabled("feature_new_room_list"); + return SettingsStore.getValue("feature_new_room_list"); } public static addListener(handler: () => void): RoomListStoreTempToken { diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 88789d3a50..b3f1c2b146 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -19,47 +19,29 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomUpdateCause, TagID } from "../../models"; import { SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; -import * as Unread from '../../../../Unread'; import { OrderingAlgorithm } from "./OrderingAlgorithm"; - -/** - * 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", -} +import { NotificationColor } from "../../../notifications/NotificationColor"; +import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore"; interface ICategorizedRoomMap { // @ts-ignore - TS wants this to be a string, but we know better - [category: Category]: Room[]; + [category: NotificationColor]: Room[]; } interface ICategoryIndex { // @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 // comments! Check the usage of Category carefully to figure out what needs changing // 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 @@ -92,10 +74,10 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // noinspection JSMethodCanBeStatic private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { const map: ICategorizedRoomMap = { - [Category.Red]: [], - [Category.Grey]: [], - [Category.Bold]: [], - [Category.Idle]: [], + [NotificationColor.Red]: [], + [NotificationColor.Grey]: [], + [NotificationColor.Bold]: [], + [NotificationColor.None]: [], }; for (const room of rooms) { const category = this.getRoomCategory(room); @@ -105,25 +87,11 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } // noinspection JSMethodCanBeStatic - private getRoomCategory(room: Room): Category { - // Function implementation borrowed from old RoomListStore - - const mentions = room.getUnreadNotificationCount('highlight') > 0; - if (mentions) { - 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; + private getRoomCategory(room: Room): NotificationColor { + // 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 state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId); + return state.color; } public async setRooms(rooms: Room[]): Promise { @@ -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 // 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 @@ -234,7 +202,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } // 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++) { const category = CATEGORY_ORDER[i]; const isLast = i === (CATEGORY_ORDER.length - 1); @@ -250,7 +218,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } // 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 // 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. @@ -261,7 +229,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { 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 // the target and not the target itself. diff --git a/src/utils/pillify.js b/src/utils/pillify.js index f708ab7770..cb140c61a4 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.js @@ -111,7 +111,7 @@ export function pillifyLinks(nodes, mxEvent, pills) { type={Pill.TYPE_AT_ROOM_MENTION} inMessage={true} room={room} - shouldShowPillAvatar={true} + shouldShowPillAvatar={shouldShowPillAvatar} />; ReactDOM.render(pill, pillContainer); diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index d0694a8437..e84f943708 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -1,7 +1,6 @@ import React from 'react'; import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; -import lolex from 'lolex'; 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 {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() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } +function waitForRoomListStoreUpdate() { + return new Promise((resolve) => { + RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve()); + }); +} describe('RoomList', () => { function createRoom(opts) { @@ -34,7 +40,6 @@ describe('RoomList', () => { let client = null; let root = null; const myUserId = '@me:domain'; - let clock = null; const movingRoomId = '!someroomid'; let movingRoom; @@ -43,25 +48,25 @@ describe('RoomList', () => { let myMember; let myOtherMember; - beforeEach(function() { + beforeEach(async function(done) { + RoomListStore2.TEST_MODE = true; + TestUtils.stubClient(); client = MatrixClientPeg.get(); client.credentials = {userId: myUserId}; //revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value client.getUserId = MatrixClient.prototype.getUserId; - clock = lolex.install(); - DMRoomMap.makeShared(); parentDiv = document.createElement('div'); document.body.appendChild(parentDiv); - const RoomList = sdk.getComponent('views.rooms.RoomList'); + const RoomList = sdk.getComponent('views.rooms.RoomList2'); const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); root = ReactDOM.render( - + {}} /> , parentDiv); ReactTestUtils.findRenderedComponentWithType(root, RoomList); @@ -102,23 +107,29 @@ describe('RoomList', () => { }); 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) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); parentDiv = null; } - clock.uninstall(); + await RoomListLayoutStore.instance.resetLayouts(); + await RoomListStore.instance.resetStore(); done(); }); function expectRoomInSubList(room, subListTest) { - const RoomSubList = sdk.getComponent('structures.RoomSubList'); - const RoomTile = sdk.getComponent('views.rooms.RoomTile'); + const RoomSubList = sdk.getComponent('views.rooms.RoomSublist2'); + const RoomTile = sdk.getComponent('views.rooms.RoomTile2'); const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList); const containingSubList = subLists.find(subListTest); @@ -140,20 +151,20 @@ describe('RoomList', () => { expect(expectedRoomTile.props.room).toBe(room); } - function expectCorrectMove(oldTag, newTag) { - const getTagSubListTest = (tag) => { - if (tag === undefined) return (s) => s.props.label.endsWith('Rooms'); - return (s) => s.props.tagName === tag; + function expectCorrectMove(oldTagId, newTagId) { + const getTagSubListTest = (tagId) => { + return (s) => s.props.tagId === tagId; }; // Default to finding the destination sublist with newTag - const destSubListTest = getTagSubListTest(newTag); - const srcSubListTest = getTagSubListTest(oldTag); + const destSubListTest = getTagSubListTest(newTagId); + const srcSubListTest = getTagSubListTest(oldTagId); // Set up the room that will be moved such that it has the correct state for a room in - // the section for oldTag - if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}}; - if (oldTag === DefaultTagID.DM) { + // the section for oldTagId + if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) { + movingRoom.tags = {[oldTagId]: {}}; + } else if (oldTagId === DefaultTagID.DM) { // Mock inverse m.direct DMRoomMap.shared().roomToUser = { [movingRoom.roomId]: '@someotheruser:domain', @@ -162,17 +173,12 @@ describe('RoomList', () => { dis.dispatch({action: 'MatrixActions.sync', prevState: null, state: 'PREPARED', matrixClient: client}); - clock.runAll(); - expectRoomInSubList(movingRoom, srcSubListTest); 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); } @@ -269,6 +275,12 @@ describe('RoomList', () => { }; 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 dis.dispatch({action: 'select_tag', tag: '+group:domain'}, true); } @@ -277,17 +289,14 @@ describe('RoomList', () => { 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) => { return [movingRoom, otherRoom]; }; GroupStore._notifyListeners(); - // Run through RoomList debouncing - clock.runAll(); - - // By default, the test will - expectRoomInSubList(otherRoom, (s) => s.props.label.endsWith('Rooms')); + await waitForRoomListStoreUpdate(); + expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged); }); itDoesCorrectOptimisticUpdatesForDraggedRoomTiles(); diff --git a/test/end-to-end-tests/src/usecases/accept-invite.js b/test/end-to-end-tests/src/usecases/accept-invite.js index 3f208cc1fc..d38fdcd0db 100644 --- a/test/end-to-end-tests/src/usecases/accept-invite.js +++ b/test/end-to-end-tests/src/usecases/accept-invite.js @@ -15,10 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +const {findSublist} = require("./create-room"); + module.exports = async function acceptInvite(session, name) { session.log.step(`accepts "${name}" invite`); - //TODO: brittle selector - const invitesHandles = await session.queryAll('.mx_RoomTile_name.mx_RoomTile_invite'); + const inviteSublist = await findSublist(session, "invites"); + const invitesHandles = await inviteSublist.$$(".mx_RoomTile2_name"); const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { const text = await session.innerText(inviteHandle); return {inviteHandle, text}; diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index 7e219fd159..24e42b92dd 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -16,21 +16,27 @@ limitations under the License. */ 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(); } +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) { session.log.step(`creates room "${roomName}"`); - const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); - const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); - 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"); + const roomsSublist = await findSublist(session, "rooms"); + const addRoomButton = await roomsSublist.$(".mx_RoomSublist2_auxButton"); await addRoomButton.click(); 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) { session.log.step(`creates DM with ${JSON.stringify(invitees)}`); - const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); - const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); - 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"); + const dmsSublist = await findSublist(session, "people"); + const startChatButton = await dmsSublist.$(".mx_RoomSublist2_auxButton"); await startChatButton.click(); const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea'); @@ -83,4 +83,4 @@ async function createDm(session, invitees) { session.log.done(); } -module.exports = {openRoomDirectory, createRoom, createDm}; +module.exports = {openRoomDirectory, findSublist, createRoom, createDm};