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);
+}