Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
Timezone feature (#696)
Browse files Browse the repository at this point in the history
* Add timezone support

Support timezone settings

Add automatic timezone update feature

newTimezone may be undefined

Add timezone suggestion provider

Remove unused variables

update timezone from /user endpoint

Refactor autoUpdateTimezone logic

Display Timezones when in focus

Remove unused variables/functions

Remove preference_store changes completely

Move autoUpdateTimezone logic to LoggedIn component

Update snapshots for timezone in display settings

Add timezone support for profile_popover and post_time components

Add EnableTimezoneSelection config to UI related to Timezone feature

Remove .window from global.window.mm_config

Check if user timezone is null

Parse SupportedTimezones from mm_config

Update jest snapshots

Add trailing commas

Refactor global.mm_config to redux config

Updated jest snapshots

Add timezone support from redux

Disable timezone input if timezones array is zero

Move getCurrentTimezone and getTimezoneRegion to redux

Move autoUpdateTimezone to redux

Move getUserCurrentTimezone to redux selector

Remove getTimezone from UserStore

Refactor local dateTime into a component

Minor fixes

Fix failing test

Include moment-timezone for timezone support

Remove enableTimezone props

Remove EnableTimezoneSelection flag

Specify user timezone in LocalDateTime

Add FormatTime from react-intl in LocalDateTime

Move styles for timezone container in scss

Remove useMilitaryTime props from components that use <PostTime

Fix snapshot test

Add minimum server support for the timezone feature

Fix snapshot test for timezone settings

Fix typo in user_settings/display/index.js

Remove minimum server version check

* Fix test for user_settings_display

* Fix snapshot test for timezone settings

* Add ExperimentalTimezone flag

* Add fixed snapshot tests

* Updating spacing and margin for timezones

* Add last request for timezone feature

* autoUpdate for new users and update in settings if user timezone is not set

* Ensure automaticTImezone is properly not se
  • Loading branch information
csduarte authored and hmhealey committed Apr 2, 2018
1 parent 24b8e3b commit 431051f
Show file tree
Hide file tree
Showing 37 changed files with 908 additions and 106 deletions.
34 changes: 34 additions & 0 deletions components/local_date_time/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import {connect} from 'react-redux';

import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';

import {Preferences} from 'utils/constants.jsx';

import LocalDateTime from './local_date_time';

function mapStateToProps(state, props) {
const config = getConfig(state);
const currentUserId = getCurrentUserId(state);

let userTimezone;
if (props.userTimezone) {
userTimezone = props.userTimezone;
} else {
userTimezone = getUserTimezone(state, currentUserId);
}

return {
enableTimezone: config.ExperimentalTimezone === 'true',
useMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
timeZone: getUserCurrentTimezone(userTimezone),
};
}

export default connect(mapStateToProps)(LocalDateTime);
58 changes: 58 additions & 0 deletions components/local_date_time/local_date_time.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import PropTypes from 'prop-types';
import React from 'react';
import {FormattedTime} from 'react-intl';

export default class LocalDateTime extends React.PureComponent {
static propTypes = {

/*
* The time to display
*/
eventTime: PropTypes.number,

/*
* Set to display using 24 hour format
*/
useMilitaryTime: PropTypes.bool,

/*
* Current timezone of the user
*/
timeZone: PropTypes.string,

/*
* Enable timezone feature
*/
enableTimezone: PropTypes.bool,
};

render() {
const {
enableTimezone,
eventTime,
timeZone,
useMilitaryTime,
} = this.props;

const date = eventTime ? new Date(eventTime) : new Date();

const timezoneProps = enableTimezone && timeZone ? {timeZone} : {};

return (
<time
className='post__time'
dateTime={date.toISOString()}
title={date}
>
<FormattedTime
{...timezoneProps}
hour12={!useMilitaryTime}
value={date}
/>
</time>
);
}
}
13 changes: 12 additions & 1 deletion components/logged_in/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See License.txt for license information.

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';

import {checkIfMFARequired} from 'utils/route';
Expand All @@ -14,7 +16,16 @@ function mapStateToProps(state, ownProps) {

return {
mfaRequired: checkIfMFARequired(license, config, ownProps.match.url),
enableTimezone: config.ExperimentalTimezone === 'true',
};
}

export default connect(mapStateToProps)(LoggedIn);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
autoUpdateTimezone,
}, dispatch),
};
}

export default connect(mapStateToProps, mapDispatchToProps)(LoggedIn);
10 changes: 10 additions & 0 deletions components/logged_in/logged_in.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ErrorStore from 'stores/error_store.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import {getBrowserTimezone} from 'utils/timezone.jsx';
import store from 'stores/redux_store.jsx';

const dispatch = store.dispatch;
Expand All @@ -42,6 +43,7 @@ export default class LoggedIn extends React.Component {
onUserChanged() {
// Grab the current user
const user = UserStore.getCurrentUser();

if (!Utils.areObjectsEqual(this.state.user, user)) {
this.setState({
user,
Expand All @@ -57,6 +59,10 @@ export default class LoggedIn extends React.Component {
// Initialize websocket
WebSocketActions.initialize();

if (this.props.enableTimezone) {
this.props.actions.autoUpdateTimezone(getBrowserTimezone());
}

// Make sure the websockets close and reset version
$(window).on('beforeunload',
() => {
Expand Down Expand Up @@ -169,4 +175,8 @@ export default class LoggedIn extends React.Component {
LoggedIn.propTypes = {
children: PropTypes.object,
mfaRequired: PropTypes.bool.isRequired,
enableTimezone: PropTypes.bool.isRequired,
actions: PropTypes.shape({
autoUpdateTimezone: PropTypes.func.isRequired,
}).isRequired,
};
3 changes: 1 addition & 2 deletions components/post_view/post_info/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {bindActionCreators} from 'redux';
import {addReaction, removePost} from 'mattermost-redux/actions/posts';
import {isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {get, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {get} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';

import {Preferences} from 'utils/constants.jsx';
Expand All @@ -20,7 +20,6 @@ function mapStateToProps(state, ownProps) {

return {
teamId,
useMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
isFlagged: get(state, Preferences.CATEGORY_FLAGGED_POST, ownProps.post.id, null) != null,
isMobile: state.views.channel.mobileView,
enableEmojiPicker,
Expand Down
6 changes: 0 additions & 6 deletions components/post_view/post_info/post_info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ export default class PostInfo extends React.PureComponent {
*/
handleDropdownOpened: PropTypes.func.isRequired,

/*
* Set to display in 24 hour format
*/
useMilitaryTime: PropTypes.bool.isRequired,

/*
* Set to mark the post as flagged
*/
Expand Down Expand Up @@ -324,7 +319,6 @@ export default class PostInfo extends React.PureComponent {
<PostTime
isPermalink={isPermalink}
eventTime={post.create_at}
useMilitaryTime={this.props.useMilitaryTime}
postId={post.id}
/>
);
Expand Down
40 changes: 8 additions & 32 deletions components/post_view/post_time.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import {isMobile} from 'utils/user_agent.jsx';
import {isMobile as isMobileView} from 'utils/utils.jsx';
import LocalDateTime from 'components/local_date_time';

export default class PostTime extends React.PureComponent {
static propTypes = {
Expand All @@ -23,11 +24,6 @@ export default class PostTime extends React.PureComponent {
*/
eventTime: PropTypes.number.isRequired,

/*
* Set to display using 24 hour format
*/
useMilitaryTime: PropTypes.bool,

/*
* The post id of posting being rendered
*/
Expand All @@ -36,7 +32,6 @@ export default class PostTime extends React.PureComponent {

static defaultProps = {
eventTime: 0,
useMilitaryTime: false,
};

constructor(props) {
Expand All @@ -53,33 +48,14 @@ export default class PostTime extends React.PureComponent {
}
};

renderTimeTag() {
const date = new Date(this.props.eventTime);
const militaryTime = this.props.useMilitaryTime;

const hour = militaryTime ? date.getHours() : (date.getHours() % 12 || 12);
let minute = date.getMinutes();
minute = minute >= 10 ? minute : ('0' + minute);
let time = '';

if (!militaryTime) {
time = (date.getHours() >= 12 ? ' PM' : ' AM');
}

return (
<time
className='post__time'
dateTime={date.toISOString()}
title={date}
>
{hour + ':' + minute + time}
</time>
);
}

render() {
const localDateTime = (
<LocalDateTime
eventTime={this.props.eventTime}
/>
);
if (isMobile() || !this.props.isPermalink) {
return this.renderTimeTag();
return localDateTime;
}

return (
Expand All @@ -88,7 +64,7 @@ export default class PostTime extends React.PureComponent {
className='post__permalink'
onClick={this.handleClick}
>
{this.renderTimeTag()}
{localDateTime}
</Link>
);
}
Expand Down
2 changes: 2 additions & 0 deletions components/profile_popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ function mapStateToProps(state, ownProps) {

const showEmailAddress = config.ShowEmailAddress === 'true';
const enableWebrtc = config.EnableWebrtc === 'true';
const enableTimezone = config.ExperimentalTimezone === 'true';

return {
...ownProps,
showEmailAddress,
enableWebrtc,
enableTimezone,
};
}

Expand Down
28 changes: 24 additions & 4 deletions components/profile_popover/profile_popover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from 'react';
import {OverlayTrigger, Popover, Tooltip} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';

import LocalDateTime from 'components/local_date_time';
import {browserHistory} from 'utils/browser_history';
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
Expand Down Expand Up @@ -273,14 +274,18 @@ class ProfilePopover extends React.Component {
key='user-popover-fullname'
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
className='overflow--ellipsis text-nowrap padding-top'
>
{fullname}
<strong>{fullname}</strong>
</div>
</OverlayTrigger>
);
}

dataContent.push(
<hr className='divider divider--expanded'/>
);

if (this.props.user.position) {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
dataContent.push(
Expand All @@ -291,7 +296,7 @@ class ProfilePopover extends React.Component {
key='user-popover-position'
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
className='overflow--ellipsis text-nowrap padding-bottom half'
>
{position}
</div>
Expand All @@ -309,14 +314,29 @@ class ProfilePopover extends React.Component {
>
<a
href={'mailto:' + email}
className='text-nowrap text-lowercase user-popover__email'
className='text-nowrap text-lowercase user-popover__email padding-bottom half'
>
{email}
</a>
</div>
);
}

if (this.props.enableTimezone && this.props.user.timezone) {
dataContent.push(
<div
key='user-popover-local-time'
className='padding-bottom half'
>
<FormattedMessage
id='user_profile.account.localTime'
defaultMessage='Local Time: '
/>
<LocalDateTime userTimezone={this.props.user.timezone}/>
</div>
);
}

if (this.props.user.id === UserStore.getCurrentId()) {
dataContent.push(
<div
Expand Down
6 changes: 0 additions & 6 deletions components/rhs_comment/rhs_comment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export default class RhsComment extends React.Component {
user: PropTypes.object,
currentUser: PropTypes.object.isRequired,
compactDisplay: PropTypes.bool,
useMilitaryTime: PropTypes.bool.isRequired,
isFlagged: PropTypes.bool,
status: PropTypes.string,
isBusy: PropTypes.bool,
Expand Down Expand Up @@ -73,10 +72,6 @@ export default class RhsComment extends React.Component {
return true;
}

if (nextProps.useMilitaryTime !== this.props.useMilitaryTime) {
return true;
}

if (nextProps.isFlagged !== this.props.isFlagged) {
return true;
}
Expand Down Expand Up @@ -147,7 +142,6 @@ export default class RhsComment extends React.Component {
<PostTime
isPermalink={isPermalink}
eventTime={post.create_at}
useMilitaryTime={this.props.useMilitaryTime}
postId={post.id}
/>
);
Expand Down
Loading

0 comments on commit 431051f

Please sign in to comment.