@linktr.ee/messaging-react 3.3.4 → 3.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-B7AF5uOB.js → Card-B9QrjooN.js} +3 -3
- package/dist/{Card-B7AF5uOB.js.map → Card-B9QrjooN.js.map} +1 -1
- package/dist/{Card-0BgubwgM.cjs → Card-BRRlz4kq.cjs} +2 -2
- package/dist/{Card-0BgubwgM.cjs.map → Card-BRRlz4kq.cjs.map} +1 -1
- package/dist/{Card-DLmUSU4A.cjs → Card-C-FCwjGa.cjs} +2 -2
- package/dist/{Card-DLmUSU4A.cjs.map → Card-C-FCwjGa.cjs.map} +1 -1
- package/dist/{Card-DchJqvYq.js → Card-CVZzYmYW.js} +2 -2
- package/dist/{Card-DchJqvYq.js.map → Card-CVZzYmYW.js.map} +1 -1
- package/dist/{Card-CvBbAoUo.cjs → Card-D_oLlfPw.cjs} +2 -2
- package/dist/{Card-CvBbAoUo.cjs.map → Card-D_oLlfPw.cjs.map} +1 -1
- package/dist/{Card-DmPpcrSU.js → Card-DzjYyrie.js} +2 -2
- package/dist/{Card-DmPpcrSU.js.map → Card-DzjYyrie.js.map} +1 -1
- package/dist/{LockedThumbnail-BQjA4HaB.js → LockedThumbnail-CJfXY_Ut.js} +2 -2
- package/dist/{LockedThumbnail-BQjA4HaB.js.map → LockedThumbnail-CJfXY_Ut.js.map} +1 -1
- package/dist/{LockedThumbnail-D9fSb4N-.cjs → LockedThumbnail-Cth1yWnH.cjs} +2 -2
- package/dist/{LockedThumbnail-D9fSb4N-.cjs.map → LockedThumbnail-Cth1yWnH.cjs.map} +1 -1
- package/dist/index-CBtOPvxW.cjs +2 -0
- package/dist/index-CBtOPvxW.cjs.map +1 -0
- package/dist/{index-BcHUpyyw.js → index-D7eRkXoG.js} +535 -537
- package/dist/index-D7eRkXoG.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/AttachmentCard/AttachmentCard.stories.tsx +104 -0
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +33 -8
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +55 -47
- package/src/components/ChannelView.stories.tsx +8 -7
- package/src/components/CloseButton/CloseButton.stories.tsx +31 -0
- package/src/components/CustomDateSeparator/CustomDateSeparator.stories.tsx +33 -0
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.stories.tsx +63 -0
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +57 -0
- package/src/components/CustomLinkPreviewList/index.tsx +2 -54
- package/src/components/CustomMessage/CustomMessage.stories.tsx +3 -2
- package/src/components/CustomMessage/MessageTag.stories.tsx +4 -2
- package/src/components/MediaMessage/MediaMessage.stories.tsx +4 -2
- package/src/hooks/useChannelModerationActions.ts +32 -14
- package/src/stories/decorators/storyTime.ts +31 -0
- package/dist/index-BcHUpyyw.js.map +0 -1
- package/dist/index-DTZNltUC.cjs +0 -2
- package/dist/index-DTZNltUC.cjs.map +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { daysAgo, hoursAgo, now } from '../../stories/decorators/storyTime'
|
|
5
|
+
|
|
6
|
+
import { CustomDateSeparator } from '.'
|
|
7
|
+
|
|
8
|
+
type ComponentProps = React.ComponentProps<typeof CustomDateSeparator>
|
|
9
|
+
|
|
10
|
+
const meta: Meta<ComponentProps> = {
|
|
11
|
+
title: 'CustomDateSeparator',
|
|
12
|
+
component: CustomDateSeparator,
|
|
13
|
+
parameters: { layout: 'fullscreen' },
|
|
14
|
+
}
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
const Template: StoryFn<ComponentProps> = (args) => (
|
|
18
|
+
<div className="w-[640px] bg-background-primary p-6">
|
|
19
|
+
<CustomDateSeparator {...args} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const Today: StoryFn<ComponentProps> = Template.bind({})
|
|
24
|
+
Today.args = { date: hoursAgo(2) }
|
|
25
|
+
|
|
26
|
+
export const Yesterday: StoryFn<ComponentProps> = Template.bind({})
|
|
27
|
+
Yesterday.args = { date: hoursAgo(30) }
|
|
28
|
+
|
|
29
|
+
export const LastWeek: StoryFn<ComponentProps> = Template.bind({})
|
|
30
|
+
LastWeek.args = { date: daysAgo(6) }
|
|
31
|
+
|
|
32
|
+
export const Unread: StoryFn<ComponentProps> = Template.bind({})
|
|
33
|
+
Unread.args = { date: now(), unread: true }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LinkPreview, LinkPreviewStatus } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import CustomLinkPreviewCard from './CustomLinkPreviewCard'
|
|
6
|
+
|
|
7
|
+
type ComponentProps = React.ComponentProps<typeof CustomLinkPreviewCard>
|
|
8
|
+
|
|
9
|
+
const meta: Meta<ComponentProps> = {
|
|
10
|
+
title: 'CustomLinkPreviewCard',
|
|
11
|
+
component: CustomLinkPreviewCard,
|
|
12
|
+
parameters: { layout: 'centered' },
|
|
13
|
+
argTypes: {
|
|
14
|
+
onDismiss: { action: 'dismissed' },
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
export default meta
|
|
18
|
+
|
|
19
|
+
const makePreview = (overrides: Partial<LinkPreview>): LinkPreview => ({
|
|
20
|
+
og_scrape_url: 'https://example.com',
|
|
21
|
+
status: LinkPreviewStatus.LOADED,
|
|
22
|
+
...overrides,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const Template: StoryFn<ComponentProps> = (args) => (
|
|
26
|
+
<div className="p-12">
|
|
27
|
+
<CustomLinkPreviewCard {...args} />
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export const WithTitleAndImage: StoryFn<ComponentProps> = Template.bind({})
|
|
32
|
+
WithTitleAndImage.args = {
|
|
33
|
+
link: makePreview({
|
|
34
|
+
og_scrape_url: 'https://linktr.ee/example',
|
|
35
|
+
title: 'Linktree — the link in your bio',
|
|
36
|
+
image_url: '/image-thumbnail.jpg',
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const TitleOnly: StoryFn<ComponentProps> = Template.bind({})
|
|
41
|
+
TitleOnly.args = {
|
|
42
|
+
link: makePreview({
|
|
43
|
+
og_scrape_url: 'https://example.com/blog/post',
|
|
44
|
+
title: 'A long article title that explains why widgets are important',
|
|
45
|
+
}),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const UrlOnly: StoryFn<ComponentProps> = Template.bind({})
|
|
49
|
+
UrlOnly.args = {
|
|
50
|
+
link: makePreview({
|
|
51
|
+
og_scrape_url: 'https://example.com/raw',
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const VeryLongUrl: StoryFn<ComponentProps> = Template.bind({})
|
|
56
|
+
VeryLongUrl.args = {
|
|
57
|
+
link: makePreview({
|
|
58
|
+
og_scrape_url:
|
|
59
|
+
'https://example.com/very/long/path/with/many/segments/and/query/parameters?param1=value1¶m2=value2',
|
|
60
|
+
title: 'Page with an extremely long URL',
|
|
61
|
+
image_url: '/image-thumbnail.jpg',
|
|
62
|
+
}),
|
|
63
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { XIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LinkPreview } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
interface CustomLinkPreviewCardProps {
|
|
6
|
+
link: LinkPreview
|
|
7
|
+
onDismiss: (url: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
|
|
11
|
+
link,
|
|
12
|
+
onDismiss,
|
|
13
|
+
}) => {
|
|
14
|
+
const { og_scrape_url, title, image_url } = link
|
|
15
|
+
|
|
16
|
+
const handleDismissLink = (e: React.MouseEvent) => {
|
|
17
|
+
e.preventDefault()
|
|
18
|
+
onDismiss(og_scrape_url)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
href={og_scrape_url}
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
|
|
27
|
+
>
|
|
28
|
+
{image_url && (
|
|
29
|
+
<img
|
|
30
|
+
src={image_url}
|
|
31
|
+
alt={title || ''}
|
|
32
|
+
className="h-[180px] w-full rounded-[20px] object-cover"
|
|
33
|
+
/>
|
|
34
|
+
)}
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={handleDismissLink}
|
|
38
|
+
className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
|
|
39
|
+
aria-label="Close link preview"
|
|
40
|
+
>
|
|
41
|
+
<XIcon className="size-4 text-black/90" />
|
|
42
|
+
</button>
|
|
43
|
+
<div className="p-2">
|
|
44
|
+
{title && (
|
|
45
|
+
<div className="text-[14px] font-medium leading-5 text-white">
|
|
46
|
+
{title}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<div className="text-[12px] leading-4 text-white/55">
|
|
50
|
+
{og_scrape_url}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</a>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default CustomLinkPreviewCard
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { XIcon } from '@phosphor-icons/react'
|
|
2
1
|
import React from 'react'
|
|
3
2
|
import {
|
|
4
|
-
LinkPreview,
|
|
5
3
|
LinkPreviewsManager,
|
|
6
4
|
LinkPreviewsManagerState,
|
|
7
5
|
} from 'stream-chat'
|
|
8
6
|
import { useMessageComposer, useStateStore } from 'stream-chat-react'
|
|
9
7
|
|
|
8
|
+
import CustomLinkPreviewCard from './CustomLinkPreviewCard'
|
|
9
|
+
|
|
10
10
|
const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
|
|
11
11
|
linkPreviews: Array.from(state.previews.values()).filter(
|
|
12
12
|
(preview) =>
|
|
@@ -15,58 +15,6 @@ const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
|
|
|
15
15
|
),
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
interface CustomLinkPreviewCardProps {
|
|
19
|
-
link: LinkPreview
|
|
20
|
-
onDismiss: (url: string) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
|
|
24
|
-
link,
|
|
25
|
-
onDismiss,
|
|
26
|
-
}) => {
|
|
27
|
-
const { og_scrape_url, title, image_url } = link
|
|
28
|
-
|
|
29
|
-
const handleDismissLink = (e: React.MouseEvent) => {
|
|
30
|
-
e.preventDefault()
|
|
31
|
-
onDismiss(og_scrape_url)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<a
|
|
36
|
-
href={og_scrape_url}
|
|
37
|
-
target="_blank"
|
|
38
|
-
rel="noopener noreferrer"
|
|
39
|
-
className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
|
|
40
|
-
>
|
|
41
|
-
{image_url && (
|
|
42
|
-
<img
|
|
43
|
-
src={image_url}
|
|
44
|
-
alt={title || ''}
|
|
45
|
-
className="h-[180px] w-full rounded-[20px] object-cover"
|
|
46
|
-
/>
|
|
47
|
-
)}
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
onClick={handleDismissLink}
|
|
51
|
-
className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
|
|
52
|
-
aria-label="Close link preview"
|
|
53
|
-
>
|
|
54
|
-
<XIcon className="size-4 text-black/90" />
|
|
55
|
-
</button>
|
|
56
|
-
<div className="p-2">
|
|
57
|
-
{title && (
|
|
58
|
-
<div className="text-[14px] font-medium leading-5 text-white">
|
|
59
|
-
{title}
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
<div className="text-[12px] leading-4 text-white/55">
|
|
63
|
-
{og_scrape_url}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</a>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
18
|
export const CustomLinkPreviewList = () => {
|
|
71
19
|
const { linkPreviewsManager } = useMessageComposer()
|
|
72
20
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
WithComponents,
|
|
18
18
|
} from 'stream-chat-react'
|
|
19
19
|
|
|
20
|
+
import { minutesAgo } from '../../stories/decorators/storyTime'
|
|
20
21
|
import {
|
|
21
22
|
currentUserArgType,
|
|
22
23
|
StoryUser,
|
|
@@ -46,8 +47,8 @@ const createMockChannel = async (
|
|
|
46
47
|
const mockMessages = messages.map((msg, index) => ({
|
|
47
48
|
...msg,
|
|
48
49
|
type: msg.type ?? ('regular' as const),
|
|
49
|
-
created_at:
|
|
50
|
-
updated_at:
|
|
50
|
+
created_at: minutesAgo(messages.length - index),
|
|
51
|
+
updated_at: minutesAgo(messages.length - index),
|
|
51
52
|
html: `<p>${msg.text}</p>`,
|
|
52
53
|
attachments: msg.attachments ?? [],
|
|
53
54
|
latest_reactions: [],
|
|
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import { now } from '../../stories/decorators/storyTime'
|
|
6
|
+
|
|
5
7
|
import { MessageTag } from './MessageTag'
|
|
6
8
|
|
|
7
9
|
type ComponentProps = React.ComponentProps<typeof MessageTag>
|
|
@@ -28,8 +30,8 @@ const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
|
|
|
28
30
|
id: 'msg-1',
|
|
29
31
|
text: options?.text ?? 'Hello world',
|
|
30
32
|
type: 'regular',
|
|
31
|
-
created_at:
|
|
32
|
-
updated_at:
|
|
33
|
+
created_at: now(),
|
|
34
|
+
updated_at: now(),
|
|
33
35
|
metadata: options?.metadata,
|
|
34
36
|
}) as LocalMessage
|
|
35
37
|
|
|
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import type { LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import { now } from '../../stories/decorators/storyTime'
|
|
6
|
+
|
|
5
7
|
import { MediaMessage } from '.'
|
|
6
8
|
|
|
7
9
|
const meta: Meta<typeof MediaMessage> = {
|
|
@@ -20,8 +22,8 @@ const base = (overrides: Partial<LocalMessage> = {}): LocalMessage => {
|
|
|
20
22
|
id: 'msg-1',
|
|
21
23
|
text: '',
|
|
22
24
|
type: 'regular',
|
|
23
|
-
created_at:
|
|
24
|
-
updated_at:
|
|
25
|
+
created_at: now(),
|
|
26
|
+
updated_at: now(),
|
|
25
27
|
deleted_at: null,
|
|
26
28
|
pinned_at: null,
|
|
27
29
|
status: 'received',
|
|
@@ -75,33 +75,46 @@ export const useChannelModerationActions = ({
|
|
|
75
75
|
logLabel = 'useChannelModerationActions',
|
|
76
76
|
}: UseChannelModerationActionsParams): ChannelModerationActions => {
|
|
77
77
|
const { service, debug } = useMessagingContext()
|
|
78
|
+
const participantId = participant?.user?.id
|
|
79
|
+
const willLookup = Boolean(
|
|
80
|
+
enabled && showBlockParticipant && service && participantId
|
|
81
|
+
)
|
|
82
|
+
|
|
78
83
|
const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
// Tracks the participant + service the most recent lookup has completed
|
|
85
|
+
// for. Keying on both so a service swap (e.g. MessagingProvider rebuilding
|
|
86
|
+
// its StreamChatService when config/apiKey/debug changes) re-triggers the
|
|
87
|
+
// loading state for the same participant — otherwise the menu would
|
|
88
|
+
// briefly show actionable Block/Unblock against the new service with the
|
|
89
|
+
// old service's blocked result. Computing the flag at render time — rather
|
|
90
|
+
// than from a useState updated inside useEffect — also closes the brief
|
|
91
|
+
// window where the menu would render the regular (enabled) Block button
|
|
92
|
+
// before the effect flipped the disabled placeholder on.
|
|
93
|
+
const [resolvedFor, setResolvedFor] = useState<{
|
|
94
|
+
participantId: string
|
|
95
|
+
service: unknown
|
|
96
|
+
} | null>(null)
|
|
81
97
|
const [isLeaving, setIsLeaving] = useState(false)
|
|
82
98
|
const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
|
|
83
99
|
|
|
100
|
+
const isCheckingBlockedStatus =
|
|
101
|
+
willLookup &&
|
|
102
|
+
(resolvedFor?.participantId !== participantId ||
|
|
103
|
+
resolvedFor?.service !== service)
|
|
104
|
+
|
|
84
105
|
// Resolve whether the participant is blocked whenever the participant or
|
|
85
106
|
// surface visibility changes.
|
|
86
107
|
useEffect(() => {
|
|
87
108
|
// When the lookup is skipped (Block action hidden, surface disabled, or no
|
|
88
109
|
// participant), clear any stale blocked state so a previous participant's
|
|
89
110
|
// value can't leak into the next conversation.
|
|
90
|
-
if (
|
|
91
|
-
!enabled ||
|
|
92
|
-
!showBlockParticipant ||
|
|
93
|
-
!service ||
|
|
94
|
-
!participant?.user?.id
|
|
95
|
-
) {
|
|
111
|
+
if (!willLookup || !service || !participantId) {
|
|
96
112
|
setIsParticipantBlocked(false)
|
|
97
|
-
|
|
113
|
+
setResolvedFor(null)
|
|
98
114
|
return
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
let cancelled = false
|
|
102
|
-
const participantId = participant.user.id
|
|
103
|
-
|
|
104
|
-
setIsCheckingBlockedStatus(true)
|
|
105
118
|
|
|
106
119
|
void (async () => {
|
|
107
120
|
try {
|
|
@@ -117,7 +130,12 @@ export const useChannelModerationActions = ({
|
|
|
117
130
|
console.error(`[${logLabel}] Failed to check blocked status:`, error)
|
|
118
131
|
}
|
|
119
132
|
} finally {
|
|
120
|
-
|
|
133
|
+
// Mark the lookup as resolved regardless of success/failure so a
|
|
134
|
+
// rejected `getBlockedUsers()` doesn't leave the menu stuck in the
|
|
135
|
+
// disabled-spinner state. On failure the blocked flag stays at its
|
|
136
|
+
// default (false), matching the prior behavior — the user can attempt
|
|
137
|
+
// to block/unblock and the server rejects if the state is wrong.
|
|
138
|
+
if (!cancelled) setResolvedFor({ participantId, service })
|
|
121
139
|
}
|
|
122
140
|
})()
|
|
123
141
|
|
|
@@ -125,7 +143,7 @@ export const useChannelModerationActions = ({
|
|
|
125
143
|
return () => {
|
|
126
144
|
cancelled = true
|
|
127
145
|
}
|
|
128
|
-
}, [
|
|
146
|
+
}, [willLookup, service, participantId, logLabel])
|
|
129
147
|
|
|
130
148
|
const handleLeaveConversation = async () => {
|
|
131
149
|
if (isLeaving) return
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic clock for Storybook stories.
|
|
3
|
+
*
|
|
4
|
+
* Stories that render relative timestamps ("3m ago", "yesterday") must use these
|
|
5
|
+
* helpers instead of `new Date()` / `Date.now()`, or visual diffs in Chromatic
|
|
6
|
+
* will drift on every run.
|
|
7
|
+
*
|
|
8
|
+
* FROZEN_NOW is the reference instant. Helpers below produce timestamps relative
|
|
9
|
+
* to it so the rendered output is identical regardless of when the build runs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const FROZEN_NOW = new Date('2026-01-15T12:00:00.000Z')
|
|
13
|
+
|
|
14
|
+
const MS_PER_SEC = 1000
|
|
15
|
+
const MS_PER_MIN = 60 * MS_PER_SEC
|
|
16
|
+
const MS_PER_HOUR = 60 * MS_PER_MIN
|
|
17
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR
|
|
18
|
+
|
|
19
|
+
export const now = (): Date => new Date(FROZEN_NOW)
|
|
20
|
+
|
|
21
|
+
export const secondsAgo = (n: number): Date =>
|
|
22
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_SEC)
|
|
23
|
+
|
|
24
|
+
export const minutesAgo = (n: number): Date =>
|
|
25
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_MIN)
|
|
26
|
+
|
|
27
|
+
export const hoursAgo = (n: number): Date =>
|
|
28
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_HOUR)
|
|
29
|
+
|
|
30
|
+
export const daysAgo = (n: number): Date =>
|
|
31
|
+
new Date(FROZEN_NOW.getTime() - n * MS_PER_DAY)
|