Merge pull request #2575 from matrix-org/bwindels/customtags
Bring back custom tags, also badges on communities
This commit is contained in:
commit
87ddb8a453
18 changed files with 592 additions and 133 deletions
|
@ -91,6 +91,7 @@ module.exports = {
|
|||
// to JSX.
|
||||
ignorePattern: '^\\s*<',
|
||||
ignoreComments: true,
|
||||
ignoreRegExpLiterals: true,
|
||||
code: 120,
|
||||
}],
|
||||
"valid-jsdoc": ["warn"],
|
||||
|
|
|
@ -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";
|
||||
|
|
41
res/css/structures/_CustomRoomTagPanel.scss
Normal file
41
res/css/structures/_CustomRoomTagPanel.scss
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
56
res/css/structures/_TagPanelButtons.scss
Normal file
56
res/css/structures/_TagPanelButtons.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
125
src/components/structures/CustomRoomTagPanel.js
Normal file
125
src/components/structures/CustomRoomTagPanel.js
Normal 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;
|
|
@ -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 }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
});
|
||||
|
|
58
src/components/structures/TagPanelButtons.js
Normal file
58
src/components/structures/TagPanelButtons.js
Normal 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;
|
|
@ -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>;
|
||||
},
|
||||
|
|
|
@ -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],
|
||||
|
|
143
src/stores/CustomRoomTagStore.js
Normal file
143
src/stores/CustomRoomTagStore.js
Normal 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;
|
|
@ -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] || {};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue