diff options
author | David Rojas Camaggi <drojascamaggi@gmail.com> | 2017-06-06 09:45:36 -0400 |
---|---|---|
committer | Harrison Healey <harrisonmhealey@gmail.com> | 2017-06-06 09:45:36 -0400 |
commit | 46fc47e520efeff328db9b125c5339baac7b88e5 (patch) | |
tree | 99482c33de3b18dbbb0f0964b67ba0f898afe153 /webapp | |
parent | 382ba0b0d18f67d473ad677f9b5151bc5e468e14 (diff) | |
download | chat-46fc47e520efeff328db9b125c5339baac7b88e5.tar.gz chat-46fc47e520efeff328db9b125c5339baac7b88e5.tar.bz2 chat-46fc47e520efeff328db9b125c5339baac7b88e5.zip |
PLT 6416 Add StatusDropdown to profile picture in top left (#6327) (#6418)
* PLT-6416 the profile picture is always shown in the top left (#6327)
* PLT-6416 Add status icon to profile picture in left sidebar (#6327)
* PLT-6416 Add StatusDropdown to profile picture in top left (#6327)
* Fixing theme stuff for status picker
* PLT-6416 Automatically close status dropdown after selection (#6327)
* PLT-6416 Avoid render status dropdown in sidebar if isMobile (#6327)
* PLT-6416 Change icon for status change to caret-down (#6327)
* PLT-6416 Update visibility of status dropdown after window size (#6327)
* PLT-6416 Refactor status dropdown for better mouse usability (#6327)
* PLT-6416 Change status dropdown to the redux way (#6327)
* PLT-6416 Fix header style of admin sidebar (#6327)
Diffstat (limited to 'webapp')
-rw-r--r-- | webapp/components/bootstrap_span.jsx | 22 | ||||
-rw-r--r-- | webapp/components/sidebar_header.jsx | 63 | ||||
-rw-r--r-- | webapp/components/status_dropdown/index.jsx | 33 | ||||
-rw-r--r-- | webapp/components/status_dropdown/status_dropdown.jsx | 158 | ||||
-rwxr-xr-x | webapp/i18n/en.json | 5 | ||||
-rw-r--r-- | webapp/sass/components/_module.scss | 1 | ||||
-rw-r--r-- | webapp/sass/components/_status-dropdown.scss | 5 | ||||
-rw-r--r-- | webapp/sass/components/_status-icon.scss | 11 | ||||
-rw-r--r-- | webapp/sass/layout/_headers.scss | 9 | ||||
-rw-r--r-- | webapp/utils/utils.jsx | 7 | ||||
-rw-r--r-- | webapp/yarn.lock | 2 |
11 files changed, 279 insertions, 37 deletions
diff --git a/webapp/components/bootstrap_span.jsx b/webapp/components/bootstrap_span.jsx new file mode 100644 index 000000000..fb425594b --- /dev/null +++ b/webapp/components/bootstrap_span.jsx @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +class BootstrapSpan extends React.PureComponent { + + static propTypes = { + children: PropTypes.element + } + + render() { + const {children, ...props} = this.props; + delete props.bsRole; + delete props.bsClass; + + return <span {...props}>{children}</span>; + } +} + +export default BootstrapSpan; diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx index 493864a6f..51885d152 100644 --- a/webapp/components/sidebar_header.jsx +++ b/webapp/components/sidebar_header.jsx @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Client from 'client/web_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -14,57 +13,67 @@ import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import {Preferences, TutorialSteps, Constants} from 'utils/constants.jsx'; import {createMenuTip} from 'components/tutorial/tutorial_tip.jsx'; +import StatusDropdown from 'components/status_dropdown/index.jsx'; export default class SidebarHeader extends React.Component { constructor(props) { super(props); - this.toggleDropdown = this.toggleDropdown.bind(this); - this.onPreferenceChange = this.onPreferenceChange.bind(this); - this.state = this.getStateFromStores(); } componentDidMount() { PreferenceStore.addChangeListener(this.onPreferenceChange); + window.addEventListener('resize', this.handleResize); } componentWillUnmount() { PreferenceStore.removeChangeListener(this.onPreferenceChange); + window.removeEventListener('resize', this.handleResize); + } + + handleResize = () => { + const isMobile = Utils.isMobile(); + this.setState({isMobile}); } - getStateFromStores() { + getPreferences = () => { + if (!this.props.currentUser) { + return {}; + } const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, this.props.currentUser.id, 999); + const showTutorialTip = tutorialStep === TutorialSteps.MENU_POPOVER && !Utils.isMobile(); - return {showTutorialTip: tutorialStep === TutorialSteps.MENU_POPOVER && !Utils.isMobile()}; + return {showTutorialTip}; } - onPreferenceChange() { - this.setState(this.getStateFromStores()); + getStateFromStores = () => { + const preferences = this.getPreferences(); + const isMobile = Utils.isMobile(); + return {...preferences, isMobile}; } - toggleDropdown(e) { + onPreferenceChange = () => { + this.setState(this.getPreferences()); + } + + toggleDropdown = (e) => { e.preventDefault(); this.refs.dropdown.toggleDropdown(); } - render() { - var me = this.props.currentUser; - var profilePicture = null; - - if (!me) { + renderStatusDropdown = () => { + if (this.state.isMobile) { return null; } + return ( + <StatusDropdown/> + ); + } - if (me.last_picture_update) { - profilePicture = ( - <img - className='user__picture' - src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update} - /> - ); - } + render() { + const statusDropdown = this.renderStatusDropdown(); let tutorialTip = null; if (this.state.showTutorialTip) { @@ -93,12 +102,9 @@ export default class SidebarHeader extends React.Component { return ( <div className='team__header theme'> {tutorialTip} - <div> - {profilePicture} - <div className='header__info'> - <div className='user__name'>{'@' + me.username}</div> - {teamNameWithToolTip} - </div> + <div className='header__info'> + <div className='user__name'>{'@' + this.props.currentUser.username}</div> + {teamNameWithToolTip} </div> <SidebarHeaderDropdown ref='dropdown' @@ -107,6 +113,7 @@ export default class SidebarHeader extends React.Component { teamName={this.props.teamName} currentUser={this.props.currentUser} /> + {statusDropdown} </div> ); } diff --git a/webapp/components/status_dropdown/index.jsx b/webapp/components/status_dropdown/index.jsx new file mode 100644 index 000000000..bd2f7d7d0 --- /dev/null +++ b/webapp/components/status_dropdown/index.jsx @@ -0,0 +1,33 @@ +import {setStatus} from 'mattermost-redux/actions/users'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import { + getCurrentUser, + getStatusForUserId +} from 'mattermost-redux/selectors/entities/users'; +import {Client} from 'mattermost-redux/client'; + +import StatusDropdown from 'components/status_dropdown/status_dropdown.jsx'; + +function mapStateToProps(state) { + const currentUser = getCurrentUser(state); + const userId = currentUser.id; + const lastPicUpdate = currentUser.last_picture_update; + const profilePicture = Client.getProfilePictureUrl(userId, lastPicUpdate); + const status = getStatusForUserId(state, currentUser.id); + return { + userId, + profilePicture, + status + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + setStatus + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(StatusDropdown); diff --git a/webapp/components/status_dropdown/status_dropdown.jsx b/webapp/components/status_dropdown/status_dropdown.jsx new file mode 100644 index 000000000..4b173a0ea --- /dev/null +++ b/webapp/components/status_dropdown/status_dropdown.jsx @@ -0,0 +1,158 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {Dropdown} from 'react-bootstrap'; +import StatusIcon from 'components/status_icon.jsx'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {UserStatuses} from 'utils/constants.jsx'; +import BootstrapSpan from 'components/bootstrap_span.jsx'; + +export default class StatusDropdown extends React.Component { + + static propTypes = { + style: PropTypes.object, + status: PropTypes.string, + userId: PropTypes.string.isRequired, + profilePicture: PropTypes.string, + actions: PropTypes.shape({ + setStatus: PropTypes.func.isRequired + }).isRequired + } + + state = { + showDropdown: false, + mouseOver: false + } + + onMouseEnter = () => { + this.setState({mouseOver: true}); + } + + onMouseLeave = () => { + this.setState({mouseOver: false}); + } + + onToggle = (showDropdown) => { + this.setState({showDropdown}); + } + + closeDropdown = () => { + this.setState({showDropdown: false}); + } + + setStatus = (status) => { + this.props.actions.setStatus({ + user_id: this.props.userId, + status + }); + this.closeDropdown(); + } + + setOnline = (event) => { + event.preventDefault(); + this.setStatus(UserStatuses.ONLINE); + } + + setOffline = (event) => { + event.preventDefault(); + this.setStatus(UserStatuses.OFFLINE); + } + + setAway = (event) => { + event.preventDefault(); + this.setStatus(UserStatuses.AWAY); + } + + renderStatusOnlineAction = () => { + return this.renderStatusAction(UserStatuses.ONLINE, this.setOnline); + } + + renderStatusAwayAction = () => { + return this.renderStatusAction(UserStatuses.AWAY, this.setAway); + } + + renderStatusOfflineAction = () => { + return this.renderStatusAction(UserStatuses.OFFLINE, this.setOffline); + } + + renderProfilePicture = () => { + if (!this.props.profilePicture) { + return null; + } + return ( + <img + className='user__picture' + src={this.props.profilePicture} + /> + ); + } + + renderStatusAction = (status, onClick) => { + return ( + <li key={status}> + <a + href={'#'} + onClick={onClick} + > + <FormattedMessage + id={`status_dropdown.set_${status}`} + defaultMessage={status} + /> + </a> + </li> + ); + } + + renderStatusIcon = () => { + if (this.state.mouseOver) { + return ( + <span className={'status status-edit'}> + <i + className={'fa fa-caret-down'} + /> + </span> + ); + } + return ( + <StatusIcon + status={this.props.status} + /> + ); + } + + render() { + const statusIcon = this.renderStatusIcon(); + const profilePicture = this.renderProfilePicture(); + const actions = [ + this.renderStatusOnlineAction(), + this.renderStatusAwayAction(), + this.renderStatusOfflineAction() + ]; + return ( + <Dropdown + id={'status-dropdown'} + open={this.state.showDropdown} + onToggle={this.onToggle} + style={this.props.style} + > + <BootstrapSpan + bsRole={'toggle'} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + <div className='status-wrapper'> + {profilePicture} + <div className='status_dropdown__toggle'> + {statusIcon} + </div> + </div> + </BootstrapSpan> + <Dropdown.Menu> + {actions} + </Dropdown.Menu> + </Dropdown> + ); + } +} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 12ded1acb..9a1f6d6c0 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -2455,5 +2455,8 @@ "webrtc.unmute_audio": "Unmute microphone", "webrtc.unpause_video": "Turn on camera", "webrtc.unsupported": "{username} client does not support video calls.", - "youtube_video.notFound": "Video not found" + "youtube_video.notFound": "Video not found", + "status_dropdown.set_online": "Online", + "status_dropdown.set_away": "Away", + "status_dropdown.set_offline": "Offline" } diff --git a/webapp/sass/components/_module.scss b/webapp/sass/components/_module.scss index 2cf310a48..f2180d525 100644 --- a/webapp/sass/components/_module.scss +++ b/webapp/sass/components/_module.scss @@ -17,6 +17,7 @@ @import 'scrollbar'; @import 'search'; @import 'status-icon'; +@import 'status-dropdown'; @import 'suggestion-list'; @import 'tooltip'; @import 'tutorial'; diff --git a/webapp/sass/components/_status-dropdown.scss b/webapp/sass/components/_status-dropdown.scss new file mode 100644 index 000000000..3a6b89835 --- /dev/null +++ b/webapp/sass/components/_status-dropdown.scss @@ -0,0 +1,5 @@ +@charset 'UTF-8'; + +#status-dropdown { + cursor: pointer; +} diff --git a/webapp/sass/components/_status-icon.scss b/webapp/sass/components/_status-icon.scss index c2f8bca5b..ea87461c0 100644 --- a/webapp/sass/components/_status-icon.scss +++ b/webapp/sass/components/_status-icon.scss @@ -20,6 +20,17 @@ bottom: -6px; } + &.status-edit { + text-align: center; + + i { + @include opacity(1); + font-size: 10px; + position: relative; + top: 2px; + } + } + svg { max-height: 11px; diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss index cb4512ac1..e31452c9b 100644 --- a/webapp/sass/layout/_headers.scss +++ b/webapp/sass/layout/_headers.scss @@ -259,6 +259,8 @@ .team__header { @include legacy-pie-clearfix; + @include display-flex(); + @include flex-direction(row-reverse); padding: 9px 10px; position: relative; @@ -297,6 +299,7 @@ a { text-decoration: none; + @include flex-grow(1); } .sidebar-header-dropdown, @@ -307,7 +310,6 @@ position: absolute; right: 22px; top: 10px; - z-index: 5; li { width: 100%; @@ -351,16 +353,15 @@ @include border-radius(36px); float: left; height: 36px; - margin-right: 6px; width: 36px; } .header__info { @include clearfix; + @include flex-grow(1); color: $white; - padding-left: 2px; + padding-left: 8px; position: relative; - z-index: 1; } .team__name, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 2b685a793..09875fe02 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -533,7 +533,7 @@ export function applyTheme(theme) { } if (theme.sidebarHeaderBg) { - changeCss('.sidebar--left .team__header, .app__body .sidebar--menu .team__header, .app__body .post-list__timestamp > div', 'background:' + theme.sidebarHeaderBg); + changeCss('.app__body .status-wrapper .status_dropdown__toggle .status, .sidebar--left .team__header, .app__body .sidebar--menu .team__header, .app__body .post-list__timestamp > div', 'background:' + theme.sidebarHeaderBg); changeCss('.app__body .modal .modal-header', 'background:' + theme.sidebarHeaderBg); changeCss('.app__body .multi-teams .team-sidebar, .app__body #navbar .navbar-default', 'background:' + theme.sidebarHeaderBg); changeCss('@media(max-width: 768px){.app__body .search-bar__container', 'background:' + theme.sidebarHeaderBg); @@ -541,8 +541,8 @@ export function applyTheme(theme) { } if (theme.sidebarHeaderTextColor) { - changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn, .sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor); - changeCss('.app__body .sidebar-header-dropdown__icon svg', 'fill:' + theme.sidebarHeaderTextColor); + changeCss('.app__body .status-wrapper .status_dropdown__toggle .status-edit, .multi-teams .team-sidebar .team-wrapper .team-container .team-btn, .sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor); + changeCss('.app__body .sidebar--left .status-wrapper .status_dropdown__toggle .offline--icon, .app__body .sidebar-header-dropdown__icon svg', 'fill:' + theme.sidebarHeaderTextColor); changeCss('.sidebar--left .team__header .user__name, .app__body .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8)); changeCss('.sidebar--left .team__header:hover .user__name, .app__body .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor); changeCss('.app__body .modal .modal-header .modal-title, .app__body .modal .modal-header .modal-title .name, .app__body .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor); @@ -667,6 +667,7 @@ export function applyTheme(theme) { } changeCss('body', 'scrollbar-arrow-color:' + theme.centerChannelColor); changeCss('.app__body .post-create__container .post-create-body .btn-file svg, .app__body .post.post--compact .post-image__column .post-image__details svg, .app__body .modal .about-modal .about-modal__logo svg, .app__body .post .post__img svg', 'fill:' + theme.centerChannelColor); + changeCss('.sidebar--left .status .offline--icon', 'fill:' + theme.centerChannelColor); changeCss('.app__body .scrollbar--horizontal, .app__body .scrollbar--vertical', 'background:' + changeOpacity(theme.centerChannelColor, 0.5)); changeCss('.app__body .post-list__new-messages-below', 'background:' + changeColor(theme.centerChannelColor, 0.5)); changeCss('.app__body .post.post--comment .post__body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 8660bed5e..343f1582f 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4884,7 +4884,7 @@ math-expression-evaluator@^1.2.14: mattermost-redux@mattermost/mattermost-redux#webapp-master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/dfa584b61ed9d04c167be53fe16ac6432892ece1" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/eaf3d811a8f9b9814f4d07c49bfc6d91e73a38be" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" |