Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateUtils refactor, remove moment from the code, timezone fixes. #24446

Merged
merged 11 commits into from
Aug 21, 2023
5 changes: 5 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ const CONST = {
MOMENT_FORMAT_STRING: 'YYYY-MM-DD',
SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss',
FNS_FORMAT_STRING: 'yyyy-MM-dd',
LOCAL_TIME_FORMAT: 'h:mm a',
WEEKDAY_TIME_FORMAT: 'eeee',
FNS_TIMEZONE_FORMAT_STRING: "yyyy-MM-dd'T'HH:mm:ssXXX",
FNS_DB_FORMAT_STRING: 'yyyy-MM-dd HH:mm:ss.SSS',
LONG_DATE_FORMAT_WITH_WEEKDAY: 'eeee, MMMM d, yyyy',
UNIX_EPOCH: '1970-01-01 00:00:00.000',
MAX_DATE: '9999-12-31',
MIN_DATE: '0001-01-01',
Expand Down
14 changes: 3 additions & 11 deletions src/components/AutoUpdateTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,13 @@ function AutoUpdateTime(props) {
* @returns {moment} Returns the locale moment object
*/
const getCurrentUserLocalTime = useCallback(
() => DateUtils.getLocalMomentFromDatetime(props.preferredLocale, null, props.timezone.selected),
() => DateUtils.getLocalDateFromDatetime(props.preferredLocale, null, props.timezone.selected),
[props.preferredLocale, props.timezone.selected],
);

const [currentUserLocalTime, setCurrentUserLocalTime] = useState(getCurrentUserLocalTime);
const minuteRef = useRef(new Date().getMinutes());
const timezoneName = useMemo(() => {
// With non-GMT timezone, moment.zoneAbbr() will return the name of that timezone, so we can use it directly.
if (Number.isNaN(Number(currentUserLocalTime.zoneAbbr()))) {
return currentUserLocalTime.zoneAbbr();
}

// With GMT timezone, moment.zoneAbbr() will return a number, so we need to display it as GMT {abbreviations} format, e.g.: GMT +07
return `GMT ${currentUserLocalTime.zoneAbbr()}`;
}, [currentUserLocalTime]);
const timezoneName = useMemo(() => DateUtils.getZoneAbbreviation(currentUserLocalTime, props.timezone.selected), [currentUserLocalTime, props.timezone.selected]);

useEffect(() => {
// If the any of the props that getCurrentUserLocalTime depends on change, we want to update the displayed time immediately
Expand All @@ -68,7 +60,7 @@ function AutoUpdateTime(props) {
{props.translate('detailsPage.localTime')}
</Text>
<Text numberOfLines={1}>
{currentUserLocalTime.format('LT')} {timezoneName}
{DateUtils.formatToLocalTime(currentUserLocalTime)} {timezoneName}
</Text>
</View>
);
Expand Down
10 changes: 5 additions & 5 deletions src/components/ReportActionItem/ChronosOOOListActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ function ChronosOOOListActions(props) {
<OfflineWithFeedback pendingAction={lodashGet(props.action, 'pendingAction', null)}>
<View style={[styles.chatItemMessage]}>
{_.map(events, (event) => {
const start = DateUtils.getLocalMomentFromDatetime(props.preferredLocale, lodashGet(event, 'start.date', ''));
const end = DateUtils.getLocalMomentFromDatetime(props.preferredLocale, lodashGet(event, 'end.date', ''));
const start = DateUtils.getLocalDateFromDatetime(props.preferredLocale, lodashGet(event, 'start.date', ''));
const end = DateUtils.getLocalDateFromDatetime(props.preferredLocale, lodashGet(event, 'end.date', ''));
return (
<View
key={event.id}
Expand All @@ -49,12 +49,12 @@ function ChronosOOOListActions(props) {
? props.translate('chronos.oooEventSummaryFullDay', {
summary: event.summary,
dayCount: event.lengthInDays,
date: end.format('dddd LL'),
date: DateUtils.formatToLongDateWithWeekday(end),
})
: props.translate('chronos.oooEventSummaryPartialDay', {
summary: event.summary,
timePeriod: `${start.format('LT')} - ${end.format('LT')}`,
date: end.format('dddd LL'),
timePeriod: `${DateUtils.formatToLocalTime(start)} - ${DateUtils.formatToLocalTime(end)}`,
date: DateUtils.formatToLongDateWithWeekday(end),
})}
</Text>
<Button
Expand Down
143 changes: 115 additions & 28 deletions src/libs/DateUtils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import moment from 'moment-timezone';
import lodashGet from 'lodash/get';

// IMPORTANT: load any locales (other than english) that might be passed to moment.locale()
import 'moment/locale/es';
import {zonedTimeToUtc, utcToZonedTime, formatInTimeZone} from 'date-fns-tz';
import {es, enGB} from 'date-fns/locale';
import {formatDistanceToNow, subMinutes, isBefore, subMilliseconds, isToday, isTomorrow, isYesterday, startOfWeek, endOfWeek, format, setDefaultOptions} from 'date-fns';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I love the alternative!

It's 3x smaller.


import _ from 'underscore';
import Onyx from 'react-native-onyx';
Expand Down Expand Up @@ -32,6 +31,24 @@ Onyx.connect({
},
});

/**
* Gets the locale string and setting default locale for date-fns
*
* @param {String} localeString
*/
function setLocale(localeString) {
switch (localeString) {
case 'en':
setDefaultOptions({locale: enGB});
waterim marked this conversation as resolved.
Show resolved Hide resolved
break;
case 'es':
setDefaultOptions({locale: es});
waterim marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
break;
}
}

/**
* Gets the user's stored time zone NVP and returns a localized
* Moment object for the given ISO-formatted datetime string
Expand All @@ -44,13 +61,15 @@ Onyx.connect({
*
* @private
*/
function getLocalMomentFromDatetime(locale, datetime, currentSelectedTimezone = timezone.selected) {
moment.locale(locale);
function getLocalDateFromDatetime(locale, datetime, currentSelectedTimezone = timezone.selected) {
setLocale(locale);
if (!datetime) {
return moment.tz(currentSelectedTimezone);
const now = new Date();
const zonedNow = utcToZonedTime(now, currentSelectedTimezone);
return zonedNow;
}
waterim marked this conversation as resolved.
Show resolved Hide resolved

return moment.utc(datetime).tz(currentSelectedTimezone);
const parsedDatetime = new Date(`${datetime} UTC`);
return utcToZonedTime(parsedDatetime, currentSelectedTimezone);
}

/**
Expand All @@ -70,28 +89,38 @@ function getLocalMomentFromDatetime(locale, datetime, currentSelectedTimezone =
* @returns {String}
*/
function datetimeToCalendarTime(locale, datetime, includeTimeZone = false, currentSelectedTimezone, isLowercase = false) {
const date = getLocalMomentFromDatetime(locale, datetime, currentSelectedTimezone);
const date = getLocalDateFromDatetime(locale, datetime, currentSelectedTimezone);
const tz = includeTimeZone ? ' [UTC]Z' : '';

let todayAt = Localize.translate(locale, 'common.todayAt');
let tomorrowAt = Localize.translate(locale, 'common.tomorrowAt');
let yesterdayAt = Localize.translate(locale, 'common.yesterdayAt');
const at = Localize.translate(locale, 'common.conjunctionAt');

const dateFormatter = 'MMM d';
const elseDateFormatter = 'MMM d, yyyy';
const timeFormatter = 'h:mm a';
const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week
waterim marked this conversation as resolved.
Show resolved Hide resolved
const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week

if (isLowercase) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused regression.
While updating week start day to Monday on header, body still remains as Sunday for the start day

todayAt = todayAt.toLowerCase();
tomorrowAt = tomorrowAt.toLowerCase();
yesterdayAt = yesterdayAt.toLowerCase();
}

return moment(date).calendar({
sameDay: `[${todayAt}] LT${tz}`,
nextDay: `[${tomorrowAt}] LT${tz}`,
lastDay: `[${yesterdayAt}] LT${tz}`,
nextWeek: `MMM D [${at}] LT${tz}`,
lastWeek: `MMM D [${at}] LT${tz}`,
sameElse: `MMM D, YYYY [${at}] LT${tz}`,
});
if (isToday(date)) {
return `${todayAt} ${format(date, timeFormatter)}${tz}`;
}
if (isTomorrow(date)) {
return `${tomorrowAt} ${format(date, timeFormatter)}${tz}`;
}
if (isYesterday(date)) {
return `${yesterdayAt} ${format(date, timeFormatter)}${tz}`;
}
if (date >= startOfCurrentWeek && date <= endOfCurrentWeek) {
return `${format(date, dateFormatter)} ${at} ${format(date, timeFormatter)}${tz}`;
}
return `${format(date, elseDateFormatter)} ${at} ${format(date, timeFormatter)}${tz}`;
}

/**
Expand All @@ -113,16 +142,66 @@ function datetimeToCalendarTime(locale, datetime, includeTimeZone = false, curre
* @returns {String}
*/
function datetimeToRelative(locale, datetime) {
const date = getLocalMomentFromDatetime(locale, datetime);
const date = getLocalDateFromDatetime(locale, datetime);
return formatDistanceToNow(date);
}

/**
* Gets the zone abbreviation from the date
*
* e.g.
*
* PST
* EST
* GMT +07 - For GMT timezone
*
* @param {String} datetime
* @param {String} selectedTimezone
*
* @returns {String}
*/
function getZoneAbbreviation(datetime, selectedTimezone) {
return formatInTimeZone(datetime, selectedTimezone, 'zzz');
}

/**
* Format date to a long date format with weekday
*
* @param {String} datetime
*
* @returns {String} Sunday, July 9, 2023
*/
function formatToLongDateWithWeekday(datetime) {
return format(new Date(datetime), CONST.DATE.LONG_DATE_FORMAT_WITH_WEEKDAY);
}

return moment(date).fromNow();
/**
* Format date to a weekday format
*
* @param {String} datetime
*
* @returns {String} Sunday
waterim marked this conversation as resolved.
Show resolved Hide resolved
*/
function formatToDayOfWeek(datetime) {
return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT);
}

/**
* Format date to a local time
*
* @param {String} datetime
*
* @returns {String} 2:30 PM
*/
function formatToLocalTime(datetime) {
return format(new Date(datetime), CONST.DATE.LOCAL_TIME_FORMAT);
}

/**
* A throttled version of a function that updates the current date in Onyx store
*/
const updateCurrentDate = _.throttle(() => {
const currentDate = moment().format('YYYY-MM-DD');
const currentDate = format(new Date(), CONST.DATE.FNS_FORMAT_STRING);
CurrentDate.setCurrentDate(currentDate);
}, 1000 * 60 * 60 * 3); // 3 hours

Expand All @@ -140,25 +219,28 @@ function startCurrentDateUpdater() {
* @returns {Object}
*/
function getCurrentTimezone() {
const currentTimezone = moment.tz.guess(true);
const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (timezone.automatic && timezone.selected !== currentTimezone) {
return {...timezone, selected: currentTimezone};
}
return timezone;
}

// Used to throttle updates to the timezone when necessary
let lastUpdatedTimezoneTime = moment();
let lastUpdatedTimezoneTime = new Date();

/**
* @returns {Boolean}
*/
function canUpdateTimezone() {
return lastUpdatedTimezoneTime.isBefore(moment().subtract(5, 'minutes'));
const currentTime = new Date();
const fiveMinutesAgo = subMinutes(currentTime, 5);
// Compare the last updated time with five minutes ago
return isBefore(lastUpdatedTimezoneTime, fiveMinutesAgo);
}

function setTimezoneUpdated() {
lastUpdatedTimezoneTime = moment();
lastUpdatedTimezoneTime = new Date();
}

/**
Expand Down Expand Up @@ -188,7 +270,8 @@ function getDBTime(timestamp = '') {
* @returns {String}
*/
function subtractMillisecondsFromDateTime(dateTime, milliseconds) {
const newTimestamp = moment.utc(dateTime).subtract(milliseconds, 'milliseconds').valueOf();
const date = zonedTimeToUtc(dateTime, 'Etc/UTC');
const newTimestamp = subMilliseconds(date, milliseconds).valueOf();
return getDBTime(newTimestamp);
}

Expand All @@ -209,10 +292,14 @@ function getDateStringFromISOTimestamp(isoTimestamp) {
* @namespace DateUtils
*/
const DateUtils = {
formatToDayOfWeek,
formatToLongDateWithWeekday,
formatToLocalTime,
getZoneAbbreviation,
datetimeToRelative,
datetimeToCalendarTime,
startCurrentDateUpdater,
getLocalMomentFromDatetime,
getLocalDateFromDatetime,
getCurrentTimezone,
canUpdateTimezone,
setTimezoneUpdated,
Expand Down
13 changes: 6 additions & 7 deletions src/pages/home/report/ParticipantLocalTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ const propTypes = {

function getParticipantLocalTime(participant, preferredLocale) {
const reportRecipientTimezone = lodashGet(participant, 'timezone', CONST.DEFAULT_TIME_ZONE);
const reportTimezone = DateUtils.getLocalMomentFromDatetime(preferredLocale, null, reportRecipientTimezone.selected);
const currentTimezone = DateUtils.getLocalMomentFromDatetime(preferredLocale);
const reportRecipientDay = reportTimezone.format('dddd');
const currentUserDay = currentTimezone.format('dddd');

const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, null, reportRecipientTimezone.selected);
const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale);
const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone);
const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone);
if (reportRecipientDay !== currentUserDay) {
return `${reportTimezone.format('LT')} ${reportRecipientDay}`;
return `${DateUtils.formatToLocalTime(reportTimezone)} ${reportRecipientDay}`;
}
return `${reportTimezone.format('LT')}`;
return `${DateUtils.formatToLocalTime(reportTimezone)}`;
}

function ParticipantLocalTime(props) {
Expand Down
Loading
Loading