@linktr.ee/messaging-react 3.3.3 → 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.
Files changed (43) hide show
  1. package/dist/{Card-B7AF5uOB.js → Card-B9QrjooN.js} +3 -3
  2. package/dist/{Card-B7AF5uOB.js.map → Card-B9QrjooN.js.map} +1 -1
  3. package/dist/{Card-0BgubwgM.cjs → Card-BRRlz4kq.cjs} +2 -2
  4. package/dist/{Card-0BgubwgM.cjs.map → Card-BRRlz4kq.cjs.map} +1 -1
  5. package/dist/{Card-DLmUSU4A.cjs → Card-C-FCwjGa.cjs} +2 -2
  6. package/dist/{Card-DLmUSU4A.cjs.map → Card-C-FCwjGa.cjs.map} +1 -1
  7. package/dist/{Card-DchJqvYq.js → Card-CVZzYmYW.js} +2 -2
  8. package/dist/{Card-DchJqvYq.js.map → Card-CVZzYmYW.js.map} +1 -1
  9. package/dist/{Card-CvBbAoUo.cjs → Card-D_oLlfPw.cjs} +2 -2
  10. package/dist/{Card-CvBbAoUo.cjs.map → Card-D_oLlfPw.cjs.map} +1 -1
  11. package/dist/{Card-DmPpcrSU.js → Card-DzjYyrie.js} +2 -2
  12. package/dist/{Card-DmPpcrSU.js.map → Card-DzjYyrie.js.map} +1 -1
  13. package/dist/{LockedThumbnail-BQjA4HaB.js → LockedThumbnail-CJfXY_Ut.js} +2 -2
  14. package/dist/{LockedThumbnail-BQjA4HaB.js.map → LockedThumbnail-CJfXY_Ut.js.map} +1 -1
  15. package/dist/{LockedThumbnail-D9fSb4N-.cjs → LockedThumbnail-Cth1yWnH.cjs} +2 -2
  16. package/dist/{LockedThumbnail-D9fSb4N-.cjs.map → LockedThumbnail-Cth1yWnH.cjs.map} +1 -1
  17. package/dist/index-CBtOPvxW.cjs +2 -0
  18. package/dist/index-CBtOPvxW.cjs.map +1 -0
  19. package/dist/{index-BcHUpyyw.js → index-D7eRkXoG.js} +535 -537
  20. package/dist/index-D7eRkXoG.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +26 -0
  23. package/dist/index.js +1 -1
  24. package/package.json +1 -1
  25. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +104 -0
  26. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +33 -8
  27. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +55 -47
  28. package/src/components/ChannelView.stories.tsx +8 -7
  29. package/src/components/CloseButton/CloseButton.stories.tsx +31 -0
  30. package/src/components/CustomDateSeparator/CustomDateSeparator.stories.tsx +33 -0
  31. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.stories.tsx +63 -0
  32. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +57 -0
  33. package/src/components/CustomLinkPreviewList/index.tsx +2 -54
  34. package/src/components/CustomMessage/CustomMessage.stories.tsx +3 -2
  35. package/src/components/CustomMessage/MessageTag.stories.tsx +4 -2
  36. package/src/components/MediaMessage/MediaMessage.stories.tsx +4 -2
  37. package/src/hooks/useChannelModerationActions.ts +32 -14
  38. package/src/index.ts +6 -1
  39. package/src/stories/decorators/storyTime.ts +31 -0
  40. package/src/stream-custom-data.ts +28 -0
  41. package/dist/index-BcHUpyyw.js.map +0 -1
  42. package/dist/index-DTZNltUC.cjs +0 -2
  43. package/dist/index-DTZNltUC.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DTZNltUC.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-CBtOPvxW.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1222,6 +1222,29 @@ export declare interface MessagingShellProps extends ChannelViewPassthroughProps
1222
1222
 
1223
1223
  export declare function normalizeLanguageCode(language?: string): string | undefined;
1224
1224
 
1225
+ /**
1226
+ * Action intents supported by Linktree official CTA attachments.
1227
+ */
1228
+ export declare type OfficialCtaActionKind =
1229
+ /** Open the existing share-to-socials flow for the current platform. */
1230
+ 'post_to_socials'
1231
+ /** Navigate to an external URL when the CTA represents plain navigation. */
1232
+ | 'open_external_url';
1233
+
1234
+ export declare interface OfficialCtaActionPayload {
1235
+ kind?: OfficialCtaActionKind;
1236
+ label?: string;
1237
+ url?: string;
1238
+ }
1239
+
1240
+ /**
1241
+ * Structured attachment shape for server-authored Linktree official CTA cards.
1242
+ */
1243
+ export declare type OfficialCtaAttachment = Attachment & {
1244
+ action?: OfficialCtaActionPayload;
1245
+ type: 'linktree_official_cta';
1246
+ };
1247
+
1225
1248
  /**
1226
1249
  * Generic participant interface for different host environments
1227
1250
  */
@@ -1393,6 +1416,9 @@ export { }
1393
1416
 
1394
1417
 
1395
1418
  declare module 'stream-chat' {
1419
+ interface CustomAttachmentData {
1420
+ action?: OfficialCtaActionPayload;
1421
+ }
1396
1422
  interface CustomChannelData {
1397
1423
  /**
1398
1424
  * Whether the channel has at least one visitor-originated message.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-BcHUpyyw.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-D7eRkXoG.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -0,0 +1,104 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React from 'react'
3
+
4
+ import AttachmentCard, { AttachmentThumbnail } from '.'
5
+
6
+ type ComponentProps = React.ComponentProps<typeof AttachmentCard>
7
+
8
+ const meta: Meta<ComponentProps> = {
9
+ title: 'AttachmentCard',
10
+ component: AttachmentCard,
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ variant: { control: { type: 'inline-radio' }, options: ['light', 'dark'] },
14
+ },
15
+ }
16
+ export default meta
17
+
18
+ const IMAGE_URL = '/image-thumbnail.jpg'
19
+
20
+ const Template: StoryFn<ComponentProps> = (args) => <AttachmentCard {...args} />
21
+
22
+ export const LightImage: StoryFn<ComponentProps> = Template.bind({})
23
+ LightImage.args = {
24
+ variant: 'light',
25
+ title: 'sunset.jpg',
26
+ mimeType: 'image/jpeg',
27
+ detail: '2.4 MB',
28
+ thumbnail: (
29
+ <AttachmentThumbnail
30
+ mimeType="image/jpeg"
31
+ sourceUrl={IMAGE_URL}
32
+ title="sunset"
33
+ variant="light"
34
+ />
35
+ ),
36
+ }
37
+
38
+ export const DarkImage: StoryFn<ComponentProps> = Template.bind({})
39
+ DarkImage.args = {
40
+ variant: 'dark',
41
+ title: 'sunset.jpg',
42
+ mimeType: 'image/jpeg',
43
+ detail: '2.4 MB',
44
+ thumbnail: (
45
+ <AttachmentThumbnail
46
+ mimeType="image/jpeg"
47
+ sourceUrl={IMAGE_URL}
48
+ title="sunset"
49
+ variant="dark"
50
+ />
51
+ ),
52
+ }
53
+
54
+ export const PdfPlaceholder: StoryFn<ComponentProps> = Template.bind({})
55
+ PdfPlaceholder.args = {
56
+ variant: 'light',
57
+ title: 'invoice.pdf',
58
+ mimeType: 'application/pdf',
59
+ detail: '120 KB',
60
+ thumbnail: <AttachmentThumbnail mimeType="application/pdf" variant="light" />,
61
+ }
62
+
63
+ export const AudioPosterOnly: StoryFn<ComponentProps> = Template.bind({})
64
+ AudioPosterOnly.args = {
65
+ variant: 'dark',
66
+ title: 'voice-memo.m4a',
67
+ mimeType: 'audio/m4a',
68
+ detail: '0:42',
69
+ thumbnail: (
70
+ <AttachmentThumbnail
71
+ mimeType="audio/m4a"
72
+ thumbnailUrl={IMAGE_URL}
73
+ title="voice memo"
74
+ variant="dark"
75
+ />
76
+ ),
77
+ }
78
+
79
+ export const UntitledDark: StoryFn<ComponentProps> = Template.bind({})
80
+ UntitledDark.args = {
81
+ variant: 'dark',
82
+ mimeType: 'image/png',
83
+ detail: '1.1 MB',
84
+ thumbnail: <AttachmentThumbnail mimeType="image/png" variant="dark" />,
85
+ }
86
+
87
+ export const WithTopBadges: StoryFn<ComponentProps> = Template.bind({})
88
+ WithTopBadges.args = {
89
+ variant: 'light',
90
+ title: 'receipt.pdf',
91
+ mimeType: 'application/pdf',
92
+ detail: 'Paid',
93
+ thumbnail: <AttachmentThumbnail mimeType="application/pdf" variant="light" />,
94
+ topLeft: (
95
+ <span className="rounded-full bg-black/80 px-2 py-0.5 text-xs font-medium text-white">
96
+ Locked
97
+ </span>
98
+ ),
99
+ topRight: (
100
+ <span className="rounded-full bg-white/90 px-2 py-0.5 text-xs font-medium text-black">
101
+ $5.00
102
+ </span>
103
+ ),
104
+ }
@@ -19,15 +19,19 @@ const { getBlockedUsersMock, blockUserMock, unBlockUserMock } = vi.hoisted(
19
19
  })
20
20
  )
21
21
 
22
+ // Stable service + context references — production MessagingProvider keeps
23
+ // the service in useState and memoizes the context value, so the hook's
24
+ // service-keyed lookup invalidation only fires on real swaps. Returning a
25
+ // new object literal each call would re-trigger the lookup every render and
26
+ // loop forever.
27
+ const mockService = {
28
+ getBlockedUsers: getBlockedUsersMock,
29
+ blockUser: blockUserMock,
30
+ unBlockUser: unBlockUserMock,
31
+ }
32
+ const mockContext = { service: mockService, debug: false }
22
33
  vi.mock('../../providers/MessagingProvider', () => ({
23
- useMessagingContext: () => ({
24
- service: {
25
- getBlockedUsers: getBlockedUsersMock,
26
- blockUser: blockUserMock,
27
- unBlockUser: unBlockUserMock,
28
- },
29
- debug: false,
30
- }),
34
+ useMessagingContext: () => mockContext,
31
35
  }))
32
36
 
33
37
  vi.mock('../ActionButton', () => ({
@@ -229,6 +233,27 @@ describe('ChannelActionsMenu', () => {
229
233
  })
230
234
  })
231
235
 
236
+ it('recovers from a blocked-status lookup failure', async () => {
237
+ // Suppress the expected console.error for the rejected lookup so the
238
+ // test output stays clean.
239
+ const consoleErrorSpy = vi
240
+ .spyOn(console, 'error')
241
+ .mockImplementation(() => {})
242
+ getBlockedUsersMock.mockRejectedValueOnce(new Error('network down'))
243
+
244
+ renderWithProviders(<ChannelActionsMenu {...defaultProps()} />)
245
+ await openMenu()
246
+
247
+ // The lookup rejected, but the action recovers — Block becomes actionable
248
+ // rather than staying stuck in the disabled spinner state.
249
+ await waitFor(() => {
250
+ const block = screen.getByText('Block').closest('button')
251
+ expect(block).not.toBeDisabled()
252
+ })
253
+
254
+ consoleErrorSpy.mockRestore()
255
+ })
256
+
232
257
  it('calls onReportParticipantClick and opens the report page', async () => {
233
258
  const onReportParticipantClick = vi.fn()
234
259
  const windowOpenSpy = vi
@@ -7,6 +7,14 @@ import {
7
7
  StreamChat,
8
8
  } from 'stream-chat'
9
9
 
10
+ import {
11
+ daysAgo,
12
+ hoursAgo,
13
+ minutesAgo,
14
+ now,
15
+ secondsAgo,
16
+ } from '../../stories/decorators/storyTime'
17
+
10
18
  import CustomChannelPreview from './CustomChannelPreview'
11
19
 
12
20
  type ComponentProps = React.ComponentProps<typeof CustomChannelPreview>
@@ -54,7 +62,7 @@ const createMockChannel = (options: {
54
62
  participantId,
55
63
  participantImage,
56
64
  lastMessageText = 'Hey! How are you doing?',
57
- lastMessageTime = new Date(),
65
+ lastMessageTime = now(),
58
66
  unreadCount = 0,
59
67
  lastMessageAttachments,
60
68
  lastMessageMetadata,
@@ -134,7 +142,7 @@ Default.args = {
134
142
  participantId: 'participant-1',
135
143
  participantImage: 'https://i.pravatar.cc/150?img=2',
136
144
  lastMessageText: 'Hey! How are you doing?',
137
- lastMessageTime: new Date(),
145
+ lastMessageTime: now(),
138
146
  }),
139
147
  }
140
148
 
@@ -146,7 +154,7 @@ Selected.args = {
146
154
  participantId: 'participant-2',
147
155
  participantImage: 'https://i.pravatar.cc/150?img=3',
148
156
  lastMessageText: 'That sounds great!',
149
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
157
+ lastMessageTime: minutesAgo(5),
150
158
  }),
151
159
  selectedChannel: createMockChannel({
152
160
  id: 'channel-2',
@@ -164,7 +172,7 @@ WithUnreadMessages.args = {
164
172
  participantId: 'participant-3',
165
173
  participantImage: 'https://i.pravatar.cc/150?img=4',
166
174
  lastMessageText: 'Did you see my last message?',
167
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 15), // 15 minutes ago
175
+ lastMessageTime: minutesAgo(15),
168
176
  unreadCount: 3,
169
177
  }),
170
178
  }
@@ -177,7 +185,7 @@ ManyUnreadMessages.args = {
177
185
  participantId: 'participant-4',
178
186
  participantImage: 'https://i.pravatar.cc/150?img=5',
179
187
  lastMessageText: 'Please check this out!',
180
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
188
+ lastMessageTime: hoursAgo(1),
181
189
  unreadCount: 127,
182
190
  }),
183
191
  }
@@ -189,7 +197,7 @@ NoAvatar.args = {
189
197
  participantName: 'Emma Davis',
190
198
  participantId: 'participant-5',
191
199
  lastMessageText: 'Thanks for your help!',
192
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
200
+ lastMessageTime: minutesAgo(30),
193
201
  }),
194
202
  }
195
203
 
@@ -213,7 +221,7 @@ LongMessage.args = {
213
221
  participantImage: 'https://i.pravatar.cc/150?img=7',
214
222
  lastMessageText:
215
223
  'This is a very long message that should be truncated because it contains way too much text to display in the preview. We want to make sure the component handles this gracefully.',
216
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
224
+ lastMessageTime: hoursAgo(2),
217
225
  }),
218
226
  }
219
227
 
@@ -224,7 +232,7 @@ LongName.args = {
224
232
  participantName: 'Alexander Christopher Wellington-Montgomery III',
225
233
  participantId: 'participant-8',
226
234
  lastMessageText: 'Nice to meet you!',
227
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 45), // 45 minutes ago
235
+ lastMessageTime: minutesAgo(45),
228
236
  }),
229
237
  }
230
238
 
@@ -236,7 +244,7 @@ ChatbotMessage.args = {
236
244
  participantId: 'participant-chatbot',
237
245
  participantImage: 'https://i.pravatar.cc/150?img=9',
238
246
  lastMessageText: 'Thanks for reaching out! How can I help you today?',
239
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 2), // 2 minutes ago
247
+ lastMessageTime: minutesAgo(2),
240
248
  lastMessageMetadata: { custom_type: 'MESSAGE_CHATBOT' },
241
249
  }),
242
250
  }
@@ -249,7 +257,7 @@ TipMessage.args = {
249
257
  participantId: 'participant-tip',
250
258
  participantImage: 'https://i.pravatar.cc/150?img=2',
251
259
  lastMessageText: 'Take my money!',
252
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 3),
260
+ lastMessageTime: minutesAgo(3),
253
261
  lastMessageMetadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
254
262
  }),
255
263
  }
@@ -262,7 +270,7 @@ PaidMessage.args = {
262
270
  participantId: 'participant-paid',
263
271
  participantImage: 'https://i.pravatar.cc/150?img=3',
264
272
  lastMessageText: 'I paid for this message!',
265
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 10),
273
+ lastMessageTime: minutesAgo(10),
266
274
  lastMessageMetadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
267
275
  }),
268
276
  }
@@ -275,7 +283,7 @@ PaidAttachment.args = {
275
283
  participantId: 'participant-paid-attachment',
276
284
  participantImage: 'https://i.pravatar.cc/150?img=4',
277
285
  lastMessageText: 'Check out this exclusive photo!',
278
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 20),
286
+ lastMessageTime: minutesAgo(20),
279
287
  lastMessageMetadata: {
280
288
  custom_type: 'MESSAGE_ATTACHMENT',
281
289
  attachment_title: 'exclusive-photo.jpg',
@@ -322,7 +330,7 @@ export const WithCustomMessagePreview: StoryFn = () => {
322
330
  participantImage: 'https://i.pravatar.cc/150?img=36',
323
331
  lastMessageText:
324
332
  'Critical issue needs immediate attention! Please review the server logs for more details and escalate to the on-call engineer. This system outage is affecting multiple users, and we need to resolve it as soon as possible. Impacted services include messaging, notifications, and real-time updates. All hands are required to investigate and remediate the situation. If you have any questions or need additional resources, contact the incident commander immediately!',
325
- lastMessageTime: new Date(Date.now() - 1000 * 60), // 1 minute ago
333
+ lastMessageTime: minutesAgo(1),
326
334
  lastMessageMetadata: { custom_type: 'MESSAGE_URGENT' },
327
335
  unreadCount: 3,
328
336
  }),
@@ -332,7 +340,7 @@ export const WithCustomMessagePreview: StoryFn = () => {
332
340
  participantId: 'participant-priority',
333
341
  participantImage: 'https://i.pravatar.cc/150?img=37',
334
342
  lastMessageText: 'High priority message',
335
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
343
+ lastMessageTime: minutesAgo(5),
336
344
  lastMessageMetadata: { is_priority: true },
337
345
  unreadCount: 1,
338
346
  }),
@@ -342,7 +350,7 @@ export const WithCustomMessagePreview: StoryFn = () => {
342
350
  participantId: 'participant-vip',
343
351
  participantImage: 'https://i.pravatar.cc/150?img=38',
344
352
  lastMessageText: 'Thanks for the support!',
345
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago
353
+ lastMessageTime: minutesAgo(10),
346
354
  lastMessageMetadata: { is_vip: true },
347
355
  }),
348
356
  createMockChannel({
@@ -351,7 +359,7 @@ export const WithCustomMessagePreview: StoryFn = () => {
351
359
  participantId: 'participant-normal',
352
360
  participantImage: 'https://i.pravatar.cc/150?img=39',
353
361
  lastMessageText: 'Hey, how are you?',
354
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
362
+ lastMessageTime: minutesAgo(30),
355
363
  }),
356
364
  ]
357
365
 
@@ -379,7 +387,7 @@ SelectedWithUnread.args = {
379
387
  participantId: 'participant-9',
380
388
  participantImage: 'https://i.pravatar.cc/150?img=8',
381
389
  lastMessageText: 'Important update!',
382
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago
390
+ lastMessageTime: minutesAgo(10),
383
391
  unreadCount: 5,
384
392
  }),
385
393
  selectedChannel: createMockChannel({
@@ -401,7 +409,7 @@ export const MultipleChannels: StoryFn = () => {
401
409
  participantId: 'participant-1',
402
410
  participantImage: 'https://i.pravatar.cc/150?img=2',
403
411
  lastMessageText: 'Hey! How are you doing?',
404
- lastMessageTime: new Date(),
412
+ lastMessageTime: now(),
405
413
  unreadCount: 2,
406
414
  }),
407
415
  createMockChannel({
@@ -410,7 +418,7 @@ export const MultipleChannels: StoryFn = () => {
410
418
  participantId: 'participant-2',
411
419
  participantImage: 'https://i.pravatar.cc/150?img=3',
412
420
  lastMessageText: 'That sounds great!',
413
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
421
+ lastMessageTime: minutesAgo(5),
414
422
  }),
415
423
  createMockChannel({
416
424
  id: 'channel-3',
@@ -418,7 +426,7 @@ export const MultipleChannels: StoryFn = () => {
418
426
  participantId: 'participant-3',
419
427
  participantImage: 'https://i.pravatar.cc/150?img=4',
420
428
  lastMessageText: 'See you tomorrow',
421
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
429
+ lastMessageTime: hoursAgo(1),
422
430
  unreadCount: 15,
423
431
  }),
424
432
  createMockChannel({
@@ -426,7 +434,7 @@ export const MultipleChannels: StoryFn = () => {
426
434
  participantName: 'David Brown',
427
435
  participantId: 'participant-4',
428
436
  lastMessageText: 'Thanks!',
429
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
437
+ lastMessageTime: hoursAgo(2),
430
438
  }),
431
439
  ]
432
440
 
@@ -457,7 +465,7 @@ WithUrl.args = {
457
465
  participantId: 'participant-url',
458
466
  participantImage: 'https://i.pravatar.cc/150?img=10',
459
467
  lastMessageText: 'https://example.com/page',
460
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 8), // 8 minutes ago
468
+ lastMessageTime: minutesAgo(8),
461
469
  }),
462
470
  }
463
471
 
@@ -470,7 +478,7 @@ WithVeryLongUrl.args = {
470
478
  participantImage: 'https://i.pravatar.cc/150?img=11',
471
479
  lastMessageText:
472
480
  'https://example.com/very/long/path/with/many/segments/and/query/parameters?param1=value1&param2=value2&param3=value3&param4=value4&param5=very-long-value-that-makes-the-url-extremely-long',
473
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 20), // 20 minutes ago
481
+ lastMessageTime: minutesAgo(20),
474
482
  }),
475
483
  }
476
484
 
@@ -483,7 +491,7 @@ JustNow.args = {
483
491
  participantId: 'participant-just-now',
484
492
  participantImage: 'https://i.pravatar.cc/150?img=12',
485
493
  lastMessageText: 'I just sent this!',
486
- lastMessageTime: new Date(Date.now() - 1000 * 30), // 30 seconds ago
494
+ lastMessageTime: secondsAgo(30),
487
495
  }),
488
496
  }
489
497
 
@@ -495,7 +503,7 @@ FiveMinutesAgo.args = {
495
503
  participantId: 'participant-5m',
496
504
  participantImage: 'https://i.pravatar.cc/150?img=13',
497
505
  lastMessageText: 'Be right back',
498
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
506
+ lastMessageTime: minutesAgo(5),
499
507
  }),
500
508
  }
501
509
 
@@ -507,7 +515,7 @@ ThreeHoursAgo.args = {
507
515
  participantId: 'participant-3h',
508
516
  participantImage: 'https://i.pravatar.cc/150?img=14',
509
517
  lastMessageText: 'Got it, thanks!',
510
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 3), // 3 hours ago
518
+ lastMessageTime: hoursAgo(3),
511
519
  }),
512
520
  }
513
521
 
@@ -519,7 +527,7 @@ Yesterday.args = {
519
527
  participantId: 'participant-yesterday',
520
528
  participantImage: 'https://i.pravatar.cc/150?img=15',
521
529
  lastMessageText: 'See you tomorrow',
522
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 30), // 30 hours ago
530
+ lastMessageTime: hoursAgo(30),
523
531
  }),
524
532
  }
525
533
 
@@ -531,7 +539,7 @@ ThreeDaysAgo.args = {
531
539
  participantId: 'participant-3d',
532
540
  participantImage: 'https://i.pravatar.cc/150?img=16',
533
541
  lastMessageText: 'Have a great weekend!',
534
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), // 3 days ago
542
+ lastMessageTime: daysAgo(3),
535
543
  }),
536
544
  }
537
545
 
@@ -543,7 +551,7 @@ LastWeek.args = {
543
551
  participantId: 'participant-1w',
544
552
  participantImage: 'https://i.pravatar.cc/150?img=17',
545
553
  lastMessageText: 'Talk to you next week',
546
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), // 1 week ago
554
+ lastMessageTime: daysAgo(7),
547
555
  }),
548
556
  }
549
557
 
@@ -555,7 +563,7 @@ TwoWeeksAgo.args = {
555
563
  participantId: 'participant-2w',
556
564
  participantImage: 'https://i.pravatar.cc/150?img=18',
557
565
  lastMessageText: 'Sounds good',
558
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14), // 2 weeks ago
566
+ lastMessageTime: daysAgo(14),
559
567
  }),
560
568
  }
561
569
 
@@ -567,7 +575,7 @@ OneMonthAgo.args = {
567
575
  participantId: 'participant-1m',
568
576
  participantImage: 'https://i.pravatar.cc/150?img=19',
569
577
  lastMessageText: 'Happy New Year!',
570
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), // 1 month ago
578
+ lastMessageTime: daysAgo(30),
571
579
  }),
572
580
  }
573
581
 
@@ -579,7 +587,7 @@ SixMonthsAgo.args = {
579
587
  participantId: 'participant-6m',
580
588
  participantImage: 'https://i.pravatar.cc/150?img=20',
581
589
  lastMessageText: 'Long time no talk!',
582
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24 * 180), // 6 months ago
590
+ lastMessageTime: daysAgo(180),
583
591
  }),
584
592
  }
585
593
 
@@ -592,61 +600,61 @@ export const TimeVariations: StoryFn = () => {
592
600
  {
593
601
  id: 'time-just-now',
594
602
  name: 'Just Now',
595
- time: new Date(Date.now() - 1000 * 30), // 30 seconds
603
+ time: secondsAgo(30),
596
604
  message: 'Just sent',
597
605
  },
598
606
  {
599
607
  id: 'time-5m',
600
608
  name: '5 Minutes',
601
- time: new Date(Date.now() - 1000 * 60 * 5),
609
+ time: minutesAgo(5),
602
610
  message: '5 minutes ago',
603
611
  },
604
612
  {
605
613
  id: 'time-30m',
606
614
  name: '30 Minutes',
607
- time: new Date(Date.now() - 1000 * 60 * 30),
615
+ time: minutesAgo(30),
608
616
  message: '30 minutes ago',
609
617
  },
610
618
  {
611
619
  id: 'time-3h',
612
620
  name: '3 Hours',
613
- time: new Date(Date.now() - 1000 * 60 * 60 * 3),
621
+ time: hoursAgo(3),
614
622
  message: '3 hours ago',
615
623
  },
616
624
  {
617
625
  id: 'time-yesterday',
618
626
  name: 'Yesterday',
619
- time: new Date(Date.now() - 1000 * 60 * 60 * 30),
627
+ time: hoursAgo(30),
620
628
  message: 'Yesterday',
621
629
  },
622
630
  {
623
631
  id: 'time-3d',
624
632
  name: '3 Days',
625
- time: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
633
+ time: daysAgo(3),
626
634
  message: '3 days ago',
627
635
  },
628
636
  {
629
637
  id: 'time-1w',
630
638
  name: '1 Week',
631
- time: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
639
+ time: daysAgo(7),
632
640
  message: '1 week ago',
633
641
  },
634
642
  {
635
643
  id: 'time-2w',
636
644
  name: '2 Weeks',
637
- time: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
645
+ time: daysAgo(14),
638
646
  message: '2 weeks ago',
639
647
  },
640
648
  {
641
649
  id: 'time-1m',
642
650
  name: '1 Month',
643
- time: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
651
+ time: daysAgo(30),
644
652
  message: '1 month ago',
645
653
  },
646
654
  {
647
655
  id: 'time-6m',
648
656
  name: '6 Months',
649
- time: new Date(Date.now() - 1000 * 60 * 60 * 24 * 180),
657
+ time: daysAgo(180),
650
658
  message: '6 months ago',
651
659
  },
652
660
  ]
@@ -690,7 +698,7 @@ WithAttachmentAssetUrl.args = {
690
698
  participantId: 'participant-attachment-asset',
691
699
  participantImage: 'https://i.pravatar.cc/150?img=30',
692
700
  lastMessageText: '', // No text, only attachment
693
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago
701
+ lastMessageTime: minutesAgo(10),
694
702
  lastMessageAttachments: [
695
703
  {
696
704
  asset_url: 'https://example.com/document.pdf',
@@ -707,7 +715,7 @@ WithAttachmentImageUrl.args = {
707
715
  participantId: 'participant-attachment-image',
708
716
  participantImage: 'https://i.pravatar.cc/150?img=31',
709
717
  lastMessageText: '', // No text, only attachment
710
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 15), // 15 minutes ago
718
+ lastMessageTime: minutesAgo(15),
711
719
  lastMessageAttachments: [
712
720
  {
713
721
  image_url: 'https://example.com/image.jpg',
@@ -726,7 +734,7 @@ WithAttachmentOgScrapeUrl.args = {
726
734
  participantId: 'participant-attachment-og',
727
735
  participantImage: 'https://i.pravatar.cc/150?img=32',
728
736
  lastMessageText: '', // No text, only attachment
729
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 7), // 7 minutes ago
737
+ lastMessageTime: minutesAgo(7),
730
738
  lastMessageAttachments: [
731
739
  {
732
740
  og_scrape_url: 'https://example.com/article',
@@ -743,7 +751,7 @@ WithAttachmentThumbUrl.args = {
743
751
  participantId: 'participant-attachment-thumb',
744
752
  participantImage: 'https://i.pravatar.cc/150?img=33',
745
753
  lastMessageText: '', // No text, only attachment
746
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 12), // 12 minutes ago
754
+ lastMessageTime: minutesAgo(12),
747
755
  lastMessageAttachments: [
748
756
  {
749
757
  thumb_url: 'https://example.com/thumb.jpg',
@@ -760,7 +768,7 @@ WithLongAttachmentUrl.args = {
760
768
  participantId: 'participant-attachment-long',
761
769
  participantImage: 'https://i.pravatar.cc/150?img=34',
762
770
  lastMessageText: '', // No text, only attachment
763
- lastMessageTime: new Date(Date.now() - 1000 * 60 * 20), // 20 minutes ago
771
+ lastMessageTime: minutesAgo(20),
764
772
  lastMessageAttachments: [
765
773
  {
766
774
  asset_url: