Initial breakout for room list rewrite
This does a number of things (sorry): * Estimates the type changes needed to the dispatcher (later to be replaced by https://github.com/matrix-org/matrix-react-sdk/pull/4593) * Sets up the stack for a whole new room list store, and later components for usage. * Create a proxy class to ensure the app still functions as expected when the various stores are enabled/disabled * Demonstrates a possible structure for algorithms
This commit is contained in:
parent
82b55ffd77
commit
08419d195e
21 changed files with 794 additions and 47 deletions
|
@ -117,6 +117,7 @@
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
"@peculiar/webcrypto": "^1.0.22",
|
"@peculiar/webcrypto": "^1.0.22",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
|
"@types/flux": "^3.1.9",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/qrcode": "^1.3.4",
|
"@types/qrcode": "^1.3.4",
|
||||||
"@types/react": "16.9",
|
"@types/react": "16.9",
|
||||||
|
|
|
@ -15,11 +15,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { asyncAction } from './actionCreators';
|
import { asyncAction } from './actionCreators';
|
||||||
import RoomListStore, {TAG_DM} from '../stores/RoomListStore';
|
|
||||||
import Modal from '../Modal';
|
import Modal from '../Modal';
|
||||||
import * as Rooms from '../Rooms';
|
import * as Rooms from '../Rooms';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
|
import {RoomListStoreTempProxy} from "../stores/room-list/RoomListStoreTempProxy";
|
||||||
|
import {DefaultTagID} from "../stores/room-list/models";
|
||||||
|
|
||||||
const RoomListActions = {};
|
const RoomListActions = {};
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||||
|
|
||||||
// Is the tag ordered manually?
|
// Is the tag ordered manually?
|
||||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
const lists = RoomListStore.getRoomLists();
|
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||||
const newList = [...lists[newTag]];
|
const newList = [...lists[newTag]];
|
||||||
|
|
||||||
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||||
|
@ -73,11 +74,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||||
const roomId = room.roomId;
|
const roomId = room.roomId;
|
||||||
|
|
||||||
// Evil hack to get DMs behaving
|
// Evil hack to get DMs behaving
|
||||||
if ((oldTag === undefined && newTag === TAG_DM) ||
|
if ((oldTag === undefined && newTag === DefaultTagID.DM) ||
|
||||||
(oldTag === TAG_DM && newTag === undefined)
|
(oldTag === DefaultTagID.DM && newTag === undefined)
|
||||||
) {
|
) {
|
||||||
return Rooms.guessAndSetDMRoom(
|
return Rooms.guessAndSetDMRoom(
|
||||||
room, newTag === TAG_DM,
|
room, newTag === DefaultTagID.DM,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to set direct chat tag " + err);
|
console.error("Failed to set direct chat tag " + err);
|
||||||
|
@ -91,10 +92,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||||
const hasChangedSubLists = oldTag !== newTag;
|
const hasChangedSubLists = oldTag !== newTag;
|
||||||
|
|
||||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||||
// but we avoid ever doing a request with TAG_DM.
|
// but we avoid ever doing a request with DefaultTagID.DM.
|
||||||
//
|
//
|
||||||
// if we moved lists, remove the old tag
|
// if we moved lists, remove the old tag
|
||||||
if (oldTag && oldTag !== TAG_DM &&
|
if (oldTag && oldTag !== DefaultTagID.DM &&
|
||||||
hasChangedSubLists
|
hasChangedSubLists
|
||||||
) {
|
) {
|
||||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||||
|
@ -112,7 +113,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we moved lists or the ordering changed, add the new tag
|
// if we moved lists or the ordering changed, add the new tag
|
||||||
if (newTag && newTag !== TAG_DM &&
|
if (newTag && newTag !== DefaultTagID.DM &&
|
||||||
(hasChangedSubLists || metaData)
|
(hasChangedSubLists || metaData)
|
||||||
) {
|
) {
|
||||||
// metaData is the body of the PUT to set the tag, so it must
|
// metaData is the body of the PUT to set the tag, so it must
|
||||||
|
|
|
@ -26,6 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||||
import SettingsStore from '../../settings/SettingsStore';
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
|
import RoomList2 from "../views/rooms/RoomList2";
|
||||||
|
|
||||||
|
|
||||||
const LeftPanel = createReactClass({
|
const LeftPanel = createReactClass({
|
||||||
|
@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
|
||||||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let roomList = null;
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||||
|
roomList = <RoomList2
|
||||||
|
onKeyDown={this._onKeyDown}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
collapsed={this.props.collapsed}
|
||||||
|
searchFilter={this.state.searchFilter}
|
||||||
|
ref={this.collectRoomList}
|
||||||
|
onFocus={this._onFocus}
|
||||||
|
onBlur={this._onBlur}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
roomList = <RoomList
|
||||||
|
onKeyDown={this._onKeyDown}
|
||||||
|
onFocus={this._onFocus}
|
||||||
|
onBlur={this._onBlur}
|
||||||
|
ref={this.collectRoomList}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
collapsed={this.props.collapsed}
|
||||||
|
searchFilter={this.state.searchFilter}
|
||||||
|
ConferenceHandler={VectorConferenceHandler} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{ tagPanelContainer }
|
{ tagPanelContainer }
|
||||||
|
@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
|
||||||
{ exploreButton }
|
{ exploreButton }
|
||||||
{ searchBox }
|
{ searchBox }
|
||||||
</div>
|
</div>
|
||||||
<RoomList
|
{roomList}
|
||||||
onKeyDown={this._onKeyDown}
|
|
||||||
onFocus={this._onFocus}
|
|
||||||
onBlur={this._onBlur}
|
|
||||||
ref={this.collectRoomList}
|
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
|
||||||
collapsed={this.props.collapsed}
|
|
||||||
searchFilter={this.state.searchFilter}
|
|
||||||
ConferenceHandler={VectorConferenceHandler} />
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,7 +31,6 @@ import dis from '../../dispatcher';
|
||||||
import sessionStore from '../../stores/SessionStore';
|
import sessionStore from '../../stores/SessionStore';
|
||||||
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStore from "../../stores/RoomListStore";
|
|
||||||
|
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
import RoomListActions from '../../actions/RoomListActions';
|
||||||
|
@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import PlatformPeg from "../../PlatformPeg";
|
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)
|
// 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.
|
// 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.
|
// NB. this is just for server notices rather than pinned messages in general.
|
||||||
|
@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateEvents = (ev, state) => {
|
onRoomStateEvents = (ev, state) => {
|
||||||
const roomLists = RoomListStore.getRoomLists();
|
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
|
||||||
this._updateServerNoticeEvents();
|
this._updateServerNoticeEvents();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateServerNoticeEvents = async () => {
|
_updateServerNoticeEvents = async () => {
|
||||||
const roomLists = RoomListStore.getRoomLists();
|
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||||
if (!roomLists['m.server_notice']) return [];
|
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||||
|
|
||||||
const pinnedEvents = [];
|
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", "");
|
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||||
|
|
||||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||||
|
|
|
@ -34,8 +34,9 @@ import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||||
|
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
|
@ -343,10 +344,10 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
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.
|
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||||
const taggedRooms = RoomListStore.getRoomLists();
|
const taggedRooms = RoomListStoreTempProxy.getRoomLists();
|
||||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
for (const dmRoom of dmTaggedRooms) {
|
for (const dmRoom of dmTaggedRooms) {
|
||||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||||
|
|
|
@ -29,7 +29,6 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
||||||
import * as Rooms from '../../../Rooms';
|
import * as Rooms from '../../../Rooms';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||||
import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore';
|
|
||||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import RoomSubList from '../../structures/RoomSubList';
|
import RoomSubList from '../../structures/RoomSubList';
|
||||||
|
@ -41,6 +40,8 @@ import * as Receipt from "../../../utils/Receipt";
|
||||||
import {Resizer} from '../../../resizer';
|
import {Resizer} from '../../../resizer';
|
||||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||||
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
|
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
|
||||||
|
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||||
|
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||||
import * as Unread from "../../../Unread";
|
import * as Unread from "../../../Unread";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ export default createReactClass({
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -521,7 +522,7 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getTagNameForRoomId: function(roomId) {
|
getTagNameForRoomId: function(roomId) {
|
||||||
const lists = RoomListStore.getRoomLists();
|
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||||
for (const tagName of Object.keys(lists)) {
|
for (const tagName of Object.keys(lists)) {
|
||||||
for (const room of lists[tagName]) {
|
for (const room of lists[tagName]) {
|
||||||
// Should be impossible, but guard anyways.
|
// Should be impossible, but guard anyways.
|
||||||
|
@ -541,7 +542,7 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
const lists = RoomListStore.getRoomLists();
|
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||||
|
|
||||||
const filteredLists = {};
|
const filteredLists = {};
|
||||||
|
|
||||||
|
@ -773,10 +774,10 @@ export default createReactClass({
|
||||||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
list: this.state.lists[TAG_DM],
|
list: this.state.lists[DefaultTagID.DM],
|
||||||
label: _t('Direct Messages'),
|
label: _t('Direct Messages'),
|
||||||
tagName: TAG_DM,
|
tagName: DefaultTagID.DM,
|
||||||
incomingCall: incomingCallIfTaggedAs(TAG_DM),
|
incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
|
||||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
||||||
addRoomLabel: _t("Start chat"),
|
addRoomLabel: _t("Start chat"),
|
||||||
},
|
},
|
||||||
|
|
130
src/components/views/rooms/RoomList2.tsx
Normal file
130
src/components/views/rooms/RoomList2.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017, 2018 Vector Creations 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 * as React from "react";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { Layout } from '../../../resizer/distributors/roomsublist2';
|
||||||
|
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||||
|
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||||
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
collapsed: boolean;
|
||||||
|
searchFilter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Actually write stub
|
||||||
|
export class RoomSublist2 extends React.Component<any, any> {
|
||||||
|
public setHeight(size: number) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
|
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
|
||||||
|
private sublistSizes: { [tagId: string]: number } = {};
|
||||||
|
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
||||||
|
private unfilteredLayout: Layout;
|
||||||
|
private filteredLayout: Layout;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.loadSublistSizes();
|
||||||
|
this.prepareLayouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
RoomListStore.instance.addListener(() => {
|
||||||
|
console.log(RoomListStore.instance.orderedLists);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadSublistSizes() {
|
||||||
|
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
|
||||||
|
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
|
||||||
|
|
||||||
|
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
||||||
|
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveSublistSizes() {
|
||||||
|
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
|
||||||
|
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareLayouts() {
|
||||||
|
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
|
||||||
|
const sublist = this.sublistRefs[tagId];
|
||||||
|
if (sublist) sublist.current.setHeight(height);
|
||||||
|
|
||||||
|
// TODO: Check overflow
|
||||||
|
|
||||||
|
// Don't store a height for collapsed sublists
|
||||||
|
if (!this.sublistCollapseStates[tagId]) {
|
||||||
|
this.sublistSizes[tagId] = height;
|
||||||
|
this.saveSublistSizes();
|
||||||
|
}
|
||||||
|
}, this.sublistSizes, this.sublistCollapseStates, {
|
||||||
|
allowWhitespace: false,
|
||||||
|
handleHeight: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filteredLayout = new Layout((tagId: string, height: number) => {
|
||||||
|
const sublist = this.sublistRefs[tagId];
|
||||||
|
if (sublist) sublist.current.setHeight(height);
|
||||||
|
}, null, null, {
|
||||||
|
allowWhitespace: false,
|
||||||
|
handleHeight: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectSublistRef(tagId: string, ref: React.RefObject<RoomSublist2>) {
|
||||||
|
if (!ref) {
|
||||||
|
delete this.sublistRefs[tagId];
|
||||||
|
} else {
|
||||||
|
this.sublistRefs[tagId] = ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||||
|
{({onKeyDownHandler}) => (
|
||||||
|
<div
|
||||||
|
onFocus={this.props.onFocus}
|
||||||
|
onBlur={this.props.onBlur}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
className="mx_RoomList"
|
||||||
|
role="tree"
|
||||||
|
aria-label={_t("Rooms")}
|
||||||
|
// Firefox sometimes makes this element focusable due to
|
||||||
|
// overflow:scroll;, so force it out of tab order.
|
||||||
|
tabIndex={-1}
|
||||||
|
>{_t("TODO")}</div>
|
||||||
|
)}
|
||||||
|
</RovingTabIndexProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
28
src/dispatcher-types.ts
Normal file
28
src/dispatcher-types.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
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 flux from "flux";
|
||||||
|
import dis from "./dispatcher";
|
||||||
|
|
||||||
|
// TODO: Merge this with the dispatcher and centralize types
|
||||||
|
|
||||||
|
export interface ActionPayload {
|
||||||
|
[property: string]: any; // effectively "extends Object"
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ease of reference in TypeScript classes
|
||||||
|
export const defaultDispatcher: flux.Dispatcher<ActionPayload> = dis;
|
|
@ -406,6 +406,7 @@
|
||||||
"Render simple counters in room header": "Render simple counters in room header",
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
"Multiple integration managers": "Multiple integration managers",
|
"Multiple integration managers": "Multiple integration managers",
|
||||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||||
|
"Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)",
|
||||||
"Support adding custom themes": "Support adding custom themes",
|
"Support adding custom themes": "Support adding custom themes",
|
||||||
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
|
"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",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
|
@ -1116,6 +1117,7 @@
|
||||||
"Low priority": "Low priority",
|
"Low priority": "Low priority",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
"System Alerts": "System Alerts",
|
"System Alerts": "System Alerts",
|
||||||
|
"TODO": "TODO",
|
||||||
"This room": "This room",
|
"This room": "This room",
|
||||||
"Joining room …": "Joining room …",
|
"Joining room …": "Joining room …",
|
||||||
"Loading …": "Loading …",
|
"Loading …": "Loading …",
|
||||||
|
|
|
@ -131,6 +131,12 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_new_room_list": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Use the improved room list component (refresh to apply changes)"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_custom_themes": {
|
"feature_custom_themes": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td("Support adding custom themes"),
|
displayName: _td("Support adding custom themes"),
|
||||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import dis from '../dispatcher';
|
import dis from '../dispatcher';
|
||||||
import * as RoomNotifs from '../RoomNotifs';
|
import * as RoomNotifs from '../RoomNotifs';
|
||||||
import RoomListStore from './RoomListStore';
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
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))$/;
|
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,
|
trailing: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
|
||||||
this._setState({tags: this._getUpdatedTags()});
|
this._setState({tags: this._getUpdatedTags()});
|
||||||
});
|
});
|
||||||
dis.register(payload => this._onDispatch(payload));
|
dis.register(payload => this._onDispatch(payload));
|
||||||
|
@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedTags() {
|
getSortedTags() {
|
||||||
const roomLists = RoomListStore.getRoomLists();
|
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||||
|
|
||||||
const tagNames = Object.keys(this._state.tags).sort();
|
const tagNames = Object.keys(this._state.tags).sort();
|
||||||
const prefixes = tagNames.map((name, i) => {
|
const prefixes = tagNames.map((name, i) => {
|
||||||
|
@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTagNames = Object.keys(RoomListStore.getRoomLists())
|
const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists())
|
||||||
.filter((tagName) => {
|
.filter((tagName) => {
|
||||||
return !tagName.match(STANDARD_TAGS_REGEX);
|
return !tagName.match(STANDARD_TAGS_REGEX);
|
||||||
}).sort();
|
}).sort();
|
||||||
|
|
|
@ -112,11 +112,19 @@ class RoomListStore extends Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(dis);
|
super(dis);
|
||||||
|
|
||||||
|
this._checkDisabled();
|
||||||
this._init();
|
this._init();
|
||||||
this._getManualComparator = this._getManualComparator.bind(this);
|
this._getManualComparator = this._getManualComparator.bind(this);
|
||||||
this._recentsComparator = this._recentsComparator.bind(this);
|
this._recentsComparator = this._recentsComparator.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_checkDisabled() {
|
||||||
|
this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
|
||||||
|
if (this.disabled) {
|
||||||
|
console.warn("DISABLING LEGACY ROOM LIST STORE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the sorting algorithm used by the RoomListStore.
|
* Changes the sorting algorithm used by the RoomListStore.
|
||||||
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
|
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
|
||||||
|
@ -133,6 +141,8 @@ class RoomListStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
_init() {
|
_init() {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// Initialise state
|
// Initialise state
|
||||||
const defaultLists = {
|
const defaultLists = {
|
||||||
"m.server_notice": [/* { room: js-sdk room, category: string } */],
|
"m.server_notice": [/* { room: js-sdk room, category: string } */],
|
||||||
|
@ -160,6 +170,8 @@ class RoomListStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setState(newState) {
|
_setState(newState) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// If we're changing the lists, transparently change the presentation lists (which
|
// If we're changing the lists, transparently change the presentation lists (which
|
||||||
// is given to requesting components). This dramatically simplifies our code elsewhere
|
// 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
|
// while also ensuring we don't need to update all the calling components to support
|
||||||
|
@ -176,6 +188,8 @@ class RoomListStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
const logicallyReady = this._matrixClient && this._state.ready;
|
const logicallyReady = this._matrixClient && this._state.ready;
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'setting_updated': {
|
case 'setting_updated': {
|
||||||
|
@ -202,6 +216,9 @@ class RoomListStore extends Store {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._checkDisabled();
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// Always ensure that we set any state needed for settings here. It is possible that
|
// 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
|
// 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.
|
// sure that the right state is in place before we actually react to those changes.
|
||||||
|
|
213
src/stores/room-list/RoomListStore2.ts
Normal file
213
src/stores/room-list/RoomListStore2.ts
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
/*
|
||||||
|
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 {Store} from 'flux/utils';
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
import { ActionPayload, defaultDispatcher } from "../../dispatcher-types";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models";
|
||||||
|
import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm";
|
||||||
|
import TagOrderStore from "../TagOrderStore";
|
||||||
|
import { getAlgorithmInstance } from "./algorithms";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
tagsEnabled?: boolean;
|
||||||
|
|
||||||
|
preferredSort?: SortAlgorithm;
|
||||||
|
preferredAlgorithm?: ListAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoomListStore extends Store<ActionPayload> {
|
||||||
|
private state: IState = {};
|
||||||
|
private matrixClient: MatrixClient;
|
||||||
|
private initialListsGenerated = false;
|
||||||
|
private enabled = false;
|
||||||
|
private algorithm: IAlgorithm;
|
||||||
|
|
||||||
|
private readonly watchedSettings = [
|
||||||
|
'RoomList.orderAlphabetically',
|
||||||
|
'RoomList.orderByImportance',
|
||||||
|
'feature_custom_tags',
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(defaultDispatcher);
|
||||||
|
|
||||||
|
this.checkEnabled();
|
||||||
|
this.reset();
|
||||||
|
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("ENABLING NEW ROOM LIST STORE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset(): void {
|
||||||
|
// We don't call setState() because it'll cause changes to emitted which could
|
||||||
|
// crash the app during logout/signin/etc.
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readAndCacheSettingsFromStore() {
|
||||||
|
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||||
|
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
|
||||||
|
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
|
||||||
|
this.setState({
|
||||||
|
tagsEnabled,
|
||||||
|
preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
|
||||||
|
preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
|
||||||
|
});
|
||||||
|
this.setAlgorithmClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected __onDispatch(payload: ActionPayload): void {
|
||||||
|
if (payload.action === 'MatrixActions.sync') {
|
||||||
|
// Filter out anything that isn't the first PREPARED sync.
|
||||||
|
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkEnabled();
|
||||||
|
if (!this.enabled) return;
|
||||||
|
|
||||||
|
this.matrixClient = payload.matrixClient;
|
||||||
|
|
||||||
|
// Update any settings here, as some may have happened before we were logically ready.
|
||||||
|
this.readAndCacheSettingsFromStore();
|
||||||
|
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
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();
|
||||||
|
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)) {
|
||||||
|
this.readAndCacheSettingsFromStore();
|
||||||
|
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
this.regenerateAllLists(); // regenerate the lists now
|
||||||
|
}
|
||||||
|
} else 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).
|
||||||
|
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
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (payload.action === 'MatrixActions.Room.tags') {
|
||||||
|
// TODO: Update room from tags
|
||||||
|
} else if (payload.action === 'MatrixActions.room.timeline') {
|
||||||
|
// TODO: Update room from new events
|
||||||
|
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
||||||
|
// TODO: Update room from decrypted event
|
||||||
|
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
|
||||||
|
// TODO: Update DMs
|
||||||
|
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
||||||
|
// TODO: Update room from membership change
|
||||||
|
} else if (payload.action === 'MatrixActions.room') {
|
||||||
|
// TODO: Update room from creation/join
|
||||||
|
} else if (payload.action === 'view_room') {
|
||||||
|
// TODO: Update sticky room
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setState(newState: IState) {
|
||||||
|
if (!this.enabled) return;
|
||||||
|
|
||||||
|
this.state = Object.assign(this.state, newState);
|
||||||
|
this.__emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAlgorithmClass() {
|
||||||
|
this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async regenerateAllLists() {
|
||||||
|
console.log("REGEN");
|
||||||
|
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
|
||||||
|
const roomTags = TagOrderStore.getOrderedTags() || [];
|
||||||
|
console.log("rtags", roomTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.algorithm.populateTags(tags);
|
||||||
|
await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
|
||||||
|
|
||||||
|
this.initialListsGenerated = true;
|
||||||
|
|
||||||
|
// TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RoomListStore {
|
||||||
|
private static internalInstance: _RoomListStore;
|
||||||
|
|
||||||
|
public static get instance(): _RoomListStore {
|
||||||
|
if (!RoomListStore.internalInstance) {
|
||||||
|
RoomListStore.internalInstance = new _RoomListStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoomListStore.internalInstance;
|
||||||
|
}
|
||||||
|
}
|
49
src/stores/room-list/RoomListStoreTempProxy.ts
Normal file
49
src/stores/room-list/RoomListStoreTempProxy.ts
Normal file
|
@ -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 { TagID } from "./models";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import RoomListStore from "./RoomListStore2";
|
||||||
|
import OldRoomListStore from "../RoomListStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.addListener(handler);
|
||||||
|
} else {
|
||||||
|
return OldRoomListStore.addListener(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getRoomLists(): {[tagId in TagID]: Room[]} {
|
||||||
|
if (RoomListStoreTempProxy.isUsingNewStore()) {
|
||||||
|
return RoomListStore.instance.orderedLists;
|
||||||
|
} else {
|
||||||
|
return OldRoomListStore.getRoomLists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
src/stores/room-list/algorithms/ChaoticAlgorithm.ts
Normal file
100
src/stores/room-list/algorithms/ChaoticAlgorithm.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
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 { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
|
import { DefaultTagID } from "../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A demonstration/temporary algorithm to verify the API surface works.
|
||||||
|
* TODO: Remove this before shipping
|
||||||
|
*/
|
||||||
|
export class ChaoticAlgorithm implements IAlgorithm {
|
||||||
|
|
||||||
|
private cached: ITagMap = {};
|
||||||
|
private sortAlgorithms: ITagSortingMap;
|
||||||
|
private rooms: Room[] = [];
|
||||||
|
|
||||||
|
constructor(private representativeAlgorithm: ListAlgorithm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrderedRooms(): ITagMap {
|
||||||
|
return this.cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateTags(tagSortingMap: ITagSortingMap): Promise<any> {
|
||||||
|
if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
|
||||||
|
this.sortAlgorithms = tagSortingMap;
|
||||||
|
this.setKnownRooms(this.rooms); // regenerate the room lists
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRoomUpdate(room): Promise<boolean> {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setKnownRooms(rooms: Room[]): Promise<any> {
|
||||||
|
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 = {};
|
||||||
|
for (const tagId in this.sortAlgorithms) {
|
||||||
|
// noinspection JSUnfilteredForInLoop
|
||||||
|
newTags[tagId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can avoid doing work, do so.
|
||||||
|
if (!rooms.length) {
|
||||||
|
this.cached = newTags;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove logging
|
||||||
|
console.log('setting known rooms - regen in progress');
|
||||||
|
console.log({alg: this.representativeAlgorithm});
|
||||||
|
|
||||||
|
// Step through each room and determine which tags it should be in.
|
||||||
|
// We don't care about ordering or sorting here - we're simply organizing things.
|
||||||
|
for (const room of rooms) {
|
||||||
|
const tags = room.tags;
|
||||||
|
let inTag = false;
|
||||||
|
for (const tagId in tags) {
|
||||||
|
// noinspection JSUnfilteredForInLoop
|
||||||
|
if (isNullOrUndefined(newTags[tagId])) {
|
||||||
|
// skip the tag if we don't know about it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inTag = true;
|
||||||
|
|
||||||
|
// noinspection JSUnfilteredForInLoop
|
||||||
|
newTags[tagId].push(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the room wasn't pushed to a tag, push it to the untagged tag.
|
||||||
|
if (!inTag) {
|
||||||
|
newTags[DefaultTagID.Untagged].push(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do sorting
|
||||||
|
|
||||||
|
// Finally, assign the tags to our cache
|
||||||
|
this.cached = newTags;
|
||||||
|
}
|
||||||
|
}
|
95
src/stores/room-list/algorithms/IAlgorithm.ts
Normal file
95
src/stores/room-list/algorithms/IAlgorithm.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
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 enum Category {
|
||||||
|
Red = "RED",
|
||||||
|
Grey = "GREY",
|
||||||
|
Bold = "BOLD",
|
||||||
|
Idle = "IDLE",
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Convert IAlgorithm to an abstract class?
|
||||||
|
// TODO: Add locking support to avoid concurrent writes
|
||||||
|
// TODO: EventEmitter support
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an algorithm for the RoomListStore to use
|
||||||
|
*/
|
||||||
|
export interface IAlgorithm {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
populateTags(tagSortingMap: ITagSortingMap): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
getOrderedRooms(): ITagMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
setKnownRooms(rooms: Room[]): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @returns {Promise<boolean>} A promise which resolve to true or false
|
||||||
|
* depending on whether or not getOrderedRooms() should be called after
|
||||||
|
* processing.
|
||||||
|
*/
|
||||||
|
handleRoomUpdate(room: Room): Promise<boolean>;
|
||||||
|
}
|
36
src/stores/room-list/algorithms/index.ts
Normal file
36
src/stores/room-list/algorithms/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
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 { IAlgorithm, ListAlgorithm } from "./IAlgorithm";
|
||||||
|
import { ChaoticAlgorithm } from "./ChaoticAlgorithm";
|
||||||
|
|
||||||
|
const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = {
|
||||||
|
[ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural),
|
||||||
|
[ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an instance of the defined algorithm
|
||||||
|
* @param {ListAlgorithm} algorithm The algorithm to get an instance of.
|
||||||
|
* @returns {IAlgorithm} The algorithm instance.
|
||||||
|
*/
|
||||||
|
export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm {
|
||||||
|
if (!ALGORITHM_FACTORIES[algorithm]) {
|
||||||
|
throw new Error(`${algorithm} is not a known algorithm`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALGORITHM_FACTORIES[algorithm]();
|
||||||
|
}
|
36
src/stores/room-list/models.ts
Normal file
36
src/stores/room-list/models.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
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;
|
|
@ -14,7 +14,7 @@ import DMRoomMap from '../../../../src/utils/DMRoomMap.js';
|
||||||
import GroupStore from '../../../../src/stores/GroupStore.js';
|
import GroupStore from '../../../../src/stores/GroupStore.js';
|
||||||
|
|
||||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
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() {
|
function generateRoomId() {
|
||||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
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
|
// Set up the room that will be moved such that it has the correct state for a room in
|
||||||
// the section for oldTag
|
// the section for oldTag
|
||||||
if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}};
|
if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}};
|
||||||
if (oldTag === TAG_DM) {
|
if (oldTag === DefaultTagID.DM) {
|
||||||
// Mock inverse m.direct
|
// Mock inverse m.direct
|
||||||
DMRoomMap.shared().roomToUser = {
|
DMRoomMap.shared().roomToUser = {
|
||||||
[movingRoom.roomId]: '@someotheruser:domain',
|
[movingRoom.roomId]: '@someotheruser:domain',
|
||||||
|
@ -180,7 +180,7 @@ describe('RoomList', () => {
|
||||||
// TODO: Re-enable dragging tests when we support dragging again.
|
// TODO: Re-enable dragging tests when we support dragging again.
|
||||||
describe.skip('does correct optimistic update when dragging from', () => {
|
describe.skip('does correct optimistic update when dragging from', () => {
|
||||||
it('rooms to people', () => {
|
it('rooms to people', () => {
|
||||||
expectCorrectMove(undefined, TAG_DM);
|
expectCorrectMove(undefined, DefaultTagID.DM);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rooms to favourites', () => {
|
it('rooms to favourites', () => {
|
||||||
|
@ -195,15 +195,15 @@ describe('RoomList', () => {
|
||||||
// Whe running the app live, it updates when some other event occurs (likely the
|
// Whe running the app live, it updates when some other event occurs (likely the
|
||||||
// m.direct arriving) that these tests do not fire.
|
// m.direct arriving) that these tests do not fire.
|
||||||
xit('people to rooms', () => {
|
xit('people to rooms', () => {
|
||||||
expectCorrectMove(TAG_DM, undefined);
|
expectCorrectMove(DefaultTagID.DM, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('people to favourites', () => {
|
it('people to favourites', () => {
|
||||||
expectCorrectMove(TAG_DM, 'm.favourite');
|
expectCorrectMove(DefaultTagID.DM, 'm.favourite');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('people to lowpriority', () => {
|
it('people to lowpriority', () => {
|
||||||
expectCorrectMove(TAG_DM, 'm.lowpriority');
|
expectCorrectMove(DefaultTagID.DM, 'm.lowpriority');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('low priority to rooms', () => {
|
it('low priority to rooms', () => {
|
||||||
|
@ -211,7 +211,7 @@ describe('RoomList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('low priority to people', () => {
|
it('low priority to people', () => {
|
||||||
expectCorrectMove('m.lowpriority', TAG_DM);
|
expectCorrectMove('m.lowpriority', DefaultTagID.DM);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('low priority to low priority', () => {
|
it('low priority to low priority', () => {
|
||||||
|
@ -223,7 +223,7 @@ describe('RoomList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('favourites to people', () => {
|
it('favourites to people', () => {
|
||||||
expectCorrectMove('m.favourite', TAG_DM);
|
expectCorrectMove('m.favourite', DefaultTagID.DM);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('favourites to low priority', () => {
|
it('favourites to low priority', () => {
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"react"
|
"react",
|
||||||
|
"flux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -1218,6 +1218,19 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||||
|
|
||||||
|
"@types/fbemitter@*":
|
||||||
|
version "2.0.32"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"
|
||||||
|
integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw=
|
||||||
|
|
||||||
|
"@types/flux@^3.1.9":
|
||||||
|
version "3.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711"
|
||||||
|
integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA==
|
||||||
|
dependencies:
|
||||||
|
"@types/fbemitter" "*"
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/glob@^7.1.1":
|
"@types/glob@^7.1.1":
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
||||||
|
|
Loading…
Reference in a new issue