Merge pull request #2575 from matrix-org/bwindels/customtags

Bring back custom tags, also badges on communities
This commit is contained in:
Bruno Windels 2019-02-07 11:37:17 +00:00 committed by GitHub
commit 87ddb8a453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 592 additions and 133 deletions

View file

@ -91,6 +91,7 @@ module.exports = {
// to JSX.
ignorePattern: '^\\s*<',
ignoreComments: true,
ignoreRegExpLiterals: true,
code: 120,
}],
"valid-jsdoc": ["warn"],

View file

@ -4,6 +4,7 @@
@import "./structures/_CompatibilityPage.scss";
@import "./structures/_ContextualMenu.scss";
@import "./structures/_CreateRoom.scss";
@import "./structures/_CustomRoomTagPanel.scss";
@import "./structures/_FilePanel.scss";
@import "./structures/_GroupView.scss";
@import "./structures/_HomePage.scss";
@ -19,6 +20,7 @@
@import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss";
@import "./structures/_TagPanel.scss";
@import "./structures/_TagPanelButtons.scss";
@import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss";
@import "./structures/_ViewSource.scss";

View file

@ -0,0 +1,41 @@
/*
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.
*/
.mx_LeftPanel_tagPanelContainer {
display: flex;
flex-direction: column;
}
.mx_CustomRoomTagPanel {
background-color: $tagpanel-bg-color;
max-height: 40%;
}
.mx_CustomRoomTagPanel .mx_AccessibleButton {
margin: 9px auto;
width: 40px;
}
.mx_CustomRoomTagPanel .mx_BaseAvatar_image {
box-sizing: border-box;
width: 40px;
height: 40px;
}
.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected .mx_BaseAvatar_image {
border: 3px solid $warning-color;
border-radius: 40px;
}

View file

@ -33,6 +33,11 @@ limitations under the License.
flex: 0 0 140px;
}
.mx_LeftPanel_tagPanelContainer {
flex: 0 0 70px;
height: 100%;
}
.mx_LeftPanel_hideButton {
position: absolute;
top: 10px;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
.mx_TagPanel {
flex: 0 0 70px;
flex: 1;
background-color: $tagpanel-bg-color;
cursor: pointer;
@ -68,10 +68,13 @@ limitations under the License.
height: 100%;
}
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
height: 40px;
padding: 5px 0 4px 0;
}
.mx_TagPanel .mx_TagTile {
padding-top: 9px;
padding-bottom: 9px;
margin: 9px 0;
// opacity: 0.5;
position: relative;
}
@ -81,13 +84,7 @@ limitations under the License.
// opacity: 1;
}
.mx_TagPanel .mx_TagTile.mx_TagTile_selected {
/* To offset border of mx_TagTile_avatar */
padding: 3px 0px;
}
.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar {
border: 3px solid $accent-color;
background-color: $accent-color;
border-radius: 40px;
@ -97,6 +94,13 @@ limitations under the License.
width: 40px;
}
.mx_TagPanel .mx_TagTile_selected .mx_BaseAvatar_image {
border: 3px solid $accent-color;
height: 40px;
width: 40px;
box-sizing: border-box;
}
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
filter: none;
}
@ -112,7 +116,7 @@ limitations under the License.
height: 15px;
position: absolute;
right: -5px;
top: 1px;
top: -8px;
border-radius: 8px;
background-color: $neutral-badge-color;
color: #ffffff;
@ -124,39 +128,22 @@ limitations under the License.
padding-right: 4px;
}
.mx_TagPanel_groupsButton {
flex: 0;
margin: 17px 0 3px 0;
}
.mx_TagPanel_groupsButton > .mx_GroupsButton:before {
mask: url('$(res)/img/feather-icons/users.svg');
mask-position: center 11px;
}
.mx_TagPanel_groupsButton > .mx_TagPanel_report:before {
mask: url('$(res)/img/feather-icons/life-buoy.svg');
mask-position: center 9px;
}
.mx_TagPanel_groupsButton > .mx_AccessibleButton {
margin-bottom: 12px;
height: 40px;
width: 40px;
border-radius: 20px;
background-color: $roomheader-addroom-color;
.mx_TagTile_avatar {
position: relative;
/* overwrite mx_RoleButton inline-block */
display: block !important;
&:before {
background-color: $tagpanel-bg-color;
mask-repeat: no-repeat;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
.mx_TagTile_badge {
position: absolute;
right: -4px;
top: -2px;
border-radius: 8px;
color: $accent-fg-color;
font-weight: 600;
font-size: 14px;
padding: 0 5px;
background-color: $roomtile-name-color;
}
.mx_TagTile_badgeHighlight {
background-color: $warning-color;
}

View file

@ -0,0 +1,56 @@
/*
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.
*/
.mx_TagPanelButtons {
background-color: $tagpanel-bg-color;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 17px 0 3px 0;
}
.mx_TagPanelButtons > .mx_GroupsButton:before {
mask: url('$(res)/img/feather-icons/users.svg');
mask-position: center 11px;
}
.mx_TagPanelButtons > .mx_TagPanelButtons_report:before {
mask: url('$(res)/img/feather-icons/life-buoy.svg');
mask-position: center 9px;
}
.mx_TagPanelButtons > .mx_AccessibleButton {
margin-bottom: 12px;
height: 40px;
width: 40px;
border-radius: 20px;
background-color: $roomheader-addroom-color;
position: relative;
/* overwrite mx_RoleButton inline-block */
display: block !important;
&:before {
background-color: $tagpanel-bg-color;
mask-repeat: no-repeat;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}

View file

@ -23,6 +23,47 @@ export const ALL_MESSAGES = 'all_messages';
export const MENTIONS_ONLY = 'mentions_only';
export const MUTE = 'mute';
function _shouldShowNotifBadge(roomNotifState) {
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
return showBadgeInStates.indexOf(roomNotifState) > -1;
}
function _shouldShowMentionBadge(roomNotifState) {
return roomNotifState !== MUTE;
}
export function aggregateNotificationCount(rooms) {
return rooms.reduce((result, room, index) => {
const roomNotifState = getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const notificationCount = room.getUnreadNotificationCount();
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
const badges = notifBadges || mentionBadges;
if (badges) {
result.count += notificationCount;
if (highlight) {
result.highlight = true;
}
}
return result;
}, {count: 0, highlight: false});
}
export function getRoomHasBadge(room) {
const roomNotifState = getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const notificationCount = room.getUnreadNotificationCount();
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
return notifBadges || mentionBadges;
}
export function getRoomNotifsState(roomId) {
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;

View file

@ -0,0 +1,125 @@
/*
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 CustomRoomTagStore from '../../stores/CustomRoomTagStore';
import AutoHideScrollbar from './AutoHideScrollbar';
import sdk from '../../index';
import dis from '../../dispatcher';
import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils';
class CustomRoomTagPanel extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: CustomRoomTagStore.getSortedTags(),
};
}
componentWillMount() {
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({tags: CustomRoomTagStore.getSortedTags()});
});
}
componentWillUnmount() {
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
}
render() {
const tags = this.state.tags.map((tag) => {
return (<CustomRoomTagTile tag={tag} key={tag.name} />);
});
const classes = classNames('mx_CustomRoomTagPanel', {
mx_CustomRoomTagPanel_empty: this.state.tags.length === 0,
});
return (<div className={classes}>
<div className="mx_CustomRoomTagPanel_divider" />
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
{tags}
</AutoHideScrollbar>
</div>);
}
}
class CustomRoomTagTile extends React.Component {
constructor(props) {
super(props);
this.state = {hover: false};
this.onClick = this.onClick.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
}
onMouseOver() {
this.setState({hover: true});
}
onMouseOut() {
this.setState({hover: false});
}
onClick() {
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
}
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const tag = this.props.tag;
const avatarHeight = 40;
const className = classNames({
CustomRoomTagPanel_tileSelected: tag.selected,
});
const name = tag.name;
const badge = tag.badge;
let badgeElement;
if (badge) {
const badgeClasses = classNames({
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight,
});
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
const tip = (this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />);
return (
<AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar
name={tag.avatarLetter}
idName={name}
width={avatarHeight}
height={avatarHeight}
/>
{ badgeElement }
{ tip }
</div>
</AccessibleButton>
);
}
}
export default CustomRoomTagPanel;

View file

@ -24,7 +24,7 @@ import { KeyCode } from '../../Keyboard';
import sdk from '../../index';
import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore';
@ -183,12 +183,20 @@ const LeftPanel = React.createClass({
render: function() {
const RoomList = sdk.getComponent('rooms.RoomList');
const TagPanel = sdk.getComponent('structures.TagPanel');
const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel');
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
const tagPanel = tagPanelEnabled ? <TagPanel /> : <div />;
let tagPanelContainer;
if (tagPanelEnabled) {
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
<CustomRoomTagPanel />
<TagPanelButtons />
</div>);
}
const containerClasses = classNames(
"mx_LeftPanel_container", "mx_fadable",
@ -204,9 +212,10 @@ const LeftPanel = React.createClass({
onCleared={ this.onSearchCleared }
collapsed={this.props.collapsed} />);
return (
<div className={containerClasses}>
{ tagPanel }
{ tagPanelContainer }
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<TopLeftMenuButton collapsed={ this.props.collapsed } />
{ searchBox }

View file

@ -127,46 +127,6 @@ const RoomSubList = React.createClass({
});
},
_shouldShowNotifBadge: function(roomNotifState) {
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
return showBadgeInStates.indexOf(roomNotifState) > -1;
},
_shouldShowMentionBadge: function(roomNotifState) {
return roomNotifState !== RoomNotifs.MUTE;
},
/**
* Total up all the notification counts from the rooms
*
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
*/
roomNotificationCount: function() {
const self = this;
if (this.props.isInvite) {
return [0, true];
}
return this.props.list.reduce(function(result, room, index) {
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const notificationCount = room.getUnreadNotificationCount();
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
const badges = notifBadges || mentionBadges;
if (badges) {
result[0] += notificationCount;
if (highlight) {
result[1] = true;
}
}
return result;
}, [0, false]);
},
_updateSubListCount: function() {
// Force an update by setting the state to the current state
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
@ -197,22 +157,12 @@ const RoomSubList = React.createClass({
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
// find first room which has notifications and switch to it
for (const room of this.props.list) {
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const notificationCount = room.getUnreadNotificationCount();
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
if (notifBadges || mentionBadges) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
return;
}
const room = this.props.lists.find(room => RoomNotifs.getRoomHasBadge(room));
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
}
},
@ -240,9 +190,11 @@ const RoomSubList = React.createClass({
_getHeaderJsx: function(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const subListNotifications = this.roomNotificationCount();
const subListNotifCount = subListNotifications[0];
const subListNotifHighlight = subListNotifications[1];
const subListNotifications = !this.props.isInvite ?
RoomNotifs.aggregateNotificationCount(this.props.list) :
{count: 0, highlight: true};
const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight;
let badge;
if (!this.props.collapsed) {

View file

@ -23,7 +23,6 @@ import GroupActions from '../../actions/GroupActions';
import sdk from '../../index';
import dis from '../../dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
@ -48,8 +47,6 @@ const TagPanel = React.createClass({
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.on("sync", this._onClientSync);
this._dispatcherRef = dis.register(this._onAction);
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
@ -70,9 +67,6 @@ const TagPanel = React.createClass({
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
if (this._dispatcherRef) {
dis.unregister(this._dispatcherRef);
}
},
_onGroupMyMembership() {
@ -106,21 +100,11 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'deselect_tags'});
},
_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 DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const ActionButton = sdk.getComponent("elements.ActionButton");
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -174,13 +158,6 @@ const TagPanel = React.createClass({
) }
</Droppable>
</GeminiScrollbarWrapper>
<div className="mx_TagPanel_divider" />
<div className="mx_TagPanel_groupsButton">
<GroupsButton />
<ActionButton
className="mx_TagPanel_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>
</div>;
},
});

View file

@ -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 (<div className="mx_TagPanelButtons">
<GroupsButton />
<ActionButton
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>);
},
});
export default TagPanelButtons;

View file

@ -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 = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
}
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
@ -186,6 +198,7 @@ export default React.createClass({
/>
{ tip }
{ contextButton }
{ badgeElement }
</div>
</AccessibleButton>;
},

View file

@ -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],

View file

@ -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;

View file

@ -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] || {};
}

View file

@ -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) {

View file

@ -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;
}