feat(message-parser): add timestamps pattern (#31810)
Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com>pull/31558/head
parent
b876e4e0fc
commit
5ad65ff3da
@ -0,0 +1,24 @@ |
||||
--- |
||||
"@rocket.chat/message-parser": patch |
||||
--- |
||||
|
||||
feat(message-parser): add timestamps pattern |
||||
|
||||
### Usage |
||||
|
||||
Pattern: <t:{timestamp}:?{format}> |
||||
|
||||
- {timestamp} is a Unix timestamp |
||||
- {format} is an optional parameter that can be used to customize the date and time format. |
||||
|
||||
#### Formats |
||||
|
||||
| Format | Description | Example | |
||||
| ------ | ------------------------- | --------------------------------------- | |
||||
| `t` | Short time | 12:00 AM | |
||||
| `T` | Long time | 12:00:00 AM | |
||||
| `d` | Short date | 12/31/2020 | |
||||
| `D` | Long date | Thursday, December 31, 2020 | |
||||
| `f` | Full date and time | Thursday, December 31, 2020 12:00 AM | |
||||
| `F` | Full date and time (long) | Thursday, December 31, 2020 12:00:00 AM | |
||||
| `R` | Relative time | 1 year ago | |
||||
@ -0,0 +1,21 @@ |
||||
import { Component } from 'react'; |
||||
|
||||
export class ErrorBoundary extends Component<{ fallback: React.ReactNode }, { hasError: boolean }> { |
||||
constructor(props: { fallback: React.ReactNode }) { |
||||
super(props); |
||||
this.state = { hasError: false }; |
||||
} |
||||
|
||||
static getDerivedStateFromError() { |
||||
return { hasError: true }; |
||||
} |
||||
|
||||
render() { |
||||
if (this.state.hasError) { |
||||
// You can render any custom fallback UI
|
||||
return this.props.fallback; |
||||
} |
||||
|
||||
return this.props.children; |
||||
} |
||||
} |
||||
@ -0,0 +1,128 @@ |
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
||||
import { Tag } from '@rocket.chat/fuselage'; |
||||
import type * as MessageParser from '@rocket.chat/message-parser'; |
||||
import { format } from 'date-fns'; |
||||
import { useContext, useEffect, useState, type ReactElement } from 'react'; |
||||
import { ErrorBoundary } from 'react-error-boundary'; |
||||
|
||||
import { MarkupInteractionContext } from '../../MarkupInteractionContext'; |
||||
import { timeAgo } from './timeago'; |
||||
|
||||
type BoldSpanProps = { |
||||
children: MessageParser.Timestamp; |
||||
}; |
||||
|
||||
// | `f` | Full date and time | Thursday, December 31, 2020 12:00 AM |
|
||||
// | `F` | Full date and time (long) | Thursday, December 31, 2020 12:00:00 AM |
|
||||
// | `R` | Relative time | 1 year ago |
|
||||
|
||||
const Timestamp = ({ children }: BoldSpanProps): ReactElement => { |
||||
const { enableTimestamp } = useContext(MarkupInteractionContext); |
||||
|
||||
if (!enableTimestamp) { |
||||
return <>{`<t:${children.value.timestamp}:${children.value.format}>`}</>; |
||||
} |
||||
|
||||
switch (children.value.format) { |
||||
case 't': // Short time format
|
||||
return <ShortTime value={parseInt(children.value.timestamp)} />; |
||||
case 'T': // Long time format
|
||||
return <LongTime value={parseInt(children.value.timestamp)} />; |
||||
case 'd': // Short date format
|
||||
return <ShortDate value={parseInt(children.value.timestamp)} />; |
||||
case 'D': // Long date format
|
||||
return <LongDate value={parseInt(children.value.timestamp)} />; |
||||
case 'f': // Full date and time format
|
||||
return <FullDate value={parseInt(children.value.timestamp)} />; |
||||
|
||||
case 'F': // Full date and time (long) format
|
||||
return <FullDateLong value={parseInt(children.value.timestamp)} />; |
||||
|
||||
case 'R': // Relative time format
|
||||
return ( |
||||
<ErrorBoundary fallback={<>{new Date().toUTCString()}</>}> |
||||
<RelativeTime key={children.value.timestamp} value={parseInt(children.value.timestamp)} /> |
||||
</ErrorBoundary> |
||||
); |
||||
|
||||
default: |
||||
return <time dateTime={children.value.timestamp}> {JSON.stringify(children.value.timestamp)}</time>; |
||||
} |
||||
}; |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const ShortTime = ({ value }: { value: number }) => <Time value={format(new Date(value), 'p')} dateTime={new Date(value).toISOString()} />; |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const LongTime = ({ value }: { value: number }) => <Time value={format(new Date(value), 'pp')} dateTime={new Date(value).toISOString()} />; |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const ShortDate = ({ value }: { value: number }) => <Time value={format(new Date(value), 'P')} dateTime={new Date(value).toISOString()} />; |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const LongDate = ({ value }: { value: number }) => <Time value={format(new Date(value), 'Pp')} dateTime={new Date(value).toISOString()} />; |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const FullDate = ({ value }: { value: number }) => ( |
||||
<Time value={format(new Date(value), 'PPPppp')} dateTime={new Date(value).toISOString()} /> |
||||
); |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const FullDateLong = ({ value }: { value: number }) => ( |
||||
<Time value={format(new Date(value), 'PPPPpppp')} dateTime={new Date(value).toISOString()} /> |
||||
); |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const Time = ({ value, dateTime }: { value: string; dateTime: string }) => ( |
||||
<time |
||||
title={new Date(dateTime).toLocaleString()} |
||||
dateTime={dateTime} |
||||
style={{ |
||||
display: 'inline-block', |
||||
}} |
||||
> |
||||
<Tag> {value}</Tag> |
||||
</time> |
||||
); |
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const RelativeTime = ({ value }: { value: number }) => { |
||||
const { language } = useContext(MarkupInteractionContext); |
||||
const [time, setTime] = useState(() => timeAgo(value, language ?? 'en')); |
||||
const [timeToRefresh, setTimeToRefresh] = useState(() => getTimeToRefresh(value)); |
||||
|
||||
useEffect(() => { |
||||
const interval = setInterval(() => { |
||||
setTime(timeAgo(value, 'en')); |
||||
setTimeToRefresh(getTimeToRefresh(value)); |
||||
}, timeToRefresh); |
||||
|
||||
return () => clearInterval(interval); |
||||
}, [value, timeToRefresh]); |
||||
|
||||
return <Time value={time} dateTime={new Date(value).toISOString()} />; |
||||
}; |
||||
|
||||
const getTimeToRefresh = (time: number): number => { |
||||
const timeToRefresh = time - Date.now(); |
||||
|
||||
// less than 1 minute
|
||||
if (timeToRefresh < 60000) { |
||||
return 1000; |
||||
} |
||||
|
||||
// if the difference is in the minutes range, we should refresh the time in 1 minute / 2
|
||||
if (timeToRefresh < 3600000) { |
||||
return 60000 / 2; |
||||
} |
||||
|
||||
// if the difference is in the hours range, we should refresh the time in 5 minutes
|
||||
if (timeToRefresh < 86400000) { |
||||
return 60000 * 5; |
||||
} |
||||
|
||||
// refresh the time in 1 hour
|
||||
return 3600000; |
||||
}; |
||||
|
||||
export default Timestamp; |
||||
@ -0,0 +1,41 @@ |
||||
export function timeAgo(dateParam: number, locale: string): string { |
||||
const int = new Intl.RelativeTimeFormat(locale, { style: 'long' }); |
||||
|
||||
const date = new Date(dateParam).getTime(); |
||||
const today = new Date().getTime(); |
||||
const seconds = Math.round((date - today) / 1000); |
||||
const minutes = Math.round(seconds / 60); |
||||
const hours = Math.round(minutes / 60); |
||||
const days = Math.round(hours / 24); |
||||
const weeks = Math.round(days / 7); |
||||
|
||||
const months = new Date(date).getMonth() - new Date().getMonth(); |
||||
|
||||
const years = new Date(date).getFullYear() - new Date().getFullYear(); |
||||
|
||||
if (Math.abs(seconds) < 60) { |
||||
return int.format(seconds, 'seconds'); |
||||
} |
||||
|
||||
if (Math.abs(minutes) < 60) { |
||||
return int.format(minutes, 'minutes'); |
||||
} |
||||
|
||||
if (Math.abs(hours) < 24) { |
||||
return int.format(hours, 'hours'); |
||||
} |
||||
|
||||
if (Math.abs(days) < 7) { |
||||
return int.format(days, 'days'); |
||||
} |
||||
|
||||
if (Math.abs(weeks) < 4) { |
||||
return int.format(weeks, 'weeks'); |
||||
} |
||||
|
||||
if (Math.abs(months) < 12) { |
||||
return int.format(months, 'months'); |
||||
} |
||||
|
||||
return int.format(years, 'years'); |
||||
} |
||||
@ -0,0 +1,27 @@ |
||||
import { parse } from '../src'; |
||||
import { bold, paragraph, plain, strike, timestamp } from '../src/utils'; |
||||
|
||||
test.each([ |
||||
[`<t:1708551317>`, [paragraph([timestamp('1708551317')])]], |
||||
[`<t:1708551317:R>`, [paragraph([timestamp('1708551317', 'R')])]], |
||||
[ |
||||
'hello <t:1708551317>', |
||||
[paragraph([plain('hello '), timestamp('1708551317')])], |
||||
], |
||||
])('parses %p', (input, output) => { |
||||
expect(parse(input)).toMatchObject(output); |
||||
}); |
||||
|
||||
test.each([ |
||||
['<t:1708551317:I>', [paragraph([plain('<t:1708551317:I>')])]], |
||||
['<t:17>', [paragraph([plain('<t:17>')])]], |
||||
])('parses %p', (input, output) => { |
||||
expect(parse(input)).toMatchObject(output); |
||||
}); |
||||
|
||||
test.each([ |
||||
['~<t:1708551317>~', [paragraph([strike([timestamp('1708551317')])])]], |
||||
['*<t:1708551317>*', [paragraph([bold([plain('<t:1708551317>')])])]], |
||||
])('parses %p', (input, output) => { |
||||
expect(parse(input)).toMatchObject(output); |
||||
}); |
||||
Loading…
Reference in new issue