@linktr.ee/messaging-react 3.3.4 → 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 (56) hide show
  1. package/dist/{Card-DLmUSU4A.cjs → Card-BlviN8Fb.cjs} +2 -2
  2. package/dist/{Card-DLmUSU4A.cjs.map → Card-BlviN8Fb.cjs.map} +1 -1
  3. package/dist/{Card-DmPpcrSU.js → Card-C4ncqjxJ.js} +2 -2
  4. package/dist/{Card-DmPpcrSU.js.map → Card-C4ncqjxJ.js.map} +1 -1
  5. package/dist/{Card-0BgubwgM.cjs → Card-Cn7Zxc6U.cjs} +2 -2
  6. package/dist/{Card-0BgubwgM.cjs.map → Card-Cn7Zxc6U.cjs.map} +1 -1
  7. package/dist/{Card-DchJqvYq.js → Card-DE5bfj0l.js} +2 -2
  8. package/dist/{Card-DchJqvYq.js.map → Card-DE5bfj0l.js.map} +1 -1
  9. package/dist/{Card-B7AF5uOB.js → Card-IjOI7UXs.js} +3 -3
  10. package/dist/{Card-B7AF5uOB.js.map → Card-IjOI7UXs.js.map} +1 -1
  11. package/dist/{Card-CvBbAoUo.cjs → Card-KgQxeR-B.cjs} +2 -2
  12. package/dist/{Card-CvBbAoUo.cjs.map → Card-KgQxeR-B.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-BQjA4HaB.js → LockedThumbnail-4-54cyJG.js} +2 -2
  14. package/dist/{LockedThumbnail-BQjA4HaB.js.map → LockedThumbnail-4-54cyJG.js.map} +1 -1
  15. package/dist/{LockedThumbnail-D9fSb4N-.cjs → LockedThumbnail-DL5NZzWJ.cjs} +2 -2
  16. package/dist/{LockedThumbnail-D9fSb4N-.cjs.map → LockedThumbnail-DL5NZzWJ.cjs.map} +1 -1
  17. package/dist/{index-BcHUpyyw.js → index-C2wfgpUU.js} +855 -823
  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 +226 -0
  26. package/src/components/Avatar/Avatar.stories.tsx +20 -0
  27. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +33 -8
  28. package/src/components/ChannelList/ChannelList.stories.tsx +5 -0
  29. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +77 -47
  30. package/src/components/ChannelView.stories.tsx +8 -7
  31. package/src/components/ChannelView.test.tsx +12 -1
  32. package/src/components/ChannelView.tsx +34 -17
  33. package/src/components/CloseButton/CloseButton.stories.tsx +31 -0
  34. package/src/components/CustomDateSeparator/CustomDateSeparator.stories.tsx +33 -0
  35. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.stories.tsx +63 -0
  36. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +57 -0
  37. package/src/components/CustomLinkPreviewList/index.tsx +2 -54
  38. package/src/components/CustomMessage/CustomMessage.stories.tsx +3 -18
  39. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +13 -0
  40. package/src/components/CustomMessage/MessageTag.stories.tsx +22 -2
  41. package/src/components/CustomMessage/MessageVoteButtons.stories.tsx +9 -0
  42. package/src/components/CustomMessageInput/index.tsx +14 -4
  43. package/src/components/CustomSystemMessage/CustomSystemMessage.stories.tsx +54 -0
  44. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +7 -0
  45. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +11 -1
  46. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +4 -0
  47. package/src/components/MediaMessage/MediaMessage.stories.tsx +4 -2
  48. package/src/components/MediaMessage/MediaMessage.test.tsx +0 -38
  49. package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -84
  50. package/src/components/SearchInput/SearchInput.test.tsx +0 -8
  51. package/src/hooks/useChannelModerationActions.ts +32 -14
  52. package/src/stories/decorators/storyTime.ts +31 -0
  53. package/src/utils/formatRelativeTime.test.ts +1 -32
  54. package/dist/index-BcHUpyyw.js.map +0 -1
  55. package/dist/index-DTZNltUC.cjs +0 -2
  56. package/dist/index-DTZNltUC.cjs.map +0 -1
@@ -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" />
@@ -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
- const [isCheckingBlockedStatus, setIsCheckingBlockedStatus] =
80
- useState(false)
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
- setIsCheckingBlockedStatus(false)
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
- if (!cancelled) setIsCheckingBlockedStatus(false)
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
- }, [enabled, service, participant?.user?.id, showBlockParticipant, logLabel])
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)
@@ -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', () => {