Timeline date headers
This commit is contained in:
parent
0fa069f260
commit
f9440bf594
41 changed files with 1183 additions and 771 deletions
|
@ -1,7 +1,15 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const ONE_DAY = 24 * 3600 * 1000;
|
||||
import type { Moment } from 'moment';
|
||||
import moment from 'moment';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import * as log from '../logging/log';
|
||||
import { DAY, HOUR, MINUTE, MONTH, WEEK } from './durations';
|
||||
|
||||
const MAX_FORMAT_STRING_LENGTH = 50;
|
||||
|
||||
type RawTimestamp = Readonly<number | Date | Moment>;
|
||||
|
||||
export function isMoreRecentThan(timestamp: number, delta: number): boolean {
|
||||
return timestamp > Date.now() - delta;
|
||||
|
@ -20,5 +28,124 @@ export function isInFuture(timestamp: number): boolean {
|
|||
}
|
||||
|
||||
export function toDayMillis(timestamp: number): number {
|
||||
return timestamp - (timestamp % ONE_DAY);
|
||||
return timestamp - (timestamp % DAY);
|
||||
}
|
||||
|
||||
const isSameDay = (a: RawTimestamp, b: RawTimestamp): boolean =>
|
||||
moment(a).isSame(b, 'day');
|
||||
|
||||
const isToday = (rawTimestamp: RawTimestamp): boolean =>
|
||||
isSameDay(rawTimestamp, Date.now());
|
||||
|
||||
const isYesterday = (rawTimestamp: RawTimestamp): boolean =>
|
||||
isSameDay(rawTimestamp, moment().subtract(1, 'day'));
|
||||
|
||||
// This sanitization is probably unnecessary, but we do it just in case someone translates
|
||||
// a super long format string and causes performance issues.
|
||||
function sanitizeFormatString(
|
||||
rawFormatString: string,
|
||||
fallback: string
|
||||
): string {
|
||||
if (rawFormatString.length > MAX_FORMAT_STRING_LENGTH) {
|
||||
log.error(
|
||||
`Format string ${JSON.stringify(
|
||||
rawFormatString
|
||||
)} is too long. Falling back to ${fallback}`
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
return rawFormatString;
|
||||
}
|
||||
|
||||
export function formatDateTimeShort(
|
||||
i18n: LocalizerType,
|
||||
rawTimestamp: RawTimestamp
|
||||
): string {
|
||||
const timestamp = rawTimestamp.valueOf();
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
|
||||
if (diff < MINUTE) {
|
||||
return i18n('justNow');
|
||||
}
|
||||
|
||||
if (diff < HOUR) {
|
||||
return i18n('minutesAgo', [Math.floor(diff / MINUTE).toString()]);
|
||||
}
|
||||
|
||||
const m = moment(timestamp);
|
||||
|
||||
if (isToday(timestamp)) {
|
||||
return m.format('LT');
|
||||
}
|
||||
|
||||
if (diff < WEEK && m.isSame(now, 'month')) {
|
||||
return m.format('ddd');
|
||||
}
|
||||
|
||||
if (m.isSame(now, 'year')) {
|
||||
return m.format(i18n('timestampFormat_M') || 'MMM D');
|
||||
}
|
||||
|
||||
return m.format('ll');
|
||||
}
|
||||
|
||||
export function formatDateTimeLong(
|
||||
i18n: LocalizerType,
|
||||
rawTimestamp: RawTimestamp
|
||||
): string {
|
||||
let rawFormatString: string;
|
||||
if (isToday(rawTimestamp)) {
|
||||
rawFormatString = i18n('timestampFormat__long__today');
|
||||
} else if (isYesterday(rawTimestamp)) {
|
||||
rawFormatString = i18n('timestampFormat__long__yesterday');
|
||||
} else {
|
||||
rawFormatString = 'lll';
|
||||
}
|
||||
const formatString = sanitizeFormatString(rawFormatString, 'lll');
|
||||
|
||||
return moment(rawTimestamp).format(formatString);
|
||||
}
|
||||
|
||||
export function formatTime(
|
||||
i18n: LocalizerType,
|
||||
rawTimestamp: RawTimestamp
|
||||
): string {
|
||||
const timestamp = rawTimestamp.valueOf();
|
||||
const diff = Date.now() - timestamp;
|
||||
|
||||
if (diff < MINUTE) {
|
||||
return i18n('justNow');
|
||||
}
|
||||
|
||||
if (diff < HOUR) {
|
||||
return i18n('minutesAgo', [Math.floor(diff / MINUTE).toString()]);
|
||||
}
|
||||
|
||||
return moment(timestamp).format('LT');
|
||||
}
|
||||
|
||||
export function formatDate(
|
||||
i18n: LocalizerType,
|
||||
rawTimestamp: RawTimestamp
|
||||
): string {
|
||||
if (isToday(rawTimestamp)) {
|
||||
return i18n('today');
|
||||
}
|
||||
|
||||
if (isYesterday(rawTimestamp)) {
|
||||
return i18n('yesterday');
|
||||
}
|
||||
|
||||
const m = moment(rawTimestamp);
|
||||
|
||||
const formatI18nKey =
|
||||
Math.abs(m.diff(Date.now())) < 6 * MONTH
|
||||
? 'TimelineDateHeader--date-in-last-6-months'
|
||||
: 'TimelineDateHeader--date-older-than-6-months';
|
||||
const rawFormatString = i18n(formatI18nKey);
|
||||
const formatString = sanitizeFormatString(rawFormatString, 'LL');
|
||||
|
||||
return m.format(formatString);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue