Timezone feature (#1456)

* Add automatic and manual timezone support

Update users timezone and scrollTo selected timezone in SelectTimezone coponent

Clean styles for SelectTimezone screen

Add auto-timezone update when login or enter app

Hide timezone feature behind config

Fix requested changes

Parse SupportedTimezone from config

Add localTime and localized post stamps

Add trailing commas

Include all timezone util methods from redux

Remove EnableTimezoneSelection flag

WIP get timezones from server

Pull supportedTimezones from api

Minor fixes

Remove wrapWithPreventDoubleTap

Revert back to react-intl formatDate

Include timeZone prop in FormattedTime

Refactor Timezone row into component

Minor cosmetic changes

Add minimum server support for the timezone feature

Move getSupportedTimezones to componentWillMount

Move autoUpdateTimezone function to handleSuccessfulLogin

Specify user timezone in profile_header

Remove format props from FormattedTime

Add ExperimentalTimezone flag

Replace Client().getServerVersion() with entities.general.serverVersion

Add isTimezoneEnabled helper function

Move isMinimumServerVersion to utils/timezone.js

* Fix style errors

* Remove date-time-format-timezone polyfill

* Feedback changes

* Use timezone selector from redux

* Explicitly pass hour12 props to intl.formatDate

* Update package-lock

* Revert iOS project file changes

* Fix license header

* Include timezone related paths in modulePaths

* Fix license header

* Fix minor issue with rebasing

* Fix issue with getconfig in GeneralSettings

* Update package-lock
This commit is contained in:
Stan Chan 2018-06-27 03:55:03 -07:00 committed by Elias Nahum
parent 830022cb82
commit 583d6b74ee
21 changed files with 715 additions and 23 deletions

View file

@ -3,12 +3,14 @@
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {app} from 'app/mattermost';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
@ -40,6 +42,11 @@ export function handleSuccessfulLogin() {
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
}
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {

View file

@ -3,35 +3,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
export default class FormattedTime extends React.PureComponent {
static propTypes = {
value: PropTypes.any.isRequired,
timeZone: PropTypes.string,
children: PropTypes.func,
hour12: PropTypes.bool,
};
static contextTypes = {
intl: intlShape.isRequired,
};
render() {
const {
value,
children,
timeZone,
hour12,
} = this.props;
const {intl} = this.context;
const date = new Date(value);
const militaryTime = !hour12;
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');
}
const formattedTime = hour + ':' + minute + time;
const timezoneProps = timeZone ? {timeZone} : {};
const formattedTime = intl.formatDate(value, {
...timezoneProps,
hour: 'numeric',
minute: 'numeric',
hour12,
});
if (typeof children === 'function') {
return children(formattedTime);

View file

@ -6,12 +6,14 @@ import {connect} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
import {getBool, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getUser, getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {fromAutoResponder} from 'app/utils/general';
import {isTimezoneEnabled} from 'app/utils/timezone';
import PostHeader from './post_header';
@ -22,8 +24,11 @@ function makeMapStateToProps() {
const post = getPost(state, ownProps.postId);
const commentedOnUser = getUser(state, ownProps.commentedOnUserId);
const user = getUser(state, post.user_id) || {};
const currentUser = getCurrentUser(state);
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
const enableTimezone = isTimezoneEnabled(state);
const userTimezone = enableTimezone && getUserCurrentTimezone(currentUser.timezone);
return {
commentedOnDisplayName: ownProps.commentedOnUserId ? displayUsername(commentedOnUser, teammateNameDisplay) : '',
@ -39,6 +44,7 @@ function makeMapStateToProps() {
overrideUsername: post.props && post.props.override_username,
theme: getTheme(state),
username: user.username,
userTimezone,
};
};
}

View file

@ -39,7 +39,9 @@ export default class PostHeader extends PureComponent {
showFullDate: PropTypes.bool,
theme: PropTypes.object.isRequired,
username: PropTypes.string,
userTimezone: PropTypes.string,
isFlagged: PropTypes.bool,
enableTimezone: PropTypes.bool,
};
static defaultProps = {
@ -172,6 +174,7 @@ export default class PostHeader extends PureComponent {
createAt,
isPendingOrFailedPost,
isSearchResult,
userTimezone,
militaryTime,
onPress,
renderReplies,
@ -192,6 +195,7 @@ export default class PostHeader extends PureComponent {
</Text>
<Text style={style.time}>
<FormattedTime
timeZone={userTimezone}
hour12={!militaryTime}
value={createAt}
/>
@ -202,6 +206,7 @@ export default class PostHeader extends PureComponent {
dateComponent = (
<Text style={style.time}>
<FormattedTime
timeZone={userTimezone}
hour12={!militaryTime}
value={createAt}
/>

View file

@ -41,6 +41,7 @@ export default class SearchBarAndroid extends PureComponent {
inputHeight: PropTypes.number,
inputBorderRadius: PropTypes.number,
blurOnSubmit: PropTypes.bool,
showArrow: PropTypes.bool,
value: PropTypes.string,
containerStyle: CustomPropTypes.Style,
};
@ -52,6 +53,7 @@ export default class SearchBarAndroid extends PureComponent {
blurOnSubmit: true,
placeholder: 'Search',
showCancelButton: true,
showArrow: true,
placeholderTextColor: changeOpacity('#000', 0.5),
containerStyle: {},
onSearchButtonPress: () => true,
@ -156,6 +158,7 @@ export default class SearchBarAndroid extends PureComponent {
tintColorSearch,
containerStyle,
value,
showArrow,
} = this.props;
const {isFocused} = this.state;
@ -192,7 +195,7 @@ export default class SearchBarAndroid extends PureComponent {
},
]}
>
{isFocused ?
{isFocused && showArrow ?
<TouchableWithoutFeedback
onPress={this.onCancelButtonPress}
style={{paddingRight: 15}}

View file

@ -61,6 +61,8 @@ export default class Entry extends PureComponent {
navigator: PropTypes.object,
isLandscape: PropTypes.bool,
hydrationComplete: PropTypes.bool,
enableTimezone: PropTypes.bool,
deviceTimezone: PropTypes.string,
initializeModules: PropTypes.func.isRequired,
actions: PropTypes.shape({
setDeviceToken: PropTypes.func.isRequired,
@ -106,6 +108,13 @@ export default class Entry extends PureComponent {
};
listenForHydration = () => {
const {
actions: {
autoUpdateTimezone,
},
enableTimezone,
deviceTimezone,
} = this.props;
const {getState} = store;
const state = getState();
@ -116,6 +125,10 @@ export default class Entry extends PureComponent {
if (state.views.root.hydrationComplete) {
this.unsubscribeFromStore();
if (enableTimezone) {
autoUpdateTimezone(deviceTimezone);
}
this.setAppCredentials();
this.setStartupThemes();
this.handleNotification();

View file

@ -4,10 +4,12 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {setDeviceToken} from 'mattermost-redux/actions/general';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {isLandscape} from 'app/selectors/device';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
const lazyLoadEntry = () => {
return require('./entry').default;
@ -16,11 +18,16 @@ const lazyLoadEntry = () => {
function mapStateToProps(state) {
const config = getConfig(state);
const enableTimezone = isTimezoneEnabled(state);
const deviceTimezone = getDeviceTimezone();
return {
config,
theme: getTheme(state),
isLandscape: isLandscape(state),
hydrationComplete: state.views.root.hydrationComplete,
enableTimezone,
deviceTimezone,
};
}
@ -28,6 +35,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setDeviceToken,
autoUpdateTimezone,
}, dispatch),
};
}

View file

@ -48,11 +48,13 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(require('app/screens/select_team').default), store, Provider);
Navigation.registerComponent('SelectTimezone', () => wrapWithContextProvider(require('app/screens/timezone/select_timezone').default), store, Provider);
Navigation.registerComponent('Settings', () => wrapWithContextProvider(require('app/screens/settings/general').default), store, Provider);
Navigation.registerComponent('SSO', () => wrapWithContextProvider(require('app/screens/sso').default), store, Provider);
Navigation.registerComponent('Table', () => wrapWithContextProvider(require('app/screens/table').default), store, Provider);
Navigation.registerComponent('TableImage', () => wrapWithContextProvider(require('app/screens/table_image').default), store, Provider);
Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider);
Navigation.registerComponent('TimezoneSettings', () => wrapWithContextProvider(require('app/screens/timezone').default), store, Provider);
Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(require('app/screens/user_profile').default), store, Provider);
}

View file

@ -20,6 +20,7 @@ export default class DisplaySettings extends PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
enableTimezone: PropTypes.bool.isRequired,
};
static contextTypes = {
@ -53,12 +54,30 @@ export default class DisplaySettings extends PureComponent {
this.setState({showClockDisplaySettings: true});
});
goToTimezoneSettings = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
navigator.push({
screen: 'TimezoneSettings',
title: intl.formatMessage({id: 'mobile.advanced_settings.timezone', defaultMessage: 'Timezone'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
});
closeClockDisplaySettings = () => {
this.setState({showClockDisplaySettings: false});
};
render() {
const {theme} = this.props;
const {theme, enableTimezone} = this.props;
const {showClockDisplaySettings} = this.state;
const style = getStyleSheet(theme);
@ -72,6 +91,24 @@ export default class DisplaySettings extends PureComponent {
);
}
let timezoneOption;
const disableClockDisplaySeparator = enableTimezone;
if (enableTimezone) {
timezoneOption = (
<SettingsItem
defaultMessage='Timezone'
i18nId='mobile.advanced_settings.timezone'
iconName='ios-globe'
iconType='ion'
onPress={this.goToTimezoneSettings}
separator={false}
showArrow={false}
theme={theme}
/>
);
}
return (
<View style={style.container}>
<StatusBar/>
@ -83,10 +120,11 @@ export default class DisplaySettings extends PureComponent {
iconName='ios-time'
iconType='ion'
onPress={this.goToClockDisplaySettings}
separator={false}
separator={disableClockDisplaySeparator}
showArrow={false}
theme={theme}
/>
{timezoneOption}
<View style={style.divider}/>
</View>
{clockDisplayModal}

View file

@ -5,10 +5,15 @@ import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isTimezoneEnabled} from 'app/utils/timezone';
import DisplaySettings from './display_settings';
function mapStateToProps(state) {
const enableTimezone = isTimezoneEnabled(state);
return {
enableTimezone,
theme: getTheme(state),
};
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getSupportedTimezones} from 'mattermost-redux/actions/general';
import {getSupportedTimezones as getTimezones} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {updateUser} from 'app/actions/views/edit_profile';
import Timezone from './timezone';
function mapStateToProps(state) {
const timezones = getTimezones(state);
const currentUser = getCurrentUser(state);
const userTimezone = getUserTimezone(state, currentUser.id);
return {
user: currentUser,
theme: getTheme(state),
userTimezone,
timezones,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getSupportedTimezones,
updateUser,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Timezone);

View file

@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getSupportedTimezones} from 'mattermost-redux/selectors/entities/general';
import SelectTimezone from './select_timezone';
function mapStateToProps(state, props) {
const {selectedTimezone} = props;
const supportedTimezones = getSupportedTimezones(state);
let index = 0;
const timezoneIndex = supportedTimezones.findIndex((timezone) => timezone === selectedTimezone);
if (timezoneIndex > 0) {
index = timezoneIndex;
}
return {
theme: getTheme(state),
timezones: supportedTimezones,
initialScrollIndex: index,
};
}
export default connect(mapStateToProps)(SelectTimezone);

View file

@ -0,0 +1,159 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
View,
FlatList,
Platform,
} from 'react-native';
import {getTimezoneRegion} from 'mattermost-redux/utils/timezone_utils';
import {intlShape} from 'react-intl';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import SelectTimezoneRow from './select_timezone_row';
import {ListTypes} from 'app/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const ITEM_HEIGHT = 45;
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
export default class Timezone extends PureComponent {
static propTypes = {
selectedTimezone: PropTypes.string.isRequired,
initialScrollIndex: PropTypes.number.isRequired,
timezones: PropTypes.array.isRequired,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
this.state = {
value: '',
timezones: props.timezones,
};
}
filteredTimezones = (timezonePrefix) => {
if (timezonePrefix.length === 0) {
return this.state.timezones;
}
const lowerCasePrefix = timezonePrefix.toLowerCase();
return this.state.timezones.filter((t) => (
getTimezoneRegion(t).toLowerCase().indexOf(lowerCasePrefix) >= 0 ||
t.toLowerCase().indexOf(lowerCasePrefix) >= 0
));
};
timezoneSelected = (timezone) => {
this.props.onBack(timezone);
this.props.navigator.pop();
};
handleTextChanged = (value) => {
this.setState({value});
};
keyExtractor = (item) => item;
getItemLayout = (data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
});
renderItem = ({item: timezone}) => {
return (
<SelectTimezoneRow
theme={this.props.theme}
timezone={timezone}
selectedTimezone={this.props.selectedTimezone}
onPress={this.timezoneSelected}
/>
);
};
render() {
const {theme, initialScrollIndex} = this.props;
const {value} = this.state;
const {intl} = this.context;
const style = getStyleSheet(theme);
const searchBarInput = {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 15,
};
return (
<View style={style.container}>
<StatusBar/>
<View style={style.header}>
<SearchBar
ref='searchBar'
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={Platform.OS === 'ios' ? 33 : 46}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
onChangeText={this.handleTextChanged}
autoCapitalize='none'
value={value}
containerStyle={style.searchBarContainer}
showArrow={false}
/>
</View>
<FlatList
data={this.filteredTimezones(value)}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
getItemLayout={this.getItemLayout}
keyboardShouldPersistTaps='always'
keyboardDismissMode='on-drag'
maxToRenderPerBatch={15}
initialScrollIndex={initialScrollIndex}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
header: {
backgroundColor: theme.sidebarHeaderBg,
width: '100%',
...Platform.select({
android: {
height: 46,
justifyContent: 'center',
},
ios: {
height: 44,
},
}),
},
};
});

View file

@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
Text,
View,
} from 'react-native';
import CheckMark from 'app/components/checkmark';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const ITEM_HEIGHT = 45;
export default class SelectTimezoneRow extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
timezone: PropTypes.string.isRequired,
selectedTimezone: PropTypes.string,
onPress: PropTypes.func.isRequired,
};
timezoneSelected = () => {
const {timezone, onPress} = this.props;
onPress(timezone);
};
render() {
const {theme, timezone, selectedTimezone} = this.props;
const styles = getStyleSheet(theme);
const selected = timezone === selectedTimezone && (
<CheckMark
width={12}
height={12}
color={theme.linkColor}
/>
);
return (
<TouchableOpacity
style={styles.itemContainer}
key={timezone}
onPress={this.timezoneSelected}
>
<View style={styles.item}>
<Text style={styles.itemText}>
{timezone}
</Text>
</View>
{selected}
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
itemContainer: {
flexDirection: 'row',
alignItems: 'center',
width: '100%',
paddingHorizontal: 15,
height: ITEM_HEIGHT,
},
item: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
},
itemText: {
fontSize: 15,
color: theme.centerChannelColor,
},
};
});

View file

@ -0,0 +1,232 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
View,
Text,
Platform,
} from 'react-native';
import {intlShape} from 'react-intl';
import {getTimezoneRegion} from 'mattermost-redux/utils/timezone_utils';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import Section from 'app/screens/settings/section';
import SectionItem from 'app/screens/settings/section_item';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {getDeviceTimezone} from 'app/utils/timezone';
export default class Timezone extends PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
timezones: PropTypes.array.isRequired,
user: PropTypes.object.isRequired,
userTimezone: PropTypes.shape({
useAutomaticTimezone: PropTypes.bool.isRequired,
automaticTimezone: PropTypes.string.isRequired,
manualTimezone: PropTypes.string.isRequired,
}).isRequired,
actions: PropTypes.shape({
getSupportedTimezones: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
}).isRequired,
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
this.state = {
useAutomaticTimezone: props.userTimezone.useAutomaticTimezone,
};
}
componentWillMount() {
const {actions: {getSupportedTimezones}, timezones} = this.props;
if (timezones.length === 0) {
getSupportedTimezones();
}
}
updateAutomaticTimezone = (useAutomaticTimezone) => {
const {userTimezone: {manualTimezone}} = this.props;
let automaticTimezone = '';
this.setState({useAutomaticTimezone});
if (useAutomaticTimezone) {
automaticTimezone = getDeviceTimezone();
this.submitUser({
useAutomaticTimezone,
automaticTimezone,
manualTimezone,
});
return;
}
if (manualTimezone.length > 0) {
// Preserve state change in server if manualTimezone exists
this.submitUser({
useAutomaticTimezone,
automaticTimezone,
manualTimezone,
});
}
};
updateManualTimezone = (manualTimezone) => {
this.submitUser({
useAutomaticTimezone: false,
automaticTimezone: '',
manualTimezone,
});
};
submitUser = ({
useAutomaticTimezone,
automaticTimezone,
manualTimezone,
}) => {
const {user} = this.props;
const timezone = {
useAutomaticTimezone: useAutomaticTimezone.toString(),
automaticTimezone,
manualTimezone,
};
const updatedUser = {
...user,
timezone,
};
this.props.actions.updateUser(updatedUser);
};
goToSelectTimezone = () => {
const {
userTimezone: {manualTimezone},
navigator,
theme,
} = this.props;
const {intl} = this.context;
this.goingBack = false;
navigator.push({
backButtonTitle: '',
screen: 'SelectTimezone',
title: intl.formatMessage({id: 'mobile.timezone_settings.select', defaultMessage: 'Select Timezone'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
selectedTimezone: manualTimezone,
onBack: this.updateManualTimezone,
},
});
};
render() {
const {
theme,
userTimezone: {
automaticTimezone,
manualTimezone,
},
} = this.props;
const {useAutomaticTimezone} = this.state;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<View style={style.wrapper}>
<Section
disableHeader={true}
theme={theme}
>
<View style={style.divider}/>
<SectionItem
label={(
<FormattedText
id='mobile.timezone_settings.automatically'
defaultMessage='Set automatically'
/>
)}
description={(
<Text>
{useAutomaticTimezone && getTimezoneRegion(automaticTimezone)}
</Text>
)}
action={this.updateAutomaticTimezone}
actionType='toggle'
selected={useAutomaticTimezone}
theme={theme}
/>
{!useAutomaticTimezone && (
<View>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='mobile.timezone_settings.manual'
defaultMessage='Change timezone'
/>
)}
description={(
<Text>
{getTimezoneRegion(manualTimezone)}
</Text>
)}
action={this.goToSelectTimezone}
actionType='arrow'
theme={theme}
/>
</View>
)}
<View style={style.divider}/>
</Section>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
wrapper: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
flex: 1,
...Platform.select({
ios: {
paddingTop: 35,
},
}),
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
marginLeft: 15,
},
};
});

View file

@ -7,14 +7,19 @@ import {connect} from 'react-redux';
import {setChannelDisplayName} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTeammateNameDisplaySetting, getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import Preferences from 'mattermost-redux/constants/preferences';
import {isTimezoneEnabled} from 'app/utils/timezone';
import UserProfile from './user_profile';
function mapStateToProps(state, ownProps) {
const config = getConfig(state);
const {createChannel: createChannelRequest} = state.requests.channels;
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
const enableTimezone = isTimezoneEnabled(state);
return {
config,
@ -22,6 +27,8 @@ function mapStateToProps(state, ownProps) {
currentDisplayName: state.views.channel.displayName,
user: state.entities.users.profiles[ownProps.userId],
teammateNameDisplay: getTeammateNameDisplaySetting(state),
enableTimezone,
militaryTime,
theme: getTheme(state),
};
}

View file

@ -12,8 +12,11 @@ import {
import {intlShape} from 'react-intl';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import ProfilePicture from 'app/components/profile_picture';
import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import StatusBar from 'app/components/status_bar';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
@ -33,6 +36,8 @@ export default class UserProfile extends PureComponent {
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
militaryTime: PropTypes.bool.isRequired,
enableTimezone: PropTypes.bool.isRequired,
};
static contextTypes = {
@ -91,6 +96,34 @@ export default class UserProfile extends PureComponent {
return null;
};
buildTimezoneBlock = () => {
const {theme, user, militaryTime} = this.props;
const style = createStyleSheet(theme);
const currentTimezone = getUserCurrentTimezone(user.timezone);
if (!currentTimezone) {
return null;
}
const nowDate = new Date();
return (
<View>
<FormattedText
id='mobile.routes.user_profile.local_time'
defaultMessage='LOCAL TIME'
style={style.header}
/>
<Text style={style.text}>
<FormattedTime
timeZone={currentTimezone}
hour12={!militaryTime}
value={nowDate}
/>
</Text>
</View>
);
};
sendMessage = async () => {
const {intl} = this.context;
const {actions, currentDisplayName, teammateNameDisplay, user} = this.props;
@ -162,7 +195,7 @@ export default class UserProfile extends PureComponent {
};
render() {
const {config, theme, user} = this.props;
const {config, theme, user, enableTimezone} = this.props;
const style = createStyleSheet(theme);
if (!user) {
@ -186,6 +219,7 @@ export default class UserProfile extends PureComponent {
<Text style={style.username}>{`@${user.username}`}</Text>
</View>
<View style={style.content}>
{enableTimezone && this.buildTimezoneBlock()}
{this.buildDisplayBlock('username')}
{config.ShowEmailAddress === 'true' && this.buildDisplayBlock('email')}
{this.buildDisplayBlock('position')}

16
app/utils/timezone.js Normal file
View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DeviceInfo from 'react-native-device-info';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
export function getDeviceTimezone() {
return DeviceInfo.getTimezone();
}
export function isTimezoneEnabled(state) {
const {config} = state.entities.general;
const serverVersion = state.entities.general.serverVersion;
return config.ExperimentalTimezone === 'true' && isMinimumServerVersion(serverVersion, 4, 9);
}

View file

@ -2229,6 +2229,7 @@
"mobile.advanced_settings.reset_message": "\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n",
"mobile.advanced_settings.reset_title": "Reset Cache",
"mobile.advanced_settings.title": "Advanced Settings",
"mobile.advanced_settings.timezone": "Timezone",
"mobile.android.camera_permission_denied_description": "To take photos and videos with your camera, please change your permission settings.",
"mobile.android.camera_permission_denied_title": "Camera access is required",
"mobile.android.permission_denied_dismiss": "Dismiss",
@ -2500,6 +2501,7 @@
"mobile.routes.thread_dm": "Direct Message Thread",
"mobile.routes.user_profile": "Profile",
"mobile.routes.user_profile.send_message": "Send Message",
"mobile.routes.user_profile.local_time": "LOCAL TIME",
"mobile.search.from_modifier_description": "to find posts from specific users",
"mobile.search.from_modifier_title": "username",
"mobile.search.in_modifier_description": "to find posts in specific channels",
@ -2533,6 +2535,9 @@
"mobile.share_extension.send": "Send",
"mobile.share_extension.team": "Team",
"mobile.suggestion.members": "Members",
"mobile.timezone_settings.automatically": "Set automatically",
"mobile.timezone_settings.manual": "Change timezone",
"mobile.timezone_settings.select": "Select Timezone",
"mobile.video.save_error_message": "To save the video file you need to download it first.",
"mobile.video.save_error_title": "Save Video Error",
"mobile.video_playback.failed_description": "An error occurred while trying to play the video.\n",

6
package-lock.json generated
View file

@ -5155,7 +5155,7 @@
},
"commonmark": {
"version": "github:mattermost/commonmark.js#e5c34706cafa2924370772ff47937b833648a6de",
"from": "github:mattermost/commonmark.js#e5c34706cafa2924370772ff47937b833648a6de",
"from": "commonmark@github:mattermost/commonmark.js#e5c34706cafa2924370772ff47937b833648a6de",
"requires": {
"entities": "~ 1.1.1",
"mdurl": "~ 1.0.1",
@ -15141,7 +15141,7 @@
},
"redux-offline": {
"version": "git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"from": "git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"from": "redux-offline@git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"requires": {
"redux-persist": "^4.5.0"
}
@ -15714,7 +15714,7 @@
},
"rn-placeholder": {
"version": "github:enahum/rn-placeholder#4b065f892d7a7f9d921a969f2e72e609ebc0a212",
"from": "github:enahum/rn-placeholder#4b065f892d7a7f9d921a969f2e72e609ebc0a212",
"from": "rn-placeholder@github:enahum/rn-placeholder#4b065f892d7a7f9d921a969f2e72e609ebc0a212",
"requires": {
"prop-types": "^15.5.10"
}

View file

@ -338,6 +338,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/app/utils/tap.js',
'./node_modules/app/utils/theme.js',
'./node_modules/app/utils/time_tracker.js',
'./node_modules/app/utils/timezone.js',
'./node_modules/app/utils/tooltip.js',
'./node_modules/app/utils/url.js',
'./node_modules/app/utils/wrap_context_provider.js',
@ -690,6 +691,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/node_modules/mattermost-redux/actions/roles.js',
'./node_modules/node_modules/mattermost-redux/actions/search.js',
'./node_modules/node_modules/mattermost-redux/actions/teams.js',
'./node_modules/node_modules/mattermost-redux/actions/timezone.js',
'./node_modules/node_modules/mattermost-redux/actions/users.js',
'./node_modules/node_modules/mattermost-redux/actions/websocket.js',
'./node_modules/node_modules/mattermost-redux/client/client.js',
@ -762,6 +764,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/node_modules/mattermost-redux/selectors/entities/preferences.js',
'./node_modules/node_modules/mattermost-redux/selectors/entities/roles.js',
'./node_modules/node_modules/mattermost-redux/selectors/entities/teams.js',
'./node_modules/node_modules/mattermost-redux/selectors/entities/timezone.js',
'./node_modules/node_modules/mattermost-redux/selectors/entities/typing.js',
'./node_modules/node_modules/mattermost-redux/selectors/entities/users.js',
'./node_modules/node_modules/mattermost-redux/store/configureStore.prod.js',
@ -781,6 +784,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/node_modules/mattermost-redux/utils/preference_utils.js',
'./node_modules/node_modules/mattermost-redux/utils/team_utils.js',
'./node_modules/node_modules/mattermost-redux/utils/theme_utils.js',
'./node_modules/node_modules/mattermost-redux/utils/timezone_utils.js',
'./node_modules/node_modules/mattermost-redux/utils/user_utils.js',
'./node_modules/node_modules/mdurl/decode.js',
'./node_modules/node_modules/mdurl/encode.js',