mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Merge branch 'develop' into element
This commit is contained in:
commit
4fe4788c2e
39 changed files with 357 additions and 339 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -48,15 +48,15 @@ limitations under the License.
|
|||
}
|
||||
|
||||
&.mx_NotificationBadge_2char {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 16px;
|
||||
width: $font-16px;
|
||||
height: $font-16px;
|
||||
border-radius: $font-16px;
|
||||
}
|
||||
|
||||
&.mx_NotificationBadge_3char {
|
||||
width: 26px;
|
||||
height: 16px;
|
||||
border-radius: 16px;
|
||||
width: $font-26px;
|
||||
height: $font-16px;
|
||||
border-radius: $font-16px;
|
||||
}
|
||||
|
||||
// The following is the floating badge
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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
|
||||
|
@ -48,12 +48,6 @@ limitations under the License.
|
|||
height: 24px;
|
||||
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 {
|
||||
flex: 1;
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
|
@ -198,6 +201,8 @@ limitations under the License.
|
|||
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
mask-image: linear-gradient(0deg, transparent, black 3px);
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_resizerHandles_showNButton {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -35,16 +35,7 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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. *
|
||||
*******************************************************************/
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -111,6 +102,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
const newVal = BreadcrumbsStore.instance.visible;
|
||||
if (newVal !== this.state.showBreadcrumbs) {
|
||||
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.
|
||||
for (const header of targetStyles.keys()) {
|
||||
const style = targetStyles.get(header);
|
||||
const headerContainer = header.parentElement; // .mx_RoomSublist2_headerContainer
|
||||
|
||||
if (style.makeInvisible) {
|
||||
// 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) {
|
||||
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")) {
|
||||
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 (!header.classList.contains("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`;
|
||||
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")) {
|
||||
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) {
|
||||
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>) => {
|
||||
const list = ev.target as HTMLDivElement;
|
||||
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) => {
|
||||
let element = this.focusedElement;
|
||||
|
||||
|
@ -346,6 +345,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
onQueryUpdate={this.onSearch}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onVerticalArrow={this.onKeyDown}
|
||||
onEnter={this.onEnter}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_LeftPanel2_exploreButton"
|
||||
|
@ -374,8 +374,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
onResize={this.onResize}
|
||||
/>;
|
||||
|
||||
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
|
||||
|
||||
const containerClasses = classNames({
|
||||
"mx_LeftPanel2": true,
|
||||
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
|
||||
|
|
|
@ -668,8 +668,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
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 = (
|
||||
<LeftPanel2
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
|
|
|
@ -50,7 +50,7 @@ import PageTypes from '../../PageTypes';
|
|||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
|
@ -74,6 +74,7 @@ import {
|
|||
} from "../../toasts/AnalyticsToast";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -460,7 +461,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
|
||||
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<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
|
||||
* @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;
|
||||
}
|
||||
|
||||
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<IProps, IState> {
|
|||
}
|
||||
});
|
||||
cli.on("crypto.warning", (type) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
switch (type) {
|
||||
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
|
|
@ -25,20 +25,11 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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 {
|
||||
onQueryUpdate: (newQuery: string) => void;
|
||||
isMinimized: boolean;
|
||||
onVerticalArrow(ev: React.KeyboardEvent);
|
||||
onEnter(ev: React.KeyboardEvent);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -115,6 +106,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -170,6 +170,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
|
||||
// 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");
|
||||
};
|
||||
|
||||
|
|
|
@ -99,6 +99,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
defaultToInitialLetter = true,
|
||||
onClick,
|
||||
inputRef,
|
||||
className,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
@ -138,7 +139,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
<AccessibleButton
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className="mx_BaseAvatar"
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
onClick={onClick}
|
||||
inputRef={inputRef}
|
||||
>
|
||||
|
@ -149,7 +150,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
} else {
|
||||
return (
|
||||
<span
|
||||
className="mx_BaseAvatar"
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
ref={inputRef}
|
||||
{...otherProps}
|
||||
role="presentation"
|
||||
|
@ -164,7 +165,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
if (onClick !== null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
|
@ -180,7 +181,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
|
|
|
@ -126,16 +126,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
<BaseAvatar {...otherProps}
|
||||
name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls}
|
||||
onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import React from "react";
|
|||
import classNames from "classnames";
|
||||
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
||||
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 { XOR } from "../../../@types/common";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
|
|
@ -27,16 +27,7 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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. *
|
||||
*******************************************************************/
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
|
|
@ -41,16 +41,7 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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. *
|
||||
*******************************************************************/
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -231,6 +222,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
|
||||
private renderCommunityInvites(): React.ReactElement[] {
|
||||
// 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 => {
|
||||
if (g.myMembership !== 'invite') return false;
|
||||
return !this.searchFilter || this.searchFilter.matches(g.name || "");
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import {createRef} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -48,16 +48,7 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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. *
|
||||
*******************************************************************/
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // 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;
|
||||
// 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
|
||||
// when fully expanded. Note that the show more button might still be shown when not fully expanded,
|
||||
// but in this case it will take the space of a tile and we don't need to reserve space for it.
|
||||
if (this.numTiles > this.layout.defaultVisibleTiles) {
|
||||
// when fully expanded. We cannot check against the layout's defaultVisible tile count
|
||||
// because there are conditions in which we need to know that the 'show more' button
|
||||
// is present while well under the default tile limit.
|
||||
if (this.numTiles > this.numVisibleTiles) {
|
||||
padding += SHOW_N_BUTTON_HEIGHT;
|
||||
}
|
||||
return padding;
|
||||
|
@ -236,10 +228,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onShowAllClick = () => {
|
||||
// read number of visible tiles before we mutate it
|
||||
const numVisibleTiles = this.numVisibleTiles;
|
||||
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
||||
this.applyHeightChange(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>) => {
|
||||
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,9 +594,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
|
||||
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
|
||||
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
||||
// this fixes https://github.com/vector-im/riot-web/issues/14413
|
||||
(e.target as HTMLDivElement).scrollTop = 0;
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const visibleTiles = this.renderVisibleTiles();
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist2': true,
|
||||
|
@ -613,11 +616,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
const showMoreAtMinHeight = minTiles < this.numTiles;
|
||||
const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
|
||||
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({
|
||||
'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
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
|
@ -704,7 +711,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist2_resizeBox"
|
||||
enable={handles}
|
||||
>
|
||||
<div className="mx_RoomSublist2_tiles">
|
||||
<div className="mx_RoomSublist2_tiles" onScroll={this.onScrollPrevent}>
|
||||
{visibleTiles}
|
||||
</div>
|
||||
{showNButton}
|
||||
|
|
|
@ -55,16 +55,7 @@ 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
|
||||
|
||||
/*******************************************************************
|
||||
* 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. *
|
||||
*******************************************************************/
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -124,7 +115,6 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
|
|||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -276,6 +266,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
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();
|
||||
|
@ -299,7 +300,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
await setRoomNotifsState(this.props.room.roomId, newState);
|
||||
} catch (error) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -315,7 +316,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
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;
|
||||
}
|
||||
|
@ -387,8 +388,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
private renderGeneralMenu(): React.ReactElement {
|
||||
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 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");
|
||||
|
||||
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 {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
||||
<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 {
|
||||
// TODO: Invites: https://github.com/vector-im/riot-web/issues/14198
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile2': true,
|
||||
'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;
|
||||
if (typeof name !== 'string') name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
|
|
@ -34,6 +34,7 @@ interface IState {
|
|||
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> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
|||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
|
||||
const MAX_ROOMS = 20; // arbitrary
|
||||
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 {
|
||||
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) {
|
||||
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 +85,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
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 +96,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
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);
|
||||
|
@ -99,8 +104,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
private onMyMembership = async (room: Room) => {
|
||||
// We turn on breadcrumbs by default once the user has at least 1 room to show.
|
||||
if (!this.state.enabled) {
|
||||
// Only turn on breadcrumbs is the user hasn't explicitly turned it off again.
|
||||
const settingValueRaw = SettingsStore.getValue("breadcrumbs", null, /*excludeDefault=*/true);
|
||||
if (this.meetsRoomRequirement && isNullOrUndefined(settingValueRaw)) {
|
||||
await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { NotificationColor } from "./NotificationColor";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../room-list/membership";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
|
|
@ -192,7 +192,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
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') {
|
||||
|
|
|
@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||
import TagOrderStore from "../TagOrderStore";
|
||||
import { AsyncStore } from "../AsyncStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
@ -29,11 +28,11 @@ import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
|
|||
import { TagWatcher } from "./TagWatcher";
|
||||
import RoomViewStore from "../RoomViewStore";
|
||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "./membership";
|
||||
import { ListLayout } from "./ListLayout";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||
import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -45,8 +44,13 @@ interface IState {
|
|||
*/
|
||||
export const LISTS_UPDATE_EVENT = "lists_update";
|
||||
|
||||
export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||
private _matrixClient: MatrixClient;
|
||||
export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
|
||||
/**
|
||||
* Set to true if you're running tests on the store. Should not be touched in
|
||||
* any other environment.
|
||||
*/
|
||||
public static TEST_MODE = false;
|
||||
|
||||
private initialListsGenerated = false;
|
||||
private enabled = false;
|
||||
private algorithm = new Algorithm();
|
||||
|
@ -74,12 +78,51 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|||
}
|
||||
|
||||
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() {
|
||||
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 +142,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|||
* 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,48 +165,32 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|||
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
|
||||
// us to instead wait for a more convenient time to run our updates.
|
||||
setImmediate(() => this.onDispatchAsync(payload));
|
||||
}
|
||||
|
||||
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
|
||||
if (!this.enabled) return;
|
||||
|
||||
if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
|
||||
// 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.
|
||||
// Everything here requires a MatrixClient or some sort of logical readiness.
|
||||
const logicallyReady = this.matrixClient && this.initialListsGenerated;
|
||||
if (!logicallyReady) return;
|
||||
|
||||
|
@ -390,7 +417,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|||
|
||||
// logic must match calculateListOrder
|
||||
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 definedSort = this.getTagSorting(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.
|
||||
*
|
||||
* 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 = {};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -30,12 +30,10 @@ import {
|
|||
SortAlgorithm
|
||||
} from "./models";
|
||||
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 { 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.
|
||||
*/
|
||||
|
@ -698,8 +696,8 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
let didTagChange = false;
|
||||
if (cause === RoomUpdateCause.PossibleTagChange) {
|
||||
let didTagChange = false;
|
||||
const oldTags = this.roomIdsToTags[room.roomId] || [];
|
||||
const newTags = this.getTagsForRoom(room);
|
||||
const diff = arrayDiff(oldTags, newTags);
|
||||
|
@ -713,6 +711,11 @@ export class Algorithm extends EventEmitter {
|
|||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
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) {
|
||||
if (!window.mx_QuietRoomListLogging) {
|
||||
|
@ -812,7 +815,7 @@ export class Algorithm extends EventEmitter {
|
|||
return false;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
let changed = didTagChange;
|
||||
for (const tag of tags) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
|
|
@ -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<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
|
||||
// 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.
|
||||
|
||||
|
|
|
@ -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
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as Unread from "../../../../Unread";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the browser's determination of alphabetic.
|
||||
|
|
|
@ -19,7 +19,7 @@ import { TagID } from "../../models";
|
|||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
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
|
||||
|
@ -33,12 +33,17 @@ export class RecentAlgorithm implements IAlgorithm {
|
|||
// of the rooms to each other.
|
||||
|
||||
// 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
|
||||
// actually changed (probably needs to be done higher up?) then we could do an
|
||||
// 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 getLastTs = (r: Room) => {
|
||||
|
@ -68,7 +73,6 @@ export class RecentAlgorithm implements IAlgorithm {
|
|||
const ev = r.timeline[i];
|
||||
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)) {
|
||||
return ev.getTs();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
<DragDropContext>
|
||||
<WrappedRoomList searchFilter="" />
|
||||
<WrappedRoomList searchFilter="" onResize={() => {}} />
|
||||
</DragDropContext>
|
||||
, 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();
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue