diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 1fb18ec41e..135a51c7cd 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -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) } diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index 2753d5c4da..a79bdafeb5 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -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 = (
{FormattingUtils.formatCount(badge.count)}
); + badgeElement = (
{FormattingUtils.formatCount(badgeNotifState.count)}
); } return ( diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index dfefeacde5..43e75ffe88 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -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 { const tagPanel = !this.state.showTagPanel ? null : (
+ {SettingsStore.isFeatureEnabled("feature_custom_tags") ? : null}
); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 3b5a7a0ce7..33c844e955 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -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) => 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 { 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 { 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 { 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 { 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 { 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} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 998d582853..1b508feb19 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1156,6 +1156,7 @@ "Low priority": "Low priority", "System Alerts": "System Alerts", "Historical": "Historical", + "Custom Tag": "Custom Tag", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index b002971e2e..9967708c29 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -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; } } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 0cb1e094b1..46dd870541 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -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 { public async regenerateAllLists({trigger = true}) { console.warn("Regenerating all room lists"); + const rooms = this.matrixClient.getVisibleRooms(); + const customTags = new Set(); + 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; diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts index 81cbc84be1..dc445d10ba 100644 --- a/src/stores/room-list/TagWatcher.ts +++ b/src/stores/room-list/TagWatcher.ts @@ -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(); constructor(private store: RoomListStoreClass) { @@ -43,8 +42,6 @@ export class TagWatcher { } const newFilters = new Map(); - - // 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; diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 9c75dab251..9e920064b0 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -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()); diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 0ccd623544..7d3902f552 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -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", diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 79656d7675..f7f4787896 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -25,3 +25,13 @@ export function getEnumValues(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(e: T, val: string | number): boolean { + return getEnumValues(e).includes(val); +}