@linktr.ee/messaging-react 3.6.1 → 3.7.0-rc-1783304153

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 (39) hide show
  1. package/dist/{Card-CxS7GNqp.js → Card-BAt0xifq.js} +2 -2
  2. package/dist/{Card-CxS7GNqp.js.map → Card-BAt0xifq.js.map} +1 -1
  3. package/dist/{Card-B0lzpuYf.js → Card-BB34_MDl.js} +3 -3
  4. package/dist/{Card-B0lzpuYf.js.map → Card-BB34_MDl.js.map} +1 -1
  5. package/dist/{Card-DcUPqd79.cjs → Card-BDKGSydN.cjs} +2 -2
  6. package/dist/{Card-DcUPqd79.cjs.map → Card-BDKGSydN.cjs.map} +1 -1
  7. package/dist/{Card-BuR6mbUu.cjs → Card-BGJY5HWf.cjs} +2 -2
  8. package/dist/{Card-BuR6mbUu.cjs.map → Card-BGJY5HWf.cjs.map} +1 -1
  9. package/dist/{Card-BrC7mKtr.js → Card-C3-Ea5ho.js} +2 -2
  10. package/dist/{Card-BrC7mKtr.js.map → Card-C3-Ea5ho.js.map} +1 -1
  11. package/dist/{Card-d0_xEfdM.cjs → Card-DnFtpG1h.cjs} +2 -2
  12. package/dist/{Card-d0_xEfdM.cjs.map → Card-DnFtpG1h.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-BDtKMJjz.cjs → LockedThumbnail-Bl9UC9sQ.cjs} +2 -2
  14. package/dist/{LockedThumbnail-BDtKMJjz.cjs.map → LockedThumbnail-Bl9UC9sQ.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-BaDgF8qJ.js → LockedThumbnail-so7ftBnG.js} +2 -2
  16. package/dist/{LockedThumbnail-BaDgF8qJ.js.map → LockedThumbnail-so7ftBnG.js.map} +1 -1
  17. package/dist/{index-BPfehHDD.js → index-2HMjfjM-.js} +1013 -1006
  18. package/dist/index-2HMjfjM-.js.map +1 -0
  19. package/dist/index-BgzqZorr.cjs +2 -0
  20. package/dist/index-BgzqZorr.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +14 -2
  23. package/dist/index.js +1 -1
  24. package/dist/testing.cjs +2 -0
  25. package/dist/testing.cjs.map +1 -0
  26. package/dist/testing.d.ts +116 -0
  27. package/dist/testing.js +107 -0
  28. package/dist/testing.js.map +1 -0
  29. package/package.json +7 -2
  30. package/src/providers/MessagingProvider.test.tsx +30 -0
  31. package/src/providers/MessagingProvider.tsx +24 -5
  32. package/src/testing/createMockMessagingClient.stories.tsx +77 -0
  33. package/src/testing/createMockMessagingClient.ts +231 -0
  34. package/src/testing/index.ts +7 -0
  35. package/src/types.ts +15 -2
  36. package/dist/index-BPfehHDD.js.map +0 -1
  37. package/dist/index-G4pnfm4m.cjs +0 -2
  38. package/dist/index-G4pnfm4m.cjs.map +0 -1
  39. /package/dist/assets/{index.css → messaging-react.css} +0 -0
@@ -0,0 +1,231 @@
1
+ import type {
2
+ Channel,
3
+ MessageResponse,
4
+ QueryChannelAPIResponse,
5
+ SendMessageAPIResponse,
6
+ } from 'stream-chat'
7
+ import { StreamChat } from 'stream-chat'
8
+
9
+ /** A participant in the mock conversation. */
10
+ export interface MockMessagingUser {
11
+ id: string
12
+ name?: string
13
+ image?: string
14
+ }
15
+
16
+ /**
17
+ * A seed message, authored from the viewer's perspective. `from: 'me'` is the
18
+ * connected user (the viewer); `from: 'them'` is the other participant.
19
+ */
20
+ export interface MockMessage {
21
+ id?: string
22
+ text?: string
23
+ from: 'me' | 'them'
24
+ /** Stream attachment payloads, rendered by the toolkit's attachment gate. */
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ attachments?: any[]
27
+ /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */
28
+ metadata?: Record<string, unknown>
29
+ }
30
+
31
+ export interface CreateMockMessagingClientOptions {
32
+ /** The connected/viewing user. */
33
+ currentUser: MockMessagingUser
34
+ /** The other party in the direct conversation (e.g. the creator/linker). */
35
+ participant: MockMessagingUser
36
+ /** Seed conversation, oldest-first. */
37
+ messages?: MockMessage[]
38
+ /** Channel id; defaults to `mock-dm`. */
39
+ channelId?: string
40
+ /**
41
+ * Optional canned reply. Called after the viewer sends a message; return the
42
+ * reply text (or a partial message) to have `participant` echo a response, or
43
+ * a falsy value for no reply. Drives the "send echo" in `dev:mock-messaging`.
44
+ */
45
+ onSend?: (
46
+ text: string
47
+ ) => string | { text?: string; attachments?: unknown[] } | null | undefined
48
+ }
49
+
50
+ export interface MockMessagingClient {
51
+ /** Pass to `<MessagingProvider client={...}>`. */
52
+ client: StreamChat
53
+ /** The seeded direct-conversation channel. */
54
+ channel: Channel
55
+ /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */
56
+ participantFilterId: string
57
+ }
58
+
59
+ const REPLY_DELAY_MS = 600
60
+
61
+ /**
62
+ * Builds an offline Stream client seeded with a single direct conversation, so
63
+ * consumers can render the **real** messaging UI (MessagingProvider →
64
+ * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`
65
+ * surfaces and Storybook. It is a real `StreamChat` whose network calls
66
+ * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are
67
+ * replaced with in-memory behaviour driven by Stream's own channel-state
68
+ * machinery, so rendering fidelity matches production.
69
+ *
70
+ * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by
71
+ * production code.
72
+ */
73
+ export function createMockMessagingClient({
74
+ currentUser,
75
+ participant,
76
+ messages = [],
77
+ channelId = 'mock-dm',
78
+ onSend,
79
+ }: CreateMockMessagingClientOptions): MockMessagingClient {
80
+ const client = new StreamChat('mock-api-key', {
81
+ allowServerSideConnect: true,
82
+ })
83
+
84
+ // Mark connected without opening a WebSocket. `MessagingProvider` renders
85
+ // `<Chat client={client}>` directly for an injected client, so it never
86
+ // calls connectUser itself.
87
+ client.connectUser = async () => ({ me: currentUser }) as never
88
+ client.disconnectUser = async () => undefined
89
+ client.userID = currentUser.id
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ client.user = currentUser as any
92
+
93
+ const cid = `messaging:${channelId}`
94
+
95
+ const toStreamMessage = (
96
+ message: MockMessage,
97
+ index: number
98
+ ): MessageResponse => {
99
+ const author = message.from === 'me' ? currentUser : participant
100
+ // Stagger timestamps oldest-first so the list orders naturally.
101
+ const createdAt = new Date(
102
+ Date.now() - (messages.length - index) * 60_000
103
+ ).toISOString()
104
+ return {
105
+ id: message.id ?? `mock-msg-${index}`,
106
+ text: message.text ?? '',
107
+ type: 'regular',
108
+ html: message.text ? `<p>${message.text}</p>` : '',
109
+ user: author,
110
+ attachments: message.attachments ?? [],
111
+ latest_reactions: [],
112
+ own_reactions: [],
113
+ reaction_counts: {},
114
+ reaction_scores: {},
115
+ reply_count: 0,
116
+ status: 'received',
117
+ cid,
118
+ created_at: createdAt,
119
+ updated_at: createdAt,
120
+ mentioned_users: [],
121
+ ...(message.metadata ? { metadata: message.metadata } : {}),
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ } as any
124
+ }
125
+
126
+ const seededMessages = messages.map(toStreamMessage)
127
+
128
+ const memberState = {
129
+ [currentUser.id]: {
130
+ user: { ...currentUser, is_account: false },
131
+ user_id: currentUser.id,
132
+ role: 'owner',
133
+ is_account: false,
134
+ },
135
+ [participant.id]: {
136
+ user: { ...participant, is_account: true },
137
+ user_id: participant.id,
138
+ role: 'member',
139
+ is_account: true,
140
+ },
141
+ }
142
+
143
+ const channel = client.channel('messaging', channelId, {
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ members: [currentUser.id, participant.id] as any,
146
+ })
147
+
148
+ // Seed state locally instead of fetching. Mirrors the proven ChannelView
149
+ // story mock: override `watch` to populate `state` and return a canned
150
+ // QueryChannelAPIResponse.
151
+ channel.watch = async () => {
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ channel.state.messages = seededMessages as unknown as any[]
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ channel.state.members = memberState as unknown as any
156
+ return {
157
+ channel: { members: [currentUser.id, participant.id] },
158
+ members: [],
159
+ messages: seededMessages,
160
+ watchers: [],
161
+ pinned_messages: [],
162
+ duration: '0ms',
163
+ } as unknown as QueryChannelAPIResponse
164
+ }
165
+
166
+ const appendMessage = (message: MessageResponse) => {
167
+ // addMessageSorted is Stream's own state mutation (dedupes by id), so the
168
+ // toolkit re-renders through the real channel-state path.
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ channel.state.addMessageSorted(message as any)
171
+ client.dispatchEvent({
172
+ type: 'message.new',
173
+ cid,
174
+ message,
175
+ user: message.user ?? undefined,
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ } as any)
178
+ }
179
+
180
+ let sentCount = 0
181
+ channel.sendMessage = async (message) => {
182
+ const text =
183
+ typeof message === 'string'
184
+ ? message
185
+ : ((message as { text?: string })?.text ?? '')
186
+ const sent = {
187
+ ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ ...(typeof message === 'object' ? (message as any) : {}),
190
+ user: currentUser,
191
+ cid,
192
+ } as MessageResponse
193
+
194
+ // stream-chat-react adds the outgoing message optimistically; addMessageSorted
195
+ // dedupes by id, so confirming here is safe and idempotent.
196
+ appendMessage(sent)
197
+
198
+ const reply = onSend?.(text)
199
+ if (reply) {
200
+ const replyText = typeof reply === 'string' ? reply : reply.text
201
+ const replyAttachments =
202
+ typeof reply === 'string' ? undefined : reply.attachments
203
+ setTimeout(() => {
204
+ appendMessage(
205
+ toStreamMessage(
206
+ {
207
+ text: replyText,
208
+ from: 'them',
209
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
210
+ attachments: replyAttachments as any,
211
+ id: `mock-reply-${sentCount}`,
212
+ },
213
+ seededMessages.length
214
+ )
215
+ )
216
+ }, REPLY_DELAY_MS)
217
+ }
218
+
219
+ return { message: sent } as unknown as SendMessageAPIResponse
220
+ }
221
+
222
+ // MessagingShell finds the direct conversation via queryChannels; always
223
+ // return the one seeded channel regardless of the filter.
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ client.queryChannels = async () => [channel] as any
226
+
227
+ // Initialise state up-front so first render already has the conversation.
228
+ void channel.watch()
229
+
230
+ return { client, channel, participantFilterId: participant.id }
231
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ createMockMessagingClient,
3
+ type CreateMockMessagingClientOptions,
4
+ type MockMessage,
5
+ type MockMessagingClient,
6
+ type MockMessagingUser,
7
+ } from './createMockMessagingClient'
package/src/types.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  ChannelSort,
11
11
  LocalMessage,
12
12
  SendMessageAPIResponse,
13
+ StreamChat,
13
14
  } from 'stream-chat'
14
15
  import type {
15
16
  ChannelPreviewUIComponentProps,
@@ -448,8 +449,20 @@ export interface MessagingShellProps extends ChannelViewPassthroughProps {
448
449
  export interface MessagingProviderProps {
449
450
  children: React.ReactNode
450
451
  user: MessagingUser | null
451
- serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>
452
- apiKey: string
452
+ /**
453
+ * An already-constructed Stream client. When provided it takes precedence
454
+ * over `apiKey`: the provider renders `<Chat>` against this exact instance
455
+ * and does NOT create a StreamChatService or call `connectUser` — the caller
456
+ * owns the client's connection lifecycle. This is the seam for offline/dev
457
+ * (`createMockMessagingClient` from `@linktr.ee/messaging-react/testing`) and
458
+ * for tests, so consumers render the real UI instead of forking it.
459
+ * `apiKey`/`serviceConfig` are ignored while `client` is set.
460
+ */
461
+ client?: StreamChat
462
+ /** Required unless `client` is provided. */
463
+ serviceConfig?: Omit<StreamChatServiceConfig, 'apiKey'>
464
+ /** Required unless `client` is provided. */
465
+ apiKey?: string
453
466
  capabilities?: MessagingCapabilities
454
467
  debug?: boolean
455
468
  }