Skip to content
Open
Changes from all commits
Commits
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
73 changes: 65 additions & 8 deletions app/components/post_list/date_separator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -38,13 +40,62 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});

const DATE_FORMATS = {
withinYear: {month: 'short', day: 'numeric'},
afterYear: {dateStyle: 'medium'},
} satisfies Record<string, FormattedDateFormat>;
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)) {
Expand All @@ -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 <Text {...otherProps}>{formatted}</Text>;
}
}

return (
<FormattedDate
{...otherProps}
format={format}
format={AFTER_YEAR_FORMAT}
timezone={timezone}
value={date}
/>
);
Expand Down