Support custom tags in the room list again

Fixes https://github.com/vector-im/riot-web/issues/14091

Design needs work, however this is behind labs anyways. This re-implements the behaviour of the old room list.

The implementation ended up being a lot easier due to early confusion with what the TagOrderStore and TagPanel take care of. Turns out they don't deal with tags, but groups. As such, we don't need to do anything with filtering (though we keep some sanity checks in place for safety), and just have to wire up the CustomRoomTagPanel and CustomRoomTagStore.
pull/21833/head
Travis Ralston 2020-07-20 16:51:16 -06:00
parent 4de1645ac7
commit a0b2859436
10 changed files with 89 additions and 54 deletions

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Update design for custom tags to match new designs
.mx_LeftPanel_tagPanelContainer {
display: flex;
flex-direction: column;
@ -50,7 +52,7 @@ limitations under the License.
background-color: $accent-color-alt;
width: 5px;
position: absolute;
left: -15px;
left: -9px;
border-radius: 0 3px 3px 0;
top: 2px; // 10 [padding-top] - (56 - 40)/2
top: 12px; // just feels right (see comment above about designs needing to be updated)
}

View File

@ -72,17 +72,17 @@ class CustomRoomTagTile extends React.Component {
const tag = this.props.tag;
const avatarHeight = 40;
const className = classNames({
CustomRoomTagPanel_tileSelected: tag.selected,
"CustomRoomTagPanel_tileSelected": tag.selected,
});
const name = tag.name;
const badge = tag.badge;
const badgeNotifState = tag.badgeNotifState;
let badgeElement;
if (badge) {
if (badgeNotifState) {
const badgeClasses = classNames({
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight,
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
});
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
}
return (

View File

@ -17,6 +17,7 @@ limitations under the License.
import * as React from "react";
import { createRef } from "react";
import TagPanel from "./TagPanel";
import CustomRoomTagPanel from "./CustomRoomTagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
@ -361,6 +362,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
{SettingsStore.isFeatureEnabled("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div>
);

View File

@ -26,7 +26,7 @@ import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist from "./RoomSublist";
@ -41,6 +41,7 @@ import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SettingsStore from "../../../settings/SettingsStore";
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -77,6 +78,7 @@ const ALWAYS_VISIBLE_TAGS: TagID[] = [
interface ITagAesthetics {
sectionLabel: string;
sectionLabelRaw?: string;
addRoomLabel?: string;
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
isInvite: boolean;
@ -130,9 +132,22 @@ const TAG_AESTHETICS: {
},
};
function customTagAesthetics(tagId: TagID): ITagAesthetics {
if (tagId.startsWith("u.")) {
tagId = tagId.substring(2);
}
return {
sectionLabel: _td("Custom Tag"),
sectionLabelRaw: tagId,
isInvite: false,
defaultHidden: false,
};
}
export default class RoomList extends React.Component<IProps, IState> {
private searchFilter: NameFilterCondition = new NameFilterCondition();
private dispatcherRef;
private customTagStoreRef;
constructor(props: IProps) {
super(props);
@ -161,12 +176,14 @@ export default class RoomList extends React.Component<IProps, IState> {
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update
}
public componentWillUnmount() {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove();
}
private onAction = (payload: ActionPayload) => {
@ -257,12 +274,18 @@ export default class RoomList extends React.Component<IProps, IState> {
private renderSublists(): React.ReactElement[] {
const components: React.ReactElement[] = [];
for (const orderedTagId of TAG_ORDER) {
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
// Populate custom tags if needed
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
const tagOrder = TAG_ORDER.reduce((p, c) => {
if (c === CUSTOM_TAGS_BEFORE_TAG) {
const customTags = Object.keys(this.state.sublists)
.filter(t => isCustomTag(t))
.filter(t => CustomRoomTagStore.getTags()[t]); // isSelected
p.push(...customTags);
}
p.push(c);
return p;
}, [] as TagID[]);
for (const orderedTagId of tagOrder) {
const orderedRooms = this.state.sublists[orderedTagId] || [];
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
@ -270,7 +293,9 @@ export default class RoomList extends React.Component<IProps, IState> {
continue; // skip tag - not needed
}
const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId];
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId)
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
@ -281,7 +306,7 @@ export default class RoomList extends React.Component<IProps, IState> {
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isMinimized={this.props.isMinimized}

View File

@ -1,5 +1,6 @@
/*
Copyright 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.
@ -13,15 +14,14 @@ 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 dis from '../dispatcher/dispatcher';
import * as RoomNotifs from '../RoomNotifs';
import EventEmitter from 'events';
import { throttle } from "lodash";
import {throttle} from "lodash";
import SettingsStore from "../settings/SettingsStore";
import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
// TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
import {isCustomTag} from "./room-list/models";
function commonPrefix(a, b) {
const len = Math.min(a.length, b.length);
@ -84,8 +84,6 @@ class CustomRoomTagStore extends EventEmitter {
}
getSortedTags() {
const roomLists = RoomListStore.instance.orderedLists;
const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => {
const isFirst = i === 0;
@ -97,14 +95,14 @@ class CustomRoomTagStore extends EventEmitter {
return longestPrefix;
});
return tagNames.map((name, i) => {
const notifs = RoomNotifs.aggregateNotificationCount(roomLists[name]);
let badge;
if (notifs.count !== 0) {
badge = notifs;
const notifs = RoomNotificationStateStore.instance.getListState(name);
let badgeNotifState;
if (notifs.hasUnreadCount) {
badgeNotifState = notifs;
}
const avatarLetter = name.substr(prefixes[i].length, 1);
const selected = this._state.tags[name];
return {name, avatarLetter, badge, selected};
return {name, avatarLetter, badgeNotifState, selected};
});
}
@ -139,16 +137,12 @@ class CustomRoomTagStore extends EventEmitter {
return;
}
const newTagNames = Object.keys(RoomListStore.instance.orderedLists)
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);
}).sort();
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
const prevTags = this._state && this._state.tags;
const newTags = newTagNames.reduce((newTags, tagName) => {
newTags[tagName] = (prevTags && prevTags[tagName]) || false;
return newTags;
return newTagNames.reduce((c, tagName) => {
c[tagName] = (prevTags && prevTags[tagName]) || false;
return c;
}, {});
return newTags;
}
}

View File

@ -17,7 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import TagOrderStore from "../TagOrderStore";
import { Room } from "matrix-js-sdk/src/models/room";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
@ -33,6 +33,7 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import RoomListLayoutStore from "./RoomListLayoutStore";
import { MarkedExecution } from "../../utils/MarkedExecution";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import { isEnumValue } from "../../utils/enums";
interface IState {
tagsEnabled?: boolean;
@ -527,25 +528,28 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
public async regenerateAllLists({trigger = true}) {
console.warn("Regenerating all room lists");
const rooms = this.matrixClient.getVisibleRooms();
const customTags = new Set<TagID>();
if (this.state.tagsEnabled) {
for (const room of rooms) {
if (!room.tags) continue;
const tags = Object.keys(room.tags).filter(t => isCustomTag(t));
tags.forEach(t => customTags.add(t));
}
}
const sorts: ITagSortingMap = {};
const orders: IListOrderingMap = {};
for (const tagId of OrderedDefaultTagIDs) {
const allTags = [...OrderedDefaultTagIDs, ...Array.from(customTags)];
for (const tagId of allTags) {
sorts[tagId] = this.calculateTagSorting(tagId);
orders[tagId] = this.calculateListOrder(tagId);
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
}
if (this.state.tagsEnabled) {
// TODO: Fix custom tags: https://github.com/vector-im/riot-web/issues/14091
const roomTags = TagOrderStore.getOrderedTags() || [];
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("rtags", roomTags);
}
await this.algorithm.populateTags(sorts, orders);
await this.algorithm.setKnownRooms(this.matrixClient.getVisibleRooms());
await this.algorithm.setKnownRooms(rooms);
this.initialListsGenerated = true;

View File

@ -20,10 +20,9 @@ import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
/**
* Watches for changes in tags/groups to manage filters on the provided RoomListStore
* Watches for changes in groups to manage filters on the provided RoomListStore
*/
export class TagWatcher {
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStoreClass) {
@ -43,8 +42,6 @@ export class TagWatcher {
}
const newFilters = new Map<string, CommunityFilterCondition>();
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
const filterableTags = newTags.filter(t => t.startsWith("+"));
for (const tag of filterableTags) {
@ -64,8 +61,6 @@ export class TagWatcher {
// Update the room list store's filters
const diff = arrayDiff(lastTags, newTags);
for (const tag of diff.added) {
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
// Ref https://github.com/vector-im/riot-web/issues/14091
const filter = newFilters.get(tag);
if (!filter) continue;

View File

@ -563,9 +563,6 @@ export class Algorithm extends EventEmitter {
}
public getTagsForRoom(room: Room): TagID[] {
// XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
// different use case and therefore different performance curve
const tags: TagID[] = [];
const membership = getEffectiveMembership(room.getMyMembership());

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { isEnumValue } from "../../utils/enums";
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
@ -36,6 +38,10 @@ export const OrderedDefaultTagIDs = [
export type TagID = string | DefaultTagID;
export function isCustomTag(tagId: TagID): boolean {
return !isEnumValue(DefaultTagID, tagId);
}
export enum RoomUpdateCause {
Timeline = "TIMELINE",
PossibleTagChange = "POSSIBLE_TAG_CHANGE",

View File

@ -25,3 +25,13 @@ export function getEnumValues<T>(e: any): T[] {
.filter(k => ['string', 'number'].includes(typeof(e[k])))
.map(k => e[k]);
}
/**
* Determines if a given value is a valid value for the provided enum.
* @param e The enum to check against.
* @param val The value to search for.
* @returns True if the enum contains the value.
*/
export function isEnumValue<T>(e: T, val: string | number): boolean {
return getEnumValues(e).includes(val);
}