diff --git a/.eslintrc.js b/.eslintrc.js index fae41ec159..8474cd86d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -91,6 +91,7 @@ module.exports = { // to JSX. ignorePattern: '^\\s*<', ignoreComments: true, + ignoreRegExpLiterals: true, code: 120, }], "valid-jsdoc": ["warn"], diff --git a/res/css/_components.scss b/res/css/_components.scss index dc2091b389..fe331504f2 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss new file mode 100644 index 0000000000..f02421db2c --- /dev/null +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -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; +} diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 941417eccc..9dbfe696a5 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -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; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index bf1746bd0e..084dcbfb47 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -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; } diff --git a/res/css/structures/_TagPanelButtons.scss b/res/css/structures/_TagPanelButtons.scss new file mode 100644 index 0000000000..f7cb189e25 --- /dev/null +++ b/res/css/structures/_TagPanelButtons.scss @@ -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; + } +} diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 91e49fe09b..77fb5efb76 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -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; diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js new file mode 100644 index 0000000000..4e33f0a22d --- /dev/null +++ b/src/components/structures/CustomRoomTagPanel.js @@ -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 (); + }); + + const classes = classNames('mx_CustomRoomTagPanel', { + mx_CustomRoomTagPanel_empty: this.state.tags.length === 0, + }); + + return (
+
+ + {tags} + +
); + } +} + +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 = (
{FormattingUtils.formatCount(badge.count)}
); + } + + const tip = (this.state.hover ? + : +
); + return ( + +
+ + { badgeElement } + { tip } +
+
+ ); + } +} + +export default CustomRoomTagPanel; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index cb374a02a3..0ae9b032d1 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -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 ? :
; + let tagPanelContainer; + if (tagPanelEnabled) { + tagPanelContainer = (
+ + + +
); + } const containerClasses = classNames( "mx_LeftPanel_container", "mx_fadable", @@ -204,9 +212,10 @@ const LeftPanel = React.createClass({ onCleared={ this.onSearchCleared } collapsed={this.props.collapsed} />); + return (
- { tagPanel } + { tagPanelContainer }