;
},
});
diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js
new file mode 100644
index 0000000000..e976fdd436
--- /dev/null
+++ b/src/components/structures/TagPanelButtons.js
@@ -0,0 +1,58 @@
+/*
+Copyright 2019 New Vector Ltd.
+
+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.
+*/
+
+import React from 'react';
+import sdk from '../../index';
+import dis from '../../dispatcher';
+import Modal from '../../Modal';
+import { _t } from '../../languageHandler';
+
+const TagPanelButtons = React.createClass({
+ displayName: 'TagPanelButtons',
+
+
+ componentWillMount: function() {
+ this._dispatcherRef = dis.register(this._onAction);
+ },
+
+ componentWillUnmount() {
+ if (this._dispatcherRef) {
+ dis.unregister(this._dispatcherRef);
+ this._dispatcherRef = null;
+ }
+ },
+
+ _onAction(payload) {
+ if (payload.action === "show_redesign_feedback_dialog") {
+ const RedesignFeedbackDialog =
+ sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
+ Modal.createDialog(RedesignFeedbackDialog);
+ }
+ },
+
+ render() {
+ const GroupsButton = sdk.getComponent('elements.GroupsButton');
+ const ActionButton = sdk.getComponent("elements.ActionButton");
+
+ return (
+
+
+
);
+ },
+});
+export default TagPanelButtons;
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index ecbcc7213d..f5ee60a2d8 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -23,9 +23,11 @@ import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import * as ContextualMenu from '../../structures/ContextualMenu';
+import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
+import TagOrderStore from '../../../stores/TagOrderStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@@ -168,6 +170,16 @@ export default React.createClass({
mx_TagTile_selected: this.props.selected,
});
+ const badge = TagOrderStore.getGroupBadge(this.props.tag);
+ let badgeElement;
+ if (badge && !this.state.hover) {
+ const badgeClasses = classNames({
+ "mx_TagTile_badge": true,
+ "mx_TagTile_badgeHighlight": badge.highlight,
+ });
+ badgeElement = (
{FormattingUtils.formatCount(badge.count)}
);
+ }
+
const tip = this.state.hover ?
:
;
@@ -186,6 +198,7 @@ export default React.createClass({
/>
{ tip }
{ contextButton }
+ { badgeElement }
;
},
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 534988922f..8418ab6d6f 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -32,11 +32,12 @@ import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt');
import TagOrderStore from '../../../stores/TagOrderStore';
import RoomListStore from '../../../stores/RoomListStore';
+import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle';
-import {Resizer} from '../../../resizer'
+import {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -121,6 +122,7 @@ module.exports = React.createClass({
incomingCall: null,
selectedTags: [],
hover: false,
+ customTags: CustomRoomTagStore.getTags(),
};
},
@@ -170,6 +172,12 @@ module.exports = React.createClass({
this._delayedRefreshRoomList();
});
+ this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
+ this.setState({
+ customTags: CustomRoomTagStore.getTags(),
+ });
+ });
+
this.refreshRoomList();
// order of the sublists
@@ -266,6 +274,9 @@ module.exports = React.createClass({
if (this._roomListStoreToken) {
this._roomListStoreToken.remove();
}
+ if (this._customTagStoreToken) {
+ this._customTagStoreToken.remove();
+ }
// NB: GroupStore is not a Flux.Store
if (this._groupStoreToken) {
@@ -717,7 +728,7 @@ module.exports = React.createClass({
];
const tagSubLists = Object.keys(this.state.lists)
.filter((tagName) => {
- return !tagName.match(STANDARD_TAGS_REGEX);
+ return this.state.customTags[tagName] && !tagName.match(STANDARD_TAGS_REGEX);
}).map((tagName) => {
return {
list: this.state.lists[tagName],
diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js
new file mode 100644
index 0000000000..1983fa7462
--- /dev/null
+++ b/src/stores/CustomRoomTagStore.js
@@ -0,0 +1,143 @@
+/*
+Copyright 2019 New Vector Ltd
+
+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.
+*/
+import dis from '../dispatcher';
+import * as RoomNotifs from '../RoomNotifs';
+import RoomListStore from './RoomListStore';
+import EventEmitter from 'events';
+
+const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
+
+function commonPrefix(a, b) {
+ const len = Math.min(a.length, b.length);
+ let prefix;
+ for (let i = 0; i < len; ++i) {
+ if (a.charAt(i) !== b.charAt(i)) {
+ prefix = a.substr(0, i);
+ break;
+ }
+ }
+ if (prefix === undefined) {
+ prefix = a.substr(0, len);
+ }
+ const spaceIdx = prefix.indexOf(' ');
+ if (spaceIdx !== -1) {
+ prefix = prefix.substr(0, spaceIdx + 1);
+ }
+ if (prefix.length >= 2) {
+ return prefix;
+ }
+ return "";
+}
+/**
+ * A class for storing application state for ordering tags in the TagPanel.
+ */
+class CustomRoomTagStore extends EventEmitter {
+ constructor() {
+ super();
+ // Initialise state
+ this._state = {tags: this._getUpdatedTags()};
+
+ this._roomListStoreToken = RoomListStore.addListener(() => {
+ this._setState({tags: this._getUpdatedTags()});
+ });
+ dis.register(payload => this._onDispatch(payload));
+ }
+
+ getTags() {
+ return this._state.tags;
+ }
+
+ _setState(newState) {
+ this._state = Object.assign(this._state, newState);
+ this.emit("change");
+ }
+
+ addListener(callback) {
+ this.on("change", callback);
+ return {
+ remove: () => {
+ this.removeListener("change", callback);
+ },
+ };
+ }
+
+ getSortedTags() {
+ const roomLists = RoomListStore.getRoomLists();
+
+ const tagNames = Object.keys(this._state.tags).sort();
+ const prefixes = tagNames.map((name, i) => {
+ const isFirst = i === 0;
+ const isLast = i === tagNames.length - 1;
+ const backwardsPrefix = !isFirst ? commonPrefix(name, tagNames[i - 1]) : "";
+ const forwardsPrefix = !isLast ? commonPrefix(name, tagNames[i + 1]) : "";
+ const longestPrefix = backwardsPrefix.length > forwardsPrefix.length ?
+ backwardsPrefix : forwardsPrefix;
+ return longestPrefix;
+ });
+ return tagNames.map((name, i) => {
+ const notifs = RoomNotifs.aggregateNotificationCount(roomLists[name]);
+ let badge;
+ if (notifs.count !== 0) {
+ badge = notifs;
+ }
+ const avatarLetter = name.substr(prefixes[i].length, 1);
+ const selected = this._state.tags[name];
+ return {name, avatarLetter, badge, selected};
+ });
+ }
+
+
+ _onDispatch(payload) {
+ switch (payload.action) {
+ case 'select_custom_room_tag': {
+ const oldTags = this._state.tags;
+ if (oldTags.hasOwnProperty(payload.tag)) {
+ const tag = {};
+ tag[payload.tag] = !oldTags[payload.tag];
+ const tags = Object.assign({}, oldTags, tag);
+ this._setState({tags});
+ }
+ }
+ break;
+ case 'on_logged_out': {
+ this._state = {};
+ if (this._roomListStoreToken) {
+ this._roomListStoreToken.remove();
+ this._roomListStoreToken = null;
+ }
+ }
+ break;
+ }
+ }
+
+ _getUpdatedTags() {
+ const newTagNames = Object.keys(RoomListStore.getRoomLists())
+ .filter((tagName) => {
+ return !tagName.match(STANDARD_TAGS_REGEX);
+ }).sort();
+ const prevTags = this._state && this._state.tags;
+ const newTags = newTagNames.reduce((newTags, tagName) => {
+ newTags[tagName] = (prevTags && prevTags[tagName]) || false;
+ return newTags;
+ }, {});
+ return newTags;
+ }
+}
+
+if (global.singletonCustomRoomTagStore === undefined) {
+ global.singletonCustomRoomTagStore = new CustomRoomTagStore();
+}
+export default global.singletonCustomRoomTagStore;
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index 4ac1e42e2e..637e87b728 100644
--- a/src/stores/GroupStore.js
+++ b/src/stores/GroupStore.js
@@ -203,6 +203,14 @@ class GroupStore extends EventEmitter {
return this._ready[id][groupId];
}
+ getGroupIdsForRoomId(roomId) {
+ const groupIds = Object.keys(this._state[this.STATE_KEY.GroupRooms]);
+ return groupIds.filter(groupId => {
+ const rooms = this._state[this.STATE_KEY.GroupRooms][groupId] || [];
+ return rooms.some(room => room.roomId === roomId);
+ });
+ }
+
getSummary(groupId) {
return this._state[this.STATE_KEY.Summary][groupId] || {};
}
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
index ed0974b0e4..61e17821bd 100644
--- a/src/stores/RoomListStore.js
+++ b/src/stores/RoomListStore.js
@@ -224,9 +224,9 @@ class RoomListStore extends Store {
}
}
- // ignore tags we don't know about
+ // ignore any m. tag names we don't know about
tagNames = tagNames.filter((t) => {
- return lists[t] !== undefined;
+ return !t.startsWith('m.') || lists[t] !== undefined;
});
if (tagNames.length) {
diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js
index eef078d8da..8ecc47d430 100644
--- a/src/stores/TagOrderStore.js
+++ b/src/stores/TagOrderStore.js
@@ -15,7 +15,10 @@ limitations under the License.
*/
import {Store} from 'flux/utils';
import dis from '../dispatcher';
+import GroupStore from './GroupStore';
import Analytics from '../Analytics';
+import * as RoomNotifs from "../RoomNotifs";
+import MatrixClientPeg from '../MatrixClientPeg';
const INITIAL_STATE = {
orderedTags: null,
@@ -47,7 +50,15 @@ class TagOrderStore extends Store {
__onDispatch(payload) {
switch (payload.action) {
// Initialise state after initial sync
+ case 'view_room': {
+ const relatedGroupIds = GroupStore.getGroupIdsForRoomId(payload.room_id);
+ this._updateBadges(relatedGroupIds);
+ break;
+ }
case 'MatrixActions.sync': {
+ if (payload.state === 'SYNCING' || payload.state === 'PREPARED') {
+ this._updateBadges();
+ }
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
break;
}
@@ -164,6 +175,20 @@ class TagOrderStore extends Store {
}
}
+ _updateBadges(groupIds = this._state.joinedGroupIds) {
+ if (groupIds && groupIds.length) {
+ const client = MatrixClientPeg.get();
+ const changedBadges = {};
+ groupIds.forEach(groupId => {
+ const rooms = GroupStore.getGroupRooms(groupId).map(r => client.getRoom(r.roomId));
+ const badge = rooms && RoomNotifs.aggregateNotificationCount(rooms);
+ changedBadges[groupId] = (badge && badge.count !== 0) ? badge : undefined;
+ });
+ const newBadges = Object.assign({}, this._state.badges, changedBadges);
+ this._setState({badges: newBadges});
+ }
+ }
+
_updateOrderedTags() {
this._setState({
orderedTags:
@@ -190,6 +215,11 @@ class TagOrderStore extends Store {
return tagsToKeep.concat(groupIdsToAdd);
}
+ getGroupBadge(groupId) {
+ const badges = this._state.badges;
+ return badges && badges[groupId];
+ }
+
getOrderedTags() {
return this._state.orderedTags;
}