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

Implement ReportDateIndicator #35897

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
269b820
Implement ReportDateIndicator
MrRefactor Feb 6, 2024
316c015
Merge branch 'main' into proposal/18101
MrRefactor Feb 27, 2024
809397f
Changed design and fixed value update issue
MrRefactor Feb 27, 2024
5cc2697
Fix positioning on web/desktop
MrRefactor Feb 28, 2024
f18a943
Merge branch 'main' into proposal/18101
MrRefactor Feb 28, 2024
fbf7a9a
Prettier fixes
MrRefactor Feb 28, 2024
3c7a727
Fix padding of ReportDateIndicator
MrRefactor Feb 28, 2024
9f7f16c
Review fixes
MrRefactor Feb 29, 2024
c2eda4c
Fix tests
MrRefactor Feb 29, 2024
e6f556f
Merge branch 'main' into proposal/18101
MrRefactor Feb 29, 2024
578b6c3
Merge branch 'main' into proposal/18101
MrRefactor Apr 5, 2024
60c733a
Remove floating indicator and fix positioning of indicators between m…
MrRefactor Apr 24, 2024
3a86f02
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk Apr 24, 2024
a4d9d24
fix: types
MrMuzyk Apr 24, 2024
d6d4843
fix: ios fix
MrMuzyk Apr 24, 2024
74f5f9d
fix: rename one method
MrMuzyk Apr 25, 2024
be81733
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk Apr 25, 2024
3af24ac
fix: tests
MrMuzyk Apr 25, 2024
c4e726b
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk Apr 30, 2024
d935e29
fix: instantly show today indicator
MrMuzyk Apr 30, 2024
bb6b73a
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk Apr 30, 2024
3766f48
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk Apr 30, 2024
5972b30
Merge branch 'main' of https://github.com/Expensify/App into proposal…
MrMuzyk May 7, 2024
7283192
fix: linter fix
MrMuzyk May 7, 2024
418a56f
fix: change format to tzformat to fix tz offset
MrMuzyk May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/components/LocaleContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type LocaleContextProps = {
/** Formats a datetime to local date and time string */
datetimeToCalendarTime: (datetime: string, includeTimezone: boolean, isLowercase?: boolean) => string;

/** Formats a datetime to local time string */
datetimeToLocalString: (datetime: string, includeTimezone: boolean) => string;

/** Updates date-fns internal locale */
updateLocale: () => void;

Expand All @@ -62,6 +65,7 @@ const LocaleContext = createContext<LocaleContextProps>({
numberFormat: () => '',
datetimeToRelative: () => '',
datetimeToCalendarTime: () => '',
datetimeToLocalString: () => '',
updateLocale: () => '',
formatPhoneNumber: () => '',
toLocaleDigit: () => '',
Expand All @@ -87,8 +91,13 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {}

const datetimeToCalendarTime = useMemo<LocaleContextProps['datetimeToCalendarTime']>(
() =>
(datetime, includeTimezone, isLowercase = false) =>
DateUtils.datetimeToCalendarTime(locale, datetime, includeTimezone, selectedTimezone, isLowercase),
(datetime, isLowercase = false) =>
DateUtils.datetimeToCalendarTime(locale, datetime, selectedTimezone, isLowercase),
[locale, selectedTimezone],
);

const datetimeToLocalString = useMemo<LocaleContextProps['datetimeToLocalString']>(
() => (datetime, includedTimezone) => DateUtils.datetimeToLocalString(locale, datetime, selectedTimezone, includedTimezone),
[locale, selectedTimezone],
);

Expand All @@ -106,13 +115,14 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {}
numberFormat,
datetimeToRelative,
datetimeToCalendarTime,
datetimeToLocalString,
updateLocale,
formatPhoneNumber,
toLocaleDigit,
fromLocaleDigit,
preferredLocale: locale,
}),
[translate, numberFormat, datetimeToRelative, datetimeToCalendarTime, updateLocale, formatPhoneNumber, toLocaleDigit, fromLocaleDigit, locale],
[translate, numberFormat, datetimeToRelative, datetimeToCalendarTime, datetimeToLocalString, updateLocale, formatPhoneNumber, toLocaleDigit, fromLocaleDigit, locale],
);

return <LocaleContext.Provider value={contextValue}>{children}</LocaleContext.Provider>;
Expand Down
5 changes: 4 additions & 1 deletion src/components/withLocalize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ const withLocalizePropTypes = {
/** Converts a datetime into a localized string representation that's relative to current moment in time */
datetimeToRelative: PropTypes.func.isRequired,

/** Formats a datetime to local date and time string */
/** Formats a datetime to local date */
datetimeToCalendarTime: PropTypes.func.isRequired,

/** Formats a datetime to local time string */
datetimeToLocalString: PropTypes.func.isRequired,

/** Updates date-fns internal locale */
updateLocale: PropTypes.func.isRequired,

Expand Down
6 changes: 3 additions & 3 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ export default {
recentDestinations: 'Recent destinations',
timePrefix: "It's",
conjunctionFor: 'for',
todayAt: 'Today at',
tomorrowAt: 'Tomorrow at',
yesterdayAt: 'Yesterday at',
today: 'Today',
tomorrow: 'Tomorrow',
yesterday: 'Yesterday',
conjunctionAt: 'at',
genericErrorMessage: 'Oops... something went wrong and your request could not be completed. Please try again later.',
error: {
Expand Down
6 changes: 3 additions & 3 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ export default {
recentDestinations: 'Destinos recientes',
timePrefix: 'Son las',
conjunctionFor: 'para',
todayAt: 'Hoy a las',
tomorrowAt: 'Mañana a las',
yesterdayAt: 'Ayer a las',
today: 'Hoy',
tomorrow: 'Mañana',
yesterday: 'Ayer',
conjunctionAt: 'a',
genericErrorMessage: 'Ups... algo no ha ido bien y la acción no se ha podido completar. Por favor, inténtalo más tarde.',
error: {
Expand Down
62 changes: 45 additions & 17 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,44 +176,58 @@ function isYesterday(date: Date, timeZone: SelectedTimezone): boolean {
}

/**
* Formats an ISO-formatted datetime string to local date and time string
* Formats an ISO-formatted datetime string to local date
*
* e.g.
*
* Jan 20 at 5:30 PM within the past year
* Jan 20, 2019 at 5:30 PM anything over 1 year ago
* Jan 20 within current week
* Jan 20, 2019 anything before current week
*/
function datetimeToCalendarTime(locale: Locale, datetime: string, includeTimeZone = false, currentSelectedTimezone: SelectedTimezone = timezone.selected, isLowercase = false): string {
function datetimeToCalendarTime(locale: Locale, datetime: string, currentSelectedTimezone: SelectedTimezone = timezone.selected, isLowercase = false): string {
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 weekStartsOn = getWeekStartsOn();

let today = Localize.translate(locale, 'common.today');
let tomorrow = Localize.translate(locale, 'common.tomorrow');
let yesterday = Localize.translate(locale, 'common.yesterday');

const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn});
const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn});

if (isLowercase) {
todayAt = todayAt.toLowerCase();
tomorrowAt = tomorrowAt.toLowerCase();
yesterdayAt = yesterdayAt.toLowerCase();
today = today.toLowerCase();
tomorrow = tomorrow.toLowerCase();
yesterday = yesterday.toLowerCase();
}

if (isToday(date, currentSelectedTimezone)) {
return `${todayAt} ${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
return `${today}`;
}
if (isTomorrow(date, currentSelectedTimezone)) {
return `${tomorrowAt} ${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
return `${tomorrow}`;
}
if (isYesterday(date, currentSelectedTimezone)) {
return `${yesterdayAt} ${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
return `${yesterday}`;
MrRefactor marked this conversation as resolved.
Show resolved Hide resolved
}
if (date >= startOfCurrentWeek && date <= endOfCurrentWeek) {
return `${format(date, CONST.DATE.MONTH_DAY_ABBR_FORMAT)} ${at} ${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
return `${format(date, CONST.DATE.MONTH_DAY_ABBR_FORMAT)}`;
MrRefactor marked this conversation as resolved.
Show resolved Hide resolved
}
return `${format(date, CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT)} ${at} ${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
return `${format(date, CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT)}`;
MrRefactor marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Formats an ISO-formatted datetime string to time string
*
* e.g.
*
* 5:30 PM
*/

function datetimeToLocalString(locale: Locale, datetime: string, currentSelectedTimezone: SelectedTimezone = timezone.selected, includeTimeZone = false): string {
const date = getLocalDateFromDatetime(locale, datetime, currentSelectedTimezone);
const tz = includeTimeZone ? ' [UTC]Z' : '';

return `${format(date, CONST.DATE.LOCAL_TIME_FORMAT)}${tz}`;
}

/**
Expand Down Expand Up @@ -766,6 +780,18 @@ function getLastBusinessDayOfMonth(inputDate: Date): number {

return getDate(currentDate);
}
/**
* Formats datetime to selected format type
*
* eg.
*
* Dec 14, 2023
*/

function formatDate(datetime: Date, formatString: string = CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT) {
const date = new Date(datetime);
return `${format(date, formatString)}`;
MrRefactor marked this conversation as resolved.
Show resolved Hide resolved
}

const DateUtils = {
formatToDayOfWeek,
Expand All @@ -774,6 +800,7 @@ const DateUtils = {
getZoneAbbreviation,
datetimeToRelative,
datetimeToCalendarTime,
datetimeToLocalString,
startCurrentDateUpdater,
getLocalDateFromDatetime,
getCurrentTimezone,
Expand Down Expand Up @@ -813,6 +840,7 @@ const DateUtils = {
formatToSupportedTimezone,
enrichMoneyRequestTimestamp,
getLastBusinessDayOfMonth,
formatDate,
};

export default DateUtils;
5 changes: 5 additions & 0 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionItemThread from './ReportActionItemThread';
import reportActionPropTypes from './reportActionPropTypes';
import ReportAttachmentsContext from './ReportAttachmentsContext';
import ReportDateIndicator from './ReportDateIndicator';

const propTypes = {
...windowDimensionsPropTypes,
Expand Down Expand Up @@ -124,6 +125,9 @@ const propTypes = {

/** Callback to be called on onPress */
onPress: PropTypes.func,

/** Should we show the date indicator? */
showDateIndicator: PropTypes.bool.isRequired,
};

const defaultProps = {
Expand Down Expand Up @@ -717,6 +721,7 @@ function ReportActionItem(props) {
withoutFocusOnSecondaryInteraction
accessibilityLabel={props.translate('accessibilityHints.chatMessage')}
>
{props.showDateIndicator && <ReportDateIndicator created={props.action.created} />}
<Hoverable
shouldHandleScroll
isDisabled={!_.isUndefined(props.draftMessage)}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/report/ReportActionItemDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ type ReportActionItemDateProps = ReportActionItemDateOnyxProps & {
};

function ReportActionItemDate({created}: ReportActionItemDateProps) {
const {datetimeToCalendarTime} = useLocalize();
const {datetimeToLocalString} = useLocalize();
const styles = useThemeStyles();

return <Text style={[styles.chatItemMessageHeaderTimestamp]}>{datetimeToCalendarTime(created, false, false)}</Text>;
return <Text style={[styles.chatItemMessageHeaderTimestamp]}>{datetimeToLocalString(created, false)}</Text>;
}

ReportActionItemDate.displayName = 'ReportActionItemDate';
Expand Down
1 change: 1 addition & 0 deletions src/pages/home/report/ReportActionItemParentAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide
isMostRecentIOUReportAction={false}
shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker}
index={index}
showDateIndicator={false}
/>
{!ancestor.shouldHideThreadDividerLine && <View style={[styles.threadDividerLine]} />}
</OfflineWithFeedback>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/report/ReportActionItemThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ type ReportActionItemThreadProps = {
function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) {
const styles = useThemeStyles();

const {translate, datetimeToCalendarTime} = useLocalize();
const {translate, datetimeToLocalString} = useLocalize();

const numberOfRepliesText = numberOfReplies > CONST.MAX_THREAD_REPLIES_PREVIEW ? `${CONST.MAX_THREAD_REPLIES_PREVIEW}+` : `${numberOfReplies}`;
const replyText = numberOfReplies === 1 ? translate('threads.reply') : translate('threads.replies');

const timeStamp = datetimeToCalendarTime(mostRecentReply, false);
const timeStamp = datetimeToLocalString(mostRecentReply, false);

return (
<View style={[styles.chatItemMessage]}>
Expand Down
59 changes: 58 additions & 1 deletion src/pages/home/report/ReportActionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import FloatingMessageCounter from './FloatingMessageCounter';
import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader';
import reportActionPropTypes from './reportActionPropTypes';
import ReportActionsListItemRenderer from './ReportActionsListItemRenderer';
import ReportDateIndicator from './ReportDateIndicator';

const propTypes = {
/** The report currently being looked at */
Expand Down Expand Up @@ -159,6 +160,8 @@ function ReportActionsList({
const hasFooterRendered = useRef(false);
const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated);
const lastReadTimeRef = useRef(report.lastReadTime);
const [dateIndicatorLabel, setDateIndicatorLabel] = useState('');
const [visibleItemIndex, setVisibleItemIndex] = useState(0);

const sortedVisibleReportActions = useMemo(
() => _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors),
Expand All @@ -179,6 +182,49 @@ function ReportActionsList({
opacity: opacity.value,
}));

useEffect(() => {
if (visibleItemIndex === -1 || visibleItemIndex === sortedReportActions.length - 1) {
setDateIndicatorLabel('');
}
setDateIndicatorLabel(sortedReportActions[visibleItemIndex]);
}, [sortedReportActions, visibleItemIndex]);

/**
* Determines whether we should display the date indicator label in chat messages
* @return {Boolean}
*/
const shouldShowStaticDateIndicator = useCallback(
(index) => {
if (index === sortedReportActions.length - 1) {
return true;
}

const currentItem = sortedReportActions[index];
const nextItem = sortedReportActions[index + 1];

if (nextItem) {
return DateUtils.formatDate(currentItem.created) !== DateUtils.formatDate(nextItem.created);
}
},
[sortedReportActions],
);

const onViewableItemsChanged = useCallback(
({viewableItems}) => {
if (viewableItems.length <= 0) {
return null;
}

const firstVisibleItem = viewableItems[viewableItems.length - 1];
const {index, isViewable} = firstVisibleItem;
MrRefactor marked this conversation as resolved.
Show resolved Hide resolved

if (isViewable) {
setVisibleItemIndex(index);
}
},
[setVisibleItemIndex],
);

useEffect(() => {
opacity.value = withTiming(1, {duration: 100});
}, [opacity]);
Expand Down Expand Up @@ -419,9 +465,10 @@ function ReportActionsList({
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
shouldHideThreadDividerLine={shouldHideThreadDividerLine}
shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)}
showDateIndicator={shouldShowStaticDateIndicator(index)}
/>
),
[report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker],
[report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldShowStaticDateIndicator, shouldDisplayNewMarker],
);

// Native mobile does not render updates flatlist the changes even though component did update called.
Expand Down Expand Up @@ -483,6 +530,12 @@ function ReportActionsList({
isActive={isFloatingMessageCounterVisible && !!currentUnreadMarker}
onClick={scrollToBottomAndMarkReportAsRead}
/>
{dateIndicatorLabel ? (
<ReportDateIndicator
created={dateIndicatorLabel.created}
style={[styles.pAbsolute, styles.t0, styles.l0, styles.r0, styles.pt1, styles.chatItemDateIndicatorWrapper]}
/>
) : null}
<Animated.View style={[animatedStyles, styles.flex1, !shouldShowReportRecipientLocalTime && !hideComposer ? styles.pb4 : {}]}>
<InvertedFlatList
accessibilityLabel={translate('sidebarScreen.listOfChatMessages')}
Expand All @@ -505,6 +558,10 @@ function ReportActionsList({
onScroll={trackVerticalScrolling}
onScrollToIndexFailed={() => {}}
extraData={extraData}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={{
itemVisiblePercentThreshold: 100,
}}
/>
</Animated.View>
</>
Expand Down
5 changes: 5 additions & 0 deletions src/pages/home/report/ReportActionsListItemRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const propTypes = {

/** Linked report action ID */
linkedReportActionID: PropTypes.string,

/** should we display? */
showDateIndicator: PropTypes.bool.isRequired,
};

const defaultProps = {
Expand All @@ -49,6 +52,7 @@ function ReportActionsListItemRenderer({
shouldHideThreadDividerLine,
shouldDisplayNewMarker,
linkedReportActionID,
showDateIndicator,
}) {
const shouldDisplayParentAction =
reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED &&
Expand Down Expand Up @@ -143,6 +147,7 @@ function ReportActionsListItemRenderer({
}
isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID}
index={index}
showDateIndicator={showDateIndicator}
/>
);
}
Expand Down
Loading
Loading