@linktr.ee/messaging-react 3.3.5 → 3.3.6-rc-1780987607

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 (46) hide show
  1. package/dist/{Card-C-FCwjGa.cjs → Card-BlviN8Fb.cjs} +2 -2
  2. package/dist/{Card-C-FCwjGa.cjs.map → Card-BlviN8Fb.cjs.map} +1 -1
  3. package/dist/{Card-DzjYyrie.js → Card-C4ncqjxJ.js} +2 -2
  4. package/dist/{Card-DzjYyrie.js.map → Card-C4ncqjxJ.js.map} +1 -1
  5. package/dist/{Card-BRRlz4kq.cjs → Card-Cn7Zxc6U.cjs} +2 -2
  6. package/dist/{Card-BRRlz4kq.cjs.map → Card-Cn7Zxc6U.cjs.map} +1 -1
  7. package/dist/{Card-CVZzYmYW.js → Card-DE5bfj0l.js} +2 -2
  8. package/dist/{Card-CVZzYmYW.js.map → Card-DE5bfj0l.js.map} +1 -1
  9. package/dist/{Card-B9QrjooN.js → Card-IjOI7UXs.js} +3 -3
  10. package/dist/{Card-B9QrjooN.js.map → Card-IjOI7UXs.js.map} +1 -1
  11. package/dist/{Card-D_oLlfPw.cjs → Card-KgQxeR-B.cjs} +2 -2
  12. package/dist/{Card-D_oLlfPw.cjs.map → Card-KgQxeR-B.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-CJfXY_Ut.js → LockedThumbnail-4-54cyJG.js} +2 -2
  14. package/dist/{LockedThumbnail-CJfXY_Ut.js.map → LockedThumbnail-4-54cyJG.js.map} +1 -1
  15. package/dist/{LockedThumbnail-Cth1yWnH.cjs → LockedThumbnail-DL5NZzWJ.cjs} +2 -2
  16. package/dist/{LockedThumbnail-Cth1yWnH.cjs.map → LockedThumbnail-DL5NZzWJ.cjs.map} +1 -1
  17. package/dist/{index-D7eRkXoG.js → index-C2wfgpUU.js} +585 -551
  18. package/dist/index-C2wfgpUU.js.map +1 -0
  19. package/dist/index-nanry0Io.cjs +2 -0
  20. package/dist/index-nanry0Io.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +5 -2
  24. package/src/components/ActionButton/ActionButton.test.tsx +0 -25
  25. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +122 -0
  26. package/src/components/Avatar/Avatar.stories.tsx +20 -0
  27. package/src/components/ChannelList/ChannelList.stories.tsx +5 -0
  28. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +22 -0
  29. package/src/components/ChannelView.test.tsx +12 -1
  30. package/src/components/ChannelView.tsx +34 -17
  31. package/src/components/CustomMessage/CustomMessage.stories.tsx +0 -16
  32. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +13 -0
  33. package/src/components/CustomMessage/MessageTag.stories.tsx +18 -0
  34. package/src/components/CustomMessage/MessageVoteButtons.stories.tsx +9 -0
  35. package/src/components/CustomMessageInput/index.tsx +14 -4
  36. package/src/components/CustomSystemMessage/CustomSystemMessage.stories.tsx +54 -0
  37. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +7 -0
  38. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +11 -1
  39. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +4 -0
  40. package/src/components/MediaMessage/MediaMessage.test.tsx +0 -38
  41. package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -84
  42. package/src/components/SearchInput/SearchInput.test.tsx +0 -8
  43. package/src/utils/formatRelativeTime.test.ts +1 -32
  44. package/dist/index-CBtOPvxW.cjs +0 -2
  45. package/dist/index-CBtOPvxW.cjs.map +0 -1
  46. package/dist/index-D7eRkXoG.js.map +0 -1
@@ -43,12 +43,18 @@ const Template: StoryFn<ComponentProps> = (args) => {
43
43
  )
44
44
  }
45
45
 
46
+ // Per-variant stories stay in Storybook for browsing/design review but are
47
+ // individually covered by rows in AllVariants — skip in Chromatic to keep
48
+ // the variant matrix as the single snapshot source of truth.
49
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
50
+
46
51
  export const Tip: StoryFn<ComponentProps> = Template.bind({})
47
52
  Tip.args = {
48
53
  message: createMockMessage({
49
54
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
50
55
  }),
51
56
  }
57
+ Tip.parameters = skipInChromatic
52
58
 
53
59
  export const TipStandalone: StoryFn<ComponentProps> = Template.bind({})
54
60
  TipStandalone.args = {
@@ -58,6 +64,7 @@ TipStandalone.args = {
58
64
  }),
59
65
  standalone: true,
60
66
  }
67
+ TipStandalone.parameters = skipInChromatic
61
68
 
62
69
  export const Paid: StoryFn<ComponentProps> = Template.bind({})
63
70
  Paid.args = {
@@ -65,11 +72,13 @@ Paid.args = {
65
72
  metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
66
73
  }),
67
74
  }
75
+ Paid.parameters = skipInChromatic
68
76
 
69
77
  export const ChatbotReceiverText: StoryFn<ComponentProps> = Template.bind({})
70
78
  ChatbotReceiverText.args = {
71
79
  message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
72
80
  }
81
+ ChatbotReceiverText.parameters = skipInChromatic
73
82
 
74
83
  export const ChatbotSenderText: StoryFn<ComponentProps> = (args) => {
75
84
  return (
@@ -84,6 +93,7 @@ ChatbotSenderText.args = {
84
93
  message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
85
94
  isMyMessage: true,
86
95
  }
96
+ ChatbotSenderText.parameters = skipInChromatic
87
97
 
88
98
  export const ChatbotSenderAttachment: StoryFn<ComponentProps> = Template.bind(
89
99
  {}
@@ -93,11 +103,13 @@ ChatbotSenderAttachment.args = {
93
103
  isMyMessage: true,
94
104
  hasAttachment: true,
95
105
  }
106
+ ChatbotSenderAttachment.parameters = skipInChromatic
96
107
 
97
108
  export const NoTag: StoryFn<ComponentProps> = Template.bind({})
98
109
  NoTag.args = {
99
110
  message: createMockMessage(),
100
111
  }
112
+ NoTag.parameters = skipInChromatic
101
113
 
102
114
  export const AllVariants: StoryFn = () => {
103
115
  return (
@@ -167,3 +179,9 @@ export const AllVariants: StoryFn = () => {
167
179
  </div>
168
180
  )
169
181
  }
182
+ // AllVariants is a design-review kitchen sink — every per-variant story
183
+ // (Tip, Paid, TipStandalone, ChatbotReceiverText, ChatbotSenderText,
184
+ // ChatbotSenderAttachment, NoTag) already covers its own row independently.
185
+ // Skip in Chromatic to avoid a "any one row changes → whole snapshot diffs"
186
+ // problem that masks which variant actually drifted.
187
+ AllVariants.parameters = { chromatic: { disableSnapshot: true } }
@@ -22,12 +22,18 @@ const Template: StoryFn<ComponentProps> = (args) => (
22
22
  </div>
23
23
  )
24
24
 
25
+ // Per-state stories (Unselected, GoodResponse, BadResponse) are individually
26
+ // covered by rows of AllVariants — skip in Chromatic. Interactive has no
27
+ // useful steady-state snapshot.
28
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
29
+
25
30
  export const Unselected: StoryFn<ComponentProps> = Template.bind({})
26
31
  Unselected.args = {
27
32
  selected: null,
28
33
  onVoteUp: () => {},
29
34
  onVoteDown: () => {},
30
35
  }
36
+ Unselected.parameters = skipInChromatic
31
37
 
32
38
  export const GoodResponse: StoryFn<ComponentProps> = Template.bind({})
33
39
  GoodResponse.args = {
@@ -35,6 +41,7 @@ GoodResponse.args = {
35
41
  onVoteUp: () => {},
36
42
  onVoteDown: () => {},
37
43
  }
44
+ GoodResponse.parameters = skipInChromatic
38
45
 
39
46
  export const BadResponse: StoryFn<ComponentProps> = Template.bind({})
40
47
  BadResponse.args = {
@@ -42,6 +49,7 @@ BadResponse.args = {
42
49
  onVoteUp: () => {},
43
50
  onVoteDown: () => {},
44
51
  }
52
+ BadResponse.parameters = skipInChromatic
45
53
 
46
54
  export const Interactive: StoryFn = () => {
47
55
  const [selected, setSelected] = useState<VoteSelection>(null)
@@ -55,6 +63,7 @@ export const Interactive: StoryFn = () => {
55
63
  </div>
56
64
  )
57
65
  }
66
+ Interactive.parameters = skipInChromatic
58
67
 
59
68
  export const AllVariants: StoryFn = () => (
60
69
  <div className="p-12 flex flex-col gap-6">
@@ -1,4 +1,5 @@
1
1
  import { ArrowUpIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
2
3
  import React, { useContext } from 'react'
3
4
  import {
4
5
  AttachmentPreviewList as DefaultAttachmentPreviewList,
@@ -101,9 +102,11 @@ export interface CustomMessageInputProps {
101
102
  * `disabled` is true.
102
103
  */
103
104
  disabledReason?: string
105
+ className?: string
104
106
  }
105
107
 
106
108
  export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
109
+ className,
107
110
  renderActions,
108
111
  renderFooter,
109
112
  disabled = false,
@@ -117,7 +120,12 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
117
120
  if (disabled) {
118
121
  return (
119
122
  <>
120
- <div className="messaging-composer-locked-panel flex w-full flex-col items-center justify-center gap-3 px-6 py-4">
123
+ <div
124
+ className={classNames(
125
+ 'messaging-composer-locked-panel flex w-full flex-col items-center justify-center gap-3',
126
+ className
127
+ )}
128
+ >
121
129
  {disabledReason ? (
122
130
  <p className="max-w-[345px] text-center text-xs font-normal leading-[1.3] tracking-[0.12px] text-black/40">
123
131
  {disabledReason}
@@ -129,17 +137,19 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
129
137
  )
130
138
  }
131
139
 
140
+ const actions = renderActions?.()
141
+
132
142
  return (
133
- <div className="flex flex-col gap-4 p-4">
143
+ <div className={classNames('flex flex-col gap-4', className)}>
134
144
  <div
135
145
  // @ts-expect-error Only React 19 onwards has `inert` in its types.
136
146
  inert={isFrozen ? '' : undefined}
137
147
  aria-disabled={isFrozen || undefined}
138
148
  className="message-input flex items-end gap-4 aria-disabled:opacity-40"
139
149
  >
140
- {renderActions && (
150
+ {actions && (
141
151
  <div className="flex h-12 shrink-0 items-center justify-center">
142
- {renderActions()}
152
+ {actions}
143
153
  </div>
144
154
  )}
145
155
  <ComposerLockedContext.Provider value={isFrozen}>
@@ -31,6 +31,10 @@ const Template: StoryFn<EventComponentProps> = (args) => (
31
31
  </div>
32
32
  )
33
33
 
34
+ // Per-variant stories stay in Storybook for browsing/design review but are
35
+ // individually covered by rows in AllVariants — skip in Chromatic.
36
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
37
+
34
38
  export const DmAgentPaused: StoryFn<EventComponentProps> = Template.bind({})
35
39
  DmAgentPaused.args = createStoryProps({
36
40
  text: 'DM Agent paused for this conversation',
@@ -38,6 +42,7 @@ DmAgentPaused.args = createStoryProps({
38
42
  custom_type: 'SYSTEM_DM_AGENT_PAUSED',
39
43
  },
40
44
  })
45
+ DmAgentPaused.parameters = skipInChromatic
41
46
 
42
47
  export const DmAgentResumed: StoryFn<EventComponentProps> = Template.bind({})
43
48
  DmAgentResumed.args = createStoryProps({
@@ -46,6 +51,7 @@ DmAgentResumed.args = createStoryProps({
46
51
  custom_type: 'SYSTEM_DM_AGENT_RESUMED',
47
52
  },
48
53
  })
54
+ DmAgentResumed.parameters = skipInChromatic
49
55
 
50
56
  export const AgeSafetyBlocked: StoryFn<EventComponentProps> = Template.bind({})
51
57
  AgeSafetyBlocked.args = createStoryProps({
@@ -54,6 +60,7 @@ AgeSafetyBlocked.args = createStoryProps({
54
60
  custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
55
61
  },
56
62
  })
63
+ AgeSafetyBlocked.parameters = skipInChromatic
57
64
 
58
65
  export const GenericFallback: StoryFn<EventComponentProps> = Template.bind({})
59
66
  GenericFallback.args = createStoryProps({
@@ -62,3 +69,50 @@ GenericFallback.args = createStoryProps({
62
69
  custom_type: 'MESSAGE_CHATBOT',
63
70
  },
64
71
  })
72
+ GenericFallback.parameters = skipInChromatic
73
+
74
+ export const AllVariants: StoryFn = () => {
75
+ const variants: { label: string; props: EventComponentProps }[] = [
76
+ {
77
+ label: 'DM Agent paused',
78
+ props: createStoryProps({
79
+ text: 'DM Agent paused for this conversation',
80
+ metadata: { custom_type: 'SYSTEM_DM_AGENT_PAUSED' },
81
+ }),
82
+ },
83
+ {
84
+ label: 'DM Agent resumed',
85
+ props: createStoryProps({
86
+ text: 'DM Agent resumed for this conversation',
87
+ metadata: { custom_type: 'SYSTEM_DM_AGENT_RESUMED' },
88
+ }),
89
+ },
90
+ {
91
+ label: 'Age safety blocked',
92
+ props: createStoryProps({
93
+ text: ' ',
94
+ metadata: { custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED' },
95
+ }),
96
+ },
97
+ {
98
+ label: 'Generic fallback',
99
+ props: createStoryProps({
100
+ text: 'Message activity event',
101
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
102
+ }),
103
+ },
104
+ ]
105
+
106
+ return (
107
+ <div className="flex flex-col gap-4 bg-white p-6">
108
+ {variants.map(({ label, props }) => (
109
+ <div key={label} className="flex flex-col gap-1">
110
+ <span className="text-xs text-stone">{label}</span>
111
+ <div className="w-[420px]">
112
+ <CustomSystemMessage {...props} />
113
+ </div>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ )
118
+ }
@@ -123,6 +123,13 @@ const meta: Meta<StoryProps> = {
123
123
  component: CustomTypingIndicator,
124
124
  parameters: {
125
125
  layout: 'centered',
126
+ // CustomTypingIndicator is animation-dominant (SMIL spinner + DOM
127
+ // appear/disappear on typing-state transitions). Even with the SMIL
128
+ // pause decorator, a snapshot here is mostly testing what frame the
129
+ // animation paused at — low value for visual regression, high flake
130
+ // potential. Skip it in Chromatic; the stories stay browsable in
131
+ // Storybook for design review.
132
+ chromatic: { disableSnapshot: true },
126
133
  },
127
134
  }
128
135
  export default meta
@@ -13,7 +13,17 @@ const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
13
13
 
14
14
  const meta: Meta = {
15
15
  title: 'LinkAttachment',
16
- parameters: { layout: 'fullscreen' },
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ // The single LinkApps story is a large matrix of every link-app card
19
+ // crossed with featured/classic layouts and locked/unlocked states.
20
+ // It's intended as a design-review canvas, not a regression target —
21
+ // a single pixel change in any cell would diff the whole thing. Skip
22
+ // it in Chromatic; the per-app/state behavior is exercised
23
+ // implicitly through the message-attachment stories that use these
24
+ // cards.
25
+ chromatic: { disableSnapshot: true },
26
+ },
17
27
  }
18
28
  export default meta
19
29
 
@@ -444,3 +444,7 @@ export const Received: StoryFn = () => {
444
444
  </Table>
445
445
  )
446
446
  }
447
+ // LockedAttachment.Received autoplays the video preview once paid; even with
448
+ // our SMIL/CSS animation freeze the <video> element advances frames
449
+ // independently and produces a non-deterministic snapshot. Skip in Chromatic.
450
+ Received.parameters = { chromatic: { disableSnapshot: true } }
@@ -253,44 +253,6 @@ describe('MediaMessage', () => {
253
253
  expect(container.firstChild).toBeNull()
254
254
  })
255
255
 
256
- it('uses dark card background for sent (Creator) messages', () => {
257
- const { container } = renderWithProviders(
258
- <MediaMessage
259
- isMyMessage={true}
260
- message={msg({
261
- attachments: [
262
- {
263
- type: 'image',
264
- image_url: 'https://cdn.example.com/photo.jpg',
265
- mime_type: 'image/jpeg',
266
- },
267
- ],
268
- })}
269
- />
270
- )
271
-
272
- expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
273
- })
274
-
275
- it('uses light card background for received (Visitor) messages', () => {
276
- const { container } = renderWithProviders(
277
- <MediaMessage
278
- isMyMessage={false}
279
- message={msg({
280
- attachments: [
281
- {
282
- type: 'image',
283
- image_url: 'https://cdn.example.com/photo.jpg',
284
- mime_type: 'image/jpeg',
285
- },
286
- ],
287
- })}
288
- />
289
- )
290
-
291
- expect(container.querySelector('.bg-white')).toBeInTheDocument()
292
- })
293
-
294
256
  it('shows Download action for received (Visitor) image attachment', () => {
295
257
  renderWithProviders(
296
258
  <MediaMessage
@@ -813,55 +813,20 @@ describe('MessageAttachment lazy-loading defaults', () => {
813
813
  })
814
814
 
815
815
  describe('bubbleGroupPositionFromStream', () => {
816
- it('returns "single" when not grouped', () => {
817
- expect(
818
- bubbleGroupPositionFromStream({
819
- groupedByUser: false,
820
- firstOfGroup: true,
821
- endOfGroup: true,
822
- })
823
- ).toBe('single')
824
- })
825
-
826
- it('returns "single" when both first and end are true (run of one)', () => {
827
- expect(
828
- bubbleGroupPositionFromStream({
829
- groupedByUser: true,
830
- firstOfGroup: true,
831
- endOfGroup: true,
832
- })
833
- ).toBe('single')
834
- })
835
-
836
- it('returns "first" when firstOfGroup is the only flag set', () => {
837
- expect(
838
- bubbleGroupPositionFromStream({
839
- groupedByUser: true,
840
- firstOfGroup: true,
841
- endOfGroup: false,
842
- })
843
- ).toBe('first')
844
- })
845
-
846
- it('returns "end" when endOfGroup is the only flag set', () => {
847
- expect(
848
- bubbleGroupPositionFromStream({
849
- groupedByUser: true,
850
- firstOfGroup: false,
851
- endOfGroup: true,
852
- })
853
- ).toBe('end')
854
- })
855
-
856
- it('returns "middle" when grouped but neither first nor end', () => {
857
- expect(
858
- bubbleGroupPositionFromStream({
859
- groupedByUser: true,
860
- firstOfGroup: false,
861
- endOfGroup: false,
862
- })
863
- ).toBe('middle')
864
- })
816
+ it.each([
817
+ { groupedByUser: false, firstOfGroup: true, endOfGroup: true, expected: 'single' },
818
+ { groupedByUser: true, firstOfGroup: true, endOfGroup: true, expected: 'single' },
819
+ { groupedByUser: true, firstOfGroup: true, endOfGroup: false, expected: 'first' },
820
+ { groupedByUser: true, firstOfGroup: false, endOfGroup: true, expected: 'end' },
821
+ { groupedByUser: true, firstOfGroup: false, endOfGroup: false, expected: 'middle' },
822
+ ])(
823
+ 'maps groupedByUser=$groupedByUser firstOfGroup=$firstOfGroup endOfGroup=$endOfGroup → $expected',
824
+ ({ groupedByUser, firstOfGroup, endOfGroup, expected }) => {
825
+ expect(
826
+ bubbleGroupPositionFromStream({ groupedByUser, firstOfGroup, endOfGroup })
827
+ ).toBe(expected)
828
+ }
829
+ )
865
830
  })
866
831
 
867
832
  describe('MessageAttachment.Bubble grouping', () => {
@@ -884,41 +849,17 @@ describe('MessageAttachment.Bubble grouping', () => {
884
849
  mimeType: 'application/pdf',
885
850
  }
886
851
 
887
- it('serializes groupPosition="single" for a standalone bubble', () => {
888
- renderWithProviders(
889
- <MessageAttachment.File.Sent {...FILE_PROPS} groupPosition="single" />
890
- )
891
- expect(
892
- screen.getByTestId('file-attachment').getAttribute('data-group-position')
893
- ).toBe('single')
894
- })
895
-
896
- it('serializes groupPosition="first" for the first bubble in a run', () => {
897
- renderWithProviders(
898
- <MessageAttachment.File.Sent {...FILE_PROPS} groupPosition="first" />
899
- )
900
- expect(
901
- screen.getByTestId('file-attachment').getAttribute('data-group-position')
902
- ).toBe('first')
903
- })
904
-
905
- it('serializes groupPosition="middle" for an interior bubble in a run', () => {
906
- renderWithProviders(
907
- <MessageAttachment.File.Sent {...FILE_PROPS} groupPosition="middle" />
908
- )
909
- expect(
910
- screen.getByTestId('file-attachment').getAttribute('data-group-position')
911
- ).toBe('middle')
912
- })
913
-
914
- it('serializes groupPosition="end" for the last bubble in a run', () => {
915
- renderWithProviders(
916
- <MessageAttachment.File.Sent {...FILE_PROPS} groupPosition="end" />
917
- )
918
- expect(
919
- screen.getByTestId('file-attachment').getAttribute('data-group-position')
920
- ).toBe('end')
921
- })
852
+ it.each(['single', 'first', 'middle', 'end'] as const)(
853
+ 'serializes groupPosition="%s" on the bubble',
854
+ (groupPosition) => {
855
+ renderWithProviders(
856
+ <MessageAttachment.File.Sent {...FILE_PROPS} groupPosition={groupPosition} />
857
+ )
858
+ expect(
859
+ screen.getByTestId('file-attachment').getAttribute('data-group-position')
860
+ ).toBe(groupPosition)
861
+ }
862
+ )
922
863
 
923
864
  it('serializes groupPosition on receiver-side bubbles too', () => {
924
865
  renderWithProviders(
@@ -17,14 +17,6 @@ describe('SearchInput', () => {
17
17
  expect(screen.getByPlaceholderText('Search messages...')).toBeInTheDocument();
18
18
  });
19
19
 
20
- it('renders with search icon', () => {
21
- renderWithProviders(
22
- <SearchInput searchQuery="" setSearchQuery={vi.fn()} placeholder="Search" />
23
- );
24
- const searchIcon = document.querySelector('svg');
25
- expect(searchIcon).toBeInTheDocument();
26
- });
27
-
28
20
  it('displays the current value', () => {
29
21
  renderWithProviders(
30
22
  <SearchInput searchQuery="test query" setSearchQuery={vi.fn()} placeholder="Search" />
@@ -39,12 +39,6 @@ describe('formatRelativeTime', () => {
39
39
  const date = new Date('2024-01-15T11:59:30Z') // 30 seconds ago
40
40
  expect(formatRelativeTime(date)).toBe('Just now')
41
41
  })
42
-
43
- it('should return "Just now" for messages 0 seconds ago', () => {
44
- mockDate('2024-01-15T12:00:00Z')
45
- const date = new Date('2024-01-15T12:00:00Z')
46
- expect(formatRelativeTime(date)).toBe('Just now')
47
- })
48
42
  })
49
43
 
50
44
  describe('Today', () => {
@@ -55,13 +49,6 @@ describe('formatRelativeTime', () => {
55
49
  // Should be "9:30 AM" format
56
50
  expect(result).toMatch(/^\d{1,2}:\d{2} (AM|PM)$/i)
57
51
  })
58
-
59
- it('should return time in 12-hour format for messages from late last night (same day)', () => {
60
- mockDate('2024-01-15T23:59:00Z')
61
- const date = new Date('2024-01-15T00:01:00Z')
62
- const result = formatRelativeTime(date)
63
- expect(result).toMatch(/^\d{1,2}:\d{2} (AM|PM)$/i)
64
- })
65
52
  })
66
53
 
67
54
  describe('Yesterday', () => {
@@ -92,13 +79,7 @@ describe('formatRelativeTime', () => {
92
79
  expect(formatRelativeTime(date)).toBe('2d')
93
80
  })
94
81
 
95
- it('should return "3d" for messages 3 days ago', () => {
96
- mockDate('2024-01-15T12:00:00Z')
97
- const date = new Date('2024-01-12T12:00:00Z')
98
- expect(formatRelativeTime(date)).toBe('3d')
99
- })
100
-
101
- it('should return "6d" for messages 6 days ago', () => {
82
+ it('should return "6d" for messages 6 days ago (boundary before weeks)', () => {
102
83
  mockDate('2024-01-15T12:00:00Z')
103
84
  const date = new Date('2024-01-09T12:00:00Z')
104
85
  expect(formatRelativeTime(date)).toBe('6d')
@@ -111,18 +92,6 @@ describe('formatRelativeTime', () => {
111
92
  const date = new Date('2024-01-08T12:00:00Z')
112
93
  expect(formatRelativeTime(date)).toBe('1w')
113
94
  })
114
-
115
- it('should return "2w" for messages 2 weeks ago', () => {
116
- mockDate('2024-01-15T12:00:00Z')
117
- const date = new Date('2024-01-01T12:00:00Z')
118
- expect(formatRelativeTime(date)).toBe('2w')
119
- })
120
-
121
- it('should return "3w" for messages 3 weeks ago', () => {
122
- mockDate('2024-01-29T12:00:00Z')
123
- const date = new Date('2024-01-08T12:00:00Z')
124
- expect(formatRelativeTime(date)).toBe('3w')
125
- })
126
95
  })
127
96
 
128
97
  describe('Date format', () => {