diff --git a/app/components/post_list/date_separator/index.tsx b/app/components/post_list/date_separator/index.tsx index 0b1ca78300..0fa3863138 100644 --- a/app/components/post_list/date_separator/index.tsx +++ b/app/components/post_list/date_separator/index.tsx @@ -2,12 +2,14 @@ // See LICENSE.txt for license information. import React from 'react'; -import {type StyleProp, type TextStyle, View, type ViewStyle} from 'react-native'; +import {useIntl} from 'react-intl'; +import {type StyleProp, Text, type TextStyle, View, type ViewStyle} from 'react-native'; import FormattedDate, {type FormattedDateFormat} from '@components/formatted_date'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; import {isSameYear, isToday, isYesterday} from '@utils/datetime'; +import {logDebug} from '@utils/log'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -38,13 +40,62 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const DATE_FORMATS = { - withinYear: {month: 'short', day: 'numeric'}, - afterYear: {dateStyle: 'medium'}, -} satisfies Record; +const AFTER_YEAR_FORMAT: FormattedDateFormat = {dateStyle: 'medium'}; + +/** + * Formats a date without the year component using dateStyle:'medium' and formatToParts. + * This avoids a Hermes bug where {month:'short', day:'numeric'} may omit the day + * on certain Android versions. + */ +function formatDateWithoutYear(locale: string, date: Date, timezone?: string | null): string | null { + try { + let timeZone: string | undefined; + if (timezone && typeof timezone === 'string') { + timeZone = timezone; + } + + const parts = [...new Intl.DateTimeFormat(locale, { + dateStyle: 'medium', + ...(timeZone ? {timeZone} : {}), + }).formatToParts(date)]; + + const yearIdx = parts.findIndex((p) => p.type === 'year'); + if (yearIdx < 0) { + return parts.map((p) => p.value).join(''); + } + + if (yearIdx === 0) { + // Year at start (e.g. ja, zh, ko, hu): remove year + following literal + const removeCount = parts[1]?.type === 'literal' ? 2 : 1; + parts.splice(0, removeCount); + } else if (yearIdx === parts.length - 1) { + // Year at end (e.g. en, de, fr): remove preceding literal + year + const start = parts[yearIdx - 1]?.type === 'literal' ? yearIdx - 1 : yearIdx; + parts.splice(start, parts.length - start); + } else { + // Year in middle (e.g. ru, uk, bg with trailing suffix): remove adjacent literals + year + let start = yearIdx; + let count = 1; + if (parts[yearIdx - 1]?.type === 'literal') { + start--; + count++; + } + if (parts[yearIdx + 1]?.type === 'literal') { + count++; + } + parts.splice(start, count); + } + + return parts.map((p) => p.value).join(''); + } catch (error) { + logDebug('Failed to format date without year', error); + return null; + } +} const RecentDate = (props: DateSeparatorProps) => { - const {date, ...otherProps} = props; + const {date, timezone, ...otherProps} = props; + const {locale} = useIntl(); const when = new Date(date); if (isToday(when)) { @@ -65,12 +116,18 @@ const RecentDate = (props: DateSeparatorProps) => { ); } - const format: FormattedDateFormat = isSameYear(when, new Date()) ? DATE_FORMATS.withinYear : DATE_FORMATS.afterYear; + if (isSameYear(when, new Date())) { + const formatted = formatDateWithoutYear(locale, when, timezone); + if (formatted) { + return {formatted}; + } + } return ( );