@linktr.ee/messaging-react 3.7.0-rc-1783327612 → 3.7.0-rc-1783330869
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 +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +18 -19
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/src/testing/createMockMessagingClient.ts +15 -4
package/dist/testing.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("stream-chat"),b=600;function
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("stream-chat"),b=600;function x({currentUser:e,participant:o,messages:m=[],channelId:h="mock-dm",onSend:r}){const n=new w.StreamChat("mock-api-key",{allowServerSideConnect:!0});n.connectUser=async()=>({me:e}),n.disconnectUser=async()=>{},n.userID=e.id,n.user=e,n.userMuteStatus=(()=>!1);const d=`messaging:${h}`,u=(t,c)=>{const i=t.from==="me"?e: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:d,created_at:a,updated_at:a,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},l=m.map(u),p={[e.id]:{user:{...e,is_account:!1},user_id:e.id,role:"owner",is_account:!1},[o.id]:{user:{...o,is_account:!0},user_id:o.id,role:"member",is_account:!0}},s=n.channel("messaging",h,{members:[e.id,o.id]});s.muteStatus=(()=>({muted:!1,createdAt:null,expiresAt:null}));let y=!1;s.watch=async()=>(y||(s.state.messages=l,y=!0),s.state.members=p,{channel:{members:[e.id,o.id]},members:[],messages:s.state.messages,watchers:[],pinned_messages:[],duration:"0ms"});const _=t=>{s.state.addMessageSorted(t),n.dispatchEvent({type:"message.new",cid:d,message:t,user:t.user??void 0})};let f=0;return s.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:e,cid:d};_(i);const a=r==null?void 0:r(c);if(a){const M=typeof a=="string"?a:a.text,g=typeof a=="string"?void 0:a.attachments;setTimeout(()=>{_(u({text:M,from:"them",attachments:g,id:`mock-reply-${f}`},l.length))},b)}return{message:i}},n.queryChannels=async()=>[s],s.watch(),{client:n,channel:s,participantFilterId:o.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/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\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.\n const createdAt = new Date(\n Date.now() - (messages.length - index) * 60_000\n ).toISOString()\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\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/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 // 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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"}
|
package/dist/testing.js
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
import { StreamChat as
|
|
2
|
-
const
|
|
1
|
+
import { StreamChat as M } from "stream-chat";
|
|
2
|
+
const b = 600;
|
|
3
3
|
function k({
|
|
4
4
|
currentUser: e,
|
|
5
5
|
participant: a,
|
|
6
|
-
messages:
|
|
6
|
+
messages: l = [],
|
|
7
7
|
channelId: h = "mock-dm",
|
|
8
|
-
onSend:
|
|
8
|
+
onSend: i
|
|
9
9
|
}) {
|
|
10
|
-
const n = new
|
|
10
|
+
const n = new M("mock-api-key", {
|
|
11
11
|
allowServerSideConnect: !0
|
|
12
12
|
});
|
|
13
13
|
n.connectUser = async () => ({ me: e }), n.disconnectUser = async () => {
|
|
14
|
-
}, n.userID = e.id, n.user = e;
|
|
14
|
+
}, n.userID = e.id, n.user = e, n.userMuteStatus = (() => !1);
|
|
15
15
|
const r = `messaging:${h}`, m = (t, c) => {
|
|
16
|
-
const
|
|
17
|
-
Date.now() - (u.length - c) * 6e4
|
|
18
|
-
).toISOString();
|
|
16
|
+
const d = t.from === "me" ? e : a, o = new Date(Date.now() - (l.length - c) * 6e4);
|
|
19
17
|
return {
|
|
20
18
|
id: t.id ?? `mock-msg-${c}`,
|
|
21
19
|
text: t.text ?? "",
|
|
22
20
|
type: "regular",
|
|
23
21
|
html: t.text ? `<p>${t.text}</p>` : "",
|
|
24
|
-
user:
|
|
22
|
+
user: d,
|
|
25
23
|
attachments: t.attachments ?? [],
|
|
26
24
|
latest_reactions: [],
|
|
27
25
|
own_reactions: [],
|
|
@@ -36,7 +34,7 @@ function k({
|
|
|
36
34
|
...t.metadata ? { metadata: t.metadata } : {}
|
|
37
35
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
36
|
};
|
|
39
|
-
},
|
|
37
|
+
}, u = l.map(m), p = {
|
|
40
38
|
[e.id]: {
|
|
41
39
|
user: { ...e, is_account: !1 },
|
|
42
40
|
user_id: e.id,
|
|
@@ -53,8 +51,9 @@ function k({
|
|
|
53
51
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
52
|
members: [e.id, a.id]
|
|
55
53
|
});
|
|
54
|
+
s.muteStatus = (() => ({ muted: !1, createdAt: null, expiresAt: null }));
|
|
56
55
|
let y = !1;
|
|
57
|
-
s.watch = async () => (y || (s.state.messages =
|
|
56
|
+
s.watch = async () => (y || (s.state.messages = u, y = !0), s.state.members = p, {
|
|
58
57
|
channel: { members: [e.id, a.id] },
|
|
59
58
|
members: [],
|
|
60
59
|
messages: s.state.messages,
|
|
@@ -73,15 +72,15 @@ function k({
|
|
|
73
72
|
};
|
|
74
73
|
let f = 0;
|
|
75
74
|
return s.sendMessage = async (t) => {
|
|
76
|
-
const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "",
|
|
77
|
-
...m({ text: c, from: "me", id: `mock-sent-${f++}` },
|
|
75
|
+
const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", d = {
|
|
76
|
+
...m({ text: c, from: "me", id: `mock-sent-${f++}` }, u.length),
|
|
78
77
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
78
|
...typeof t == "object" ? t : {},
|
|
80
79
|
user: e,
|
|
81
80
|
cid: r
|
|
82
81
|
};
|
|
83
|
-
_(
|
|
84
|
-
const o =
|
|
82
|
+
_(d);
|
|
83
|
+
const o = i == null ? void 0 : i(c);
|
|
85
84
|
if (o) {
|
|
86
85
|
const w = typeof o == "string" ? o : o.text, x = typeof o == "string" ? void 0 : o.attachments;
|
|
87
86
|
setTimeout(() => {
|
|
@@ -94,12 +93,12 @@ function k({
|
|
|
94
93
|
attachments: x,
|
|
95
94
|
id: `mock-reply-${f}`
|
|
96
95
|
},
|
|
97
|
-
|
|
96
|
+
u.length
|
|
98
97
|
)
|
|
99
98
|
);
|
|
100
|
-
},
|
|
99
|
+
}, b);
|
|
101
100
|
}
|
|
102
|
-
return { message:
|
|
101
|
+
return { message: d };
|
|
103
102
|
}, n.queryChannels = async () => [s], s.watch(), { client: n, channel: s, participantFilterId: a.id };
|
|
104
103
|
}
|
|
105
104
|
export {
|
package/dist/testing.js.map
CHANGED
|
@@ -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\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.\n const createdAt = new Date(\n Date.now() - (messages.length - index) * 60_000\n ).toISOString()\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\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":";AA0DA,MAAMA,IAAiB;AAchB,SAASC,EAA0B;AAAA,EACxC,aAAAC;AAAA,EACA,aAAAC;AAAA,EACA,UAAAC,IAAW,CAAA;AAAA,EACX,WAAAC,IAAY;AAAA,EACZ,QAAAC;AACF,GAA0D;AACxD,QAAMC,IAAS,IAAIC,EAAW,gBAAgB;AAAA,IAC5C,wBAAwB;AAAA,EAAA,CACzB;AAKD,EAAAD,EAAO,cAAc,aAAa,EAAE,IAAIL,EAAA,IACxCK,EAAO,iBAAiB;KACxBA,EAAO,SAASL,EAAY,IAE5BK,EAAO,OAAOL;AAEd,QAAMO,IAAM,aAAaJ,CAAS,IAE5BK,IAAkB,CACtBC,GACAC,MACoB;AACpB,UAAMC,IAASF,EAAQ,SAAS,OAAOT,IAAcC,GAE/CW,IAAY,IAAI;AAAA,MACpB,KAAK,IAAA,KAASV,EAAS,SAASQ,KAAS;AAAA,IAAA,EACzC,YAAA;AACF,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,IAAiBX,EAAS,IAAIM,CAAe,GAE7CM,IAAc;AAAA,IAClB,CAACd,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,GAGIc,IAAUV,EAAO,QAAQ,aAAaF,GAAW;AAAA;AAAA,IAErD,SAAS,CAACH,EAAY,IAAIC,EAAY,EAAE;AAAA,EAAA,CACzC;AAOD,MAAIe,IAAY;AAChB,EAAAD,EAAQ,QAAQ,aACTC,MAEHD,EAAQ,MAAM,WAAWF,GACzBG,IAAY,KAGdD,EAAQ,MAAM,UAAUD,GACjB;AAAA,IACL,SAAS,EAAE,SAAS,CAACd,EAAY,IAAIC,EAAY,EAAE,EAAA;AAAA,IACnD,SAAS,CAAA;AAAA,IACT,UAAUc,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,GAC7CJ,EAAO,cAAc;AAAA,MACnB,MAAM;AAAA,MACN,KAAAE;AAAA,MACA,SAAAE;AAAA,MACA,MAAMA,EAAQ,QAAQ;AAAA;AAAA,IAAA,CAEhB;AAAA,EACV;AAEA,MAAIS,IAAY;AAChB,SAAAH,EAAQ,cAAc,OAAON,MAAY;AACvC,UAAMU,IACJ,OAAOV,KAAY,WACfA,KACEA,KAAA,gBAAAA,EAA+B,SAAQ,IACzCW,IAAO;AAAA,MACX,GAAGZ,EAAgB,EAAE,MAAAW,GAAM,MAAM,MAAM,IAAI,aAAaD,GAAW,MAAML,EAAe,MAAM;AAAA;AAAA,MAE9F,GAAI,OAAOJ,KAAY,WAAYA,IAAkB,CAAA;AAAA,MACrD,MAAMT;AAAA,MACN,KAAAO;AAAA,IAAA;AAKF,IAAAU,EAAcG,CAAI;AAElB,UAAMC,IAAQjB,KAAA,gBAAAA,EAASe;AACvB,QAAIE,GAAO;AACT,YAAMC,IAAY,OAAOD,KAAU,WAAWA,IAAQA,EAAM,MACtDE,IACJ,OAAOF,KAAU,WAAW,SAAYA,EAAM;AAChD,iBAAW,MAAM;AACf,QAAAJ;AAAA,UACET;AAAA,YACE;AAAA,cACE,MAAMc;AAAA,cACN,MAAM;AAAA;AAAA,cAEN,aAAaC;AAAA,cACb,IAAI,cAAcL,CAAS;AAAA,YAAA;AAAA,YAE7BL,EAAe;AAAA,UAAA;AAAA,QACjB;AAAA,MAEJ,GAAGf,CAAc;AAAA,IACnB;AAEA,WAAO,EAAE,SAASsB,EAAA;AAAA,EACpB,GAKAf,EAAO,gBAAgB,YAAY,CAACU,CAAO,GAGtCA,EAAQ,MAAA,GAEN,EAAE,QAAAV,GAAQ,SAAAU,GAAS,qBAAqBd,EAAY,GAAA;AAC7D;"}
|
|
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 // 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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"}
|
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-1783330869",
|
|
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-1783330869",
|
|
54
54
|
"@phosphor-icons/react": "^2.1.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -89,6 +89,12 @@ export function createMockMessagingClient({
|
|
|
89
89
|
client.userID = currentUser.id
|
|
90
90
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
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
|
|
92
98
|
|
|
93
99
|
const cid = `messaging:${channelId}`
|
|
94
100
|
|
|
@@ -97,10 +103,11 @@ export function createMockMessagingClient({
|
|
|
97
103
|
index: number
|
|
98
104
|
): MessageResponse => {
|
|
99
105
|
const author = message.from === 'me' ? currentUser : participant
|
|
100
|
-
// Stagger timestamps oldest-first so the list orders naturally.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
).
|
|
106
|
+
// Stagger timestamps oldest-first so the list orders naturally. Must be a
|
|
107
|
+
// Date, not an ISO string: we seed `channel.state.messages` directly
|
|
108
|
+
// (bypassing Stream's wire→Date parsing), and stream-chat-react calls
|
|
109
|
+
// `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).
|
|
110
|
+
const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)
|
|
104
111
|
return {
|
|
105
112
|
id: message.id ?? `mock-msg-${index}`,
|
|
106
113
|
text: message.text ?? '',
|
|
@@ -144,6 +151,10 @@ export function createMockMessagingClient({
|
|
|
144
151
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
152
|
members: [currentUser.id, participant.id] as any,
|
|
146
153
|
})
|
|
154
|
+
// Read/unread bookkeeping consults channel mute status; without a live
|
|
155
|
+
// connection the real method throws, so report "not muted".
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
|
+
channel.muteStatus = (() => ({ muted: false, createdAt: null, expiresAt: null })) as any
|
|
147
158
|
|
|
148
159
|
// Seed state locally instead of fetching. Mirrors the proven ChannelView
|
|
149
160
|
// story mock: override `watch` to populate `state` and return a canned
|