diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb109cc8a30e6..71c3dc6c9af35 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -78,6 +78,6 @@ + android:value="com.expensify.chat.customairshipextender.CustomAirshipExtender" /> diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.java b/android/app/src/main/java/com/expensify/chat/MainApplication.java index 7156979a6c684..fd203ecc675d6 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.java +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.java @@ -2,24 +2,23 @@ import android.content.Context; import android.database.CursorWindow; + import androidx.multidex.MultiDexApplication; + import com.expensify.chat.bootsplash.BootSplashPackage; import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; -import com.reactnativecommunity.webview.RNCWebViewPackage; -import com.reactnativecommunity.webview.RNCWebViewPackage; -import com.reactnativecommunity.webview.RNCWebViewPackage; -import com.existfragger.rnimagesize.RNImageSizePackage; -import com.google.firebase.crashlytics.FirebaseCrashlytics; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.modules.i18nmanager.I18nUtil; import com.facebook.soloader.SoLoader; -import java.lang.reflect.InvocationTargetException; +import com.google.firebase.crashlytics.FirebaseCrashlytics; + import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.util.List; -import com.facebook.react.modules.i18nmanager.I18nUtil; public class MainApplication extends MultiDexApplication implements ReactApplication { @@ -111,13 +110,7 @@ private static void initializeFlipper( aClass .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) .invoke(null, context, reactInstanceManager); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { + } catch (Exception e) { e.printStackTrace(); } } diff --git a/android/app/src/main/java/com/expensify/chat/StartupTimer.java b/android/app/src/main/java/com/expensify/chat/StartupTimer.java index de43519fcb153..972e1dd95a0e1 100644 --- a/android/app/src/main/java/com/expensify/chat/StartupTimer.java +++ b/android/app/src/main/java/com/expensify/chat/StartupTimer.java @@ -1,4 +1,5 @@ package com.expensify.chat; + import android.util.Log; import com.facebook.react.bridge.ReactApplicationContext; diff --git a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java similarity index 94% rename from android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java rename to android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java index dd09a934fc790..0cae0bd2de6d8 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java @@ -1,4 +1,4 @@ -package com.expensify.chat; +package com.expensify.chat.customairshipextender; import android.content.Context; import androidx.annotation.NonNull; @@ -18,4 +18,4 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) NotificationListener notificationListener = airship.getPushManager().getNotificationListener(); pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider)); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java similarity index 97% rename from android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java rename to android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java index d6fc1f9e1a350..514e5aca14a05 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java @@ -1,4 +1,4 @@ -package com.expensify.chat; +package com.expensify.chat.customairshipextender; import androidx.annotation.NonNull; import com.urbanairship.push.NotificationActionButtonInfo; diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java similarity index 98% rename from android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java rename to android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 8e9f5082ef67b..deeff81bf76dc 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -1,4 +1,4 @@ -package com.expensify.chat; +package com.expensify.chat.customairshipextender; import android.content.Context; import android.graphics.Bitmap; @@ -26,6 +26,8 @@ import com.urbanairship.util.ImageUtils; import java.net.MalformedURLException; import java.net.URL; +import java.sql.Timestamp; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -126,7 +128,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil String avatar = reportAction.get("avatar").getString(); String accountID = Integer.toString(reportAction.get("actorAccountID").getInt(-1)); String message = reportAction.get("message").getList().get(0).getMap().get("text").getString(); - long time = reportAction.get("timestamp").getLong(0); + long time = Timestamp.valueOf(reportAction.get("created").getString(Instant.now().toString())).getTime(); String roomName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); String conversationTitle = "Chat with " + name; if (!roomName.isEmpty()) { diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index 1fb7c9ca9dc8c..918c055948250 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -19,11 +19,11 @@ const withLocalizePropTypes = { /** Formats number formatted according to locale and options */ numberFormat: PropTypes.func.isRequired, - /** Converts a timestamp into a localized string representation that's relative to current moment in time */ - timestampToRelative: PropTypes.func.isRequired, + /** Converts a datetime into a localized string representation that's relative to current moment in time */ + datetimeToRelative: PropTypes.func.isRequired, - /** Formats a timestamp to local date and time string */ - timestampToDateTime: PropTypes.func.isRequired, + /** Formats a datetime to local date and time string */ + datetimeToCalendarTime: PropTypes.func.isRequired, /** Returns a locally converted phone number without the country code */ toLocalPhone: PropTypes.func.isRequired, @@ -59,8 +59,8 @@ class LocaleContextProvider extends React.Component { return { translate: this.translate.bind(this), numberFormat: this.numberFormat.bind(this), - timestampToRelative: this.timestampToRelative.bind(this), - timestampToDateTime: this.timestampToDateTime.bind(this), + datetimeToRelative: this.datetimeToRelative.bind(this), + datetimeToCalendarTime: this.datetimeToCalendarTime.bind(this), fromLocalPhone: this.fromLocalPhone.bind(this), toLocalPhone: this.toLocalPhone.bind(this), fromLocaleDigit: this.fromLocaleDigit.bind(this), @@ -88,22 +88,22 @@ class LocaleContextProvider extends React.Component { } /** - * @param {Number} timestamp + * @param {String} datetime * @returns {String} */ - timestampToRelative(timestamp) { - return DateUtils.timestampToRelative(this.props.preferredLocale, timestamp); + datetimeToRelative(datetime) { + return DateUtils.datetimeToRelative(this.props.preferredLocale, datetime); } /** - * @param {Number} timestamp + * @param {String} datetime - ISO-formatted datetime string * @param {Boolean} [includeTimezone] * @returns {String} */ - timestampToDateTime(timestamp, includeTimezone) { - return DateUtils.timestampToDateTime( + datetimeToCalendarTime(datetime, includeTimezone) { + return DateUtils.datetimeToCalendarTime( this.props.preferredLocale, - timestamp, + datetime, includeTimezone, ); } diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 3582e672306d4..b3fbbbd03d8a5 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -33,27 +33,27 @@ Onyx.connect({ }); /** - * Gets the user's stored time-zone NVP and returns a localized - * Moment object for the given timestamp + * Gets the user's stored time zone NVP and returns a localized + * Moment object for the given ISO-formatted datetime string * * @param {String} locale - * @param {Number} timestamp + * @param {String} datetime * @param {String} [currentSelectedTimezone] * * @returns {Moment} * * @private */ -function getLocalMomentFromTimestamp(locale, timestamp, currentSelectedTimezone = timezone.selected) { +function getLocalMomentFromDatetime(locale, datetime, currentSelectedTimezone = timezone.selected) { moment.locale(locale); - if (!timestamp) { + if (!datetime) { return moment.tz(currentSelectedTimezone); } - return moment.unix(timestamp).tz(currentSelectedTimezone); + return moment.utc(datetime).tz(currentSelectedTimezone); } /** - * Formats a timestamp to local date and time string + * Formats an ISO-formatted datetime string to local date and time string * * e.g. * @@ -61,13 +61,13 @@ function getLocalMomentFromTimestamp(locale, timestamp, currentSelectedTimezone * Jan 20, 2019 at 5:30 PM anything over 1 year ago * * @param {String} locale - * @param {Number} timestamp + * @param {String} datetime * @param {Boolean} includeTimeZone * * @returns {String} */ -function timestampToDateTime(locale, timestamp, includeTimeZone = false) { - const date = getLocalMomentFromTimestamp(locale, timestamp); +function datetimeToCalendarTime(locale, datetime, includeTimeZone = false) { + const date = getLocalMomentFromDatetime(locale, datetime); const tz = includeTimeZone ? ' [UTC]Z' : ''; const todayAt = Localize.translate(locale, 'common.todayAt'); @@ -86,7 +86,7 @@ function timestampToDateTime(locale, timestamp, includeTimeZone = false) { } /** - * Converts a timestamp into a localized string representation + * Converts an ISO-formatted datetime string into a localized string representation * that's relative to current moment in time. * * e.g. @@ -99,12 +99,12 @@ function timestampToDateTime(locale, timestamp, includeTimeZone = false) { * Jan 20, 2019 anything over 1 year * * @param {String} locale - * @param {Number} timestamp + * @param {String} datetime * * @returns {String} */ -function timestampToRelative(locale, timestamp) { - const date = getLocalMomentFromTimestamp(locale, timestamp); +function datetimeToRelative(locale, datetime) { + const date = getLocalMomentFromDatetime(locale, datetime); return moment(date).fromNow(); } @@ -161,18 +161,29 @@ function getMicroseconds() { return Date.now() * CONST.MICROSECONDS_PER_MS; } +/** + * Returns the current time in milliseconds in the format expected by the database + * @returns {String} + */ +function currentDBTime() { + return new Date().toISOString() + .replace('T', ' ') + .replace('Z', ''); +} + /** * @namespace DateUtils */ const DateUtils = { - timestampToRelative, - timestampToDateTime, + datetimeToRelative, + datetimeToCalendarTime, startCurrentDateUpdater, - getLocalMomentFromTimestamp, + getLocalMomentFromDatetime, getCurrentTimezone, canUpdateTimezone, setTimezoneUpdated, getMicroseconds, + currentDBTime, }; export default DateUtils; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index ae04dc86c4943..2876d36ebefb4 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -3,6 +3,7 @@ import _ from 'underscore'; import lodashMerge from 'lodash/merge'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Onyx from 'react-native-onyx'; +import moment from 'moment'; import * as CollectionUtils from './CollectionUtils'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; @@ -83,7 +84,7 @@ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { } // Comments are only grouped if they happen within 5 minutes of each other - if (currentAction.action.timestamp - previousAction.action.timestamp > 300) { + if (moment(currentAction.action.created).unix() - moment(previousAction.action.created).unix() > 300) { return false; } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 9252911b4535f..0d899070a2de0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; -import moment from 'moment'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; @@ -15,6 +14,7 @@ import ROUTES from '../ROUTES'; import * as NumberUtils from './NumberUtils'; import * as NumberFormatUtils from './NumberFormatUtils'; import Permissions from './Permissions'; +import DateUtils from './DateUtils'; let sessionEmail; Onyx.connect({ @@ -643,7 +643,7 @@ function buildOptimisticReportAction(sequenceNumber, text, file) { sequenceNumber, clientID: NumberUtils.generateReportActionClientID(), avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)), - timestamp: moment().unix(), + created: DateUtils.currentDBTime(), message: [ { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, @@ -795,7 +795,7 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency, reportActionID: NumberUtils.rand64(), sequenceNumber, shouldShow: true, - timestamp: moment().unix(), + created: DateUtils.currentDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; } @@ -883,7 +883,7 @@ function buildOptimisticCreatedReportAction(ownerEmail) { automatic: false, sequenceNumber: 0, avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)), - timestamp: moment().unix(), + created: DateUtils.currentDBTime(), shouldShow: true, }, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 49dddb0cf0949..5462f203ed985 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1323,12 +1323,12 @@ Onyx.connect({ return; } - if (!action.timestamp) { + if (!action.created) { return; } // If we are past the deadline to notify for this comment don't do it - if (moment.utc(action.timestamp * 1000).isBefore(moment.utc().subtract(10, 'seconds'))) { + if (moment.utc(moment(action.created).unix() * 1000).isBefore(moment.utc().subtract(10, 'seconds'))) { handledReportActions[reportID] = handledReportActions[reportID] || {}; handledReportActions[reportID][action.sequenceNumber] = true; return; diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 7ca5afb9c8571..fc570c0c5d79d 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -95,7 +95,7 @@ class DetailsPage extends React.PureComponent { // If we have a reportID param this means that we // arrived here via the ParticipantsPage and should be allowed to navigate back to it const shouldShowBackButton = Boolean(this.props.route.params.reportID); - const timezone = details.timezone ? DateUtils.getLocalMomentFromTimestamp(this.props.preferredLocale, null, details.timezone.selected) : null; + const timezone = details.timezone ? DateUtils.getLocalMomentFromDatetime(this.props.preferredLocale, null, details.timezone.selected) : null; const GMTTime = timezone ? `${timezone.toString().split(/[+-]/)[0].slice(-3)} ${timezone.zoneAbbr()}` : ''; const currentTime = (timezone && Number.isNaN(Number(timezone.zoneAbbr()))) ? timezone.zoneAbbr() : GMTTime; const shouldShowLocalTime = !ReportUtils.hasExpensifyEmails([details.login]); diff --git a/src/pages/home/report/ParticipantLocalTime.js b/src/pages/home/report/ParticipantLocalTime.js index b62a7ba5e833e..a93d2e57adcc1 100644 --- a/src/pages/home/report/ParticipantLocalTime.js +++ b/src/pages/home/report/ParticipantLocalTime.js @@ -42,8 +42,8 @@ class ParticipantLocalTime extends PureComponent { getParticipantLocalTime() { const reportRecipientTimezone = lodashGet(this.props.participant, 'timezone', CONST.DEFAULT_TIME_ZONE); - const reportTimezone = DateUtils.getLocalMomentFromTimestamp(this.props.preferredLocale, null, reportRecipientTimezone.selected); - const currentTimezone = DateUtils.getLocalMomentFromTimestamp(this.props.preferredLocale); + const reportTimezone = DateUtils.getLocalMomentFromDatetime(this.props.preferredLocale, null, reportRecipientTimezone.selected); + const currentTimezone = DateUtils.getLocalMomentFromDatetime(this.props.preferredLocale); const reportRecipientDay = reportTimezone.format('dddd'); const currentUserDay = currentTimezone.format('dddd'); diff --git a/src/pages/home/report/ReportActionItemDate.js b/src/pages/home/report/ReportActionItemDate.js index 8ee127a721fd9..0cf570132bd9e 100644 --- a/src/pages/home/report/ReportActionItemDate.js +++ b/src/pages/home/report/ReportActionItemDate.js @@ -8,13 +8,13 @@ import {withCurrentDate} from '../../../components/OnyxProvider'; const propTypes = { /** UTC timestamp for when the action was created */ - timestamp: PropTypes.number.isRequired, + created: PropTypes.string.isRequired, ...withLocalizePropTypes, }; const ReportActionItemDate = props => ( - {props.timestampToDateTime(props.timestamp)} + {props.datetimeToCalendarTime(props.created)} ); diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 12c09b5f934f0..4ca290a8912a5 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -98,7 +98,7 @@ const ReportActionItemSingle = (props) => { /> ))} - + ) : null} {props.children} diff --git a/src/pages/home/report/reportActionPropTypes.js b/src/pages/home/report/reportActionPropTypes.js index d752047cbb970..d3cebe8ba317f 100644 --- a/src/pages/home/report/reportActionPropTypes.js +++ b/src/pages/home/report/reportActionPropTypes.js @@ -12,8 +12,8 @@ export default { /** ID of the report action */ sequenceNumber: PropTypes.number, - /** Unix timestamp */ - timestamp: PropTypes.number, + /** ISO-formatted datetime */ + created: PropTypes.string, /** report action message */ message: PropTypes.arrayOf(reportActionFragmentPropTypes), diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index cbf04d3adb582..f36c7b5560ae4 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; -import moment from 'moment'; import { beforeEach, beforeAll, afterEach, jest, describe, it, expect, } from '@jest/globals'; @@ -17,6 +16,7 @@ import Log from '../../src/libs/Log'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; import * as User from '../../src/libs/actions/User'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import DateUtils from '../../src/libs/DateUtils'; describe('actions/Report', () => { beforeAll(() => { @@ -260,7 +260,7 @@ describe('actions/Report', () => { person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], sequenceNumber: 1, shouldShow: true, - timestamp: moment().unix(), + created: DateUtils.currentDBTime(), }, }, }, @@ -325,7 +325,7 @@ describe('actions/Report', () => { avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], shouldShow: true, - timestamp: moment().unix(), + created: DateUtils.currentDBTime(), reportActionID: 'derp', }; diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index cafe2d1a7cb90..7796e66f27dd4 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -107,6 +107,7 @@ const USER_B_ACCOUNT_ID = 2; const USER_B_EMAIL = 'user_b@test.com'; const USER_C_ACCOUNT_ID = 3; const USER_C_EMAIL = 'user_c@test.com'; +const MOMENT_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; /** * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. @@ -141,18 +142,18 @@ function signInAndGetAppWithUnreadChat() { actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, automatic: false, sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + created: MOMENT_TEN_MINUTES_AGO.format(MOMENT_FORMAT), reportActionID: NumberUtils.rand64(), }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix(), USER_B_ACCOUNT_ID), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix(), USER_B_ACCOUNT_ID), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix(), USER_B_ACCOUNT_ID), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix(), USER_B_ACCOUNT_ID), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix(), USER_B_ACCOUNT_ID), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix(), USER_B_ACCOUNT_ID), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix(), USER_B_ACCOUNT_ID), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix(), USER_B_ACCOUNT_ID), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix(), USER_B_ACCOUNT_ID), + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').format(MOMENT_FORMAT), USER_B_ACCOUNT_ID), }); Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), @@ -276,7 +277,7 @@ describe('Unread Indicators', () => { actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, automatic: false, sequenceNumber: 0, - timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + created: NEW_REPORT_CREATED_MOMENT.format(MOMENT_FORMAT), reportActionID: NumberUtils.rand64(), }, 1: { @@ -285,7 +286,7 @@ describe('Unread Indicators', () => { actorAccountID: USER_C_ACCOUNT_ID, person: [{type: 'TEXT', style: 'strong', text: 'User C'}], sequenceNumber: 1, - timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + created: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').format(MOMENT_FORMAT), message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], reportActionID: NumberUtils.rand64(), }, diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js new file mode 100644 index 0000000000000..7724162b8288d --- /dev/null +++ b/tests/unit/DateUtilsTest.js @@ -0,0 +1,54 @@ +import moment from 'moment'; +import Onyx from 'react-native-onyx'; +import DateUtils from '../../src/libs/DateUtils'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; + +const LOCALE = 'en'; + +describe('DateUtils', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: {email: 'current@user.com'}, + [ONYXKEYS.PERSONAL_DETAILS]: {'current@user.com': {timezone: {selected: 'Etc/UTC'}}}, + }, + }); + return waitForPromisesToResolve(); + }); + + const datetime = '2022-11-07 00:00:00'; + it('should return a moment object with the formatted datetime when calling getLocalMomentFromDatetime', () => { + const localMoment = DateUtils.getLocalMomentFromDatetime(LOCALE, datetime, 'America/Los_Angeles'); + expect(moment.isMoment(localMoment)).toBe(true); + expect(moment(localMoment).format()).toEqual('2022-11-06T16:00:00-08:00'); + }); + + it('should return the date in calendar time when calling datetimeToCalendarTime', () => { + const today = moment.utc().set({hour: 14, minute: 32}); + expect(DateUtils.datetimeToCalendarTime(LOCALE, today)).toBe('Today at 2:32 PM'); + + const yesterday = moment.utc().subtract(1, 'days').set({hour: 7, minute: 43}); + expect(DateUtils.datetimeToCalendarTime(LOCALE, yesterday)).toBe('Yesterday at 7:43 AM'); + + const date = moment.utc('2022-11-05').set({hour: 10, minute: 17}); + expect(DateUtils.datetimeToCalendarTime(LOCALE, date)).toBe('Nov 5 at 10:17 AM'); + }); + + it('should return the date in calendar time when calling datetimeToRelative', () => { + const aFewSecondsAgo = moment().subtract(10, 'seconds'); + expect(DateUtils.datetimeToRelative(LOCALE, aFewSecondsAgo)).toBe('a few seconds ago'); + + const aMinuteAgo = moment().subtract(1, 'minute'); + expect(DateUtils.datetimeToRelative(LOCALE, aMinuteAgo)).toBe('a minute ago'); + + const anHourAgo = moment().subtract(1, 'hour'); + expect(DateUtils.datetimeToRelative(LOCALE, anHourAgo)).toBe('an hour ago'); + }); + + it('should return the date in the format expected by the database when calling currentDBTime', () => { + const currentDBTime = DateUtils.currentDBTime(); + expect(currentDBTime).toBe(moment(currentDBTime).format('YYYY-MM-DD HH:mm:ss.SSS')); + }); +}); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 0025ff9040bdc..7b4f05c16e34e 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -157,17 +157,17 @@ function setPersonalDetails(login, accountID) { /** * @param {String} actorEmail * @param {Number} sequenceNumber - * @param {Number} timestamp + * @param {String} created * @param {Number} actorAccountID * @returns {Object} */ -function buildTestReportComment(actorEmail, sequenceNumber, timestamp, actorAccountID) { +function buildTestReportComment(actorEmail, sequenceNumber, created, actorAccountID) { return { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, actorEmail, person: [{type: 'TEXT', style: 'strong', text: 'User B'}], sequenceNumber, - timestamp, + created, message: [{type: 'COMMENT', html: `Comment ${sequenceNumber}`, text: `Comment ${sequenceNumber}`}], reportActionID: NumberUtils.rand64(), actorAccountID,