-
+ {roomList}
);
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index e2aa523b8c..148d10fe8d 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -31,7 +31,6 @@ import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
-import RoomListStore from "../../stores/RoomListStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
+import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
+import { DefaultTagID } from "../../stores/room-list/models";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent {
};
onRoomStateEvents = (ev, state) => {
- const roomLists = RoomListStore.getRoomLists();
- if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
+ if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
};
_updateServerNoticeEvents = async () => {
- const roomLists = RoomListStore.getRoomLists();
- if (!roomLists['m.server_notice']) return [];
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
+ if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = [];
- for (const room of roomLists['m.server_notice']) {
+ for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 670a017934..48dc72f4fa 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -60,6 +60,7 @@ import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDisco
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme";
+import { FontWatcher } from '../../FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";
@@ -216,6 +217,7 @@ export default class MatrixChat extends React.PureComponent {
private readonly loggedInView: React.RefObject;
private readonly dispatcherRef: any;
private readonly themeWatcher: ThemeWatcher;
+ private readonly fontWatcher: FontWatcher;
constructor(props, context) {
super(props, context);
@@ -283,8 +285,11 @@ export default class MatrixChat extends React.PureComponent {
this.accountPasswordTimer = null;
this.dispatcherRef = dis.register(this.onAction);
+
this.themeWatcher = new ThemeWatcher();
+ this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
+ this.fontWatcher.start();
this.focusComposer = false;
@@ -367,6 +372,7 @@ export default class MatrixChat extends React.PureComponent {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
+ this.fontWatcher.stop();
window.removeEventListener('resize', this.handleResize);
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 6fbfdb504b..93e4668f66 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent";
+import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@@ -109,14 +110,16 @@ export default class MessagePanel extends React.Component {
showReactions: PropTypes.bool,
};
- constructor() {
- super();
+ // Force props to be loaded for useIRCLayout
+ constructor(props) {
+ super(props);
this.state = {
// previous positions the read marker has been in, so we can
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
+ useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
@@ -169,6 +172,8 @@ export default class MessagePanel extends React.Component {
this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
+
+ this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
}
componentDidMount() {
@@ -178,6 +183,7 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
+ SettingsStore.unwatchSetting(this._layoutWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@@ -196,6 +202,17 @@ export default class MessagePanel extends React.Component {
});
};
+ onLayoutChange = () => {
+ this.setState({
+ useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
+ });
+ }
+
+ useIRCLayout(ircLayoutSelected) {
+ // if room is null we are not in a normal room list
+ return ircLayoutSelected && this.props.room;
+ }
+
/* get the DOM node representing the given event */
getNodeForEventId(eventId) {
if (!this.eventNodes) {
@@ -597,6 +614,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
+ useIRCLayout={this.state.useIRCLayout}
/>
,
@@ -779,6 +797,8 @@ export default class MessagePanel extends React.Component {
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
+ "mx_IRCLayout": this.state.useIRCLayout,
+ "mx_GroupLayout": !this.state.useIRCLayout,
},
);
@@ -792,6 +812,15 @@ export default class MessagePanel extends React.Component {
);
}
+ let ircResizer = null;
+ if (this.state.useIRCLayout) {
+ ircResizer = ;
+ }
+
return (
{ topSpinner }
{ this._getEventTiles() }
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index d1abacd2d8..090f3de22a 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
-import toRem from "../../utils/rem";
+import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore
const debug = false;
@@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
setHeight = (height) => {
if (this._subList.current) {
- this._subList.current.style.height = toRem(height);
+ this._subList.current.style.height = toPx(height);
}
this._updateLazyRenderHeight(height);
};
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 4f44c1a169..cb0114b243 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -144,6 +144,11 @@ export default createReactClass({
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier: PropTypes.object,
+
+ /* fixedChildren: allows for children to be passed which are rendered outside
+ * of the wrapper
+ */
+ fixedChildren: PropTypes.node,
},
getDefaultProps: function() {
@@ -881,6 +886,7 @@ export default createReactClass({
return (
+ { this.props.fixedChildren }
{ this.props.children }
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js
index 81fbc08c0f..704e6438c8 100644
--- a/src/components/views/avatars/BaseAvatar.js
+++ b/src/components/views/avatars/BaseAvatar.js
@@ -24,7 +24,7 @@ import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import toRem from "../../../utils/rem";
+import {toPx} from "../../../utils/units";
export default createReactClass({
displayName: 'BaseAvatar',
@@ -166,9 +166,9 @@ export default createReactClass({
const textNode = (
{ initialLetter }
@@ -179,8 +179,8 @@ export default createReactClass({
alt="" title={title} onError={this.onError}
aria-hidden="true"
style={{
- width: toRem(width),
- height: toRem(height)
+ width: toPx(width),
+ height: toPx(height)
}} />
);
if (onClick != null) {
@@ -210,8 +210,8 @@ export default createReactClass({
onClick={onClick}
onError={this.onError}
style={{
- width: toRem(width),
- height: toRem(height),
+ width: toPx(width),
+ height: toPx(height),
}}
title={title} alt=""
inputRef={inputRef}
@@ -224,8 +224,8 @@ export default createReactClass({
src={imageUrl}
onError={this.onError}
style={{
- width: toRem(width),
- height: toRem(height),
+ width: toPx(width),
+ height: toPx(height),
}}
title={title} alt=""
ref={inputRef}
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 1e624f7545..4274c938cc 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -34,9 +34,10 @@ import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
-import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
+import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
+import {DefaultTagID} from "../../../stores/room-list/models";
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
@@ -344,10 +345,10 @@ export default class InviteDialog extends React.PureComponent {
_buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
- // Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
+ // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
- const taggedRooms = RoomListStore.getRoomLists();
- const dmTaggedRooms = taggedRooms[TAG_DM];
+ const taggedRooms = RoomListStoreTempProxy.getRoomLists();
+ const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 210773c524..4592d921a9 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
+import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
@@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_settingsIcon",
,
));
+ tabs.push(new Tab(
+ _td("Appearance"),
+ "mx_UserSettingsDialog_appearanceIcon",
+ ,
+ ));
tabs.push(new Tab(
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
index 7e51e76f6c..a16202ed93 100644
--- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
+++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
@@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
- this.state.backupInfo,
+ this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback },
);
});
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
new file mode 100644
index 0000000000..98f86fd524
--- /dev/null
+++ b/src/components/views/elements/Draggable.tsx
@@ -0,0 +1,84 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+
+interface IProps {
+ className: string,
+ dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
+ onMouseUp: (event: MouseEvent) => void,
+}
+
+interface IState {
+ onMouseMove: (event: MouseEvent) => void,
+ onMouseUp: (event: MouseEvent) => void,
+ location: ILocationState,
+}
+
+export interface ILocationState {
+ currentX: number,
+ currentY: number,
+}
+
+export default class Draggable extends React.Component {
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ onMouseMove: this.onMouseMove.bind(this),
+ onMouseUp: this.onMouseUp.bind(this),
+ location: {
+ currentX: 0,
+ currentY: 0,
+ },
+ };
+ }
+
+ private onMouseDown = (event: MouseEvent): void => {
+ this.setState({
+ location: {
+ currentX: event.clientX,
+ currentY: event.clientY,
+ },
+ });
+
+ document.addEventListener("mousemove", this.state.onMouseMove);
+ document.addEventListener("mouseup", this.state.onMouseUp);
+ console.log("Mouse down")
+ }
+
+ private onMouseUp = (event: MouseEvent): void => {
+ document.removeEventListener("mousemove", this.state.onMouseMove);
+ document.removeEventListener("mouseup", this.state.onMouseUp);
+ this.props.onMouseUp(event);
+ console.log("Mouse up")
+ }
+
+ private onMouseMove(event: MouseEvent): void {
+ console.log("Mouse Move")
+ const newLocation = this.props.dragFunc(this.state.location, event);
+
+ this.setState({
+ location: newLocation,
+ });
+ }
+
+ render() {
+ return
+ }
+
+}
\ No newline at end of file
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
new file mode 100644
index 0000000000..596d46bf36
--- /dev/null
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -0,0 +1,88 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
+import Draggable, {ILocationState} from './Draggable';
+
+interface IProps {
+ // Current room
+ roomId: string,
+ minWidth: number,
+ maxWidth: number,
+};
+
+interface IState {
+ width: number,
+ IRCLayoutRoot: HTMLElement,
+};
+
+export default class IRCTimelineProfileResizer extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
+ IRCLayoutRoot: null,
+ }
+ };
+
+ componentDidMount() {
+ this.setState({
+ IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
+ }, () => this.updateCSSWidth(this.state.width))
+ }
+
+ private dragFunc = (location: ILocationState, event: React.MouseEvent): ILocationState => {
+ const offset = event.clientX - location.currentX;
+ const newWidth = this.state.width + offset;
+
+ console.log({offset})
+ // If we're trying to go smaller than min width, don't.
+ if (newWidth < this.props.minWidth) {
+ return location;
+ }
+
+ if (newWidth > this.props.maxWidth) {
+ return location;
+ }
+
+ this.setState({
+ width: newWidth,
+ });
+
+ this.updateCSSWidth.bind(this)(newWidth);
+
+ return {
+ currentX: event.clientX,
+ currentY: location.currentY,
+ }
+ }
+
+ private updateCSSWidth(newWidth: number) {
+ this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
+ }
+
+ private onMoueUp(event: MouseEvent) {
+ if (this.props.roomId) {
+ SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
+ }
+ }
+
+ render() {
+ return
+ }
+};
\ No newline at end of file
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js
index 7f9bfdebf4..06f025f236 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.js
@@ -156,16 +156,70 @@ export default class PersistedElement extends React.Component {
child.style.display = visible ? 'block' : 'none';
}
+ /*
+ * Clip element bounding rectangle to that of the parent elements.
+ * This is not a full visibility check, but prevents the persisted
+ * element from overflowing parent containers when inside a scrolled
+ * area.
+ */
+ _getClippedBoundingClientRect(element) {
+ let parentElement = element.parentElement;
+ let rect = element.getBoundingClientRect();
+
+ rect = new DOMRect(rect.left, rect.top, rect.width, rect.height);
+
+ while (parentElement) {
+ const parentRect = parentElement.getBoundingClientRect();
+
+ if (parentRect.left > rect.left) {
+ rect.width = rect.width - (parentRect.left - rect.left);
+ rect.x = parentRect.x;
+ }
+
+ if (parentRect.top > rect.top) {
+ rect.height = rect.height - (parentRect.top - rect.top);
+ rect.y = parentRect.y;
+ }
+
+ if (parentRect.right < rect.right) {
+ rect.width = rect.width - (rect.right - parentRect.right);
+ }
+
+ if (parentRect.bottom < rect.bottom) {
+ rect.height = rect.height - (rect.bottom - parentRect.bottom);
+ }
+
+ parentElement = parentElement.parentElement;
+ }
+
+ if (rect.width < 0) rect.width = 0;
+ if (rect.height < 0) rect.height = 0;
+
+ return rect;
+ }
+
updateChildPosition(child, parent) {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
+ const clipRect = this._getClippedBoundingClientRect(parent);
+
+ Object.assign(child.parentElement.style, {
+ position: 'absolute',
+ top: clipRect.top + 'px',
+ left: clipRect.left + 'px',
+ width: clipRect.width + 'px',
+ height: clipRect.height + 'px',
+ overflow: "hidden",
+ });
+
Object.assign(child.style, {
position: 'absolute',
- top: parentRect.top + 'px',
- left: parentRect.left + 'px',
+ top: (parentRect.top - clipRect.top) + 'px',
+ left: (parentRect.left - clipRect.left) + 'px',
width: parentRect.width + 'px',
height: parentRect.height + 'px',
+ overflow: "hidden",
});
}
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 8d3a7307b9..e7f7196ac6 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -37,6 +37,8 @@ export default class ReplyThread extends React.Component {
// called when the ReplyThread contents has changed, including EventTiles thereof
onHeightChanged: PropTypes.func.isRequired,
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
+ // Specifies which layout to use.
+ useIRCLayout: PropTypes.bool,
};
static contextType = MatrixClientContext;
@@ -176,12 +178,17 @@ export default class ReplyThread extends React.Component {
};
}
- static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) {
+ static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
if (!ReplyThread.getParentEventId(parentEv)) {
- return ;
+ return ;
}
- return ;
+ return ;
}
componentDidMount() {
@@ -331,11 +338,13 @@ export default class ReplyThread extends React.Component {
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()}
- isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
+ isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
+ useIRCLayout={this.props.useIRCLayout}
+ />
;
});
- return
+ return
{ header }
{ evTiles }
;
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
new file mode 100644
index 0000000000..f76a4684d3
--- /dev/null
+++ b/src/components/views/elements/Slider.tsx
@@ -0,0 +1,146 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as React from 'react';
+
+interface IProps {
+ // A callback for the selected value
+ onSelectionChange: (value: number) => void;
+
+ // The current value of the slider
+ value: number;
+
+ // The range and values of the slider
+ // Currently only supports an ascending, constant interval range
+ values: number[];
+
+ // A function for formatting the the values
+ displayFunc: (value: number) => string;
+
+ // Whether the slider is disabled
+ disabled: boolean;
+}
+
+export default class Slider extends React.Component {
+ // offset is a terrible inverse approximation.
+ // if the values represents some function f(x) = y where x is the
+ // index of the array and y = values[x] then offset(f, y) = x
+ // s.t f(x) = y.
+ // it assumes a monotonic function and interpolates linearly between
+ // y values.
+ // Offset is used for finding the location of a value on a
+ // non linear slider.
+ private offset(values: number[], value: number): number {
+ // the index of the first number greater than value.
+ let closest = values.reduce((prev, curr) => {
+ return (value > curr ? prev + 1 : prev);
+ }, 0);
+
+ // Off the left
+ if (closest === 0) {
+ return 0;
+ }
+
+ // Off the right
+ if (closest === values.length) {
+ return 100;
+ }
+
+ // Now
+ const closestLessValue = values[closest - 1];
+ const closestGreaterValue = values[closest];
+
+ const intervalWidth = 1 / (values.length - 1);
+
+ const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
+
+ return 100 * (closest - 1 + linearInterpolation) * intervalWidth
+
+ }
+
+ render(): React.ReactNode {
+ const dots = this.props.values.map(v =>
+ {} : () => this.props.onSelectionChange(v)}
+ key={v}
+ disabled={this.props.disabled}
+ />);
+
+ let selection = null;
+
+ if (!this.props.disabled) {
+ const offset = this.offset(this.props.values, this.props.value);
+ selection =
+ );
+ }
+}
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
new file mode 100644
index 0000000000..42b65cba87
--- /dev/null
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -0,0 +1,219 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 New Vector Ltd
+Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { createRef } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import classNames from "classnames";
+import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
+import AccessibleButton from "../../views/elements/AccessibleButton";
+import RoomAvatar from "../../views/avatars/RoomAvatar";
+import Tooltip from "../../views/elements/Tooltip";
+import dis from '../../../dispatcher/dispatcher';
+import { Key } from "../../../Keyboard";
+import * as RoomNotifs from '../../../RoomNotifs';
+import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
+import * as Unread from '../../../Unread';
+import * as FormattingUtils from "../../../utils/FormattingUtils";
+
+/*******************************************************************
+ * 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 {
+ room: Room;
+
+ // TODO: Allow falsifying counts (for invites and stuff)
+ // TODO: Transparency? Was this ever used?
+ // TODO: Incoming call boxes?
+}
+
+interface IBadgeState {
+ showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room
+ numUnread: number; // used only if showBadge or showBadgeHighlight is true
+ hasUnread: number; // used to make the room bold
+ showBadgeHighlight: boolean; // make the badge red
+ isInvite: boolean; // show a `!` instead of a number
+}
+
+interface IState extends IBadgeState {
+ hover: boolean;
+}
+
+export default class RoomTile2 extends React.Component {
+ private roomTile = createRef();
+
+ // TODO: Custom status
+ // TODO: Lock icon
+ // TODO: Presence indicator
+ // TODO: e2e shields
+ // TODO: Handle changes to room aesthetics (name, join rules, etc)
+ // TODO: scrollIntoView?
+ // TODO: hover, badge, etc
+ // TODO: isSelected for hover effects
+ // TODO: Context menu
+ // TODO: a11y
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ hover: false,
+
+ ...this.getBadgeState(),
+ };
+ }
+
+ public componentWillUnmount() {
+ // TODO: Listen for changes to the badge count and update as needed
+ }
+
+ private updateBadgeCount() {
+ this.setState({...this.getBadgeState()});
+ }
+
+ private getBadgeState(): IBadgeState {
+ // TODO: Make this code path faster
+ const highlightCount = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
+ const numUnread = RoomNotifs.getUnreadNotificationCount(this.props.room);
+ const showBadge = Unread.doesRoomHaveUnreadMessages(this.props.room);
+ const myMembership = getEffectiveMembership(this.props.room.getMyMembership());
+ const isInvite = myMembership === EffectiveMembership.Invite;
+ const notifState = RoomNotifs.getRoomNotifsState(this.props.room.roomId);
+ const shouldShowNotifBadge = RoomNotifs.shouldShowNotifBadge(notifState);
+ const shouldShowHighlightBadge = RoomNotifs.shouldShowMentionBadge(notifState);
+
+ return {
+ showBadge: (showBadge && shouldShowNotifBadge) || isInvite,
+ numUnread,
+ hasUnread: showBadge,
+ showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite,
+ isInvite,
+ };
+ }
+
+ private onTileMouseEnter = () => {
+ this.setState({hover: true});
+ };
+
+ private onTileMouseLeave = () => {
+ this.setState({hover: false});
+ };
+
+ private onTileClick = (ev: React.KeyboardEvent) => {
+ dis.dispatch({
+ action: 'view_room',
+ // TODO: Support show_room_tile in new room list
+ show_room_tile: true, // make sure the room is visible in the list
+ room_id: this.props.room.roomId,
+ clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
+ });
+ };
+
+ public render(): React.ReactElement {
+ // TODO: Collapsed state
+ // TODO: Invites
+ // TODO: a11y proper
+ // TODO: Render more than bare minimum
+
+ const classes = classNames({
+ 'mx_RoomTile': true,
+ // 'mx_RoomTile_selected': this.state.selected,
+ 'mx_RoomTile_unread': this.state.numUnread > 0 || this.state.hasUnread,
+ 'mx_RoomTile_unreadNotify': this.state.showBadge,
+ 'mx_RoomTile_highlight': this.state.showBadgeHighlight,
+ 'mx_RoomTile_invited': this.state.isInvite,
+ // 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
+ 'mx_RoomTile_noBadges': !this.state.showBadge,
+ // 'mx_RoomTile_transparent': this.props.transparent,
+ // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
+ });
+
+ const avatarClasses = classNames({
+ 'mx_RoomTile_avatar': true,
+ });
+
+
+ let badge;
+ if (this.state.showBadge) {
+ const badgeClasses = classNames({
+ 'mx_RoomTile_badge': true,
+ 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
+ });
+ const formattedCount = this.state.isInvite ? `!` : FormattingUtils.formatCount(this.state.numUnread);
+ badge =
{formattedCount}
;
+ }
+
+ // 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
+
+ const nameClasses = classNames({
+ 'mx_RoomTile_name': true,
+ 'mx_RoomTile_invite': this.state.isInvite,
+ 'mx_RoomTile_badgeShown': this.state.showBadge,
+ });
+
+ // TODO: Support collapsed state properly
+ let tooltip = null;
+ if (false) { // isCollapsed
+ if (this.state.hover) {
+ tooltip =
+ }
+ }
+
+ return (
+
+
+ {({onFocus, isActive, ref}) =>
+
+
+
+
+
+
+
+
+
+ {name}
+
+
+ {badge}
+
+ {tooltip}
+
+ }
+
+
+ );
+ }
+}
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js
new file mode 100644
index 0000000000..5b49dd0abd
--- /dev/null
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js
@@ -0,0 +1,281 @@
+/*
+Copyright 2019 New Vector Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import {_t} from "../../../../../languageHandler";
+import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
+import * as sdk from "../../../../../index";
+import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
+import Field from "../../../elements/Field";
+import Slider from "../../../elements/Slider";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import dis from "../../../../../dispatcher/dispatcher";
+import { FontWatcher } from "../../../../../FontWatcher";
+
+export default class AppearanceUserSettingsTab extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ fontSize: SettingsStore.getValue("fontSize", null),
+ ...this._calculateThemeState(),
+ customThemeUrl: "",
+ customThemeMessage: {isError: false, text: ""},
+ useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
+ };
+ }
+
+ _calculateThemeState() {
+ // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
+ // show the right values for things.
+
+ const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
+ const systemThemeExplicit = SettingsStore.getValueAt(
+ SettingLevel.DEVICE, "use_system_theme", null, false, true);
+ const themeExplicit = SettingsStore.getValueAt(
+ SettingLevel.DEVICE, "theme", null, false, true);
+
+ // If the user has enabled system theme matching, use that.
+ if (systemThemeExplicit) {
+ return {
+ theme: themeChoice,
+ useSystemTheme: true,
+ };
+ }
+
+ // If the user has set a theme explicitly, use that (no system theme matching)
+ if (themeExplicit) {
+ return {
+ theme: themeChoice,
+ useSystemTheme: false,
+ };
+ }
+
+ // Otherwise assume the defaults for the settings
+ return {
+ theme: themeChoice,
+ useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
+ };
+ }
+
+ _onThemeChange = (e) => {
+ const newTheme = e.target.value;
+ if (this.state.theme === newTheme) return;
+
+ // doing getValue in the .catch will still return the value we failed to set,
+ // so remember what the value was before we tried to set it so we can revert
+ const oldTheme = SettingsStore.getValue('theme');
+ SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
+ dis.dispatch({action: 'recheck_theme'});
+ this.setState({theme: oldTheme});
+ });
+ this.setState({theme: newTheme});
+ // The settings watcher doesn't fire until the echo comes back from the
+ // server, so to make the theme change immediately we need to manually
+ // do the dispatch now
+ // XXX: The local echoed value appears to be unreliable, in particular
+ // when settings custom themes(!) so adding forceTheme to override
+ // the value from settings.
+ dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
+ };
+
+ _onUseSystemThemeChanged = (checked) => {
+ this.setState({useSystemTheme: checked});
+ SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
+ dis.dispatch({action: 'recheck_theme'});
+ };
+
+ _onFontSizeChanged = (size) => {
+ this.setState({fontSize: size});
+ SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
+ };
+
+ _onValidateFontSize = ({value}) => {
+ console.log({value});
+
+ const parsedSize = parseFloat(value);
+ const min = FontWatcher.MIN_SIZE;
+ const max = FontWatcher.MAX_SIZE;
+
+ if (isNaN(parsedSize)) {
+ return {valid: false, feedback: _t("Size must be a number")};
+ }
+
+ if (!(min <= parsedSize && parsedSize <= max)) {
+ return {
+ valid: false,
+ feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max}),
+ };
+ }
+
+ SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value);
+ return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
+ }
+
+ _onAddCustomTheme = async () => {
+ let currentThemes = SettingsStore.getValue("custom_themes");
+ if (!currentThemes) currentThemes = [];
+ currentThemes = currentThemes.map(c => c); // cheap clone
+
+ if (this._themeTimer) {
+ clearTimeout(this._themeTimer);
+ }
+
+ try {
+ const r = await fetch(this.state.customThemeUrl);
+ const themeInfo = await r.json();
+ if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
+ this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
+ return;
+ }
+ currentThemes.push(themeInfo);
+ } catch (e) {
+ console.error(e);
+ this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
+ return; // Don't continue on error
+ }
+
+ await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
+ this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
+
+ this._themeTimer = setTimeout(() => {
+ this.setState({customThemeMessage: {text: "", isError: false}});
+ }, 3000);
+ };
+
+ _onCustomThemeChange = (e) => {
+ this.setState({customThemeUrl: e.target.value});
+ };
+
+ render() {
+ return (
+
{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4cb12e7df5..96ccf1589d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -400,15 +400,20 @@
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"Failed to join room": "Failed to join room",
+ "Font scaling": "Font scaling",
"Message Pinning": "Message Pinning",
"Custom user status messages": "Custom user status messages",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
+ "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)",
"Support adding custom themes": "Support adding custom themes",
+ "Use IRC layout": "Use IRC layout",
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
"Show info about bridges in room settings": "Show info about bridges in room settings",
+ "Font size": "Font size",
+ "Custom font size": "Custom font size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
@@ -453,6 +458,7 @@
"Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session",
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
"Manually verify all remote sessions": "Manually verify all remote sessions",
+ "IRC display name width": "IRC display name width",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@@ -746,22 +752,26 @@
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
"Manage integrations": "Manage integrations",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
+ "Size must be a number": "Size must be a number",
+ "Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt",
+ "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt",
+ "Invalid theme schema.": "Invalid theme schema.",
+ "Error downloading theme information.": "Error downloading theme information.",
+ "Theme added!": "Theme added!",
+ "Appearance": "Appearance",
+ "Custom theme URL": "Custom theme URL",
+ "Add theme": "Add theme",
+ "Theme": "Theme",
"Flair": "Flair",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
- "Invalid theme schema.": "Invalid theme schema.",
- "Error downloading theme information.": "Error downloading theme information.",
- "Theme added!": "Theme added!",
"Profile": "Profile",
"Email addresses": "Email addresses",
"Phone numbers": "Phone numbers",
"Set a new account password...": "Set a new account password...",
"Account": "Account",
"Language and region": "Language and region",
- "Custom theme URL": "Custom theme URL",
- "Add theme": "Add theme",
- "Theme": "Theme",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
"Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
@@ -1113,6 +1123,7 @@
"Direct Messages": "Direct Messages",
"Start chat": "Start chat",
"Rooms": "Rooms",
+ "Create room": "Create room",
"Low priority": "Low priority",
"Historical": "Historical",
"System Alerts": "System Alerts",
@@ -1159,6 +1170,9 @@
"Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.",
"Not now": "Not now",
"Don't ask me again": "Don't ask me again",
+ "Jump to first unread room.": "Jump to first unread room.",
+ "Jump to first invite.": "Jump to first invite.",
+ "Add room": "Add room",
"Options": "Options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
@@ -2050,9 +2064,6 @@
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call",
"There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?",
- "Jump to first unread room.": "Jump to first unread room.",
- "Jump to first invite.": "Jump to first invite.",
- "Add room": "Add room",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed",
diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js
index 02151f8474..d372c38405 100644
--- a/src/indexing/EventIndex.js
+++ b/src/indexing/EventIndex.js
@@ -489,14 +489,20 @@ export default class EventIndex extends EventEmitter {
return object;
});
- // Create a new checkpoint so we can continue crawling the room for
- // messages.
- const newCheckpoint = {
- roomId: checkpoint.roomId,
- token: res.end,
- fullCrawl: checkpoint.fullCrawl,
- direction: checkpoint.direction,
- };
+ let newCheckpoint;
+
+ // The token can be null for some reason. Don't create a checkpoint
+ // in that case since adding it to the db will fail.
+ if (res.end) {
+ // Create a new checkpoint so we can continue crawling the room
+ // for messages.
+ newCheckpoint = {
+ roomId: checkpoint.roomId,
+ token: res.end,
+ fullCrawl: checkpoint.fullCrawl,
+ direction: checkpoint.direction,
+ };
+ }
try {
for (let i = 0; i < redactionEvents.length; i++) {
@@ -506,6 +512,15 @@ export default class EventIndex extends EventEmitter {
const eventsAlreadyAdded = await indexManager.addHistoricEvents(
events, newCheckpoint, checkpoint);
+
+ // We didn't get a valid new checkpoint from the server, nothing
+ // to do here anymore.
+ if (!newCheckpoint) {
+ console.log("EventIndex: The server didn't return a valid ",
+ "new checkpoint, not continuing the crawl.", checkpoint);
+ continue;
+ }
+
// If all events were already indexed we assume that we catched
// up with our index and don't need to crawl the room further.
// Let us delete the checkpoint in that case, otherwise push
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index e5027e0d37..9f9d7898cb 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -133,7 +133,6 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
body.append("cross_signing_supported_by_hs",
String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")));
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
- body.append("ssss_key_needs_upgrade", String(await client.secretStorageKeyNeedsUpgrade()));
}
}
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 5c6d843349..8df75b2c8b 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -29,6 +29,7 @@ import ThemeController from './controllers/ThemeController';
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
+import FontSizeController from './controllers/FontSizeController';
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
@@ -94,6 +95,12 @@ export const SETTINGS = {
// // not use this for new settings.
// invertedSettingName: "my-negative-setting",
// },
+ "feature_font_scaling": {
+ isFeature: true,
+ displayName: _td("Font scaling"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_pinning": {
isFeature: true,
displayName: _td("Message Pinning"),
@@ -131,12 +138,24 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_new_room_list": {
+ isFeature: true,
+ displayName: _td("Use the improved room list (in development - refresh to apply changes)"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_custom_themes": {
isFeature: true,
displayName: _td("Support adding custom themes"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_irc_ui": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Use IRC layout'),
+ default: false,
+ isFeature: true,
+ },
"mjolnirRooms": {
supportedLevels: ['account'],
default: [],
@@ -158,6 +177,17 @@ export const SETTINGS = {
displayName: _td("Show info about bridges in room settings"),
default: false,
},
+ "fontSize": {
+ displayName: _td("Font size"),
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: 16,
+ controller: new FontSizeController(),
+ },
+ "useCustomFontSize": {
+ displayName: _td("Custom font size"),
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: false,
+ },
"MessageComposerInput.suggestEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable Emoji suggestions while typing'),
@@ -519,4 +549,11 @@ export const SETTINGS = {
MatrixClient.prototype.setCryptoTrustCrossSignedDevices, true,
),
},
+ "ircDisplayNameWidth": {
+ // We specifically want to have room-device > device so that users may set a device default
+ // with a per-room override.
+ supportedLevels: ['room-device', 'device'],
+ displayName: _td("IRC display name width"),
+ default: 80,
+ },
};
diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js
index 36111dd46f..4b18a27c6c 100644
--- a/src/settings/SettingsStore.js
+++ b/src/settings/SettingsStore.js
@@ -370,6 +370,21 @@ export default class SettingsStore {
return SettingsStore._getFinalValue(setting, level, roomId, null, null);
}
+ /**
+ * Gets the default value of a setting.
+ * @param {string} settingName The name of the setting to read the value of.
+ * @param {String} roomId The room ID to read the setting value in, may be null.
+ * @return {*} The default value
+ */
+ static getDefaultValue(settingName) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+
+ return SETTINGS[settingName].default;
+ }
+
static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) {
let resultingValue = calculatedValue;
diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js
new file mode 100644
index 0000000000..3ef01ab99b
--- /dev/null
+++ b/src/settings/controllers/FontSizeController.js
@@ -0,0 +1,32 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import SettingController from "./SettingController";
+import dis from "../../dispatcher/dispatcher";
+
+export default class FontSizeController extends SettingController {
+ constructor() {
+ super();
+ }
+
+ onChange(level, roomId, newValue) {
+ // Dispatch font size change so that everything open responds to the change.
+ dis.dispatch({
+ action: "update-font-size",
+ size: newValue,
+ });
+ }
+}
diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts
new file mode 100644
index 0000000000..3519050078
--- /dev/null
+++ b/src/stores/AsyncStore.ts
@@ -0,0 +1,107 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { EventEmitter } from 'events';
+import AwaitLock from 'await-lock';
+import { Dispatcher } from "flux";
+import { ActionPayload } from "../dispatcher/payloads";
+
+/**
+ * The event/channel to listen for in an AsyncStore.
+ */
+export const UPDATE_EVENT = "update";
+
+/**
+ * Represents a minimal store which works similar to Flux stores. Instead
+ * of everything needing to happen in a dispatch cycle, everything can
+ * happen async to that cycle.
+ *
+ * The store operates by using Object.assign() to mutate state - it sends the
+ * state objects (current and new) through the function onto a new empty
+ * object. Because of this, it is recommended to break out your state to be as
+ * safe as possible. The state mutations are also locked, preventing concurrent
+ * writes.
+ *
+ * All updates to the store happen on the UPDATE_EVENT event channel with the
+ * one argument being the instance of the store.
+ *
+ * To update the state, use updateState() and preferably await the result to
+ * help prevent lock conflicts.
+ */
+export abstract class AsyncStore extends EventEmitter {
+ private storeState: T = {};
+ private lock = new AwaitLock();
+ private readonly dispatcherRef: string;
+
+ /**
+ * Creates a new AsyncStore using the given dispatcher.
+ * @param {Dispatcher} dispatcher The dispatcher to rely upon.
+ */
+ protected constructor(private dispatcher: Dispatcher) {
+ super();
+
+ this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
+ }
+
+ /**
+ * The current state of the store. Cannot be mutated.
+ */
+ protected get state(): T {
+ return Object.freeze(this.storeState);
+ }
+
+ /**
+ * Stops the store's listening functions, such as the listener to the dispatcher.
+ */
+ protected stop() {
+ if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef);
+ }
+
+ /**
+ * Updates the state of the store.
+ * @param {T|*} newState The state to update in the store using Object.assign()
+ */
+ protected async updateState(newState: T | Object) {
+ await this.lock.acquireAsync();
+ try {
+ this.storeState = Object.assign({}, this.storeState, newState);
+ this.emit(UPDATE_EVENT, this);
+ } finally {
+ await this.lock.release();
+ }
+ }
+
+ /**
+ * Resets the store's to the provided state or an empty object.
+ * @param {T|*} newState The new state of the store.
+ * @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT.
+ */
+ protected async reset(newState: T | Object = null, quiet = false) {
+ await this.lock.acquireAsync();
+ try {
+ this.storeState = (newState || {});
+ if (!quiet) this.emit(UPDATE_EVENT, this);
+ } finally {
+ await this.lock.release();
+ }
+ }
+
+ /**
+ * Called when the dispatcher broadcasts a dispatch event.
+ * @param {ActionPayload} payload The event being dispatched.
+ */
+ protected abstract onDispatch(payload: ActionPayload);
+}
diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js
index c67868e2c6..48c80294b4 100644
--- a/src/stores/CustomRoomTagStore.js
+++ b/src/stores/CustomRoomTagStore.js
@@ -15,10 +15,10 @@ limitations under the License.
*/
import dis from '../dispatcher/dispatcher';
import * as RoomNotifs from '../RoomNotifs';
-import RoomListStore from './RoomListStore';
import EventEmitter from 'events';
import { throttle } from "lodash";
import SettingsStore from "../settings/SettingsStore";
+import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy";
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -60,7 +60,7 @@ class CustomRoomTagStore extends EventEmitter {
trailing: true,
},
);
- this._roomListStoreToken = RoomListStore.addListener(() => {
+ this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
this._setState({tags: this._getUpdatedTags()});
});
dis.register(payload => this._onDispatch(payload));
@@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter {
}
getSortedTags() {
- const roomLists = RoomListStore.getRoomLists();
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => {
@@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter {
return;
}
- const newTagNames = Object.keys(RoomListStore.getRoomLists())
+ const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists())
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);
}).sort();
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
index d7b6759195..c19b2f8bc2 100644
--- a/src/stores/RoomListStore.js
+++ b/src/stores/RoomListStore.js
@@ -92,11 +92,19 @@ class RoomListStore extends Store {
constructor() {
super(dis);
+ this._checkDisabled();
this._init();
this._getManualComparator = this._getManualComparator.bind(this);
this._recentsComparator = this._recentsComparator.bind(this);
}
+ _checkDisabled() {
+ this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
+ if (this.disabled) {
+ console.warn("👋 legacy room list store has been disabled");
+ }
+ }
+
/**
* Changes the sorting algorithm used by the RoomListStore.
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
@@ -113,6 +121,8 @@ class RoomListStore extends Store {
}
_init() {
+ if (this.disabled) return;
+
// Initialise state
const defaultLists = {
"m.server_notice": [/* { room: js-sdk room, category: string } */],
@@ -140,6 +150,8 @@ class RoomListStore extends Store {
}
_setState(newState) {
+ if (this.disabled) return;
+
// If we're changing the lists, transparently change the presentation lists (which
// is given to requesting components). This dramatically simplifies our code elsewhere
// while also ensuring we don't need to update all the calling components to support
@@ -156,6 +168,8 @@ class RoomListStore extends Store {
}
__onDispatch(payload) {
+ if (this.disabled) return;
+
const logicallyReady = this._matrixClient && this._state.ready;
switch (payload.action) {
case 'setting_updated': {
@@ -182,6 +196,9 @@ class RoomListStore extends Store {
break;
}
+ this._checkDisabled();
+ if (this.disabled) return;
+
// Always ensure that we set any state needed for settings here. It is possible that
// setting updates trigger on startup before we are ready to sync, so we want to make
// sure that the right state is in place before we actually react to those changes.
diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md
new file mode 100644
index 0000000000..82a6e841db
--- /dev/null
+++ b/src/stores/room-list/README.md
@@ -0,0 +1,125 @@
+# Room list sorting
+
+It's so complicated it needs its own README.
+
+## Algorithms involved
+
+There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting.
+Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting
+Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting
+algorithm determines how rooms get ordered within tags affected by the list algorithm.
+
+Behaviour of the room list takes the shape of determining what features the room list supports, as well
+as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which
+is described later in this doc, is an example of an algorithm which makes heavy behavioural changes
+to the room list.
+
+Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
+the power to decide when and how to apply the tag sorting, if at all.
+
+### Tag sorting algorithm: Alphabetical
+
+When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem
+for the browser. All we do is a simple string comparison and expect the browser to return something
+useful.
+
+### Tag sorting algorithm: Manual
+
+Manual sorting makes use of the `order` property present on all tags for a room, per the
+[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values
+of `order` cause rooms to appear closer to the top of the list.
+
+### Tag sorting algorithm: Recent
+
+Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm
+in the room list system which determines whether an event type is capable of bubbling up in the room list.
+Normally events like room messages, stickers, and room security changes will be considered useful enough
+to cause a shift in time.
+
+Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually
+consistent this means that from time to time a room might plummet or skyrocket across the tag due to the
+timestamp contained within the event (generated server-side by the sender's server).
+
+### List ordering algorithm: Natural
+
+This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no
+behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list.
+Historically, it's been the only option in Riot and extremely common in most chat applications due to
+its relative deterministic behaviour.
+
+### List ordering algorithm: Importance
+
+On the other end of the spectrum, this is the most complicated algorithm which exists. There's major
+behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances.
+
+Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags
+simply get the manual sorting algorithm applied to them with no further involvement from the importance
+algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
+relative (perceived) importance to the user:
+
+* **Red**: The room has unread mentions waiting for the user.
+* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
+ messages which cause a push notification or badge count. Typically, this is the default as rooms get
+ set to 'All Messages'.
+* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
+ a badge/notification count (or 'Mentions Only'/'Muted').
+* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
+ last read it.
+
+Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
+above bold, etc.
+
+Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
+gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example)
+being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
+collectively the tag will be sorted into categories with red being at the top.
+
+
+
+The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing.
+The sticky room will remain in position on the room list regardless of other factors going on as typically
+clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms
+above the selected room at all times, where N is the number of rooms above the selected rooms when it was
+selected.
+
+For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one
+room above their selection at all times. If they receive another notification, and the tag ordering is
+specified as Recent, they'll see the new notification go to the top position, and the one that was previously
+there fall behind the sticky room.
+
+The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the
+tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another
+room, the previous sticky room gets recalculated to determine which category it needs to be in as the user
+could have been scrolled up while new messages were received.
+
+Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what
+kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user
+selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.
+This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain
+2 rooms above the sticky room.
+
+An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
+exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
+the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed.
+The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
+put the sticky room in a position where it's had to decrease N will not increase N.
+
+## Responsibilities of the store
+
+The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
+an object containing the tags it needs to worry about and the rooms within. The room list component will
+decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
+all kinds of filtering.
+
+## Class breakdowns
+
+The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
+of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also
+responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map:
+tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented
+to the user). Various list-specific utilities are also included, though they are expected to move
+somewhere more general when needed. For example, the `membership` utilities could easily be moved
+elsewhere as needed.
+
+The various bits throughout the room list store should also have jsdoc of some kind to help describe
+what they do and how they work.
diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts
new file mode 100644
index 0000000000..881b8fd3cf
--- /dev/null
+++ b/src/stores/room-list/RoomListStore2.ts
@@ -0,0 +1,253 @@
+/*
+Copyright 2018, 2019 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import SettingsStore from "../../settings/SettingsStore";
+import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
+import { Algorithm } from "./algorithms/list-ordering/Algorithm";
+import TagOrderStore from "../TagOrderStore";
+import { AsyncStore } from "../AsyncStore";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
+import { getListAlgorithmInstance } from "./algorithms/list-ordering";
+import { ActionPayload } from "../../dispatcher/payloads";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+
+interface IState {
+ tagsEnabled?: boolean;
+
+ preferredSort?: SortAlgorithm;
+ preferredAlgorithm?: ListAlgorithm;
+}
+
+/**
+ * The event/channel which is called when the room lists have been changed. Raised
+ * with one argument: the instance of the store.
+ */
+export const LISTS_UPDATE_EVENT = "lists_update";
+
+class _RoomListStore extends AsyncStore {
+ private matrixClient: MatrixClient;
+ private initialListsGenerated = false;
+ private enabled = false;
+ private algorithm: Algorithm;
+
+ private readonly watchedSettings = [
+ 'RoomList.orderAlphabetically',
+ 'RoomList.orderByImportance',
+ 'feature_custom_tags',
+ ];
+
+ constructor() {
+ super(defaultDispatcher);
+
+ this.checkEnabled();
+ for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
+ }
+
+ public get orderedLists(): ITagMap {
+ if (!this.algorithm) return {}; // No tags yet.
+ return this.algorithm.getOrderedRooms();
+ }
+
+ // TODO: Remove enabled flag when the old RoomListStore goes away
+ private checkEnabled() {
+ this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
+ if (this.enabled) {
+ console.log("⚡ new room list store engaged");
+ }
+ }
+
+ private async readAndCacheSettingsFromStore() {
+ const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
+ const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
+ const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
+ await this.updateState({
+ tagsEnabled,
+ preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
+ preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
+ });
+ this.setAlgorithmClass();
+ }
+
+ protected async onDispatch(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 this once the RoomListStore becomes default
+ 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();
+ }
+
+ // 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.
+ this.reset(null, true);
+ this.matrixClient = null;
+ this.initialListsGenerated = false; // we'll want to regenerate them
+ }
+
+ // Everything below here requires a MatrixClient or some sort of logical readiness.
+ const logicallyReady = this.matrixClient && this.initialListsGenerated;
+ if (!logicallyReady) return;
+
+ if (payload.action === 'setting_updated') {
+ if (this.watchedSettings.includes(payload.settingName)) {
+ console.log("Regenerating room lists: Settings changed");
+ await this.readAndCacheSettingsFromStore();
+
+ await this.regenerateAllLists(); // regenerate the lists now
+ }
+ }
+
+ if (!this.algorithm) {
+ // This shouldn't happen because `initialListsGenerated` implies we have an algorithm.
+ throw new Error("Room list store has no algorithm to process dispatcher update with");
+ }
+
+ if (payload.action === 'MatrixActions.Room.receipt') {
+ // First see if the receipt event is for our own user. If it was, trigger
+ // a room update (we probably read the room on a different device).
+ // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle
+ const myUserId = this.matrixClient.getUserId();
+ for (const eventId of Object.keys(payload.event.getContent())) {
+ const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {});
+ if (receiptUsers.includes(myUserId)) {
+ // TODO: Update room now that it's been read
+ console.log(payload);
+ return;
+ }
+ }
+ } else if (payload.action === 'MatrixActions.Room.tags') {
+ // TODO: Update room from tags
+ console.log(payload);
+ } else if (payload.action === 'MatrixActions.Room.timeline') {
+ const eventPayload = (payload); // TODO: Type out the dispatcher types
+
+ // Ignore non-live events (backfill)
+ if (!eventPayload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent) return;
+
+ const roomId = eventPayload.event.getRoomId();
+ const room = this.matrixClient.getRoom(roomId);
+ console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
+ await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
+ } else if (payload.action === 'MatrixActions.Event.decrypted') {
+ const eventPayload = (payload); // TODO: Type out the dispatcher types
+ const roomId = eventPayload.event.getRoomId();
+ const room = this.matrixClient.getRoom(roomId);
+ if (!room) {
+ console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);
+ return;
+ }
+ console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
+ // TODO: Check that e2e rooms are calculated correctly on initial load.
+ // It seems like when viewing the room the timeline is decrypted, rather than at startup. This could
+ // cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :(
+ await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
+ } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
+ // TODO: Update DMs
+ console.log(payload);
+ } else if (payload.action === 'MatrixActions.Room.myMembership') {
+ // TODO: Update room from membership change
+ console.log(payload);
+ } else if (payload.action === 'MatrixActions.Room') {
+ // TODO: Update room from creation/join
+ console.log(payload);
+ } else if (payload.action === 'view_room') {
+ // TODO: Update sticky room
+ console.log(payload);
+ }
+ }
+
+ private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise {
+ const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
+ if (shouldUpdate) {
+ console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
+ this.emit(LISTS_UPDATE_EVENT, this);
+ }
+ }
+
+ private getSortAlgorithmFor(tagId: TagID): SortAlgorithm {
+ switch (tagId) {
+ case DefaultTagID.Invite:
+ case DefaultTagID.Untagged:
+ case DefaultTagID.Archived:
+ case DefaultTagID.LowPriority:
+ case DefaultTagID.DM:
+ return this.state.preferredSort;
+ case DefaultTagID.Favourite:
+ default:
+ return SortAlgorithm.Manual;
+ }
+ }
+
+ protected async updateState(newState: IState) {
+ if (!this.enabled) return;
+
+ await super.updateState(newState);
+ }
+
+ private setAlgorithmClass() {
+ this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
+ }
+
+ private async regenerateAllLists() {
+ console.warn("Regenerating all room lists");
+ const tags: ITagSortingMap = {};
+ for (const tagId of OrderedDefaultTagIDs) {
+ tags[tagId] = this.getSortAlgorithmFor(tagId);
+ }
+
+ if (this.state.tagsEnabled) {
+ // TODO: Find a more reliable way to get tags (this doesn't work)
+ const roomTags = TagOrderStore.getOrderedTags() || [];
+ console.log("rtags", roomTags);
+ }
+
+ await this.algorithm.populateTags(tags);
+ await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
+
+ this.initialListsGenerated = true;
+
+ this.emit(LISTS_UPDATE_EVENT, this);
+ }
+}
+
+export default class RoomListStore {
+ private static internalInstance: _RoomListStore;
+
+ public static get instance(): _RoomListStore {
+ if (!RoomListStore.internalInstance) {
+ RoomListStore.internalInstance = new _RoomListStore();
+ }
+
+ return RoomListStore.internalInstance;
+ }
+}
diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts
new file mode 100644
index 0000000000..0268cf0a46
--- /dev/null
+++ b/src/stores/room-list/RoomListStoreTempProxy.ts
@@ -0,0 +1,49 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import SettingsStore from "../../settings/SettingsStore";
+import RoomListStore from "./RoomListStore2";
+import OldRoomListStore from "../RoomListStore";
+import { UPDATE_EVENT } from "../AsyncStore";
+import { ITagMap } from "./algorithms/models";
+
+/**
+ * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when
+ * it is available to everyone.
+ *
+ * TODO: Remove this when RoomListStore gets fully replaced.
+ */
+export class RoomListStoreTempProxy {
+ public static isUsingNewStore(): boolean {
+ return SettingsStore.isFeatureEnabled("feature_new_room_list");
+ }
+
+ public static addListener(handler: () => void) {
+ if (RoomListStoreTempProxy.isUsingNewStore()) {
+ return RoomListStore.instance.on(UPDATE_EVENT, handler);
+ } else {
+ return OldRoomListStore.addListener(handler);
+ }
+ }
+
+ public static getRoomLists(): ITagMap {
+ if (RoomListStoreTempProxy.isUsingNewStore()) {
+ return RoomListStore.instance.orderedLists;
+ } else {
+ return OldRoomListStore.getRoomLists();
+ }
+ }
+}
diff --git a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts
new file mode 100644
index 0000000000..e154847847
--- /dev/null
+++ b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts
@@ -0,0 +1,177 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { DefaultTagID, RoomUpdateCause, TagID } from "../../models";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
+import { EffectiveMembership, splitRoomsByMembership } from "../../membership";
+import { ITagMap, ITagSortingMap } from "../models";
+import DMRoomMap from "../../../../utils/DMRoomMap";
+
+// TODO: Add locking support to avoid concurrent writes?
+// TODO: EventEmitter support? Might not be needed.
+
+/**
+ * Represents a list ordering algorithm. This class will take care of tag
+ * management (which rooms go in which tags) and ask the implementation to
+ * deal with ordering mechanics.
+ */
+export abstract class Algorithm {
+ protected cached: ITagMap = {};
+ protected sortAlgorithms: ITagSortingMap;
+ protected rooms: Room[] = [];
+ protected roomIdsToTags: {
+ [roomId: string]: TagID[];
+ } = {};
+
+ protected constructor() {
+ }
+
+ /**
+ * Asks the Algorithm to regenerate all lists, using the tags given
+ * as reference for which lists to generate and which way to generate
+ * them.
+ * @param {ITagSortingMap} tagSortingMap The tags to generate.
+ * @returns {Promise<*>} A promise which resolves when complete.
+ */
+ public async populateTags(tagSortingMap: ITagSortingMap): Promise {
+ if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
+ this.sortAlgorithms = tagSortingMap;
+ return this.setKnownRooms(this.rooms);
+ }
+
+ /**
+ * Gets an ordered set of rooms for the all known tags.
+ * @returns {ITagMap} The cached list of rooms, ordered,
+ * for each tag. May be empty, but never null/undefined.
+ */
+ public getOrderedRooms(): ITagMap {
+ return this.cached;
+ }
+
+ /**
+ * Seeds the Algorithm with a set of rooms. The algorithm will discard all
+ * previously known information and instead use these rooms instead.
+ * @param {Room[]} rooms The rooms to force the algorithm to use.
+ * @returns {Promise<*>} A promise which resolves when complete.
+ */
+ public async setKnownRooms(rooms: Room[]): Promise {
+ if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
+ if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
+
+ this.rooms = rooms;
+
+ const newTags: ITagMap = {};
+ for (const tagId in this.sortAlgorithms) {
+ // noinspection JSUnfilteredForInLoop
+ newTags[tagId] = [];
+ }
+
+ // If we can avoid doing work, do so.
+ if (!rooms.length) {
+ await this.generateFreshTags(newTags); // just in case it wants to do something
+ this.cached = newTags;
+ return;
+ }
+
+ // Split out the easy rooms first (leave and invite)
+ const memberships = splitRoomsByMembership(rooms);
+ for (const room of memberships[EffectiveMembership.Invite]) {
+ console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
+ newTags[DefaultTagID.Invite].push(room);
+ }
+ for (const room of memberships[EffectiveMembership.Leave]) {
+ console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
+ newTags[DefaultTagID.Archived].push(room);
+ }
+
+ // Now process all the joined rooms. This is a bit more complicated
+ for (const room of memberships[EffectiveMembership.Join]) {
+ let tags = Object.keys(room.tags || {});
+
+ if (tags.length === 0) {
+ // Check to see if it's a DM if it isn't anything else
+ if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
+ tags = [DefaultTagID.DM];
+ }
+ }
+
+ let inTag = false;
+ if (tags.length > 0) {
+ for (const tag of tags) {
+ console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
+ if (!isNullOrUndefined(newTags[tag])) {
+ console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
+ newTags[tag].push(room);
+ inTag = true;
+ }
+ }
+ }
+
+ if (!inTag) {
+ // TODO: Determine if DM and push there instead
+ newTags[DefaultTagID.Untagged].push(room);
+ console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
+ }
+ }
+
+ await this.generateFreshTags(newTags);
+
+ this.cached = newTags;
+ this.updateTagsFromCache();
+ }
+
+ /**
+ * Updates the roomsToTags map
+ */
+ protected updateTagsFromCache() {
+ const newMap = {};
+
+ const tags = Object.keys(this.cached);
+ for (const tagId of tags) {
+ const rooms = this.cached[tagId];
+ for (const room of rooms) {
+ if (!newMap[room.roomId]) newMap[room.roomId] = [];
+ newMap[room.roomId].push(tagId);
+ }
+ }
+
+ this.roomIdsToTags = newMap;
+ }
+
+ /**
+ * Called when the Algorithm believes a complete regeneration of the existing
+ * lists is needed.
+ * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
+ * will already have the rooms which belong to it - they just need ordering. Must
+ * be mutated in place.
+ * @returns {Promise<*>} A promise which resolves when complete.
+ */
+ protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise;
+
+ /**
+ * Asks the Algorithm to update its knowledge of a room. For example, when
+ * a user tags a room, joins/creates a room, or leaves a room the Algorithm
+ * should be told that the room's info might have changed. The Algorithm
+ * may no-op this request if no changes are required.
+ * @param {Room} room The room which might have affected sorting.
+ * @param {RoomUpdateCause} cause The reason for the update being triggered.
+ * @returns {Promise} A promise which resolve to true or false
+ * depending on whether or not getOrderedRooms() should be called after
+ * processing.
+ */
+ public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise;
+}
diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts
new file mode 100644
index 0000000000..c72cdc2e1c
--- /dev/null
+++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts
@@ -0,0 +1,298 @@
+/*
+Copyright 2018, 2019 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Algorithm } from "./Algorithm";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomUpdateCause, TagID } from "../../models";
+import { ITagMap, SortAlgorithm } from "../models";
+import { sortRoomsWithAlgorithm } from "../tag-sorting";
+import * as Unread from '../../../../Unread';
+
+/**
+ * The determined category of a room.
+ */
+export enum Category {
+ /**
+ * The room has unread mentions within.
+ */
+ Red = "RED",
+ /**
+ * The room has unread notifications within. Note that these are not unread
+ * mentions - they are simply messages which the user has asked to cause a
+ * badge count update or push notification.
+ */
+ Grey = "GREY",
+ /**
+ * The room has unread messages within (grey without the badge).
+ */
+ Bold = "BOLD",
+ /**
+ * The room has no relevant unread messages within.
+ */
+ Idle = "IDLE",
+}
+
+interface ICategorizedRoomMap {
+ // @ts-ignore - TS wants this to be a string, but we know better
+ [category: Category]: Room[];
+}
+
+interface ICategoryIndex {
+ // @ts-ignore - TS wants this to be a string, but we know better
+ [category: Category]: 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];
+
+/**
+ * An implementation of the "importance" algorithm for room list sorting. Where
+ * the tag sorting algorithm does not interfere, rooms will be ordered into
+ * categories of varying importance to the user. Alphabetical sorting does not
+ * interfere with this algorithm, however manual ordering does.
+ *
+ * The importance of a room is defined by the kind of notifications, if any, are
+ * present on the room. These are classified internally as Red, Grey, Bold, and
+ * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy
+ * version of grey, and idle means all activity has been seen by the user.
+ *
+ * The algorithm works by monitoring all room changes, including new messages in
+ * tracked rooms, to determine if it needs a new category or different placement
+ * within the same category. For more information, see the comments contained
+ * within the class.
+ */
+export class ImportanceAlgorithm extends Algorithm {
+
+ // HOW THIS WORKS
+ // --------------
+ //
+ // This block of comments assumes you've read the README one level higher.
+ // You should do that if you haven't already.
+ //
+ // Tags are fed into the algorithmic functions from the Algorithm superclass,
+ // which cause subsequent updates to the room list itself. Categories within
+ // those tags are tracked as index numbers within the array (zero = top), with
+ // each sticky room being tracked separately. Internally, the category index
+ // can be found from `this.indices[tag][category]` and the sticky room information
+ // from `this.stickyRoom`.
+ //
+ // The room list store is always provided with the `this.cached` results, which are
+ // updated as needed and not recalculated often. For example, when a room needs to
+ // move within a tag, the array in `this.cached` will be spliced instead of iterated.
+ // The `indices` help track the positions of each category to make splicing easier.
+
+ private indices: {
+ // @ts-ignore - TS wants this to be a string but we know better than it
+ [tag: TagID]: ICategoryIndex;
+ } = {};
+
+ // TODO: Use this (see docs above)
+ private stickyRoom: {
+ roomId: string;
+ tag: TagID;
+ fromTop: number;
+ } = {
+ roomId: null,
+ tag: null,
+ fromTop: 0,
+ };
+
+ constructor() {
+ super();
+ console.log("Constructed an ImportanceAlgorithm");
+ }
+
+ // noinspection JSMethodCanBeStatic
+ private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
+ const map: ICategorizedRoomMap = {
+ [Category.Red]: [],
+ [Category.Grey]: [],
+ [Category.Bold]: [],
+ [Category.Idle]: [],
+ };
+ for (const room of rooms) {
+ const category = this.getRoomCategory(room);
+ map[category].push(room);
+ }
+ return map;
+ }
+
+ // 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;
+ }
+
+ protected async generateFreshTags(updatedTagMap: ITagMap): Promise {
+ for (const tagId of Object.keys(updatedTagMap)) {
+ const unorderedRooms = updatedTagMap[tagId];
+
+ const sortBy = this.sortAlgorithms[tagId];
+ if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`);
+
+ if (sortBy === SortAlgorithm.Manual) {
+ // Manual tags essentially ignore the importance algorithm, so don't do anything
+ // special about them.
+ updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy);
+ } else {
+ // Every other sorting type affects the categories, not the whole tag.
+ const categorized = this.categorizeRooms(unorderedRooms);
+ for (const category of Object.keys(categorized)) {
+ const roomsToOrder = categorized[category];
+ categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy);
+ }
+
+ const newlyOrganized: Room[] = [];
+ const newIndices: ICategoryIndex = {};
+
+ for (const category of CATEGORY_ORDER) {
+ newIndices[category] = newlyOrganized.length;
+ newlyOrganized.push(...categorized[category]);
+ }
+
+ this.indices[tagId] = newIndices;
+ updatedTagMap[tagId] = newlyOrganized;
+ }
+ }
+ }
+
+ public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise {
+ const tags = this.roomIdsToTags[room.roomId];
+ if (!tags) {
+ console.warn(`No tags known for "${room.name}" (${room.roomId})`);
+ return false;
+ }
+ const category = this.getRoomCategory(room);
+ let changed = false;
+ for (const tag of tags) {
+ if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) {
+ continue; // Nothing to do here.
+ }
+
+ const taggedRooms = this.cached[tag];
+ const indices = this.indices[tag];
+ let roomIdx = taggedRooms.indexOf(room);
+ if (roomIdx === -1) {
+ console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`);
+ roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId);
+ }
+ if (roomIdx === -1) {
+ throw new Error(`Room ${room.roomId} has no index in ${tag}`);
+ }
+
+ // Try to avoid doing array operations if we don't have to: only move rooms within
+ // the categories if we're jumping categories
+ const oldCategory = this.getCategoryFromIndices(roomIdx, indices);
+ if (oldCategory !== category) {
+ // Move the room and update the indices
+ this.moveRoomIndexes(1, oldCategory, category, indices);
+ taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
+ taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted)
+ // Note: if moveRoomIndexes() is called after the splice then the insert operation
+ // will happen in the wrong place. Because we would have already adjusted the index
+ // for the category, we don't need to determine how the room is moving in the list.
+ // If we instead tried to insert before updating the indices, we'd have to determine
+ // whether the room was moving later (towards IDLE) or earlier (towards RED) from its
+ // current position, as it'll affect the category's start index after we remove the
+ // room from the array.
+ }
+
+ // The room received an update, so take out the slice and sort it. This should be relatively
+ // quick because the room is 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 thing quite far (alphabetic with a Z room
+ // for example), the list should already be sorted well enough that it can rip through the
+ // array and slot the changed room in quickly.
+ const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1]
+ ? Number.MAX_SAFE_INTEGER
+ : indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]];
+ const startIdx = indices[category];
+ const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
+ const unsortedSlice = taggedRooms.splice(startIdx, numSort);
+ const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]);
+ taggedRooms.splice(startIdx, 0, ...sorted);
+
+ // Finally, flag that we've done something
+ changed = true;
+ }
+ return changed;
+ }
+
+ private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category {
+ for (let i = 0; i < CATEGORY_ORDER.length; i++) {
+ const category = CATEGORY_ORDER[i];
+ const isLast = i === (CATEGORY_ORDER.length - 1);
+ const startIdx = indices[category];
+ const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indices[CATEGORY_ORDER[i + 1]];
+ if (index >= startIdx && index < endIdx) {
+ return category;
+ }
+ }
+
+ // "Should never happen" disclaimer goes here
+ throw new Error("Programming error: somehow you've ended up with an index that isn't in a category");
+ }
+
+ private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, 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.
+ // We also need to update subsequent categories as they'll all shift by nRooms, so we
+ // loop over the order to achieve that.
+
+ for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) {
+ const nextCategory = CATEGORY_ORDER[i];
+ indices[nextCategory] -= nRooms;
+ }
+
+ for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) {
+ const nextCategory = CATEGORY_ORDER[i];
+ indices[nextCategory] += nRooms;
+ }
+
+ // Do a quick check to see if we've completely broken the index
+ for (let i = 1; i <= CATEGORY_ORDER.length; i++) {
+ const lastCat = CATEGORY_ORDER[i - 1];
+ const thisCat = CATEGORY_ORDER[i];
+
+ if (indices[lastCat] > indices[thisCat]) {
+ // "should never happen" disclaimer goes here
+ console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`);
+
+ // TODO: Regenerate index when this happens
+ }
+ }
+ }
+}
diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
new file mode 100644
index 0000000000..44a501e592
--- /dev/null
+++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
@@ -0,0 +1,56 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Algorithm } from "./Algorithm";
+import { ITagMap } from "../models";
+import { sortRoomsWithAlgorithm } from "../tag-sorting";
+
+/**
+ * Uses the natural tag sorting algorithm order to determine tag ordering. No
+ * additional behavioural changes are present.
+ */
+export class NaturalAlgorithm extends Algorithm {
+
+ constructor() {
+ super();
+ console.log("Constructed a NaturalAlgorithm");
+ }
+
+ protected async generateFreshTags(updatedTagMap: ITagMap): Promise {
+ for (const tagId of Object.keys(updatedTagMap)) {
+ const unorderedRooms = updatedTagMap[tagId];
+
+ const sortBy = this.sortAlgorithms[tagId];
+ if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`);
+
+ updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy);
+ }
+ }
+
+ public async handleRoomUpdate(room, cause): Promise {
+ const tags = this.roomIdsToTags[room.roomId];
+ if (!tags) {
+ console.warn(`No tags known for "${room.name}" (${room.roomId})`);
+ return false;
+ }
+ for (const tag of tags) {
+ // TODO: Optimize this loop to avoid useless operations
+ // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
+ this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]);
+ }
+ return true; // assume we changed something
+ }
+}
diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts
new file mode 100644
index 0000000000..bcccd150cd
--- /dev/null
+++ b/src/stores/room-list/algorithms/list-ordering/index.ts
@@ -0,0 +1,38 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Algorithm } from "./Algorithm";
+import { ImportanceAlgorithm } from "./ImportanceAlgorithm";
+import { ListAlgorithm } from "../models";
+import { NaturalAlgorithm } from "./NaturalAlgorithm";
+
+const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = {
+ [ListAlgorithm.Natural]: () => new NaturalAlgorithm(),
+ [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(),
+};
+
+/**
+ * Gets an instance of the defined algorithm
+ * @param {ListAlgorithm} algorithm The algorithm to get an instance of.
+ * @returns {Algorithm} The algorithm instance.
+ */
+export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm {
+ if (!ALGORITHM_FACTORIES[algorithm]) {
+ throw new Error(`${algorithm} is not a known algorithm`);
+ }
+
+ return ALGORITHM_FACTORIES[algorithm]();
+}
diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts
new file mode 100644
index 0000000000..284600a776
--- /dev/null
+++ b/src/stores/room-list/algorithms/models.ts
@@ -0,0 +1,42 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { TagID } from "../models";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+export enum SortAlgorithm {
+ Manual = "MANUAL",
+ Alphabetic = "ALPHABETIC",
+ Recent = "RECENT",
+}
+
+export enum ListAlgorithm {
+ // Orders Red > Grey > Bold > Idle
+ Importance = "IMPORTANCE",
+
+ // Orders however the SortAlgorithm decides
+ Natural = "NATURAL",
+}
+
+export interface ITagSortingMap {
+ // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
+ [tagId: TagID]: SortAlgorithm;
+}
+
+export interface ITagMap {
+ // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
+ [tagId: TagID]: Room[];
+}
diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts
new file mode 100644
index 0000000000..8d74ebd11e
--- /dev/null
+++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts
@@ -0,0 +1,32 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+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.
+ */
+export class AlphabeticAlgorithm implements IAlgorithm {
+ public async sortRooms(rooms: Room[], tagId: TagID): Promise {
+ return rooms.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+ }
+}
diff --git a/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts
new file mode 100644
index 0000000000..6c22ee0c9c
--- /dev/null
+++ b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts
@@ -0,0 +1,31 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+import { TagID } from "../../models";
+
+/**
+ * Represents a tag sorting algorithm.
+ */
+export interface IAlgorithm {
+ /**
+ * Sorts the given rooms according to the sorting rules of the algorithm.
+ * @param {Room[]} rooms The rooms to sort.
+ * @param {TagID} tagId The tag ID in which the rooms are being sorted.
+ * @returns {Promise} Resolves to the sorted rooms.
+ */
+ sortRooms(rooms: Room[], tagId: TagID): Promise;
+}
diff --git a/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts
new file mode 100644
index 0000000000..b8c0357633
--- /dev/null
+++ b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts
@@ -0,0 +1,31 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+import { TagID } from "../../models";
+import { IAlgorithm } from "./IAlgorithm";
+
+/**
+ * Sorts rooms according to the tag's `order` property on the room.
+ */
+export class ManualAlgorithm implements IAlgorithm {
+ public async sortRooms(rooms: Room[], tagId: TagID): Promise {
+ const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
+ return rooms.sort((a, b) => {
+ return getOrderProp(a) - getOrderProp(b);
+ });
+ }
+}
diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
new file mode 100644
index 0000000000..df84c051f0
--- /dev/null
+++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
@@ -0,0 +1,81 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+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 last event's timestamp in each room that seems
+ * useful to the user.
+ */
+export class RecentAlgorithm implements IAlgorithm {
+ public async sortRooms(rooms: Room[], tagId: TagID): Promise {
+ // We cache the timestamp lookup to avoid iterating forever on the timeline
+ // of events. This cache only survives a single sort though.
+ // We wouldn't need this if `.sort()` didn't constantly try and compare all
+ // of the rooms to each other.
+
+ // TODO: We could probably improve the sorting algorithm here by finding changes.
+ // 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 tsCache: { [roomId: string]: number } = {};
+ const getLastTs = (r: Room) => {
+ if (tsCache[r.roomId]) {
+ return tsCache[r.roomId];
+ }
+
+ const ts = (() => {
+ // Apparently we can have rooms without timelines, at least under testing
+ // environments. Just return MAX_INT when this happens.
+ if (!r || !r.timeline) {
+ return Number.MAX_SAFE_INTEGER;
+ }
+
+ for (let i = r.timeline.length - 1; i >= 0; --i) {
+ 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() === MatrixClientPeg.get().getUserId()
+ || Unread.eventTriggersUnreadCount(ev)) {
+ return ev.getTs();
+ }
+ }
+
+ // we might only have events that don't trigger the unread indicator,
+ // in which case use the oldest event even if normally it wouldn't count.
+ // This is better than just assuming the last event was forever ago.
+ if (r.timeline.length && r.timeline[0].getTs()) {
+ return r.timeline[0].getTs();
+ } else {
+ return Number.MAX_SAFE_INTEGER;
+ }
+ })();
+
+ tsCache[r.roomId] = ts;
+ return ts;
+ };
+
+ return rooms.sort((a, b) => {
+ return getLastTs(a) - getLastTs(b);
+ });
+ }
+}
diff --git a/src/stores/room-list/algorithms/tag-sorting/index.ts b/src/stores/room-list/algorithms/tag-sorting/index.ts
new file mode 100644
index 0000000000..c22865f5ba
--- /dev/null
+++ b/src/stores/room-list/algorithms/tag-sorting/index.ts
@@ -0,0 +1,53 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { SortAlgorithm } from "../models";
+import { ManualAlgorithm } from "./ManualAlgorithm";
+import { IAlgorithm } from "./IAlgorithm";
+import { TagID } from "../../models";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RecentAlgorithm } from "./RecentAlgorithm";
+import { AlphabeticAlgorithm } from "./AlphabeticAlgorithm";
+
+const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = {
+ [SortAlgorithm.Recent]: new RecentAlgorithm(),
+ [SortAlgorithm.Alphabetic]: new AlphabeticAlgorithm(),
+ [SortAlgorithm.Manual]: new ManualAlgorithm(),
+};
+
+/**
+ * Gets an instance of the defined algorithm
+ * @param {SortAlgorithm} algorithm The algorithm to get an instance of.
+ * @returns {IAlgorithm} The algorithm instance.
+ */
+export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm {
+ if (!ALGORITHM_INSTANCES[algorithm]) {
+ throw new Error(`${algorithm} is not a known algorithm`);
+ }
+
+ return ALGORITHM_INSTANCES[algorithm];
+}
+
+/**
+ * Sorts rooms in a given tag according to the algorithm given.
+ * @param {Room[]} rooms The rooms to sort.
+ * @param {TagID} tagId The tag in which the sorting is occurring.
+ * @param {SortAlgorithm} algorithm The algorithm to use for sorting.
+ * @returns {Promise} Resolves to the sorted rooms.
+ */
+export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise {
+ return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
+}
diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts
new file mode 100644
index 0000000000..3cb4bf146c
--- /dev/null
+++ b/src/stores/room-list/membership.ts
@@ -0,0 +1,72 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+
+/**
+ * Approximation of a membership status for a given room.
+ */
+export enum EffectiveMembership {
+ /**
+ * The user is effectively joined to the room. For example, actually joined
+ * or knocking on the room (when that becomes possible).
+ */
+ Join = "JOIN",
+
+ /**
+ * The user is effectively invited to the room. Currently this is a direct map
+ * to the invite membership as no other membership states are effectively
+ * invites.
+ */
+ Invite = "INVITE",
+
+ /**
+ * The user is effectively no longer in the room. For example, kicked,
+ * banned, or voluntarily left.
+ */
+ Leave = "LEAVE",
+}
+
+export interface MembershipSplit {
+ // @ts-ignore - TS wants this to be a string key, but we know better.
+ [state: EffectiveMembership]: Room[];
+}
+
+export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
+ const split: MembershipSplit = {
+ [EffectiveMembership.Invite]: [],
+ [EffectiveMembership.Join]: [],
+ [EffectiveMembership.Leave]: [],
+ };
+
+ for (const room of rooms) {
+ split[getEffectiveMembership(room.getMyMembership())].push(room);
+ }
+
+ return split;
+}
+
+export function getEffectiveMembership(membership: string): EffectiveMembership {
+ if (membership === 'invite') {
+ return EffectiveMembership.Invite;
+ } else if (membership === 'join') {
+ // TODO: Do the same for knock? Update docs as needed in the enum.
+ return EffectiveMembership.Join;
+ } else {
+ // Probably a leave, kick, or ban
+ return EffectiveMembership.Leave;
+ }
+}
diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts
new file mode 100644
index 0000000000..a0c2621077
--- /dev/null
+++ b/src/stores/room-list/models.ts
@@ -0,0 +1,42 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export enum DefaultTagID {
+ Invite = "im.vector.fake.invite",
+ Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms
+ Archived = "im.vector.fake.archived",
+ LowPriority = "m.lowpriority",
+ Favourite = "m.favourite",
+ DM = "im.vector.fake.direct",
+ ServerNotice = "m.server_notice",
+}
+
+export const OrderedDefaultTagIDs = [
+ DefaultTagID.Invite,
+ DefaultTagID.Favourite,
+ DefaultTagID.DM,
+ DefaultTagID.Untagged,
+ DefaultTagID.LowPriority,
+ DefaultTagID.ServerNotice,
+ DefaultTagID.Archived,
+];
+
+export type TagID = string | DefaultTagID;
+
+export enum RoomUpdateCause {
+ Timeline = "TIMELINE",
+ RoomRead = "ROOM_READ", // TODO: Use this.
+}
diff --git a/src/theme.js b/src/theme.js
index 1da39d50fa..72b6e93443 100644
--- a/src/theme.js
+++ b/src/theme.js
@@ -81,7 +81,7 @@ export class ThemeWatcher {
}
getEffectiveTheme() {
- // Dev note: Much of this logic is replicated in the GeneralUserSettingsTab
+ // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
// XXX: checking the isLight flag here makes checking it in the ThemeController
// itself completely redundant since we just override the result here and we're
diff --git a/src/utils/rem.js b/src/utils/units.ts
similarity index 73%
rename from src/utils/rem.js
rename to src/utils/units.ts
index 1f18c9de05..54dd6b0523 100644
--- a/src/utils/rem.js
+++ b/src/utils/units.ts
@@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+/* Simple utils for formatting style values
+ */
+
// converts a pixel value to rem.
-export default function(pixelVal) {
- return pixelVal / 15 + "rem";
+export function toRem(pixelValue: number): string {
+ return pixelValue / 15 + "rem";
+}
+
+export function toPx(pixelValue: number): string {
+ return pixelValue + "px";
}
diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index 59671327ce..4e93b3bb64 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -206,7 +206,7 @@ describe("", () => {
'Hey ' +
'' +
'Member' +
'');
});
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index 235ed61016..d0694a8437 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -14,7 +14,7 @@ import DMRoomMap from '../../../../src/utils/DMRoomMap.js';
import GroupStore from '../../../../src/stores/GroupStore.js';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
-import {TAG_DM} from "../../../../src/stores/RoomListStore";
+import {DefaultTagID} from "../../../../src/stores/room-list/models";
function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain';
@@ -153,7 +153,7 @@ describe('RoomList', () => {
// 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 === TAG_DM) {
+ if (oldTag === DefaultTagID.DM) {
// Mock inverse m.direct
DMRoomMap.shared().roomToUser = {
[movingRoom.roomId]: '@someotheruser:domain',
@@ -180,7 +180,7 @@ describe('RoomList', () => {
// TODO: Re-enable dragging tests when we support dragging again.
describe.skip('does correct optimistic update when dragging from', () => {
it('rooms to people', () => {
- expectCorrectMove(undefined, TAG_DM);
+ expectCorrectMove(undefined, DefaultTagID.DM);
});
it('rooms to favourites', () => {
@@ -195,15 +195,15 @@ describe('RoomList', () => {
// Whe running the app live, it updates when some other event occurs (likely the
// m.direct arriving) that these tests do not fire.
xit('people to rooms', () => {
- expectCorrectMove(TAG_DM, undefined);
+ expectCorrectMove(DefaultTagID.DM, undefined);
});
it('people to favourites', () => {
- expectCorrectMove(TAG_DM, 'm.favourite');
+ expectCorrectMove(DefaultTagID.DM, 'm.favourite');
});
it('people to lowpriority', () => {
- expectCorrectMove(TAG_DM, 'm.lowpriority');
+ expectCorrectMove(DefaultTagID.DM, 'm.lowpriority');
});
it('low priority to rooms', () => {
@@ -211,7 +211,7 @@ describe('RoomList', () => {
});
it('low priority to people', () => {
- expectCorrectMove('m.lowpriority', TAG_DM);
+ expectCorrectMove('m.lowpriority', DefaultTagID.DM);
});
it('low priority to low priority', () => {
@@ -223,7 +223,7 @@ describe('RoomList', () => {
});
it('favourites to people', () => {
- expectCorrectMove('m.favourite', TAG_DM);
+ expectCorrectMove('m.favourite', DefaultTagID.DM);
});
it('favourites to low priority', () => {
diff --git a/yarn.lock b/yarn.lock
index 93118dab22..6cdc771c5b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1847,6 +1847,11 @@ autoprefixer@^9.0.0:
postcss "^7.0.27"
postcss-value-parser "^4.0.3"
+await-lock@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.0.1.tgz#b3f65fdf66e08f7538260f79b46c15bcfc18cadd"
+ integrity sha512-ntLi9fzlMT/vWjC1wwVI11/cSRJ3nTS35qVekNc9WnaoMOP2eWH0RvIqwLQkDjX4a4YynsKEv+Ere2VONp9wxg==
+
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"