@linktr.ee/messaging-react 3.7.0-rc-1783333932 → 3.7.0-rc-1783334820
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/{Card-C8fHW9JN.js → Card-B00W_V2V.js} +2 -2
- package/dist/{Card-C8fHW9JN.js.map → Card-B00W_V2V.js.map} +1 -1
- package/dist/{Card-DPysc-d8.js → Card-B22ztgFZ.js} +3 -3
- package/dist/{Card-DPysc-d8.js.map → Card-B22ztgFZ.js.map} +1 -1
- package/dist/{Card-BApYAXry.js → Card-B6M_Z_2p.js} +2 -2
- package/dist/{Card-BApYAXry.js.map → Card-B6M_Z_2p.js.map} +1 -1
- package/dist/{Card-CZ6WAwfm.cjs → Card-B9wzbhJO.cjs} +2 -2
- package/dist/{Card-CZ6WAwfm.cjs.map → Card-B9wzbhJO.cjs.map} +1 -1
- package/dist/{Card-BcTN2501.cjs → Card-Bh7-DiTk.cjs} +2 -2
- package/dist/{Card-BcTN2501.cjs.map → Card-Bh7-DiTk.cjs.map} +1 -1
- package/dist/{Card-DU_zHX8a.cjs → Card-Qudta4p2.cjs} +2 -2
- package/dist/{Card-DU_zHX8a.cjs.map → Card-Qudta4p2.cjs.map} +1 -1
- package/dist/{LockedThumbnail-CbK80G-d.js → LockedThumbnail-CrYroEV1.js} +2 -2
- package/dist/{LockedThumbnail-CbK80G-d.js.map → LockedThumbnail-CrYroEV1.js.map} +1 -1
- package/dist/{LockedThumbnail-Ca-qH-ZE.cjs → LockedThumbnail-DPIsI8LL.cjs} +2 -2
- package/dist/{LockedThumbnail-Ca-qH-ZE.cjs.map → LockedThumbnail-DPIsI8LL.cjs.map} +1 -1
- package/dist/index-B-1G-O9u.cjs +2 -0
- package/dist/index-B-1G-O9u.cjs.map +1 -0
- package/dist/{index-CNoa6hhY.js → index-XS-fkenc.js} +6 -4
- package/dist/index-XS-fkenc.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +31 -16
- package/dist/index.js +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +28 -21
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/src/providers/MessagingProvider.test.tsx +43 -0
- package/src/providers/MessagingProvider.tsx +8 -1
- package/src/testing/createMockMessagingClient.ts +18 -3
- package/src/types.ts +35 -18
- package/dist/index-C1p97mNU.cjs +0 -2
- package/dist/index-C1p97mNU.cjs.map +0 -1
- package/dist/index-CNoa6hhY.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-B-1G-O9u.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1232,29 +1232,44 @@ declare interface MessagingContextValue {
|
|
|
1232
1232
|
export declare const MessagingProvider: default_2.FC<MessagingProviderProps>;
|
|
1233
1233
|
|
|
1234
1234
|
/**
|
|
1235
|
-
*
|
|
1235
|
+
* apiKey mode: the provider builds its own Stream client from `apiKey` and owns
|
|
1236
|
+
* the connect/disconnect lifecycle. Both fields are required so a non-injected
|
|
1237
|
+
* consumer cannot compile a provider that never connects.
|
|
1236
1238
|
*/
|
|
1237
|
-
|
|
1239
|
+
declare interface MessagingProviderApiKeyProps extends MessagingProviderBaseProps {
|
|
1240
|
+
client?: undefined;
|
|
1241
|
+
apiKey: string;
|
|
1242
|
+
serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
declare interface MessagingProviderBaseProps {
|
|
1238
1246
|
children: React.ReactNode;
|
|
1239
1247
|
user: MessagingUser | null;
|
|
1240
|
-
/**
|
|
1241
|
-
* An already-constructed Stream client. When provided it takes precedence
|
|
1242
|
-
* over `apiKey`: the provider renders `<Chat>` against this exact instance
|
|
1243
|
-
* and does NOT create a StreamChatService or call `connectUser` — the caller
|
|
1244
|
-
* owns the client's connection lifecycle. This is the seam for offline/dev
|
|
1245
|
-
* (`createMockMessagingClient` from `@linktr.ee/messaging-react/testing`) and
|
|
1246
|
-
* for tests, so consumers render the real UI instead of forking it.
|
|
1247
|
-
* `apiKey`/`serviceConfig` are ignored while `client` is set.
|
|
1248
|
-
*/
|
|
1249
|
-
client?: StreamChat;
|
|
1250
|
-
/** Required unless `client` is provided. */
|
|
1251
|
-
serviceConfig?: Omit<StreamChatServiceConfig, 'apiKey'>;
|
|
1252
|
-
/** Required unless `client` is provided. */
|
|
1253
|
-
apiKey?: string;
|
|
1254
1248
|
capabilities?: MessagingCapabilities;
|
|
1255
1249
|
debug?: boolean;
|
|
1256
1250
|
}
|
|
1257
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* Injected-client mode: the provider renders `<Chat>` against an
|
|
1254
|
+
* already-constructed Stream client and does NOT create a StreamChatService or
|
|
1255
|
+
* call `connectUser` — the caller owns the client's connection lifecycle. This
|
|
1256
|
+
* is the seam for offline/dev (`createMockMessagingClient` from
|
|
1257
|
+
* `@linktr.ee/messaging-react/testing`) and for tests, so consumers render the
|
|
1258
|
+
* real UI instead of forking it. `apiKey`/`serviceConfig` are not used here.
|
|
1259
|
+
*/
|
|
1260
|
+
declare interface MessagingProviderInjectedClientProps extends MessagingProviderBaseProps {
|
|
1261
|
+
client: StreamChat;
|
|
1262
|
+
apiKey?: never;
|
|
1263
|
+
serviceConfig?: never;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* MessagingProvider component props. Either an injected `client` (caller owns
|
|
1268
|
+
* the lifecycle) or an `apiKey` + `serviceConfig` (provider owns it) — never
|
|
1269
|
+
* neither.
|
|
1270
|
+
*/
|
|
1271
|
+
export declare type MessagingProviderProps = MessagingProviderInjectedClientProps | MessagingProviderApiKeyProps;
|
|
1272
|
+
|
|
1258
1273
|
/**
|
|
1259
1274
|
* Direct-conversation surface for one specific participant.
|
|
1260
1275
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-
|
|
1
|
+
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-XS-fkenc.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/dist/testing.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("stream-chat");function v(s,{stubConnection:n=!1}={}){const a=new b.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 w=600;function x({currentUser:s,participant:n,messages:a=[],channelId:y="mock-dm",onSend:d}){const r=v(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),_={[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:[]})),e.hide=(async()=>({}));let h=!1;e.watch=async()=>(h||(e.state.messages=m,h=!0),e.state.members=_,{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 p=0,g=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-${p++}`},m.length),...typeof t=="object"?t:{},user:s,cid:u};f(i);const o=d==null?void 0:d(c);if(o){const M=typeof o=="string"?o:o.text,k=typeof o=="string"?void 0:o.attachments,C=`mock-reply-${g++}`;setTimeout(()=>{f(l({text:M,from:"them",attachments:k,id:C},m.length))},w)}return{message:i}},r.queryChannels=async()=>[e],e.watch(),{client:r,channel:e,participantFilterId:n.id}}exports.createMockMessagingClient=x;
|
|
2
2
|
//# sourceMappingURL=testing.cjs.map
|
package/dist/testing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 = (() => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })) 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 // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) 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 let replyCount = 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(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\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 // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\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: replyId,\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","replyCount","text","sent","reply","replyText","replyAttachments","replyId"],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { StreamChat as
|
|
2
|
-
function
|
|
3
|
-
const a = new
|
|
1
|
+
import { StreamChat as g } from "stream-chat";
|
|
2
|
+
function v(s, { stubConnection: n = !1 } = {}) {
|
|
3
|
+
const a = new g("mock-api-key", {
|
|
4
4
|
allowServerSideConnect: !0
|
|
5
5
|
});
|
|
6
6
|
return a.userID = s.id, a.user = s, n && (a.connectUser = async () => ({ me: s }), a.disconnectUser = async () => {
|
|
7
7
|
}, a.userMuteStatus = (() => !1)), a;
|
|
8
8
|
}
|
|
9
|
-
const
|
|
10
|
-
function
|
|
9
|
+
const C = 600;
|
|
10
|
+
function S({
|
|
11
11
|
currentUser: s,
|
|
12
12
|
participant: n,
|
|
13
13
|
messages: a = [],
|
|
14
14
|
channelId: y = "mock-dm",
|
|
15
15
|
onSend: d
|
|
16
16
|
}) {
|
|
17
|
-
const r =
|
|
17
|
+
const r = v(s, {
|
|
18
18
|
stubConnection: !0
|
|
19
|
-
}), u = `messaging:${y}`,
|
|
19
|
+
}), u = `messaging:${y}`, l = (t, c) => {
|
|
20
20
|
const i = t.from === "me" ? s : n, o = new Date(Date.now() - (a.length - c) * 6e4);
|
|
21
21
|
return {
|
|
22
22
|
id: t.id ?? `mock-msg-${c}`,
|
|
@@ -38,7 +38,7 @@ function b({
|
|
|
38
38
|
...t.metadata ? { metadata: t.metadata } : {}
|
|
39
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
40
|
};
|
|
41
|
-
},
|
|
41
|
+
}, m = a.map(l), p = {
|
|
42
42
|
[s.id]: {
|
|
43
43
|
user: { ...s, is_account: !1 },
|
|
44
44
|
user_id: s.id,
|
|
@@ -55,11 +55,15 @@ function b({
|
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
56
|
members: [s.id, n.id]
|
|
57
57
|
});
|
|
58
|
-
e.muteStatus = (() => ({
|
|
58
|
+
e.muteStatus = (() => ({
|
|
59
|
+
muted: !1,
|
|
60
|
+
createdAt: null,
|
|
61
|
+
expiresAt: null
|
|
62
|
+
})), e.markRead = (async () => ({})), e.keystroke = (async () => {
|
|
59
63
|
}), e.stopTyping = (async () => {
|
|
60
|
-
}), e.sendReaction = (async () => ({})), e.deleteReaction = (async () => ({})), e.countUnread = (() => 0), e.query = (async () => ({ messages: [] }));
|
|
64
|
+
}), e.sendReaction = (async () => ({})), e.deleteReaction = (async () => ({})), e.countUnread = (() => 0), e.query = (async () => ({ messages: [] })), e.hide = (async () => ({}));
|
|
61
65
|
let h = !1;
|
|
62
|
-
e.watch = async () => (h || (e.state.messages =
|
|
66
|
+
e.watch = async () => (h || (e.state.messages = m, h = !0), e.state.members = p, {
|
|
63
67
|
channel: { members: [s.id, n.id] },
|
|
64
68
|
members: [],
|
|
65
69
|
messages: e.state.messages,
|
|
@@ -76,10 +80,13 @@ function b({
|
|
|
76
80
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
81
|
});
|
|
78
82
|
};
|
|
79
|
-
let _ = 0;
|
|
83
|
+
let _ = 0, k = 0;
|
|
80
84
|
return e.sendMessage = async (t) => {
|
|
81
85
|
const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", i = {
|
|
82
|
-
...
|
|
86
|
+
...l(
|
|
87
|
+
{ text: c, from: "me", id: `mock-sent-${_++}` },
|
|
88
|
+
m.length
|
|
89
|
+
),
|
|
83
90
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
91
|
...typeof t == "object" ? t : {},
|
|
85
92
|
user: s,
|
|
@@ -88,26 +95,26 @@ function b({
|
|
|
88
95
|
f(i);
|
|
89
96
|
const o = d == null ? void 0 : d(c);
|
|
90
97
|
if (o) {
|
|
91
|
-
const
|
|
98
|
+
const w = typeof o == "string" ? o : o.text, x = typeof o == "string" ? void 0 : o.attachments, M = `mock-reply-${k++}`;
|
|
92
99
|
setTimeout(() => {
|
|
93
100
|
f(
|
|
94
|
-
|
|
101
|
+
l(
|
|
95
102
|
{
|
|
96
|
-
text:
|
|
103
|
+
text: w,
|
|
97
104
|
from: "them",
|
|
98
105
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
-
attachments:
|
|
100
|
-
id:
|
|
106
|
+
attachments: x,
|
|
107
|
+
id: M
|
|
101
108
|
},
|
|
102
|
-
|
|
109
|
+
m.length
|
|
103
110
|
)
|
|
104
111
|
);
|
|
105
|
-
},
|
|
112
|
+
}, C);
|
|
106
113
|
}
|
|
107
114
|
return { message: i };
|
|
108
115
|
}, r.queryChannels = async () => [e], e.watch(), { client: r, channel: e, participantFilterId: n.id };
|
|
109
116
|
}
|
|
110
117
|
export {
|
|
111
|
-
|
|
118
|
+
S as createMockMessagingClient
|
|
112
119
|
};
|
|
113
120
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 = (() => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })) 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 // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) 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 let replyCount = 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(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\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 // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\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: replyId,\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","replyCount","text","sent","reply","replyText","replyAttachments","replyId"],"mappings":";AAwBO,SAASA,EACdC,GACA,EAAE,gBAAAC,IAAiB,GAAA,IAA6C,CAAA,GACpD;AACZ,QAAMC,IAAS,IAAIC,EAAW,gBAAgB;AAAA,IAC5C,wBAAwB;AAAA,EAAA,CACzB;AACD,SAAAD,EAAO,SAASF,EAAK,IACrBE,EAAO,OAAOF,GAEVC,MACFC,EAAO,cAAc,aAAa,EAAE,IAAIF,EAAA,IACxCE,EAAO,iBAAiB;KACxBA,EAAO,kBAAkB,MAAM,MAG1BA;AACT;ACmBA,MAAME,IAAiB;AAchB,SAASC,EAA0B;AAAA,EACxC,aAAAC;AAAA,EACA,aAAAC;AAAA,EACA,UAAAC,IAAW,CAAA;AAAA,EACX,WAAAC,IAAY;AAAA,EACZ,QAAAC;AACF,GAA0D;AAMxD,QAAMR,IAASH,EAA2BO,GAAa;AAAA,IACrD,gBAAgB;AAAA,EAAA,CACjB,GAEKK,IAAM,aAAaF,CAAS,IAE5BG,IAAkB,CACtBC,GACAC,MACoB;AACpB,UAAMC,IAASF,EAAQ,SAAS,OAAOP,IAAcC,GAK/CS,IAAY,IAAI,KAAK,KAAK,SAASR,EAAS,SAASM,KAAS,GAAM;AAC1E,WAAO;AAAA,MACL,IAAID,EAAQ,MAAM,YAAYC,CAAK;AAAA,MACnC,MAAMD,EAAQ,QAAQ;AAAA,MACtB,MAAM;AAAA,MACN,MAAMA,EAAQ,OAAO,MAAMA,EAAQ,IAAI,SAAS;AAAA,MAChD,MAAME;AAAA,MACN,aAAaF,EAAQ,eAAe,CAAA;AAAA,MACpC,kBAAkB,CAAA;AAAA,MAClB,eAAe,CAAA;AAAA,MACf,iBAAiB,CAAA;AAAA,MACjB,iBAAiB,CAAA;AAAA,MACjB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,KAAAF;AAAA,MACA,YAAYK;AAAA,MACZ,YAAYA;AAAA,MACZ,iBAAiB,CAAA;AAAA,MACjB,GAAIH,EAAQ,WAAW,EAAE,UAAUA,EAAQ,SAAA,IAAa,CAAA;AAAA;AAAA,IAAC;AAAA,EAG7D,GAEMI,IAAiBT,EAAS,IAAII,CAAe,GAE7CM,IAAc;AAAA,IAClB,CAACZ,EAAY,EAAE,GAAG;AAAA,MAChB,MAAM,EAAE,GAAGA,GAAa,YAAY,GAAA;AAAA,MACpC,SAASA,EAAY;AAAA,MACrB,MAAM;AAAA,MACN,YAAY;AAAA,IAAA;AAAA,IAEd,CAACC,EAAY,EAAE,GAAG;AAAA,MAChB,MAAM,EAAE,GAAGA,GAAa,YAAY,GAAA;AAAA,MACpC,SAASA,EAAY;AAAA,MACrB,MAAM;AAAA,MACN,YAAY;AAAA,IAAA;AAAA,EACd,GAGIY,IAAUjB,EAAO,QAAQ,aAAaO,GAAW;AAAA;AAAA,IAErD,SAAS,CAACH,EAAY,IAAIC,EAAY,EAAE;AAAA,EAAA,CACzC;AAID,EAAAY,EAAQ,cAAc,OAAO;AAAA,IAC3B,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,EAAA,KASbA,EAAQ,YAAY,aAAa,CAAA,KACjCA,EAAQ,aAAa,YAAA;AAAA,MACrBA,EAAQ,cAAc,YAAA;AAAA,MACtBA,EAAQ,gBAAgB,aAAa,CAAA,KACrCA,EAAQ,kBAAkB,aAAa,CAAA,KACvCA,EAAQ,eAAe,MAAM,IAE7BA,EAAQ,SAAS,aAAa,EAAE,UAAU,CAAA,EAAC,KAG3CA,EAAQ,QAAQ,aAAa,CAAA;AAQ7B,MAAIC,IAAY;AAChB,EAAAD,EAAQ,QAAQ,aACTC,MAEHD,EAAQ,MAAM,WAAWF,GACzBG,IAAY,KAGdD,EAAQ,MAAM,UAAUD,GACjB;AAAA,IACL,SAAS,EAAE,SAAS,CAACZ,EAAY,IAAIC,EAAY,EAAE,EAAA;AAAA,IACnD,SAAS,CAAA;AAAA,IACT,UAAUY,EAAQ,MAAM;AAAA,IACxB,UAAU,CAAA;AAAA,IACV,iBAAiB,CAAA;AAAA,IACjB,UAAU;AAAA,EAAA;AAId,QAAME,IAAgB,CAACR,MAA6B;AAIlD,IAAAM,EAAQ,MAAM,iBAAiBN,CAAc,GAC7CX,EAAO,cAAc;AAAA,MACnB,MAAM;AAAA,MACN,KAAAS;AAAA,MACA,SAAAE;AAAA,MACA,MAAMA,EAAQ,QAAQ;AAAA;AAAA,IAAA,CAEhB;AAAA,EACV;AAEA,MAAIS,IAAY,GACZC,IAAa;AACjB,SAAAJ,EAAQ,cAAc,OAAON,MAAY;AACvC,UAAMW,IACJ,OAAOX,KAAY,WACfA,KACEA,KAAA,gBAAAA,EAA+B,SAAQ,IACzCY,IAAO;AAAA,MACX,GAAGb;AAAA,QACD,EAAE,MAAAY,GAAM,MAAM,MAAM,IAAI,aAAaF,GAAW,GAAA;AAAA,QAChDL,EAAe;AAAA,MAAA;AAAA;AAAA,MAGjB,GAAI,OAAOJ,KAAY,WAAYA,IAAkB,CAAA;AAAA,MACrD,MAAMP;AAAA,MACN,KAAAK;AAAA,IAAA;AAKF,IAAAU,EAAcI,CAAI;AAElB,UAAMC,IAAQhB,KAAA,gBAAAA,EAASc;AACvB,QAAIE,GAAO;AACT,YAAMC,IAAY,OAAOD,KAAU,WAAWA,IAAQA,EAAM,MACtDE,IACJ,OAAOF,KAAU,WAAW,SAAYA,EAAM,aAI1CG,IAAU,cAAcN,GAAY;AAC1C,iBAAW,MAAM;AACf,QAAAF;AAAA,UACET;AAAA,YACE;AAAA,cACE,MAAMe;AAAA,cACN,MAAM;AAAA;AAAA,cAEN,aAAaC;AAAA,cACb,IAAIC;AAAA,YAAA;AAAA,YAENZ,EAAe;AAAA,UAAA;AAAA,QACjB;AAAA,MAEJ,GAAGb,CAAc;AAAA,IACnB;AAEA,WAAO,EAAE,SAASqB,EAAA;AAAA,EACpB,GAKAvB,EAAO,gBAAgB,YAAY,CAACiB,CAAO,GAGtCA,EAAQ,MAAA,GAEN,EAAE,QAAAjB,GAAQ,SAAAiB,GAAS,qBAAqBZ,EAAY,GAAA;AAC7D;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.7.0-rc-
|
|
3
|
+
"version": "3.7.0-rc-1783334820",
|
|
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-
|
|
53
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1783334820",
|
|
54
54
|
"@phosphor-icons/react": "^2.1.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -322,4 +322,47 @@ describe('MessagingProvider', () => {
|
|
|
322
322
|
expect(connectUser).not.toHaveBeenCalled()
|
|
323
323
|
expect(disconnectUser).not.toHaveBeenCalled()
|
|
324
324
|
})
|
|
325
|
+
|
|
326
|
+
it('does not reconnect a stale service after switching to an injected client', async () => {
|
|
327
|
+
const { connectUser } = setupServiceMock()
|
|
328
|
+
|
|
329
|
+
const { rerender } = render(
|
|
330
|
+
<MessagingProvider
|
|
331
|
+
apiKey="mock-api-key"
|
|
332
|
+
user={{ id: 'user-1', name: 'User One' } as MessagingUser}
|
|
333
|
+
serviceConfig={{
|
|
334
|
+
fetchToken: async () => 'token',
|
|
335
|
+
createChannel: async () => ({ channelId: 'ch-1' }),
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
<Probe onState={() => {}} />
|
|
339
|
+
</MessagingProvider>
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
// apiKey path connects user-1 through its service.
|
|
343
|
+
await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
|
|
344
|
+
|
|
345
|
+
const injectedClient = {
|
|
346
|
+
userID: 'injected-user',
|
|
347
|
+
} as unknown as StreamChat
|
|
348
|
+
|
|
349
|
+
// Switch the same provider to an injected client and change the user. The
|
|
350
|
+
// user-connection effect must not drive the new user onto the stale
|
|
351
|
+
// apiKey-path service.
|
|
352
|
+
rerender(
|
|
353
|
+
<MessagingProvider
|
|
354
|
+
client={injectedClient}
|
|
355
|
+
user={{ id: 'user-2', name: 'User Two' } as MessagingUser}
|
|
356
|
+
>
|
|
357
|
+
<Probe onState={() => {}} />
|
|
358
|
+
</MessagingProvider>
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
await act(async () => {
|
|
362
|
+
await Promise.resolve()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Still only the single user-1 connect — user-2 never reached the service.
|
|
366
|
+
expect(connectUser).toHaveBeenCalledTimes(1)
|
|
367
|
+
})
|
|
325
368
|
})
|
|
@@ -216,6 +216,13 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
216
216
|
dependencies: { service: !!service, userId: user?.id },
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
+
// The injected client owns its own connection — never connect a service for
|
|
220
|
+
// it. Without this guard a `user` change in injected-client mode would run
|
|
221
|
+
// service.connectUser on a stale apiKey-path service (the toggle case).
|
|
222
|
+
if (injectedClient) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
219
226
|
if (!service || !user) {
|
|
220
227
|
debugLog('⚠️ USER CONNECTION SKIPPED', 'Missing service or user')
|
|
221
228
|
return
|
|
@@ -273,7 +280,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
connectUser()
|
|
276
|
-
}, [service, user, debugLog, isConnected])
|
|
283
|
+
}, [service, user, debugLog, isConnected, injectedClient])
|
|
277
284
|
|
|
278
285
|
// Disconnect tied to the client's lifetime: only when `chatClient` changes
|
|
279
286
|
// (i.e. `apiKey` changes) or the provider unmounts — never on a mere
|
|
@@ -146,7 +146,11 @@ export function createMockMessagingClient({
|
|
|
146
146
|
// Read/unread bookkeeping consults channel mute status; without a live
|
|
147
147
|
// connection the real method throws, so report "not muted".
|
|
148
148
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
-
channel.muteStatus = (() => ({
|
|
149
|
+
channel.muteStatus = (() => ({
|
|
150
|
+
muted: false,
|
|
151
|
+
createdAt: null,
|
|
152
|
+
expiresAt: null,
|
|
153
|
+
})) as any
|
|
150
154
|
|
|
151
155
|
// Neutralise the rest of the connection-dependent channel API that
|
|
152
156
|
// stream-chat-react calls during the chat lifecycle. Each of these POSTs to
|
|
@@ -162,6 +166,9 @@ export function createMockMessagingClient({
|
|
|
162
166
|
channel.countUnread = (() => 0) as any
|
|
163
167
|
// Pagination: report no older pages so "load more" resolves to a no-op.
|
|
164
168
|
channel.query = (async () => ({ messages: [] }) as any) as any
|
|
169
|
+
// Leave/Delete Conversation calls channel.hide(); the real method POSTs to
|
|
170
|
+
// Stream, so no-op it to keep the action working offline.
|
|
171
|
+
channel.hide = (async () => ({}) as any) as any
|
|
165
172
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
166
173
|
|
|
167
174
|
// Seed state locally instead of fetching. Mirrors the proven ChannelView
|
|
@@ -203,13 +210,17 @@ export function createMockMessagingClient({
|
|
|
203
210
|
}
|
|
204
211
|
|
|
205
212
|
let sentCount = 0
|
|
213
|
+
let replyCount = 0
|
|
206
214
|
channel.sendMessage = async (message) => {
|
|
207
215
|
const text =
|
|
208
216
|
typeof message === 'string'
|
|
209
217
|
? message
|
|
210
218
|
: ((message as { text?: string })?.text ?? '')
|
|
211
219
|
const sent = {
|
|
212
|
-
...toStreamMessage(
|
|
220
|
+
...toStreamMessage(
|
|
221
|
+
{ text, from: 'me', id: `mock-sent-${sentCount++}` },
|
|
222
|
+
seededMessages.length
|
|
223
|
+
),
|
|
213
224
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
225
|
...(typeof message === 'object' ? (message as any) : {}),
|
|
215
226
|
user: currentUser,
|
|
@@ -225,6 +236,10 @@ export function createMockMessagingClient({
|
|
|
225
236
|
const replyText = typeof reply === 'string' ? reply : reply.text
|
|
226
237
|
const replyAttachments =
|
|
227
238
|
typeof reply === 'string' ? undefined : reply.attachments
|
|
239
|
+
// Capture a unique id before scheduling: two sends within REPLY_DELAY_MS
|
|
240
|
+
// would otherwise both read the same later `sentCount` at fire time and
|
|
241
|
+
// emit colliding reply ids, so Stream's id-based dedupe drops one echo.
|
|
242
|
+
const replyId = `mock-reply-${replyCount++}`
|
|
228
243
|
setTimeout(() => {
|
|
229
244
|
appendMessage(
|
|
230
245
|
toStreamMessage(
|
|
@@ -233,7 +248,7 @@ export function createMockMessagingClient({
|
|
|
233
248
|
from: 'them',
|
|
234
249
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
235
250
|
attachments: replyAttachments as any,
|
|
236
|
-
id:
|
|
251
|
+
id: replyId,
|
|
237
252
|
},
|
|
238
253
|
seededMessages.length
|
|
239
254
|
)
|
package/src/types.ts
CHANGED
|
@@ -443,26 +443,43 @@ export interface MessagingShellProps extends ChannelViewPassthroughProps {
|
|
|
443
443
|
initialParticipantData?: Participant
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
|
|
447
|
-
* MessagingProvider component props
|
|
448
|
-
*/
|
|
449
|
-
export interface MessagingProviderProps {
|
|
446
|
+
interface MessagingProviderBaseProps {
|
|
450
447
|
children: React.ReactNode
|
|
451
448
|
user: MessagingUser | null
|
|
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
|
|
466
449
|
capabilities?: MessagingCapabilities
|
|
467
450
|
debug?: boolean
|
|
468
451
|
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Injected-client mode: the provider renders `<Chat>` against an
|
|
455
|
+
* already-constructed Stream client and does NOT create a StreamChatService or
|
|
456
|
+
* call `connectUser` — the caller owns the client's connection lifecycle. This
|
|
457
|
+
* is the seam for offline/dev (`createMockMessagingClient` from
|
|
458
|
+
* `@linktr.ee/messaging-react/testing`) and for tests, so consumers render the
|
|
459
|
+
* real UI instead of forking it. `apiKey`/`serviceConfig` are not used here.
|
|
460
|
+
*/
|
|
461
|
+
interface MessagingProviderInjectedClientProps extends MessagingProviderBaseProps {
|
|
462
|
+
client: StreamChat
|
|
463
|
+
apiKey?: never
|
|
464
|
+
serviceConfig?: never
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* apiKey mode: the provider builds its own Stream client from `apiKey` and owns
|
|
469
|
+
* the connect/disconnect lifecycle. Both fields are required so a non-injected
|
|
470
|
+
* consumer cannot compile a provider that never connects.
|
|
471
|
+
*/
|
|
472
|
+
interface MessagingProviderApiKeyProps extends MessagingProviderBaseProps {
|
|
473
|
+
client?: undefined
|
|
474
|
+
apiKey: string
|
|
475
|
+
serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* MessagingProvider component props. Either an injected `client` (caller owns
|
|
480
|
+
* the lifecycle) or an `apiKey` + `serviceConfig` (provider owns it) — never
|
|
481
|
+
* neither.
|
|
482
|
+
*/
|
|
483
|
+
export type MessagingProviderProps =
|
|
484
|
+
| MessagingProviderInjectedClientProps
|
|
485
|
+
| MessagingProviderApiKeyProps
|