diff --git a/app/components/formatted_date.js b/app/components/formatted_date.js index 370e51222..8641688b1 100644 --- a/app/components/formatted_date.js +++ b/app/components/formatted_date.js @@ -3,35 +3,33 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {injectIntl, intlShape} from 'react-intl'; +import moment from 'moment-timezone'; import {Text} from 'react-native'; -class FormattedDate extends React.PureComponent { +export default class FormattedDate extends React.PureComponent { static propTypes = { - intl: intlShape.isRequired, - value: PropTypes.any.isRequired, format: PropTypes.string, - children: PropTypes.func, + timeZone: PropTypes.string, + value: PropTypes.any.isRequired, + }; + + static defaultProps = { + format: 'ddd, MMM DD, YYYY', }; render() { const { - intl, + format, + timeZone, value, - children, ...props } = this.props; - Reflect.deleteProperty(props, 'format'); - - const formattedDate = intl.formatDate(value, this.props); - - if (typeof children === 'function') { - return children(formattedDate); + let formattedDate = moment(value).format(format); + if (timeZone) { + formattedDate = moment.tz(value, timeZone).format(format); } return {formattedDate}; } } - -export default injectIntl(FormattedDate); diff --git a/app/components/formatted_time.js b/app/components/formatted_time.js index 9f9ea57dd..64cc06569 100644 --- a/app/components/formatted_time.js +++ b/app/components/formatted_time.js @@ -3,7 +3,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {intlShape} from 'react-intl'; import {Text} from 'react-native'; import moment from 'moment-timezone'; @@ -15,30 +14,19 @@ export default class FormattedTime extends React.PureComponent { hour12: PropTypes.bool, }; - static contextTypes = { - intl: intlShape.isRequired, - }; - getFormattedTime = () => { - const {intl} = this.context; - const { value, timeZone, hour12, } = this.props; + const format = hour12 ? 'hh:mm A' : 'HH:mm'; if (timeZone) { - const format = hour12 ? 'hh:mm A' : 'HH:mm'; return moment.tz(value, timeZone).format(format); } - // If no timezone is defined fallback to the previous implementation - return intl.formatDate(new Date(value), { - hour: 'numeric', - minute: 'numeric', - hour12, - }); + return moment(value).format(format); }; render() { diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js index 850a55eb6..10afaf367 100644 --- a/app/components/post_header/post_header.js +++ b/app/components/post_header/post_header.js @@ -202,7 +202,10 @@ export default class PostHeader extends PureComponent { dateComponent = ( - + - - - diff --git a/app/i18n/index.js b/app/i18n/index.js index 7cc58dd93..5271638ed 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.js @@ -4,6 +4,7 @@ import 'intl'; import {addLocaleData} from 'react-intl'; import enLocaleData from 'react-intl/locale-data/en'; +import moment from 'moment'; import en from 'assets/i18n/en.json'; @@ -16,72 +17,92 @@ addLocaleData(enLocaleData); function loadTranslation(locale) { try { let localeData; + let momentData; switch (locale) { case 'de': TRANSLATIONS.de = require('assets/i18n/de.json'); localeData = require('react-intl/locale-data/de'); + momentData = require('moment/locale/de'); break; case 'es': TRANSLATIONS.es = require('assets/i18n/es.json'); localeData = require('react-intl/locale-data/es'); + momentData = require('moment/locale/es'); break; case 'fr': TRANSLATIONS.fr = require('assets/i18n/fr.json'); localeData = require('react-intl/locale-data/fr'); + momentData = require('moment/locale/fr'); break; case 'it': TRANSLATIONS.it = require('assets/i18n/it.json'); localeData = require('react-intl/locale-data/it'); + momentData = require('moment/locale/it'); break; case 'ja': TRANSLATIONS.ja = require('assets/i18n/ja.json'); localeData = require('react-intl/locale-data/ja'); + momentData = require('moment/locale/ja'); break; case 'ko': TRANSLATIONS.ko = require('assets/i18n/ko.json'); localeData = require('react-intl/locale-data/ko'); + momentData = require('moment/locale/ko'); break; case 'nl': TRANSLATIONS.nl = require('assets/i18n/nl.json'); localeData = require('react-intl/locale-data/nl'); + momentData = require('moment/locale/nl'); break; case 'pl': TRANSLATIONS.pl = require('assets/i18n/pl.json'); localeData = require('react-intl/locale-data/pl'); + momentData = require('moment/locale/pl'); break; case 'pt-BR': TRANSLATIONS[locale] = require('assets/i18n/pt-BR.json'); localeData = require('react-intl/locale-data/pt'); + momentData = require('moment/locale/pt-br'); break; case 'ro': TRANSLATIONS.ro = require('assets/i18n/ro.json'); localeData = require('react-intl/locale-data/ro'); + momentData = require('moment/locale/ro'); break; case 'ru': TRANSLATIONS.ru = require('assets/i18n/ru.json'); localeData = require('react-intl/locale-data/ru'); + momentData = require('moment/locale/ru'); break; case 'tr': TRANSLATIONS.tr = require('assets/i18n/tr.json'); localeData = require('react-intl/locale-data/tr'); + momentData = require('moment/locale/tr'); break; case 'uk': TRANSLATIONS.tr = require('assets/i18n/uk.json'); localeData = require('react-intl/locale-data/uk'); + momentData = require('moment/locale/uk'); break; case 'zh-CN': TRANSLATIONS[locale] = require('assets/i18n/zh-CN.json'); localeData = require('react-intl/locale-data/zh'); + momentData = require('moment/locale/zh-cn'); break; case 'zh-TW': TRANSLATIONS[locale] = require('assets/i18n/zh-TW.json'); localeData = require('react-intl/locale-data/zh'); + momentData = require('moment/locale/zh-tw'); break; } if (localeData) { addLocaleData(localeData); } + + if (momentData) { + moment.updateLocale(locale, momentData); + } } catch (e) { console.error('NO Translation found', e); //eslint-disable-line no-console } diff --git a/app/init/global_event_handler.test.js b/app/init/global_event_handler.test.js index a3abef9f5..ec0fe40f0 100644 --- a/app/init/global_event_handler.test.js +++ b/app/init/global_event_handler.test.js @@ -4,25 +4,35 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import intitialState from 'app/initial_state'; import PushNotification from 'app/push_notifications'; import GlobalEventHandler from './global_event_handler'; jest.mock('app/init/credentials', () => ({ + getCurrentServerUrl: jest.fn().mockResolvedValue(''), getAppCredentials: jest.fn(), removeAppCredentials: jest.fn(), })); +jest.mock('react-native-notifications', () => ({ + addEventListener: jest.fn(), + cancelAllLocalNotifications: jest.fn(), + setBadgesCount: jest.fn(), + NotificationAction: jest.fn(), + NotificationCategory: jest.fn(), +})); + const mockStore = configureMockStore([thunk]); -const store = mockStore({}); +const store = mockStore(intitialState); GlobalEventHandler.store = store; // TODO: Add Android test as part of https://mattermost.atlassian.net/browse/MM-17110 describe('GlobalEventHandler', () => { - it('should clear notifications on logout', () => { + it('should clear notifications on logout', async () => { const clearNotifications = jest.spyOn(PushNotification, 'clearNotifications'); - GlobalEventHandler.onLogout(); + await GlobalEventHandler.onLogout(); expect(clearNotifications).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index f5ae75dd1..f8d4a0ecf 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -36,6 +36,7 @@ export default class ChannelInfoHeader extends React.PureComponent { isArchived: PropTypes.bool.isRequired, isBot: PropTypes.bool.isRequired, isGroupConstrained: PropTypes.bool, + timeZone: PropTypes.string, }; static contextTypes = { @@ -98,6 +99,7 @@ export default class ChannelInfoHeader extends React.PureComponent { isArchived, isBot, isGroupConstrained, + timeZone, } = this.props; const style = getStyleSheet(theme); @@ -128,76 +130,76 @@ export default class ChannelInfoHeader extends React.PureComponent { {displayName} + {this.renderHasGuestText(style)} {purpose.length > 0 && - - - - - - - - + + + + + + + + } {header.length > 0 && - - - - - - - - + + + + + + + + } {isGroupConstrained && - - - + + + } {creator && - - - - + + + + } ); diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index daf752bf7..6351aa02d 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -29,6 +29,8 @@ import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils'; import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; +import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import { popTopScreen, @@ -45,6 +47,7 @@ import { selectPenultimateChannel, setChannelDisplayName, } from 'app/actions/views/channel'; +import {isLandscape} from 'app/selectors/device'; import ChannelInfo from './channel_info'; @@ -87,6 +90,12 @@ function mapStateToProps(state) { const canEditChannel = !channelIsReadOnly && showManagementOptions(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin); const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true'; + const enableTimezone = isTimezoneEnabled(state); + let timeZone = null; + if (enableTimezone) { + timeZone = getUserCurrentTimezone(currentUser.timezone); + } + return { canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin), viewArchivedChannels, @@ -103,6 +112,8 @@ function mapStateToProps(state) { theme: getTheme(state), canManageUsers, isBot, + isLandscape: isLandscape(state), + timeZone, }; } diff --git a/test/setup.js b/test/setup.js index 0ce565914..fd80f20bd 100644 --- a/test/setup.js +++ b/test/setup.js @@ -8,6 +8,7 @@ configure({adapter: new Adapter()}); const mockImpl = new MockAsyncStorage(); jest.mock('@react-native-community/async-storage', () => mockImpl); +global.window = {}; /* eslint-disable no-console */ @@ -38,6 +39,10 @@ jest.mock('NativeModules', () => { SECURITY_LEVEL_SECURE_SOFTWARE: 'SOFTWARE', SECURITY_LEVEL_SECURE_HARDWARE: 'HARDWARE', }, + RNCNetInfo: { + addEventListener: jest.fn(), + getCurrentState: jest.fn().mockResolvedValue({isConnected: true}), + }, }; }); jest.mock('NativeEventEmitter'); @@ -48,6 +53,7 @@ jest.mock('react-native-device-info', () => { getBuildNumber: () => '0', getModel: () => 'iPhone X', isTablet: () => false, + getDeviceLocale: () => 'en-US', }; });