@linktr.ee/messaging-react 3.7.0-rc-1783333034 → 3.7.0-rc-1783333932

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/testing.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const k=require("stream-chat"),v=600;function w({currentUser:s,participant:o,messages:m=[],channelId:y="mock-dm",onSend:d}){const n=new k.StreamChat("mock-api-key",{allowServerSideConnect:!0});n.connectUser=async()=>({me:s}),n.disconnectUser=async()=>{},n.userID=s.id,n.user=s,n.userMuteStatus=(()=>!1);const r=`messaging:${y}`,u=(t,c)=>{const i=t.from==="me"?s:o,a=new Date(Date.now()-(m.length-c)*6e4);return{id:t.id??`mock-msg-${c}`,text:t.text??"",type:"regular",html:t.text?`<p>${t.text}</p>`:"",user:i,attachments:t.attachments??[],latest_reactions:[],own_reactions:[],reaction_counts:{},reaction_scores:{},reply_count:0,status:"received",cid:r,created_at:a,updated_at:a,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},l=m.map(u),p={[s.id]:{user:{...s,is_account:!1},user_id:s.id,role:"owner",is_account:!1},[o.id]:{user:{...o,is_account:!0},user_id:o.id,role:"member",is_account:!0}},e=n.channel("messaging",y,{members:[s.id,o.id]});e.muteStatus=(()=>({muted:!1,createdAt:null,expiresAt:null})),e.markRead=(async()=>({})),e.keystroke=(async()=>{}),e.stopTyping=(async()=>{}),e.sendReaction=(async()=>({})),e.deleteReaction=(async()=>({})),e.countUnread=(()=>0),e.query=(async()=>({messages:[]}));let h=!1;e.watch=async()=>(h||(e.state.messages=l,h=!0),e.state.members=p,{channel:{members:[s.id,o.id]},members:[],messages:e.state.messages,watchers:[],pinned_messages:[],duration:"0ms"});const _=t=>{e.state.addMessageSorted(t),n.dispatchEvent({type:"message.new",cid:r,message:t,user:t.user??void 0})};let f=0;return e.sendMessage=async t=>{const c=typeof t=="string"?t:(t==null?void 0:t.text)??"",i={...u({text:c,from:"me",id:`mock-sent-${f++}`},l.length),...typeof t=="object"?t:{},user:s,cid:r};_(i);const a=d==null?void 0:d(c);if(a){const g=typeof a=="string"?a:a.text,M=typeof a=="string"?void 0:a.attachments;setTimeout(()=>{_(u({text:g,from:"them",attachments:M,id:`mock-reply-${f}`},l.length))},v)}return{message:i}},n.queryChannels=async()=>[e],e.watch(),{client:n,channel:e,participantFilterId:o.id}}exports.createMockMessagingClient=w;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const k=require("stream-chat");function b(s,{stubConnection:n=!1}={}){const a=new k.StreamChat("mock-api-key",{allowServerSideConnect:!0});return a.userID=s.id,a.user=s,n&&(a.connectUser=async()=>({me:s}),a.disconnectUser=async()=>{},a.userMuteStatus=(()=>!1)),a}const v=600;function w({currentUser:s,participant:n,messages:a=[],channelId:y="mock-dm",onSend:d}){const r=b(s,{stubConnection:!0}),u=`messaging:${y}`,l=(t,c)=>{const i=t.from==="me"?s:n,o=new Date(Date.now()-(a.length-c)*6e4);return{id:t.id??`mock-msg-${c}`,text:t.text??"",type:"regular",html:t.text?`<p>${t.text}</p>`:"",user:i,attachments:t.attachments??[],latest_reactions:[],own_reactions:[],reaction_counts:{},reaction_scores:{},reply_count:0,status:"received",cid:u,created_at:o,updated_at:o,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},m=a.map(l),p={[s.id]:{user:{...s,is_account:!1},user_id:s.id,role:"owner",is_account:!1},[n.id]:{user:{...n,is_account:!0},user_id:n.id,role:"member",is_account:!0}},e=r.channel("messaging",y,{members:[s.id,n.id]});e.muteStatus=(()=>({muted:!1,createdAt:null,expiresAt:null})),e.markRead=(async()=>({})),e.keystroke=(async()=>{}),e.stopTyping=(async()=>{}),e.sendReaction=(async()=>({})),e.deleteReaction=(async()=>({})),e.countUnread=(()=>0),e.query=(async()=>({messages:[]}));let h=!1;e.watch=async()=>(h||(e.state.messages=m,h=!0),e.state.members=p,{channel:{members:[s.id,n.id]},members:[],messages:e.state.messages,watchers:[],pinned_messages:[],duration:"0ms"});const f=t=>{e.state.addMessageSorted(t),r.dispatchEvent({type:"message.new",cid:u,message:t,user:t.user??void 0})};let _=0;return e.sendMessage=async t=>{const c=typeof t=="string"?t:(t==null?void 0:t.text)??"",i={...l({text:c,from:"me",id:`mock-sent-${_++}`},m.length),...typeof t=="object"?t:{},user:s,cid:u};f(i);const o=d==null?void 0:d(c);if(o){const g=typeof o=="string"?o:o.text,M=typeof o=="string"?void 0:o.attachments;setTimeout(()=>{f(l({text:g,from:"them",attachments:M,id:`mock-reply-${_}`},m.length))},v)}return{message:i}},r.queryChannels=async()=>[e],e.watch(),{client:r,channel:e,participantFilterId:n.id}}exports.createMockMessagingClient=w;
2
2
  //# sourceMappingURL=testing.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.cjs","sources":["../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n\n // Mark connected without opening a WebSocket. `MessagingProvider` renders\n // `<Chat client={client}>` directly for an injected client, so it never\n // calls connectUser itself.\n client.connectUser = async () => ({ me: currentUser }) as never\n client.disconnectUser = async () => undefined\n client.userID = currentUser.id\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.user = currentUser as any\n // Handling a `message.new` event (dispatched on send + echo below) runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws\n // \"Make sure to await connectUser() first.\" without a live WebSocket. Stub\n // the mute lookups so offline event dispatch doesn't throw.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.userMuteStatus = (() => false) as any\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.muteStatus = (() => ({ muted: false, createdAt: null, expiresAt: null })) as any\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","client","StreamChat","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
1
+ {"version":3,"file":"testing.cjs","sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\nimport type { MockMessagingUser } from './createMockMessagingClient'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport { createMockStreamChatClient } from './createMockStreamChatClient'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.muteStatus = (() => ({ muted: false, createdAt: null, expiresAt: null })) as any\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["createMockStreamChatClient","user","stubConnection","client","StreamChat","REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
package/dist/testing.js CHANGED
@@ -1,25 +1,29 @@
1
1
  import { StreamChat as x } from "stream-chat";
2
+ function M(s, { stubConnection: n = !1 } = {}) {
3
+ const a = new x("mock-api-key", {
4
+ allowServerSideConnect: !0
5
+ });
6
+ return a.userID = s.id, a.user = s, n && (a.connectUser = async () => ({ me: s }), a.disconnectUser = async () => {
7
+ }, a.userMuteStatus = (() => !1)), a;
8
+ }
2
9
  const g = 600;
3
- function M({
10
+ function b({
4
11
  currentUser: s,
5
- participant: o,
6
- messages: l = [],
12
+ participant: n,
13
+ messages: a = [],
7
14
  channelId: y = "mock-dm",
8
- onSend: i
15
+ onSend: d
9
16
  }) {
10
- const n = new x("mock-api-key", {
11
- allowServerSideConnect: !0
12
- });
13
- n.connectUser = async () => ({ me: s }), n.disconnectUser = async () => {
14
- }, n.userID = s.id, n.user = s, n.userMuteStatus = (() => !1);
15
- const r = `messaging:${y}`, m = (t, c) => {
16
- const d = t.from === "me" ? s : o, a = new Date(Date.now() - (l.length - c) * 6e4);
17
+ const r = M(s, {
18
+ stubConnection: !0
19
+ }), u = `messaging:${y}`, m = (t, c) => {
20
+ const i = t.from === "me" ? s : n, o = new Date(Date.now() - (a.length - c) * 6e4);
17
21
  return {
18
22
  id: t.id ?? `mock-msg-${c}`,
19
23
  text: t.text ?? "",
20
24
  type: "regular",
21
25
  html: t.text ? `<p>${t.text}</p>` : "",
22
- user: d,
26
+ user: i,
23
27
  attachments: t.attachments ?? [],
24
28
  latest_reactions: [],
25
29
  own_reactions: [],
@@ -27,83 +31,83 @@ function M({
27
31
  reaction_scores: {},
28
32
  reply_count: 0,
29
33
  status: "received",
30
- cid: r,
31
- created_at: a,
32
- updated_at: a,
34
+ cid: u,
35
+ created_at: o,
36
+ updated_at: o,
33
37
  mentioned_users: [],
34
38
  ...t.metadata ? { metadata: t.metadata } : {}
35
39
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
40
  };
37
- }, u = l.map(m), p = {
41
+ }, l = a.map(m), p = {
38
42
  [s.id]: {
39
43
  user: { ...s, is_account: !1 },
40
44
  user_id: s.id,
41
45
  role: "owner",
42
46
  is_account: !1
43
47
  },
44
- [o.id]: {
45
- user: { ...o, is_account: !0 },
46
- user_id: o.id,
48
+ [n.id]: {
49
+ user: { ...n, is_account: !0 },
50
+ user_id: n.id,
47
51
  role: "member",
48
52
  is_account: !0
49
53
  }
50
- }, e = n.channel("messaging", y, {
54
+ }, e = r.channel("messaging", y, {
51
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- members: [s.id, o.id]
56
+ members: [s.id, n.id]
53
57
  });
54
58
  e.muteStatus = (() => ({ muted: !1, createdAt: null, expiresAt: null })), e.markRead = (async () => ({})), e.keystroke = (async () => {
55
59
  }), e.stopTyping = (async () => {
56
60
  }), e.sendReaction = (async () => ({})), e.deleteReaction = (async () => ({})), e.countUnread = (() => 0), e.query = (async () => ({ messages: [] }));
57
61
  let h = !1;
58
- e.watch = async () => (h || (e.state.messages = u, h = !0), e.state.members = p, {
59
- channel: { members: [s.id, o.id] },
62
+ e.watch = async () => (h || (e.state.messages = l, h = !0), e.state.members = p, {
63
+ channel: { members: [s.id, n.id] },
60
64
  members: [],
61
65
  messages: e.state.messages,
62
66
  watchers: [],
63
67
  pinned_messages: [],
64
68
  duration: "0ms"
65
69
  });
66
- const _ = (t) => {
67
- e.state.addMessageSorted(t), n.dispatchEvent({
70
+ const f = (t) => {
71
+ e.state.addMessageSorted(t), r.dispatchEvent({
68
72
  type: "message.new",
69
- cid: r,
73
+ cid: u,
70
74
  message: t,
71
75
  user: t.user ?? void 0
72
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
77
  });
74
78
  };
75
- let f = 0;
79
+ let _ = 0;
76
80
  return e.sendMessage = async (t) => {
77
- const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", d = {
78
- ...m({ text: c, from: "me", id: `mock-sent-${f++}` }, u.length),
81
+ const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", i = {
82
+ ...m({ text: c, from: "me", id: `mock-sent-${_++}` }, l.length),
79
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
84
  ...typeof t == "object" ? t : {},
81
85
  user: s,
82
- cid: r
86
+ cid: u
83
87
  };
84
- _(d);
85
- const a = i == null ? void 0 : i(c);
86
- if (a) {
87
- const k = typeof a == "string" ? a : a.text, w = typeof a == "string" ? void 0 : a.attachments;
88
+ f(i);
89
+ const o = d == null ? void 0 : d(c);
90
+ if (o) {
91
+ const k = typeof o == "string" ? o : o.text, w = typeof o == "string" ? void 0 : o.attachments;
88
92
  setTimeout(() => {
89
- _(
93
+ f(
90
94
  m(
91
95
  {
92
96
  text: k,
93
97
  from: "them",
94
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
99
  attachments: w,
96
- id: `mock-reply-${f}`
100
+ id: `mock-reply-${_}`
97
101
  },
98
- u.length
102
+ l.length
99
103
  )
100
104
  );
101
105
  }, g);
102
106
  }
103
- return { message: d };
104
- }, n.queryChannels = async () => [e], e.watch(), { client: n, channel: e, participantFilterId: o.id };
107
+ return { message: i };
108
+ }, r.queryChannels = async () => [e], e.watch(), { client: r, channel: e, participantFilterId: n.id };
105
109
  }
106
110
  export {
107
- M as createMockMessagingClient
111
+ b as createMockMessagingClient
108
112
  };
109
113
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sources":["../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n\n // Mark connected without opening a WebSocket. `MessagingProvider` renders\n // `<Chat client={client}>` directly for an injected client, so it never\n // calls connectUser itself.\n client.connectUser = async () => ({ me: currentUser }) as never\n client.disconnectUser = async () => undefined\n client.userID = currentUser.id\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.user = currentUser as any\n // Handling a `message.new` event (dispatched on send + echo below) runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws\n // \"Make sure to await connectUser() first.\" without a live WebSocket. Stub\n // the mute lookups so offline event dispatch doesn't throw.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.userMuteStatus = (() => false) as any\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.muteStatus = (() => ({ muted: false, createdAt: null, expiresAt: null })) as any\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","client","StreamChat","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
1
+ {"version":3,"file":"testing.js","sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\nimport type { MockMessagingUser } from './createMockMessagingClient'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport { createMockStreamChatClient } from './createMockStreamChatClient'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.muteStatus = (() => ({ muted: false, createdAt: null, expiresAt: null })) as any\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["createMockStreamChatClient","user","stubConnection","client","StreamChat","REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.7.0-rc-1783333034",
3
+ "version": "3.7.0-rc-1783333932",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@linktr.ee/component-library": "11.8.6",
53
- "@linktr.ee/messaging-core": "2.3.0-rc-1783333034",
53
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783333932",
54
54
  "@phosphor-icons/react": "^2.1.10"
55
55
  },
56
56
  "devDependencies": {
@@ -11,6 +11,7 @@ import { Chat } from 'stream-chat-react'
11
11
 
12
12
  import { hoursAgo, minutesAgo, now } from '../stories/decorators/storyTime'
13
13
  import { mockParticipants } from '../stories/mocks'
14
+ import { createMockStreamChatClient } from '../testing/createMockStreamChatClient'
14
15
 
15
16
  import { ChannelView } from './ChannelView'
16
17
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
@@ -227,14 +228,7 @@ const Template: StoryFn<TemplateProps> = (args) => {
227
228
  typingUser,
228
229
  ...channelViewProps
229
230
  } = args
230
- const [client] = React.useState(() => {
231
- const client = new StreamChat('mock-api-key', {
232
- allowServerSideConnect: true,
233
- })
234
- client.userID = mockUser.id
235
- client.user = mockUser
236
- return client
237
- })
231
+ const [client] = React.useState(() => createMockStreamChatClient(mockUser))
238
232
 
239
233
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
240
234
 
@@ -509,14 +503,7 @@ WithMessageDecoration.parameters = {
509
503
  }
510
504
 
511
505
  const WithStarButtonTemplate: StoryFn<ComponentProps> = (args) => {
512
- const [client] = React.useState(() => {
513
- const client = new StreamChat('mock-api-key', {
514
- allowServerSideConnect: true,
515
- })
516
- client.userID = mockUser.id
517
- client.user = mockUser
518
- return client
519
- })
506
+ const [client] = React.useState(() => createMockStreamChatClient(mockUser))
520
507
 
521
508
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
522
509
 
@@ -591,14 +578,7 @@ WithStarButton.parameters = {
591
578
  }
592
579
 
593
580
  const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
594
- const [client] = React.useState(() => {
595
- const client = new StreamChat('mock-api-key', {
596
- allowServerSideConnect: true,
597
- })
598
- client.userID = mockUser.id
599
- client.user = mockUser
600
- return client
601
- })
581
+ const [client] = React.useState(() => createMockStreamChatClient(mockUser))
602
582
 
603
583
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
604
584
 
@@ -22,6 +22,7 @@ import {
22
22
  StoryUser,
23
23
  storyUsers,
24
24
  } from '../../stories/decorators/storyUser'
25
+ import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient'
25
26
  import CustomTypingIndicator from '../CustomTypingIndicator'
26
27
 
27
28
  import { CustomMessageActions } from './CustomMessageActions'
@@ -134,12 +135,7 @@ const TemplateInner: React.FC<{
134
135
  typingUser?: StoryUser
135
136
  messages: TemplateProps['messages']
136
137
  }> = ({ currentUser, typingUser, messages }) => {
137
- const [client] = React.useState(() => {
138
- const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
139
- c.userID = currentUser.id
140
- c.user = currentUser
141
- return c
142
- })
138
+ const [client] = React.useState(() => createMockStreamChatClient(currentUser))
143
139
 
144
140
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
145
141
 
@@ -464,12 +460,7 @@ const WithActionsTemplateInner: React.FC<{
464
460
  currentUser: StoryUser
465
461
  messages: TemplateProps['messages']
466
462
  }> = ({ currentUser, messages }) => {
467
- const [client] = React.useState(() => {
468
- const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
469
- c.userID = currentUser.id
470
- c.user = currentUser
471
- return c
472
- })
463
+ const [client] = React.useState(() => createMockStreamChatClient(currentUser))
473
464
 
474
465
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
475
466
 
@@ -4,6 +4,7 @@ import React, { useEffect } from 'react'
4
4
  import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
5
5
  import { Channel, Chat, type SendButtonProps } from 'stream-chat-react'
6
6
 
7
+ import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient'
7
8
  import LockedAttachment from '../LockedAttachment'
8
9
 
9
10
  import { CustomMessageInput } from '.'
@@ -79,12 +80,7 @@ type WrapperProps = {
79
80
  const Wrapper: React.FC<WrapperProps> = (props) => {
80
81
  const { frozen, renderActions, sendButton, attachmentPreviewList, renderFooter } = props
81
82
 
82
- const [client] = React.useState(() => {
83
- const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
84
- c.userID = mockUser.id
85
- c.user = mockUser
86
- return c
87
- })
83
+ const [client] = React.useState(() => createMockStreamChatClient(mockUser))
88
84
 
89
85
  const [channel, setChannel] = React.useState<ReturnType<StreamChat['channel']> | null>(null)
90
86
 
@@ -1,7 +1,8 @@
1
1
  import React from 'react'
2
- import { ConnectionOpen, StreamChat } from 'stream-chat'
3
2
  import { Chat } from 'stream-chat-react'
4
3
 
4
+ import { createMockStreamChatClient } from '../testing/createMockStreamChatClient'
5
+
5
6
  // Mock Stream Chat client for Storybook
6
7
  const mockUser = {
7
8
  id: 'storybook-user',
@@ -9,25 +10,8 @@ const mockUser = {
9
10
  image: 'https://i.pravatar.cc/150?img=1',
10
11
  }
11
12
 
12
- const createMockClient = () => {
13
- const client = new StreamChat('mock-api-key', {
14
- allowServerSideConnect: true,
15
- })
16
-
17
- // Mock the client methods
18
- client.connectUser = async () => {
19
- return { me: mockUser } as unknown as ConnectionOpen
20
- }
21
-
22
- client.disconnectUser = async () => {
23
- return Promise.resolve()
24
- }
25
-
26
- client.userID = mockUser.id
27
- client.user = mockUser
28
-
29
- return client
30
- }
13
+ const createMockClient = () =>
14
+ createMockStreamChatClient(mockUser, { stubConnection: true })
31
15
 
32
16
  export const MockChatProvider: React.FC<{ children: React.ReactNode }> = ({
33
17
  children,
@@ -6,6 +6,8 @@ import type {
6
6
  } from 'stream-chat'
7
7
  import { StreamChat } from 'stream-chat'
8
8
 
9
+ import { createMockStreamChatClient } from './createMockStreamChatClient'
10
+
9
11
  /** A participant in the mock conversation. */
10
12
  export interface MockMessagingUser {
11
13
  id: string
@@ -77,25 +79,15 @@ export function createMockMessagingClient({
77
79
  channelId = 'mock-dm',
78
80
  onSend,
79
81
  }: CreateMockMessagingClientOptions): MockMessagingClient {
80
- const client = new StreamChat('mock-api-key', {
81
- allowServerSideConnect: true,
82
+ // Offline client marked connected without a WebSocket. `stubConnection` also
83
+ // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`
84
+ // renders `<Chat client={client}>` directly so it never calls connectUser,
85
+ // and handling the `message.new` events dispatched on send + echo below runs
86
+ // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.
87
+ const client = createMockStreamChatClient(currentUser, {
88
+ stubConnection: true,
82
89
  })
83
90
 
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
- // Handling a `message.new` event (dispatched on send + echo below) runs
93
- // Channel._countMessageAsUnread → client.userMuteStatus, which throws
94
- // "Make sure to await connectUser() first." without a live WebSocket. Stub
95
- // the mute lookups so offline event dispatch doesn't throw.
96
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
- client.userMuteStatus = (() => false) as any
98
-
99
91
  const cid = `messaging:${channelId}`
100
92
 
101
93
  const toStreamMessage = (
@@ -0,0 +1,44 @@
1
+ import { StreamChat } from 'stream-chat'
2
+
3
+ import type { MockMessagingUser } from './createMockMessagingClient'
4
+
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+
7
+ export interface CreateMockStreamChatClientOptions {
8
+ /**
9
+ * Also replace the connection-dependent client methods (`connectUser`,
10
+ * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the
11
+ * client drives a flow that would otherwise open a WebSocket or throw
12
+ * "Make sure to await connectUser() first." offline. Direct `<Chat client>`
13
+ * rendering (e.g. component stories) does not need this.
14
+ */
15
+ stubConnection?: boolean
16
+ }
17
+
18
+ /**
19
+ * Builds an offline `StreamChat` for mocks and stories: a real client on a
20
+ * throwaway api key with `userID`/`user` set so `<Chat>` treats it as
21
+ * connected without a backend. The single place that knows how to construct a
22
+ * connectionless Stream client, so a stream-chat version bump only needs
23
+ * updating here rather than in each mock surface.
24
+ */
25
+ export function createMockStreamChatClient(
26
+ user: MockMessagingUser,
27
+ { stubConnection = false }: CreateMockStreamChatClientOptions = {}
28
+ ): StreamChat {
29
+ const client = new StreamChat('mock-api-key', {
30
+ allowServerSideConnect: true,
31
+ })
32
+ client.userID = user.id
33
+ client.user = user as any
34
+
35
+ if (stubConnection) {
36
+ client.connectUser = async () => ({ me: user }) as any
37
+ client.disconnectUser = async () => undefined
38
+ client.userMuteStatus = (() => false) as any
39
+ }
40
+
41
+ return client
42
+ }
43
+
44
+ /* eslint-enable @typescript-eslint/no-explicit-any */