2020-03-20 23:38:20 +03:00
|
|
|
|
/*
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-03-08 20:48:08 +03:00
|
|
|
|
import React, { ReactComponentElement } from "react";
|
2020-07-06 19:58:29 +03:00
|
|
|
|
import { Dispatcher } from "flux";
|
|
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2021-03-08 18:52:21 +03:00
|
|
|
|
import * as fbEmitter from "fbemitter";
|
2021-03-24 17:18:06 +03:00
|
|
|
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
2020-07-06 19:58:29 +03:00
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
|
import { _t, _td } from "../../../languageHandler";
|
2020-03-20 23:38:20 +03:00
|
|
|
|
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
|
|
|
|
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
2020-07-18 00:11:34 +03:00
|
|
|
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
2020-07-06 19:58:29 +03:00
|
|
|
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
2020-04-30 22:21:50 +03:00
|
|
|
|
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
2020-07-21 01:51:16 +03:00
|
|
|
|
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
|
2020-05-14 22:45:17 +03:00
|
|
|
|
import dis from "../../../dispatcher/dispatcher";
|
2020-07-02 18:04:38 +03:00
|
|
|
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
2020-07-18 00:46:46 +03:00
|
|
|
|
import RoomSublist from "./RoomSublist";
|
2020-05-14 22:45:17 +03:00
|
|
|
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
2020-07-02 18:04:38 +03:00
|
|
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
|
|
|
import GroupAvatar from "../avatars/GroupAvatar";
|
2021-03-08 20:48:08 +03:00
|
|
|
|
import ExtraTile from "./ExtraTile";
|
2020-07-02 22:28:06 +03:00
|
|
|
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
|
|
|
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
2020-07-06 19:58:29 +03:00
|
|
|
|
import { Action } from "../../../dispatcher/actions";
|
|
|
|
|
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
2020-07-09 04:26:25 +03:00
|
|
|
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
2020-07-16 23:43:43 +03:00
|
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2020-07-21 01:51:16 +03:00
|
|
|
|
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
|
2020-07-24 19:38:04 +03:00
|
|
|
|
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
2020-07-30 23:18:54 +03:00
|
|
|
|
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
2020-08-13 18:18:26 +03:00
|
|
|
|
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
2020-08-17 20:20:00 +03:00
|
|
|
|
import AccessibleButton from "../elements/AccessibleButton";
|
2020-08-31 19:19:05 +03:00
|
|
|
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
2020-12-23 22:02:01 +03:00
|
|
|
|
import CallHandler from "../../../CallHandler";
|
2021-03-24 17:18:06 +03:00
|
|
|
|
import SpaceStore, {SUGGESTED_ROOMS, UPDATE_SELECTED_SPACE} from "../../../stores/SpaceStore";
|
2021-03-02 17:19:40 +03:00
|
|
|
|
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
2021-03-09 06:12:00 +03:00
|
|
|
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
2021-03-08 18:52:21 +03:00
|
|
|
|
import RoomAvatar from "../avatars/RoomAvatar";
|
2021-03-10 22:30:06 +03:00
|
|
|
|
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
2021-03-24 17:18:06 +03:00
|
|
|
|
import { showRoomInviteDialog } from "../../../RoomInvite";
|
|
|
|
|
import Modal from "../../../Modal";
|
|
|
|
|
import SpacePublicShare from "../spaces/SpacePublicShare";
|
|
|
|
|
import InfoDialog from "../dialogs/InfoDialog";
|
2020-03-20 23:38:20 +03:00
|
|
|
|
|
|
|
|
|
interface IProps {
|
|
|
|
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
|
|
|
|
onFocus: (ev: React.FocusEvent) => void;
|
|
|
|
|
onBlur: (ev: React.FocusEvent) => void;
|
2020-07-07 00:04:30 +03:00
|
|
|
|
onResize: () => void;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
resizeNotifier: ResizeNotifier;
|
2020-06-11 23:39:28 +03:00
|
|
|
|
isMinimized: boolean;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IState {
|
2020-04-30 22:21:50 +03:00
|
|
|
|
sublists: ITagMap;
|
2020-11-11 16:01:40 +03:00
|
|
|
|
isNameFiltering: boolean;
|
2021-03-08 18:52:21 +03:00
|
|
|
|
currentRoomId?: string;
|
2021-03-24 17:18:06 +03:00
|
|
|
|
activeSpace: Room;
|
2021-03-08 18:52:21 +03:00
|
|
|
|
suggestedRooms: ISpaceSummaryRoom[];
|
2020-03-20 23:38:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
|
const TAG_ORDER: TagID[] = [
|
|
|
|
|
DefaultTagID.Invite,
|
|
|
|
|
DefaultTagID.Favourite,
|
|
|
|
|
DefaultTagID.DM,
|
|
|
|
|
DefaultTagID.Untagged,
|
|
|
|
|
|
|
|
|
|
// -- Custom Tags Placeholder --
|
|
|
|
|
|
|
|
|
|
DefaultTagID.LowPriority,
|
|
|
|
|
DefaultTagID.ServerNotice,
|
2021-03-08 18:52:21 +03:00
|
|
|
|
DefaultTagID.Suggested,
|
2020-04-30 22:21:50 +03:00
|
|
|
|
DefaultTagID.Archived,
|
|
|
|
|
];
|
|
|
|
|
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
|
|
|
|
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
|
|
|
|
DefaultTagID.DM,
|
|
|
|
|
DefaultTagID.Untagged,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface ITagAesthetics {
|
|
|
|
|
sectionLabel: string;
|
2020-07-21 01:51:16 +03:00
|
|
|
|
sectionLabelRaw?: string;
|
2020-04-30 22:21:50 +03:00
|
|
|
|
addRoomLabel?: string;
|
2020-07-24 07:12:10 +03:00
|
|
|
|
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
|
2020-08-13 18:18:26 +03:00
|
|
|
|
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
|
2020-04-30 22:21:50 +03:00
|
|
|
|
isInvite: boolean;
|
|
|
|
|
defaultHidden: boolean;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-23 22:02:01 +03:00
|
|
|
|
interface ITagAestheticsMap {
|
2020-04-30 22:21:50 +03:00
|
|
|
|
// @ts-ignore - TS wants this to be a string but we know better
|
|
|
|
|
[tagId: TagID]: ITagAesthetics;
|
2020-12-23 22:02:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we have no dialer support, we just show the create chat dialog
|
|
|
|
|
const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => {
|
|
|
|
|
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If we have dialer support, show a context menu so the user can pick between
|
|
|
|
|
// the dialer and the create chat dialog
|
|
|
|
|
const dmAddRoomContextMenu = (onFinished: () => void) => {
|
|
|
|
|
return <IconizedContextMenuOptionList first>
|
|
|
|
|
<IconizedContextMenuOption
|
|
|
|
|
label={_t("Start a Conversation")}
|
|
|
|
|
iconClassName="mx_RoomList_iconPlus"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
defaultDispatcher.dispatch({action: "view_create_chat"});
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<IconizedContextMenuOption
|
|
|
|
|
label={_t("Open dial pad")}
|
|
|
|
|
iconClassName="mx_RoomList_iconDialpad"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
defaultDispatcher.fire(Action.OpenDialPad);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</IconizedContextMenuOptionList>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const TAG_AESTHETICS: ITagAestheticsMap = {
|
2020-04-30 22:21:50 +03:00
|
|
|
|
[DefaultTagID.Invite]: {
|
|
|
|
|
sectionLabel: _td("Invites"),
|
|
|
|
|
isInvite: true,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
},
|
|
|
|
|
[DefaultTagID.Favourite]: {
|
|
|
|
|
sectionLabel: _td("Favourites"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
},
|
|
|
|
|
[DefaultTagID.DM]: {
|
2020-06-05 06:21:04 +03:00
|
|
|
|
sectionLabel: _td("People"),
|
2020-04-30 22:21:50 +03:00
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
addRoomLabel: _td("Start chat"),
|
2020-12-23 22:02:01 +03:00
|
|
|
|
// Either onAddRoom or addRoomContextMenu are set depending on whether we
|
|
|
|
|
// have dialer support.
|
2020-04-30 22:21:50 +03:00
|
|
|
|
},
|
|
|
|
|
[DefaultTagID.Untagged]: {
|
|
|
|
|
sectionLabel: _td("Rooms"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
2020-08-13 18:18:26 +03:00
|
|
|
|
addRoomLabel: _td("Add room"),
|
|
|
|
|
addRoomContextMenu: (onFinished: () => void) => {
|
2021-03-02 17:19:40 +03:00
|
|
|
|
if (SpaceStore.instance.activeSpace) {
|
|
|
|
|
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
|
|
|
|
|
MatrixClientPeg.get().getUserId());
|
|
|
|
|
|
|
|
|
|
return <IconizedContextMenuOptionList first>
|
|
|
|
|
<IconizedContextMenuOption
|
|
|
|
|
label={_t("Create new room")}
|
|
|
|
|
iconClassName="mx_RoomList_iconPlus"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!canAddRooms}
|
|
|
|
|
tooltip={canAddRooms ? undefined
|
|
|
|
|
: _t("You do not have permissions to create new rooms in this space")}
|
|
|
|
|
/>
|
|
|
|
|
<IconizedContextMenuOption
|
|
|
|
|
label={_t("Add existing room")}
|
|
|
|
|
iconClassName="mx_RoomList_iconHash"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!canAddRooms}
|
|
|
|
|
tooltip={canAddRooms ? undefined
|
|
|
|
|
: _t("You do not have permissions to add rooms to this space")}
|
|
|
|
|
/>
|
|
|
|
|
<IconizedContextMenuOption
|
2021-03-24 17:18:06 +03:00
|
|
|
|
label={_t("Explore rooms")}
|
2021-03-02 17:19:40 +03:00
|
|
|
|
iconClassName="mx_RoomList_iconExplore"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</IconizedContextMenuOptionList>;
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-13 18:18:26 +03:00
|
|
|
|
return <IconizedContextMenuOptionList first>
|
|
|
|
|
<IconizedContextMenuOption
|
|
|
|
|
label={_t("Create new room")}
|
|
|
|
|
iconClassName="mx_RoomList_iconPlus"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
defaultDispatcher.dispatch({action: "view_create_room"});
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<IconizedContextMenuOption
|
2020-08-31 19:19:05 +03:00
|
|
|
|
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
2020-08-26 19:33:05 +03:00
|
|
|
|
? _t("Explore community rooms")
|
|
|
|
|
: _t("Explore public rooms")}
|
2020-08-13 18:18:26 +03:00
|
|
|
|
iconClassName="mx_RoomList_iconExplore"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onFinished();
|
|
|
|
|
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</IconizedContextMenuOptionList>;
|
2020-07-24 07:12:10 +03:00
|
|
|
|
},
|
2020-04-30 22:21:50 +03:00
|
|
|
|
},
|
|
|
|
|
[DefaultTagID.LowPriority]: {
|
|
|
|
|
sectionLabel: _td("Low priority"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
},
|
|
|
|
|
[DefaultTagID.ServerNotice]: {
|
|
|
|
|
sectionLabel: _td("System Alerts"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
},
|
2020-06-29 05:03:04 +03:00
|
|
|
|
|
2020-08-03 18:02:26 +03:00
|
|
|
|
// TODO: Replace with archived view: https://github.com/vector-im/element-web/issues/14038
|
2020-04-30 22:21:50 +03:00
|
|
|
|
[DefaultTagID.Archived]: {
|
|
|
|
|
sectionLabel: _td("Historical"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: true,
|
|
|
|
|
},
|
2021-03-08 18:52:21 +03:00
|
|
|
|
|
|
|
|
|
[DefaultTagID.Suggested]: {
|
|
|
|
|
sectionLabel: _td("Suggested Rooms"),
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
},
|
2020-04-30 22:21:50 +03:00
|
|
|
|
};
|
|
|
|
|
|
2020-07-21 01:51:16 +03:00
|
|
|
|
function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
|
|
|
|
if (tagId.startsWith("u.")) {
|
|
|
|
|
tagId = tagId.substring(2);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
sectionLabel: _td("Custom Tag"),
|
|
|
|
|
sectionLabelRaw: tagId,
|
|
|
|
|
isInvite: false,
|
|
|
|
|
defaultHidden: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-09 06:12:00 +03:00
|
|
|
|
@replaceableComponent("views.rooms.RoomList")
|
2020-07-24 23:42:53 +03:00
|
|
|
|
export default class RoomList extends React.PureComponent<IProps, IState> {
|
2020-07-06 19:58:29 +03:00
|
|
|
|
private dispatcherRef;
|
2020-07-21 01:51:16 +03:00
|
|
|
|
private customTagStoreRef;
|
2020-12-23 22:02:01 +03:00
|
|
|
|
private tagAesthetics: ITagAestheticsMap;
|
2021-03-08 18:52:21 +03:00
|
|
|
|
private roomStoreToken: fbEmitter.EventSubscription;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
|
|
|
|
|
constructor(props: IProps) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
2020-06-04 00:07:12 +03:00
|
|
|
|
this.state = {
|
|
|
|
|
sublists: {},
|
2020-11-11 16:01:40 +03:00
|
|
|
|
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
2021-03-24 17:18:06 +03:00
|
|
|
|
activeSpace: SpaceStore.instance.activeSpace,
|
2021-03-08 18:52:21 +03:00
|
|
|
|
suggestedRooms: SpaceStore.instance.suggestedRooms,
|
2020-06-04 00:07:12 +03:00
|
|
|
|
};
|
2020-07-06 19:58:29 +03:00
|
|
|
|
|
2020-12-23 22:02:01 +03:00
|
|
|
|
// shallow-copy from the template as we need to make modifications to it
|
2021-01-04 14:51:27 +03:00
|
|
|
|
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
2020-12-23 22:02:01 +03:00
|
|
|
|
this.updateDmAddRoomAction();
|
|
|
|
|
|
2020-07-06 19:58:29 +03:00
|
|
|
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
2021-03-08 18:52:21 +03:00
|
|
|
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
2020-03-20 23:38:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public componentDidMount(): void {
|
2021-03-24 17:18:06 +03:00
|
|
|
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
2021-03-08 18:52:21 +03:00
|
|
|
|
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
2020-07-02 23:23:56 +03:00
|
|
|
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
2020-07-21 01:51:16 +03:00
|
|
|
|
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
2020-07-02 23:23:56 +03:00
|
|
|
|
this.updateLists(); // trigger the first update
|
|
|
|
|
}
|
2020-06-04 00:07:12 +03:00
|
|
|
|
|
2020-07-02 23:23:56 +03:00
|
|
|
|
public componentWillUnmount() {
|
2021-03-24 17:18:06 +03:00
|
|
|
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
2021-03-08 18:52:21 +03:00
|
|
|
|
SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
2020-07-02 23:23:56 +03:00
|
|
|
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
2020-07-06 19:58:29 +03:00
|
|
|
|
defaultDispatcher.unregister(this.dispatcherRef);
|
2020-07-21 01:51:16 +03:00
|
|
|
|
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
2021-03-08 18:52:21 +03:00
|
|
|
|
if (this.roomStoreToken) this.roomStoreToken.remove();
|
2020-03-20 23:38:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 18:52:21 +03:00
|
|
|
|
private onRoomViewStoreUpdate = () => {
|
|
|
|
|
this.setState({
|
|
|
|
|
currentRoomId: RoomViewStore.getRoomId(),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2020-12-23 22:02:01 +03:00
|
|
|
|
private updateDmAddRoomAction() {
|
2021-01-04 14:51:27 +03:00
|
|
|
|
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
2020-12-23 22:02:01 +03:00
|
|
|
|
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
|
|
|
|
dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu;
|
|
|
|
|
} else {
|
|
|
|
|
dmTagAesthetics.onAddRoom = dmOnAddRoom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-06 19:58:29 +03:00
|
|
|
|
private onAction = (payload: ActionPayload) => {
|
|
|
|
|
if (payload.action === Action.ViewRoomDelta) {
|
|
|
|
|
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
|
|
|
|
const currentRoomId = RoomViewStore.getRoomId();
|
|
|
|
|
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
|
|
|
|
|
if (room) {
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
room_id: room.roomId,
|
|
|
|
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-12-23 22:02:01 +03:00
|
|
|
|
} else if (payload.action === Action.PstnSupportUpdated) {
|
|
|
|
|
this.updateDmAddRoomAction();
|
|
|
|
|
this.updateLists();
|
2020-07-06 19:58:29 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
|
|
|
|
const lists = RoomListStore.instance.orderedLists;
|
2021-03-08 18:52:21 +03:00
|
|
|
|
const rooms: Room[] = [];
|
2020-07-06 19:58:29 +03:00
|
|
|
|
TAG_ORDER.forEach(t => {
|
|
|
|
|
let listRooms = lists[t];
|
|
|
|
|
|
|
|
|
|
if (unread) {
|
|
|
|
|
// filter to only notification rooms (and our current active room so we can index properly)
|
2020-07-09 04:26:25 +03:00
|
|
|
|
listRooms = listRooms.filter(r => {
|
2020-07-22 05:51:40 +03:00
|
|
|
|
const state = RoomNotificationStateStore.instance.getRoomState(r);
|
2020-07-09 04:26:25 +03:00
|
|
|
|
return state.room.roomId === roomId || state.isUnread;
|
2020-07-06 19:58:29 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rooms.push(...listRooms);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const currentIndex = rooms.findIndex(r => r.roomId === roomId);
|
|
|
|
|
// use slice to account for looping around the start
|
|
|
|
|
const [room] = rooms.slice((currentIndex + delta) % rooms.length);
|
|
|
|
|
return room;
|
|
|
|
|
};
|
|
|
|
|
|
2021-03-24 17:18:06 +03:00
|
|
|
|
private updateActiveSpace = (activeSpace: Room) => {
|
|
|
|
|
this.setState({ activeSpace });
|
|
|
|
|
};
|
|
|
|
|
|
2021-03-08 18:52:21 +03:00
|
|
|
|
private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
|
|
|
|
|
this.setState({ suggestedRooms });
|
|
|
|
|
};
|
|
|
|
|
|
2020-07-02 23:23:56 +03:00
|
|
|
|
private updateLists = () => {
|
2020-07-14 18:56:17 +03:00
|
|
|
|
const newLists = RoomListStore.instance.orderedLists;
|
2020-07-16 23:43:43 +03:00
|
|
|
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
2020-08-03 18:02:26 +03:00
|
|
|
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
2020-07-14 18:56:17 +03:00
|
|
|
|
console.log("new lists", newLists);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-24 06:23:45 +03:00
|
|
|
|
const previousListIds = Object.keys(this.state.sublists);
|
2020-07-30 23:18:54 +03:00
|
|
|
|
const newListIds = Object.keys(newLists).filter(t => {
|
|
|
|
|
if (!isCustomTag(t)) return true; // always include non-custom tags
|
|
|
|
|
|
|
|
|
|
// if the tag is custom though, only include it if it is enabled
|
|
|
|
|
return CustomRoomTagStore.getTags()[t];
|
|
|
|
|
});
|
2020-07-24 06:23:45 +03:00
|
|
|
|
|
2020-11-11 16:01:40 +03:00
|
|
|
|
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
|
|
|
|
|
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
|
2020-07-24 07:19:16 +03:00
|
|
|
|
if (!doUpdate) {
|
|
|
|
|
// so we didn't have the visible sublists change, but did the contents of those
|
|
|
|
|
// sublists change significantly enough to break the sticky headers? Probably, so
|
|
|
|
|
// let's check the length of each.
|
|
|
|
|
for (const tagId of newListIds) {
|
|
|
|
|
const oldRooms = this.state.sublists[tagId];
|
|
|
|
|
const newRooms = newLists[tagId];
|
|
|
|
|
if (oldRooms.length !== newRooms.length) {
|
|
|
|
|
doUpdate = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (doUpdate) {
|
2020-07-24 19:38:04 +03:00
|
|
|
|
// We have to break our reference to the room list store if we want to be able to
|
|
|
|
|
// diff the object for changes, so do that.
|
2020-08-05 11:13:01 +03:00
|
|
|
|
// @ts-ignore - ITagMap is ts-ignored so this will have to be too
|
2020-07-30 23:18:54 +03:00
|
|
|
|
const newSublists = objectWithOnly(newLists, newListIds);
|
|
|
|
|
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
2020-07-24 19:38:04 +03:00
|
|
|
|
|
2020-11-11 16:01:40 +03:00
|
|
|
|
this.setState({sublists, isNameFiltering}, () => {
|
2020-07-24 06:23:45 +03:00
|
|
|
|
this.props.onResize();
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-07-02 23:23:56 +03:00
|
|
|
|
};
|
|
|
|
|
|
2020-11-11 16:21:20 +03:00
|
|
|
|
private onStartChat = () => {
|
2020-11-11 16:36:17 +03:00
|
|
|
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
|
|
|
|
dis.dispatch({ action: "view_create_chat", initialText });
|
2020-11-11 16:21:20 +03:00
|
|
|
|
};
|
|
|
|
|
|
2020-08-17 20:20:00 +03:00
|
|
|
|
private onExplore = () => {
|
2020-11-11 16:36:17 +03:00
|
|
|
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
|
|
|
|
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
2020-08-17 20:20:00 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-03-24 17:18:06 +03:00
|
|
|
|
private onSpaceInviteClick = () => {
|
|
|
|
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
|
|
|
|
if (this.state.activeSpace.getJoinRule() === "public") {
|
|
|
|
|
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
|
|
|
|
title: _t("Invite to %(spaceName)s", { spaceName: this.state.activeSpace.name }),
|
|
|
|
|
description: <React.Fragment>
|
|
|
|
|
<span>{ _t("Share your public space") }</span>
|
|
|
|
|
<SpacePublicShare space={this.state.activeSpace} onFinished={() => modal.close()} />
|
|
|
|
|
</React.Fragment>,
|
|
|
|
|
fixedWidth: false,
|
|
|
|
|
button: false,
|
|
|
|
|
className: "mx_SpacePanel_sharePublicSpace",
|
|
|
|
|
hasCloseButton: true,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
showRoomInviteDialog(this.state.activeSpace.roomId, initialText);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-03-08 20:48:08 +03:00
|
|
|
|
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
2021-03-08 18:52:21 +03:00
|
|
|
|
return this.state.suggestedRooms.map(room => {
|
|
|
|
|
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
|
|
|
|
const avatar = (
|
|
|
|
|
<RoomAvatar
|
|
|
|
|
oobData={{
|
|
|
|
|
name,
|
|
|
|
|
avatarUrl: room.avatar_url,
|
|
|
|
|
}}
|
|
|
|
|
width={32}
|
|
|
|
|
height={32}
|
|
|
|
|
resizeMethod="crop"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
const viewRoom = () => {
|
|
|
|
|
defaultDispatcher.dispatch({
|
|
|
|
|
action: "view_room",
|
|
|
|
|
room_id: room.room_id,
|
2021-03-09 16:41:06 +03:00
|
|
|
|
oobData: {
|
|
|
|
|
avatarUrl: room.avatar_url,
|
|
|
|
|
name,
|
|
|
|
|
},
|
2021-03-08 18:52:21 +03:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
return (
|
2021-03-08 20:48:08 +03:00
|
|
|
|
<ExtraTile
|
2021-03-08 18:52:21 +03:00
|
|
|
|
isMinimized={this.props.isMinimized}
|
|
|
|
|
isSelected={this.state.currentRoomId === room.room_id}
|
|
|
|
|
displayName={name}
|
|
|
|
|
avatar={avatar}
|
|
|
|
|
onClick={viewRoom}
|
|
|
|
|
key={`suggestedRoomTile_${room.room_id}`}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 20:48:08 +03:00
|
|
|
|
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
2020-07-02 18:04:38 +03:00
|
|
|
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
2020-08-03 18:02:26 +03:00
|
|
|
|
// See https://github.com/vector-im/element-web/issues/14456
|
2020-07-02 18:04:38 +03:00
|
|
|
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
2020-08-29 03:11:08 +03:00
|
|
|
|
return g.myMembership === 'invite';
|
2020-07-02 18:04:38 +03:00
|
|
|
|
}).map(g => {
|
|
|
|
|
const avatar = (
|
|
|
|
|
<GroupAvatar
|
|
|
|
|
groupId={g.groupId}
|
|
|
|
|
groupName={g.name}
|
|
|
|
|
groupAvatarUrl={g.avatarUrl}
|
|
|
|
|
width={32} height={32} resizeMethod='crop'
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
const openGroup = () => {
|
|
|
|
|
defaultDispatcher.dispatch({
|
|
|
|
|
action: 'view_group',
|
|
|
|
|
group_id: g.groupId,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
return (
|
2021-03-08 20:48:08 +03:00
|
|
|
|
<ExtraTile
|
2020-07-02 18:04:38 +03:00
|
|
|
|
isMinimized={this.props.isMinimized}
|
|
|
|
|
isSelected={false}
|
|
|
|
|
displayName={g.name}
|
|
|
|
|
avatar={avatar}
|
|
|
|
|
notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
|
|
|
|
onClick={openGroup}
|
2020-07-02 18:27:42 +03:00
|
|
|
|
key={`temporaryGroupTile_${g.groupId}`}
|
2020-07-02 18:04:38 +03:00
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
|
private renderSublists(): React.ReactElement[] {
|
|
|
|
|
const components: React.ReactElement[] = [];
|
|
|
|
|
|
2020-07-21 01:51:16 +03:00
|
|
|
|
const tagOrder = TAG_ORDER.reduce((p, c) => {
|
|
|
|
|
if (c === CUSTOM_TAGS_BEFORE_TAG) {
|
|
|
|
|
const customTags = Object.keys(this.state.sublists)
|
2020-07-30 23:18:54 +03:00
|
|
|
|
.filter(t => isCustomTag(t));
|
2020-07-21 01:51:16 +03:00
|
|
|
|
p.push(...customTags);
|
2020-04-30 22:21:50 +03:00
|
|
|
|
}
|
2020-07-21 01:51:16 +03:00
|
|
|
|
p.push(c);
|
|
|
|
|
return p;
|
|
|
|
|
}, [] as TagID[]);
|
2020-04-30 22:21:50 +03:00
|
|
|
|
|
2020-11-11 16:21:20 +03:00
|
|
|
|
// show a skeleton UI if the user is in no rooms and they are not filtering
|
|
|
|
|
const showSkeleton = !this.state.isNameFiltering &&
|
|
|
|
|
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
2020-11-02 20:22:45 +03:00
|
|
|
|
|
2020-07-21 01:51:16 +03:00
|
|
|
|
for (const orderedTagId of tagOrder) {
|
2020-04-30 22:21:50 +03:00
|
|
|
|
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
2021-03-08 18:52:21 +03:00
|
|
|
|
|
|
|
|
|
let extraTiles = null;
|
|
|
|
|
if (orderedTagId === DefaultTagID.Invite) {
|
|
|
|
|
extraTiles = this.renderCommunityInvites();
|
|
|
|
|
} else if (orderedTagId === DefaultTagID.Suggested) {
|
|
|
|
|
extraTiles = this.renderSuggestedRooms();
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-07 05:32:37 +03:00
|
|
|
|
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
|
|
|
|
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
2020-04-30 22:21:50 +03:00
|
|
|
|
continue; // skip tag - not needed
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-21 01:51:16 +03:00
|
|
|
|
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
|
|
|
|
? customTagAesthetics(orderedTagId)
|
2020-12-23 22:02:01 +03:00
|
|
|
|
: this.tagAesthetics[orderedTagId];
|
2020-04-30 22:21:50 +03:00
|
|
|
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
|
|
|
|
|
2020-08-29 03:11:08 +03:00
|
|
|
|
components.push(<RoomSublist
|
|
|
|
|
key={`sublist-${orderedTagId}`}
|
|
|
|
|
tagId={orderedTagId}
|
|
|
|
|
forRooms={true}
|
|
|
|
|
startAsHidden={aesthetics.defaultHidden}
|
|
|
|
|
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
|
|
|
|
onAddRoom={aesthetics.onAddRoom}
|
|
|
|
|
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
|
|
|
|
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
|
|
|
|
isMinimized={this.props.isMinimized}
|
|
|
|
|
onResize={this.props.onResize}
|
2020-11-02 20:22:45 +03:00
|
|
|
|
showSkeleton={showSkeleton}
|
2021-03-08 20:48:08 +03:00
|
|
|
|
extraTiles={extraTiles}
|
2020-08-29 03:11:08 +03:00
|
|
|
|
/>);
|
2020-04-30 22:21:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return components;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
|
public render() {
|
2020-08-17 20:20:00 +03:00
|
|
|
|
let explorePrompt: JSX.Element;
|
2020-11-02 20:23:05 +03:00
|
|
|
|
if (!this.props.isMinimized) {
|
2020-11-11 16:01:40 +03:00
|
|
|
|
if (this.state.isNameFiltering) {
|
2020-11-02 20:23:05 +03:00
|
|
|
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
|
|
|
|
<div>{_t("Can't see what you’re looking for?")}</div>
|
2020-11-11 16:21:20 +03:00
|
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_startChat"
|
|
|
|
|
kind="link"
|
|
|
|
|
onClick={this.onStartChat}
|
|
|
|
|
>
|
|
|
|
|
{_t("Start a new chat")}
|
|
|
|
|
</AccessibleButton>
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_explore"
|
|
|
|
|
kind="link"
|
|
|
|
|
onClick={this.onExplore}
|
|
|
|
|
>
|
2021-03-24 17:18:06 +03:00
|
|
|
|
{ this.state.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
|
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>;
|
|
|
|
|
} else if (this.state.activeSpace) {
|
|
|
|
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
|
|
|
|
<div>{ _t("Quick actions") }</div>
|
|
|
|
|
{ this.state.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_spaceInvite"
|
|
|
|
|
onClick={this.onSpaceInviteClick}
|
|
|
|
|
>
|
|
|
|
|
{_t("Invite people")}
|
|
|
|
|
</AccessibleButton> }
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_spaceExplore"
|
|
|
|
|
onClick={this.onExplore}
|
|
|
|
|
>
|
|
|
|
|
{_t("Explore rooms")}
|
2020-11-02 20:23:05 +03:00
|
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>;
|
|
|
|
|
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
|
|
|
|
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
|
|
|
|
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
|
|
|
|
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
2021-01-28 00:39:52 +03:00
|
|
|
|
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
2020-11-02 20:23:05 +03:00
|
|
|
|
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
2021-01-28 00:39:52 +03:00
|
|
|
|
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
2020-11-02 20:23:05 +03:00
|
|
|
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
|
|
|
|
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
2020-11-11 16:21:20 +03:00
|
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_startChat"
|
|
|
|
|
kind="link"
|
|
|
|
|
onClick={this.onStartChat}
|
|
|
|
|
>
|
|
|
|
|
{_t("Start a new chat")}
|
|
|
|
|
</AccessibleButton>
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_RoomList_explorePrompt_explore"
|
|
|
|
|
kind="link"
|
|
|
|
|
onClick={this.onExplore}
|
|
|
|
|
>
|
2020-11-02 20:23:05 +03:00
|
|
|
|
{_t("Explore all public rooms")}
|
|
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-17 20:20:00 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
|
const sublists = this.renderSublists();
|
2020-03-20 23:38:20 +03:00
|
|
|
|
return (
|
|
|
|
|
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
|
|
|
|
{({onKeyDownHandler}) => (
|
|
|
|
|
<div
|
|
|
|
|
onFocus={this.props.onFocus}
|
|
|
|
|
onBlur={this.props.onBlur}
|
|
|
|
|
onKeyDown={onKeyDownHandler}
|
2020-07-18 00:27:49 +03:00
|
|
|
|
className="mx_RoomList"
|
2020-03-20 23:38:20 +03:00
|
|
|
|
role="tree"
|
|
|
|
|
aria-label={_t("Rooms")}
|
2020-08-17 20:20:00 +03:00
|
|
|
|
>
|
|
|
|
|
{sublists}
|
|
|
|
|
{explorePrompt}
|
|
|
|
|
</div>
|
2020-03-20 23:38:20 +03:00
|
|
|
|
)}
|
|
|
|
|
</RovingTabIndexProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|