Merge pull request #5024 from matrix-org/travis/room-list/custom-tags

Support custom tags in the room list again
This commit is contained in:
Travis Ralston 2020-07-21 06:46:50 -06:00 committed by GitHub
commit 37aed54d12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 90 additions and 54 deletions

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Update design for custom tags to match new designs
.mx_LeftPanel_tagPanelContainer { .mx_LeftPanel_tagPanelContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -50,7 +52,7 @@ limitations under the License.
background-color: $accent-color-alt; background-color: $accent-color-alt;
width: 5px; width: 5px;
position: absolute; position: absolute;
left: -15px; left: -9px;
border-radius: 0 3px 3px 0; 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 tag = this.props.tag;
const avatarHeight = 40; const avatarHeight = 40;
const className = classNames({ const className = classNames({
CustomRoomTagPanel_tileSelected: tag.selected, "CustomRoomTagPanel_tileSelected": tag.selected,
}); });
const name = tag.name; const name = tag.name;
const badge = tag.badge; const badgeNotifState = tag.badgeNotifState;
let badgeElement; let badgeElement;
if (badge) { if (badgeNotifState) {
const badgeClasses = classNames({ const badgeClasses = classNames({
"mx_TagTile_badge": true, "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 ( return (

View file

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

View file

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

View file

@ -1156,6 +1156,7 @@
"Low priority": "Low priority", "Low priority": "Low priority",
"System Alerts": "System Alerts", "System Alerts": "System Alerts",
"Historical": "Historical", "Historical": "Historical",
"Custom Tag": "Custom Tag",
"This room": "This room", "This room": "This room",
"Joining room …": "Joining room …", "Joining room …": "Joining room …",
"Loading …": "Loading …", "Loading …": "Loading …",

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from '../dispatcher/dispatcher'; import dis from '../dispatcher/dispatcher';
import * as RoomNotifs from '../RoomNotifs';
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 RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
// TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091 import {isCustomTag} from "./room-list/models";
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
function commonPrefix(a, b) { function commonPrefix(a, b) {
const len = Math.min(a.length, b.length); const len = Math.min(a.length, b.length);
@ -84,8 +84,6 @@ class CustomRoomTagStore extends EventEmitter {
} }
getSortedTags() { getSortedTags() {
const roomLists = RoomListStore.instance.orderedLists;
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) => {
const isFirst = i === 0; const isFirst = i === 0;
@ -97,14 +95,14 @@ class CustomRoomTagStore extends EventEmitter {
return longestPrefix; return longestPrefix;
}); });
return tagNames.map((name, i) => { return tagNames.map((name, i) => {
const notifs = RoomNotifs.aggregateNotificationCount(roomLists[name]); const notifs = RoomNotificationStateStore.instance.getListState(name);
let badge; let badgeNotifState;
if (notifs.count !== 0) { if (notifs.hasUnreadCount) {
badge = notifs; badgeNotifState = notifs;
} }
const avatarLetter = name.substr(prefixes[i].length, 1); const avatarLetter = name.substr(prefixes[i].length, 1);
const selected = this._state.tags[name]; 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; return;
} }
const newTagNames = Object.keys(RoomListStore.instance.orderedLists) const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);
}).sort();
const prevTags = this._state && this._state.tags; const prevTags = this._state && this._state.tags;
const newTags = newTagNames.reduce((newTags, tagName) => { return newTagNames.reduce((c, tagName) => {
newTags[tagName] = (prevTags && prevTags[tagName]) || false; c[tagName] = (prevTags && prevTags[tagName]) || false;
return newTags; return c;
}, {}); }, {});
return newTags;
} }
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore"; 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 TagOrderStore from "../TagOrderStore";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; 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 RoomListLayoutStore from "./RoomListLayoutStore";
import { MarkedExecution } from "../../utils/MarkedExecution"; import { MarkedExecution } from "../../utils/MarkedExecution";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import { isEnumValue } from "../../utils/enums";
interface IState { interface IState {
tagsEnabled?: boolean; tagsEnabled?: boolean;
@ -527,25 +528,28 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
public async regenerateAllLists({trigger = true}) { public async regenerateAllLists({trigger = true}) {
console.warn("Regenerating all room lists"); 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 sorts: ITagSortingMap = {};
const orders: IListOrderingMap = {}; const orders: IListOrderingMap = {};
for (const tagId of OrderedDefaultTagIDs) { const allTags = [...OrderedDefaultTagIDs, ...Array.from(customTags)];
for (const tagId of allTags) {
sorts[tagId] = this.calculateTagSorting(tagId); sorts[tagId] = this.calculateTagSorting(tagId);
orders[tagId] = this.calculateListOrder(tagId); orders[tagId] = this.calculateListOrder(tagId);
RoomListLayoutStore.instance.ensureLayoutExists(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.populateTags(sorts, orders);
await this.algorithm.setKnownRooms(this.matrixClient.getVisibleRooms()); await this.algorithm.setKnownRooms(rooms);
this.initialListsGenerated = true; this.initialListsGenerated = true;

View file

@ -20,10 +20,9 @@ import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; 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 { export class TagWatcher {
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
private filters = new Map<string, CommunityFilterCondition>(); private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStoreClass) { constructor(private store: RoomListStoreClass) {
@ -43,8 +42,6 @@ export class TagWatcher {
} }
const newFilters = new Map<string, CommunityFilterCondition>(); 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("+")); const filterableTags = newTags.filter(t => t.startsWith("+"));
for (const tag of filterableTags) { for (const tag of filterableTags) {
@ -64,8 +61,6 @@ export class TagWatcher {
// Update the room list store's filters // Update the room list store's filters
const diff = arrayDiff(lastTags, newTags); const diff = arrayDiff(lastTags, newTags);
for (const tag of diff.added) { 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); const filter = newFilters.get(tag);
if (!filter) continue; if (!filter) continue;

View file

@ -563,9 +563,6 @@ export class Algorithm extends EventEmitter {
} }
public getTagsForRoom(room: Room): TagID[] { 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 tags: TagID[] = [];
const membership = getEffectiveMembership(room.getMyMembership()); 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. limitations under the License.
*/ */
import { isEnumValue } from "../../utils/enums";
export enum DefaultTagID { export enum DefaultTagID {
Invite = "im.vector.fake.invite", Invite = "im.vector.fake.invite",
Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms 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 type TagID = string | DefaultTagID;
export function isCustomTag(tagId: TagID): boolean {
return !isEnumValue(DefaultTagID, tagId);
}
export enum RoomUpdateCause { export enum RoomUpdateCause {
Timeline = "TIMELINE", Timeline = "TIMELINE",
PossibleTagChange = "POSSIBLE_TAG_CHANGE", 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]))) .filter(k => ['string', 'number'].includes(typeof(e[k])))
.map(k => 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);
}