addons/isl/src/relativeDate.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {getCurrentLanguage, useCurrentLang} from './i18n';
b69ab319
b69ab3110/**
b69ab3111 * Originally adapted from https://github.com/azer/relative-date.
b69ab3112 */
b69ab3113const SECOND = 1000;
b69ab3114const MINUTE = SECOND * 60;
b69ab3115const HOUR = MINUTE * 60;
b69ab3116const DAY = HOUR * 24;
b69ab3117const WEEK = DAY * 7;
b69ab3118const YEAR = DAY * 365;
b69ab3119const MONTH = YEAR / 12;
b69ab3120
b69ab3121type NumberFormat = [ms: number, name: string] | [ms: number, name: string, relative: number];
b69ab3122
b69ab3123const shortFormats: Array<NumberFormat> = [
b69ab3124 [MINUTE * 0.7, 'now'],
b69ab3125 [MINUTE * 1.5, '1m'],
b69ab3126 [MINUTE * 60, 'm', MINUTE],
b69ab3127 [HOUR * 1.5, '1h'],
b69ab3128 [DAY, 'h', HOUR],
b69ab3129 [DAY * 2, '1d'],
b69ab3130 [DAY * 7, 'd', DAY],
b69ab3131 [WEEK * 1.5, '1w'],
b69ab3132 [MONTH, 'w', WEEK],
b69ab3133 [MONTH * 1.5, '1mo'],
b69ab3134 [YEAR, 'mo', MONTH],
b69ab3135 [YEAR * 1.5, '1y'],
b69ab3136 [Number.MAX_VALUE, 'y', YEAR],
b69ab3137];
b69ab3138
b69ab3139const longFormatsRelative: Array<NumberFormat> = [
b69ab3140 [MINUTE * 0.7, 'less than a minute'],
b69ab3141 [MINUTE * 1.5, 'one minute'],
b69ab3142 [MINUTE * 60, 'minutes', MINUTE],
b69ab3143 [HOUR * 1.5, 'one hour'],
b69ab3144 [DAY, 'hours', HOUR],
b69ab3145 [DAY * 2, 'one day'],
b69ab3146 [DAY * 7, 'days', DAY],
b69ab3147 [WEEK * 1.5, 'one week'],
b69ab3148 [MONTH, 'weeks', WEEK],
b69ab3149 [MONTH * 1.5, 'one month'],
b69ab3150 [YEAR, 'months', MONTH],
b69ab3151 [YEAR * 1.5, 'one year'],
b69ab3152 [Number.MAX_VALUE, 'years', YEAR],
b69ab3153];
b69ab3154
b69ab3155const longFormats: Array<NumberFormat> = [
b69ab3156 [MINUTE * 0.7, 'just now'],
b69ab3157 [MINUTE * 1.5, 'a minute ago'],
b69ab3158 [MINUTE * 60, 'minutes ago', MINUTE],
b69ab3159 [HOUR * 1.5, 'an hour ago'],
b69ab3160 [DAY, 'hours ago', HOUR],
b69ab3161 [DAY * 2, 'yesterday'],
b69ab3162 [DAY * 7, 'days ago', DAY],
b69ab3163 [WEEK * 1.5, 'a week ago'],
b69ab3164 [MONTH, 'weeks ago', WEEK],
b69ab3165 [MONTH * 1.5, 'a month ago'],
b69ab3166 [YEAR, 'months ago', MONTH],
b69ab3167 [YEAR * 1.5, 'a year ago'],
b69ab3168 [Number.MAX_VALUE, 'years ago', YEAR],
b69ab3169];
b69ab3170
b69ab3171const longFormatsNumbers: Array<NumberFormat> = [
b69ab3172 [MINUTE * 0.7, 'just now'],
b69ab3173 [MINUTE * 1.5, '1 minute ago'],
b69ab3174 [MINUTE * 60, 'minutes ago', MINUTE],
b69ab3175 [HOUR * 1.5, '1 hour ago'],
b69ab3176 [DAY, 'hours ago', HOUR],
b69ab3177 [DAY * 2, 'yesterday'],
b69ab3178 [DAY * 7, 'days ago', DAY],
b69ab3179 [WEEK * 1.5, '1 week ago'],
b69ab3180 [MONTH, 'weeks ago', WEEK],
b69ab3181 [MONTH * 1.5, '1 month ago'],
b69ab3182 [YEAR, 'months ago', MONTH],
b69ab3183 [YEAR * 1.5, '1 year ago'],
b69ab3184 [Number.MAX_VALUE, 'years ago', YEAR],
b69ab3185];
b69ab3186
b69ab3187const units = {
b69ab3188 year: 24 * 60 * 60 * 1000 * 365,
b69ab3189 month: (24 * 60 * 60 * 1000 * 365) / 12,
b69ab3190 day: 24 * 60 * 60 * 1000,
b69ab3191 hour: 60 * 60 * 1000,
b69ab3192 minute: 60 * 1000,
b69ab3193};
b69ab3194
b69ab3195/**
b69ab3196 * Format date into relative string format.
b69ab3197 * If currentLanguage is 'en', uses hard-coded time abbreviations for maximum shortness.
b69ab3198 * Other languages use currently Intl.RelativeTimeFormat if available.
b69ab3199 * if currentLanguage is 'en':
b69ab31100 * ```
b69ab31101 * relativeDate(new Date()) -> 'just now'
b69ab31102 * relativeDate(new Date() - 120000) -> '2m ago'
b69ab31103 * ```
b69ab31104 * if currentLanguage is 'de':
b69ab31105 * ```
b69ab31106 * relativeDate(new Date()) -> 'just now'
b69ab31107 * relativeDate(new Date() - 60000) -> 'now'
b69ab31108 * ```
b69ab31109 */
b69ab31110export function relativeDate(
b69ab31111 input_: number | Date,
b69ab31112 options: {
b69ab31113 reference?: number | Date;
b69ab31114 useShortVariant?: boolean;
b69ab31115 useNumbersOnly?: boolean;
b69ab31116 useRelativeForm?: boolean;
b69ab31117 },
b69ab31118): string {
b69ab31119 let input = input_;
b69ab31120 let reference = options.reference;
b69ab31121 if (input instanceof Date) {
b69ab31122 input = input.getTime();
b69ab31123 }
b69ab31124 if (!reference) {
b69ab31125 reference = now();
b69ab31126 }
b69ab31127 if (reference instanceof Date) {
b69ab31128 reference = reference.getTime();
b69ab31129 }
b69ab31130
b69ab31131 const delta = reference - input;
b69ab31132 const absDelta = Math.abs(delta);
b69ab31133
b69ab31134 // Use Intl.RelativeTimeFormat for non-en locales, if available.
b69ab31135 if (getCurrentLanguage() != 'en' && typeof Intl !== 'undefined') {
b69ab31136 for (const unit of Object.keys(units) as Array<keyof typeof units>) {
b69ab31137 if (absDelta > units[unit] || unit == 'minute') {
b69ab31138 return new Intl.RelativeTimeFormat(getCurrentLanguage(), {
b69ab31139 style: options.useShortVariant ? 'narrow' : 'short',
b69ab31140 numeric: 'auto',
b69ab31141 }).format(-Math.round(delta / units[unit]), unit);
b69ab31142 }
b69ab31143 }
b69ab31144 }
b69ab31145
b69ab31146 const formats = options.useRelativeForm
b69ab31147 ? longFormatsRelative
b69ab31148 : options.useShortVariant
b69ab31149 ? shortFormats
b69ab31150 : options.useNumbersOnly
b69ab31151 ? longFormatsNumbers
b69ab31152 : longFormats;
b69ab31153 for (const [limit, relativeFormat, remainder] of formats) {
b69ab31154 if (absDelta < limit) {
b69ab31155 if (typeof remainder === 'number') {
b69ab31156 return (
b69ab31157 Math.round(delta / remainder) + (options.useShortVariant ? '' : ' ') + relativeFormat
b69ab31158 );
b69ab31159 } else {
b69ab31160 return relativeFormat;
b69ab31161 }
b69ab31162 }
b69ab31163 }
b69ab31164
b69ab31165 throw new Error('This should never be reached.');
b69ab31166}
b69ab31167
b69ab31168/**
b69ab31169 * React component version of {@link relativeDate}.
b69ab31170 * Re-renders if the current language changes.
b69ab31171 */
b69ab31172export function RelativeDate({
b69ab31173 date,
b69ab31174 reference,
b69ab31175 useShortVariant,
b69ab31176 useNumbersOnly,
b69ab31177 useRelativeForm,
b69ab31178}: {
b69ab31179 date: number | Date;
b69ab31180 reference?: number | Date;
b69ab31181 useShortVariant?: boolean;
b69ab31182 useNumbersOnly?: boolean;
b69ab31183 useRelativeForm?: boolean;
b69ab31184}) {
b69ab31185 useCurrentLang();
b69ab31186 return <>{relativeDate(date, {reference, useShortVariant, useNumbersOnly, useRelativeForm})}</>;
b69ab31187}
b69ab31188
b69ab31189/** Get "now" for relativeDate use-case. Can be overridden by "?now=unixtime" in ISL browser environments. */
b69ab31190function now(): number {
b69ab31191 const forceNowStr =
b69ab31192 typeof window === 'undefined'
b69ab31193 ? undefined
b69ab31194 : (window as {relativeDateNowOverride?: number}).relativeDateNowOverride;
b69ab31195 return forceNowStr == null ? Date.now() : forceNowStr;
b69ab31196}