diff --git a/src/ROUTES.js b/src/ROUTES.js index 6ec7f4cf229cd..470ea9d99eb75 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -23,6 +23,8 @@ export default { SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', + SETTINGS_TIMEZONE: 'settings/profile/timezone', + SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_WORKSPACES: 'settings/workspaces', diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 7fce40647c3f5..0475b87b6b8eb 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -264,6 +264,7 @@ class BaseOptionsSelector extends Component { forceTextUnreadStyle={this.props.forceTextUnreadStyle} showTitleTooltip={this.props.showTitleTooltip} isDisabled={this.props.isDisabled} + shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} /> ) : ; return ( diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index ae41fe0f81906..885a09e553a4a 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -89,6 +89,9 @@ const propTypes = { /** Whether to show options list */ shouldShowOptions: PropTypes.bool, + + /** Whether to show a line separating options in list */ + shouldHaveOptionSeparator: PropTypes.bool, }; const defaultProps = { @@ -113,6 +116,7 @@ const defaultProps = { shouldShowOptions: true, disableArrowKeysActions: false, isDisabled: false, + shouldHaveOptionSeparator: false, }; export {propTypes, defaultProps}; diff --git a/src/languages/en.js b/src/languages/en.js index f8f18a73e0190..a9cdd4c7fe964 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -333,6 +333,11 @@ export default { john: 'John', doe: 'Doe', }, + timezonePage: { + timezone: 'Timezone', + isShownOnProfile: 'Your timezone is shown on your profile.', + getLocationAutomatically: 'Automatically determine your location.', + }, addSecondaryLoginPage: { addPhoneNumber: 'Add phone number', addEmailAddress: 'Add email address', diff --git a/src/languages/es.js b/src/languages/es.js index 572859d593775..71689626d2853 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -333,6 +333,11 @@ export default { john: 'Juan', doe: 'Nadie', }, + timezonePage: { + timezone: 'Zona horaria', + isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.', + getLocationAutomatically: 'Detecta tu ubicación automáticamente.', + }, addSecondaryLoginPage: { addPhoneNumber: 'Agregar número de teléfono', addEmailAddress: 'Agregar dirección de email', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 4f54ca2015bff..e95581674fab6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -231,6 +231,20 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_Display_Name', }, + { + getComponent: () => { + const SettingsTimezoneInitialPage = require('../../../pages/settings/Profile/TimezoneInitialPage').default; + return SettingsTimezoneInitialPage; + }, + name: 'Settings_Timezone', + }, + { + getComponent: () => { + const SettingsTimezoneSelectPage = require('../../../pages/settings/Profile/TimezoneSelectPage').default; + return SettingsTimezoneSelectPage; + }, + name: 'Settings_Timezone_Select', + }, { getComponent: () => { const SettingsAddSecondaryLoginPage = require('../../../pages/settings/AddSecondaryLoginPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 09dbe7fe0e5c4..c44a28bcc10e4 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -96,6 +96,14 @@ export default { path: ROUTES.SETTINGS_DISPLAY_NAME, exact: true, }, + Settings_Timezone: { + path: ROUTES.SETTINGS_TIMEZONE, + exact: true, + }, + Settings_Timezone_Select: { + path: ROUTES.SETTINGS_TIMEZONE_SELECT, + exact: true, + }, Settings_About: { path: ROUTES.SETTINGS_ABOUT, exact: true, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 66ec00ca992b3..b779aa63a2863 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -337,6 +337,56 @@ function updateDisplayName(firstName, lastName) { Navigation.navigate(ROUTES.SETTINGS_PROFILE); } +/** + * Updates timezone's 'automatic' setting, and updates + * selected timezone if set to automatically update. + * + * @param {Object} timezone + * @param {Boolean} timezone.automatic + * @param {String} timezone.selected + */ +function updateAutomaticTimezone(timezone) { + API.write('UpdateAutomaticTimezone', { + timezone: JSON.stringify(timezone), + }, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + timezone, + }, + }, + }], + }); +} + +/** + * Updates user's 'selected' timezone, then navigates to the + * initial Timezone page. + * + * @param {String} selectedTimezone + */ +function updateSelectedTimezone(selectedTimezone) { + const timezone = { + selected: selectedTimezone, + }; + API.write('UpdateSelectedTimezone', { + timezone: JSON.stringify(timezone), + }, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + timezone, + }, + }, + }], + }); + Navigation.navigate(ROUTES.SETTINGS_TIMEZONE); +} + /** * Fetches the local currency based on location and sets currency code/symbol to Onyx */ @@ -452,4 +502,6 @@ export { updateDisplayName, updatePronouns, clearAvatarErrors, + updateAutomaticTimezone, + updateSelectedTimezone, }; diff --git a/src/pages/settings/Profile/TimezoneInitialPage.js b/src/pages/settings/Profile/TimezoneInitialPage.js new file mode 100644 index 0000000000000..260820710cd8c --- /dev/null +++ b/src/pages/settings/Profile/TimezoneInitialPage.js @@ -0,0 +1,85 @@ +import lodashGet from 'lodash/get'; +import React from 'react'; +import {View} from 'react-native'; +import moment from 'moment-timezone'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import ROUTES from '../../../ROUTES'; +import CONST from '../../../CONST'; +import Text from '../../../components/Text'; +import styles from '../../../styles/styles'; +import Navigation from '../../../libs/Navigation/Navigation'; +import * as PersonalDetails from '../../../libs/actions/PersonalDetails'; +import compose from '../../../libs/compose'; +import Switch from '../../../components/Switch'; +import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; + +const propTypes = { + ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, +}; + +const defaultProps = { + ...withCurrentUserPersonalDetailsDefaultProps, +}; + +const TimezoneInitialPage = (props) => { + const timezone = lodashGet(props.currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); + + /** + * Updates setting for automatic timezone selection. + * Note: If we are updating automatically, we'll immediately calculate the user's timezone. + * + * @param {Boolean} isAutomatic + */ + const updateAutomaticTimezone = (isAutomatic) => { + PersonalDetails.updateAutomaticTimezone({ + automatic: isAutomatic, + selected: isAutomatic ? moment.tz.guess() : timezone.selected, + }); + }; + + return ( + + Navigation.navigate(ROUTES.SETTINGS_PROFILE)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + + {props.translate('timezonePage.isShownOnProfile')} + + + + {props.translate('timezonePage.getLocationAutomatically')} + + + + + Navigation.navigate(ROUTES.SETTINGS_TIMEZONE_SELECT)} + /> + + ); +}; + +TimezoneInitialPage.propTypes = propTypes; +TimezoneInitialPage.defaultProps = defaultProps; +TimezoneInitialPage.displayName = 'TimezoneInitialPage'; + +export default compose( + withLocalize, + withCurrentUserPersonalDetails, +)(TimezoneInitialPage); diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js new file mode 100644 index 0000000000000..62cb212d60a41 --- /dev/null +++ b/src/pages/settings/Profile/TimezoneSelectPage.js @@ -0,0 +1,103 @@ +import lodashGet from 'lodash/get'; +import React, {Component} from 'react'; +import _ from 'underscore'; +import moment from 'moment-timezone'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import ROUTES from '../../../ROUTES'; +import CONST from '../../../CONST'; +import styles from '../../../styles/styles'; +import Navigation from '../../../libs/Navigation/Navigation'; +import * as PersonalDetails from '../../../libs/actions/PersonalDetails'; +import compose from '../../../libs/compose'; +import OptionsSelector from '../../../components/OptionsSelector'; +import themeColors from '../../../styles/themes/default'; +import * as Expensicons from '../../../components/Icon/Expensicons'; + +const propTypes = { + ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, +}; + +const defaultProps = { + ...withCurrentUserPersonalDetailsDefaultProps, +}; + +class TimezoneSelectPage extends Component { + constructor(props) { + super(props); + + this.saveSelectedTimezone = this.saveSelectedTimezone.bind(this); + this.filterShownTimezones = this.filterShownTimezones.bind(this); + + this.currentSelectedTimezone = lodashGet(props.currentUserPersonalDetails, 'timezone.selected', CONST.DEFAULT_TIME_ZONE.selected); + this.allTimezones = _.chain(moment.tz.names()) + .filter(timezone => !timezone.startsWith('Etc/GMT')) + .map(timezone => ({ + text: timezone, + keyForList: timezone, + + // Add green checkmark icon & bold the timezone text + customIcon: timezone === this.currentSelectedTimezone + ? {src: Expensicons.Checkmark, color: themeColors.success} + : null, + isUnread: timezone === this.currentSelectedTimezone, + })) + .value(); + + this.state = { + timezoneInputText: this.currentSelectedTimezone, + timezoneOptions: this.allTimezones, + }; + } + + /** + * @param {Object} timezone + * @param {String} timezone.text + */ + saveSelectedTimezone({text}) { + PersonalDetails.updateSelectedTimezone(text); + } + + /** + * @param {String} searchText + */ + filterShownTimezones(searchText) { + this.setState({ + timezoneInputText: searchText, + timezoneOptions: _.filter(this.allTimezones, (tz => tz.text.toLowerCase().includes(searchText.toLowerCase()))), + }); + } + + render() { + return ( + + Navigation.navigate(ROUTES.SETTINGS_TIMEZONE)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + + ); + } +} + +TimezoneSelectPage.propTypes = propTypes; +TimezoneSelectPage.defaultProps = defaultProps; + +export default compose( + withLocalize, + withCurrentUserPersonalDetails, +)(TimezoneSelectPage);