@linktr.ee/messaging-react 3.6.1 → 3.7.0-rc-1783304153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-CxS7GNqp.js → Card-BAt0xifq.js} +2 -2
- package/dist/{Card-CxS7GNqp.js.map → Card-BAt0xifq.js.map} +1 -1
- package/dist/{Card-B0lzpuYf.js → Card-BB34_MDl.js} +3 -3
- package/dist/{Card-B0lzpuYf.js.map → Card-BB34_MDl.js.map} +1 -1
- package/dist/{Card-DcUPqd79.cjs → Card-BDKGSydN.cjs} +2 -2
- package/dist/{Card-DcUPqd79.cjs.map → Card-BDKGSydN.cjs.map} +1 -1
- package/dist/{Card-BuR6mbUu.cjs → Card-BGJY5HWf.cjs} +2 -2
- package/dist/{Card-BuR6mbUu.cjs.map → Card-BGJY5HWf.cjs.map} +1 -1
- package/dist/{Card-BrC7mKtr.js → Card-C3-Ea5ho.js} +2 -2
- package/dist/{Card-BrC7mKtr.js.map → Card-C3-Ea5ho.js.map} +1 -1
- package/dist/{Card-d0_xEfdM.cjs → Card-DnFtpG1h.cjs} +2 -2
- package/dist/{Card-d0_xEfdM.cjs.map → Card-DnFtpG1h.cjs.map} +1 -1
- package/dist/{LockedThumbnail-BDtKMJjz.cjs → LockedThumbnail-Bl9UC9sQ.cjs} +2 -2
- package/dist/{LockedThumbnail-BDtKMJjz.cjs.map → LockedThumbnail-Bl9UC9sQ.cjs.map} +1 -1
- package/dist/{LockedThumbnail-BaDgF8qJ.js → LockedThumbnail-so7ftBnG.js} +2 -2
- package/dist/{LockedThumbnail-BaDgF8qJ.js.map → LockedThumbnail-so7ftBnG.js.map} +1 -1
- package/dist/{index-BPfehHDD.js → index-2HMjfjM-.js} +1013 -1006
- package/dist/index-2HMjfjM-.js.map +1 -0
- package/dist/index-BgzqZorr.cjs +2 -0
- package/dist/index-BgzqZorr.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +116 -0
- package/dist/testing.js +107 -0
- package/dist/testing.js.map +1 -0
- package/package.json +7 -2
- package/src/providers/MessagingProvider.test.tsx +30 -0
- package/src/providers/MessagingProvider.tsx +24 -5
- package/src/testing/createMockMessagingClient.stories.tsx +77 -0
- package/src/testing/createMockMessagingClient.ts +231 -0
- package/src/testing/index.ts +7 -0
- package/src/types.ts +15 -2
- package/dist/index-BPfehHDD.js.map +0 -1
- package/dist/index-G4pnfm4m.cjs +0 -2
- package/dist/index-G4pnfm4m.cjs.map +0 -1
- /package/dist/assets/{index.css → messaging-react.css} +0 -0
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-BgzqZorr.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
|
@@ -1237,8 +1237,20 @@ export declare const MessagingProvider: default_2.FC<MessagingProviderProps>;
|
|
|
1237
1237
|
export declare interface MessagingProviderProps {
|
|
1238
1238
|
children: React.ReactNode;
|
|
1239
1239
|
user: MessagingUser | null;
|
|
1240
|
-
|
|
1241
|
-
|
|
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;
|
|
1242
1254
|
capabilities?: MessagingCapabilities;
|
|
1243
1255
|
debug?: boolean;
|
|
1244
1256
|
}
|
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-2HMjfjM-.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("stream-chat"),w=600;function b({currentUser:e,participant:a,messages:u=[],channelId:h="mock-dm",onSend:d}){const s=new M.StreamChat("mock-api-key",{allowServerSideConnect:!0});s.connectUser=async()=>({me:e}),s.disconnectUser=async()=>{},s.userID=e.id,s.user=e;const m=`messaging:${h}`,l=(t,c)=>{const r=t.from==="me"?e:a,n=new Date(Date.now()-(u.length-c)*6e4).toISOString();return{id:t.id??`mock-msg-${c}`,text:t.text??"",type:"regular",html:t.text?`<p>${t.text}</p>`:"",user:r,attachments:t.attachments??[],latest_reactions:[],own_reactions:[],reaction_counts:{},reaction_scores:{},reply_count:0,status:"received",cid:m,created_at:n,updated_at:n,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},i=u.map(l),f={[e.id]:{user:{...e,is_account:!1},user_id:e.id,role:"owner",is_account:!1},[a.id]:{user:{...a,is_account:!0},user_id:a.id,role:"member",is_account:!0}},o=s.channel("messaging",h,{members:[e.id,a.id]});o.watch=async()=>(o.state.messages=i,o.state.members=f,{channel:{members:[e.id,a.id]},members:[],messages:i,watchers:[],pinned_messages:[],duration:"0ms"});const y=t=>{o.state.addMessageSorted(t),s.dispatchEvent({type:"message.new",cid:m,message:t,user:t.user??void 0})};let _=0;return o.sendMessage=async t=>{const c=typeof t=="string"?t:(t==null?void 0:t.text)??"",r={...l({text:c,from:"me",id:`mock-sent-${_++}`},i.length),...typeof t=="object"?t:{},user:e,cid:m};y(r);const n=d==null?void 0:d(c);if(n){const g=typeof n=="string"?n:n.text,p=typeof n=="string"?void 0:n.attachments;setTimeout(()=>{y(l({text:g,from:"them",attachments:p,id:`mock-reply-${_}`},i.length))},w)}return{message:r}},s.queryChannels=async()=>[o],o.watch(),{client:s,channel:o,participantFilterId:a.id}}exports.createMockMessagingClient=b;
|
|
2
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +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.\n channel.watch = async () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\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: seededMessages,\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","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Channel } from 'stream-chat';
|
|
2
|
+
import { StreamChat } from 'stream-chat';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds an offline Stream client seeded with a single direct conversation, so
|
|
6
|
+
* consumers can render the **real** messaging UI (MessagingProvider →
|
|
7
|
+
* MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`
|
|
8
|
+
* surfaces and Storybook. It is a real `StreamChat` whose network calls
|
|
9
|
+
* (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are
|
|
10
|
+
* replaced with in-memory behaviour driven by Stream's own channel-state
|
|
11
|
+
* machinery, so rendering fidelity matches production.
|
|
12
|
+
*
|
|
13
|
+
* Ships from `@linktr.ee/messaging-react/testing` and must never be imported by
|
|
14
|
+
* production code.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createMockMessagingClient({ currentUser, participant, messages, channelId, onSend, }: CreateMockMessagingClientOptions): MockMessagingClient;
|
|
17
|
+
|
|
18
|
+
export declare interface CreateMockMessagingClientOptions {
|
|
19
|
+
/** The connected/viewing user. */
|
|
20
|
+
currentUser: MockMessagingUser;
|
|
21
|
+
/** The other party in the direct conversation (e.g. the creator/linker). */
|
|
22
|
+
participant: MockMessagingUser;
|
|
23
|
+
/** Seed conversation, oldest-first. */
|
|
24
|
+
messages?: MockMessage[];
|
|
25
|
+
/** Channel id; defaults to `mock-dm`. */
|
|
26
|
+
channelId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional canned reply. Called after the viewer sends a message; return the
|
|
29
|
+
* reply text (or a partial message) to have `participant` echo a response, or
|
|
30
|
+
* a falsy value for no reply. Drives the "send echo" in `dev:mock-messaging`.
|
|
31
|
+
*/
|
|
32
|
+
onSend?: (text: string) => string | {
|
|
33
|
+
text?: string;
|
|
34
|
+
attachments?: unknown[];
|
|
35
|
+
} | null | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A seed message, authored from the viewer's perspective. `from: 'me'` is the
|
|
40
|
+
* connected user (the viewer); `from: 'them'` is the other participant.
|
|
41
|
+
*/
|
|
42
|
+
export declare interface MockMessage {
|
|
43
|
+
id?: string;
|
|
44
|
+
text?: string;
|
|
45
|
+
from: 'me' | 'them';
|
|
46
|
+
/** Stream attachment payloads, rendered by the toolkit's attachment gate. */
|
|
47
|
+
attachments?: any[];
|
|
48
|
+
/** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */
|
|
49
|
+
metadata?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare interface MockMessagingClient {
|
|
53
|
+
/** Pass to `<MessagingProvider client={...}>`. */
|
|
54
|
+
client: StreamChat;
|
|
55
|
+
/** The seeded direct-conversation channel. */
|
|
56
|
+
channel: Channel;
|
|
57
|
+
/** Pass to `<MessagingShell initialParticipantFilter={...}>`. */
|
|
58
|
+
participantFilterId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A participant in the mock conversation. */
|
|
62
|
+
export declare interface MockMessagingUser {
|
|
63
|
+
id: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
image?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { }
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
declare module 'stream-chat' {
|
|
72
|
+
interface CustomAttachmentData {
|
|
73
|
+
action?: OfficialCtaActionPayload;
|
|
74
|
+
}
|
|
75
|
+
interface CustomChannelData {
|
|
76
|
+
/**
|
|
77
|
+
* Whether the channel has at least one visitor-originated message.
|
|
78
|
+
* Legacy channels may omit this field.
|
|
79
|
+
*/
|
|
80
|
+
has_visitor_message?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Priority score assigned by backend classification for spotlighting.
|
|
83
|
+
*/
|
|
84
|
+
priority_score?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Indicates a paid message is pending creator reply.
|
|
87
|
+
*/
|
|
88
|
+
pending_earn_message_reply?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Indicates DM agent has deferred and creator follow-up is needed.
|
|
91
|
+
*/
|
|
92
|
+
agent_deferred?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Whether DM agent auto-replies are paused for this channel.
|
|
95
|
+
*/
|
|
96
|
+
chatbot_paused?: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface CustomMessageData {
|
|
99
|
+
/**
|
|
100
|
+
* When true, hides the date timestamp in system messages.
|
|
101
|
+
* Used by CustomSystemMessage component.
|
|
102
|
+
*/
|
|
103
|
+
hide_date?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Message metadata from backend.
|
|
106
|
+
* Contains type and payment information.
|
|
107
|
+
*/
|
|
108
|
+
metadata?: MessageMetadata;
|
|
109
|
+
/**
|
|
110
|
+
* DM Agent-specific system message type.
|
|
111
|
+
* Used as a fallback when metadata.custom_type is not available.
|
|
112
|
+
*/
|
|
113
|
+
dm_agent_system_type?: DmAgentSystemType;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { StreamChat as x } from "stream-chat";
|
|
2
|
+
const M = 600;
|
|
3
|
+
function g({
|
|
4
|
+
currentUser: e,
|
|
5
|
+
participant: c,
|
|
6
|
+
messages: u = [],
|
|
7
|
+
channelId: h = "mock-dm",
|
|
8
|
+
onSend: r
|
|
9
|
+
}) {
|
|
10
|
+
const n = new x("mock-api-key", {
|
|
11
|
+
allowServerSideConnect: !0
|
|
12
|
+
});
|
|
13
|
+
n.connectUser = async () => ({ me: e }), n.disconnectUser = async () => {
|
|
14
|
+
}, n.userID = e.id, n.user = e;
|
|
15
|
+
const m = `messaging:${h}`, l = (t, a) => {
|
|
16
|
+
const d = t.from === "me" ? e : c, o = new Date(
|
|
17
|
+
Date.now() - (u.length - a) * 6e4
|
|
18
|
+
).toISOString();
|
|
19
|
+
return {
|
|
20
|
+
id: t.id ?? `mock-msg-${a}`,
|
|
21
|
+
text: t.text ?? "",
|
|
22
|
+
type: "regular",
|
|
23
|
+
html: t.text ? `<p>${t.text}</p>` : "",
|
|
24
|
+
user: d,
|
|
25
|
+
attachments: t.attachments ?? [],
|
|
26
|
+
latest_reactions: [],
|
|
27
|
+
own_reactions: [],
|
|
28
|
+
reaction_counts: {},
|
|
29
|
+
reaction_scores: {},
|
|
30
|
+
reply_count: 0,
|
|
31
|
+
status: "received",
|
|
32
|
+
cid: m,
|
|
33
|
+
created_at: o,
|
|
34
|
+
updated_at: o,
|
|
35
|
+
mentioned_users: [],
|
|
36
|
+
...t.metadata ? { metadata: t.metadata } : {}
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
};
|
|
39
|
+
}, i = u.map(l), f = {
|
|
40
|
+
[e.id]: {
|
|
41
|
+
user: { ...e, is_account: !1 },
|
|
42
|
+
user_id: e.id,
|
|
43
|
+
role: "owner",
|
|
44
|
+
is_account: !1
|
|
45
|
+
},
|
|
46
|
+
[c.id]: {
|
|
47
|
+
user: { ...c, is_account: !0 },
|
|
48
|
+
user_id: c.id,
|
|
49
|
+
role: "member",
|
|
50
|
+
is_account: !0
|
|
51
|
+
}
|
|
52
|
+
}, s = n.channel("messaging", h, {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
members: [e.id, c.id]
|
|
55
|
+
});
|
|
56
|
+
s.watch = async () => (s.state.messages = i, s.state.members = f, {
|
|
57
|
+
channel: { members: [e.id, c.id] },
|
|
58
|
+
members: [],
|
|
59
|
+
messages: i,
|
|
60
|
+
watchers: [],
|
|
61
|
+
pinned_messages: [],
|
|
62
|
+
duration: "0ms"
|
|
63
|
+
});
|
|
64
|
+
const y = (t) => {
|
|
65
|
+
s.state.addMessageSorted(t), n.dispatchEvent({
|
|
66
|
+
type: "message.new",
|
|
67
|
+
cid: m,
|
|
68
|
+
message: t,
|
|
69
|
+
user: t.user ?? void 0
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
let _ = 0;
|
|
74
|
+
return s.sendMessage = async (t) => {
|
|
75
|
+
const a = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", d = {
|
|
76
|
+
...l({ text: a, from: "me", id: `mock-sent-${_++}` }, i.length),
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
...typeof t == "object" ? t : {},
|
|
79
|
+
user: e,
|
|
80
|
+
cid: m
|
|
81
|
+
};
|
|
82
|
+
y(d);
|
|
83
|
+
const o = r == null ? void 0 : r(a);
|
|
84
|
+
if (o) {
|
|
85
|
+
const p = typeof o == "string" ? o : o.text, w = typeof o == "string" ? void 0 : o.attachments;
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
y(
|
|
88
|
+
l(
|
|
89
|
+
{
|
|
90
|
+
text: p,
|
|
91
|
+
from: "them",
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
attachments: w,
|
|
94
|
+
id: `mock-reply-${_}`
|
|
95
|
+
},
|
|
96
|
+
i.length
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}, M);
|
|
100
|
+
}
|
|
101
|
+
return { message: d };
|
|
102
|
+
}, n.queryChannels = async () => [s], s.watch(), { client: n, channel: s, participantFilterId: c.id };
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
g as createMockMessagingClient
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +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.\n channel.watch = async () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\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: seededMessages,\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","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.
|
|
3
|
+
"version": "3.7.0-rc-1783304153",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
|
+
"./testing": {
|
|
16
|
+
"types": "./dist/testing.d.ts",
|
|
17
|
+
"import": "./dist/testing.js",
|
|
18
|
+
"require": "./dist/testing.cjs"
|
|
19
|
+
},
|
|
15
20
|
"./styles.css": "./dist/assets/index.css"
|
|
16
21
|
},
|
|
17
22
|
"license": "UNLICENSED",
|
|
@@ -38,7 +43,7 @@
|
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
45
|
"@linktr.ee/component-library": "11.8.6",
|
|
41
|
-
"@linktr.ee/messaging-core": "
|
|
46
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1783304153",
|
|
42
47
|
"@phosphor-icons/react": "^2.1.10"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
@@ -258,4 +258,34 @@ describe('MessagingProvider', () => {
|
|
|
258
258
|
await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(2))
|
|
259
259
|
expect(connectUser).toHaveBeenLastCalledWith(authedUser)
|
|
260
260
|
})
|
|
261
|
+
|
|
262
|
+
it('uses an injected client directly and never connects a service', async () => {
|
|
263
|
+
const { connectUser, disconnectUser } = setupServiceMock()
|
|
264
|
+
const injectedClient = {
|
|
265
|
+
userID: 'injected-user',
|
|
266
|
+
} as unknown as StreamChat
|
|
267
|
+
const states: unknown[] = []
|
|
268
|
+
|
|
269
|
+
render(
|
|
270
|
+
<MessagingProvider client={injectedClient} user={null}>
|
|
271
|
+
<Probe onState={(s) => states.push(s)} />
|
|
272
|
+
</MessagingProvider>
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Injected client is reported connected immediately, and its instance is
|
|
276
|
+
// surfaced on the context.
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
const last = states[states.length - 1] as {
|
|
279
|
+
isConnected: boolean
|
|
280
|
+
hasClient: boolean
|
|
281
|
+
}
|
|
282
|
+
expect(last.isConnected).toBe(true)
|
|
283
|
+
expect(last.hasClient).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// The caller owns the injected client's lifecycle — the provider must not
|
|
287
|
+
// spin up a StreamChatService or connect/disconnect it.
|
|
288
|
+
expect(connectUser).not.toHaveBeenCalled()
|
|
289
|
+
expect(disconnectUser).not.toHaveBeenCalled()
|
|
290
|
+
})
|
|
261
291
|
})
|
|
@@ -52,6 +52,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
52
52
|
apiKey,
|
|
53
53
|
capabilities = {},
|
|
54
54
|
debug = false,
|
|
55
|
+
client: injectedClient,
|
|
55
56
|
}) => {
|
|
56
57
|
// Create debug logger that respects the debug prop
|
|
57
58
|
const debugLog = useCallback(
|
|
@@ -137,10 +138,18 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
137
138
|
// not the process-wide StreamChat.getInstance, which is shared across keys and
|
|
138
139
|
// provider instances.
|
|
139
140
|
const clientRef = useRef<{ apiKey: string; client: StreamChat } | null>(null)
|
|
140
|
-
if (apiKey && clientRef.current?.apiKey !== apiKey) {
|
|
141
|
+
if (!injectedClient && apiKey && clientRef.current?.apiKey !== apiKey) {
|
|
141
142
|
clientRef.current = { apiKey, client: new StreamChat(apiKey) }
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
+
// An injected client takes precedence and short-circuits the apiKey-derived
|
|
145
|
+
// client, the service, and the connect/disconnect lifecycle below — the
|
|
146
|
+
// caller owns that instance (offline/dev via `createMockMessagingClient`, or
|
|
147
|
+
// a test). Otherwise fall back to the apiKey-derived client as before.
|
|
148
|
+
const chatClient = injectedClient
|
|
149
|
+
? injectedClient
|
|
150
|
+
: apiKey
|
|
151
|
+
? (clientRef.current?.client ?? null)
|
|
152
|
+
: null
|
|
144
153
|
|
|
145
154
|
// Create the service once per client lifetime (apiKey). Keyed on `chatClient`
|
|
146
155
|
// — NOT on serviceConfig/debug — so those changing does not spin up a second
|
|
@@ -152,6 +161,12 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
152
161
|
const currentRender = renderCountRef.current
|
|
153
162
|
const currentConfig = serviceConfigRef.current
|
|
154
163
|
|
|
164
|
+
// An injected client owns its own connection — never create a service or
|
|
165
|
+
// connect for it.
|
|
166
|
+
if (injectedClient) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
if (!apiKey || !chatClient || !currentConfig) {
|
|
156
171
|
debugLog('⚠️ SERVICE INIT SKIPPED', {
|
|
157
172
|
renderCount: currentRender,
|
|
@@ -182,7 +197,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
182
197
|
// No disconnect on cleanup here — disconnection is owned by the
|
|
183
198
|
// client-cleanup effect below, keyed on `chatClient` (apiKey) and unmount.
|
|
184
199
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- serviceConfig/debug/debugLog are read via refs by design (see above); recreating on them would re-adopt the live client
|
|
185
|
-
}, [apiKey, chatClient])
|
|
200
|
+
}, [apiKey, chatClient, injectedClient])
|
|
186
201
|
|
|
187
202
|
// Track if we've already connected this user with this service to prevent duplicate connections
|
|
188
203
|
const connectedUserRef = useRef<{
|
|
@@ -324,8 +339,11 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
324
339
|
|
|
325
340
|
return {
|
|
326
341
|
service,
|
|
327
|
-
client,
|
|
328
|
-
|
|
342
|
+
// An injected client is already connected and owns its lifecycle, so
|
|
343
|
+
// surface it directly and report connected — the apiKey path's `client`
|
|
344
|
+
// state (set by connectUser) is never populated in that mode.
|
|
345
|
+
client: injectedClient ?? client,
|
|
346
|
+
isConnected: injectedClient ? true : isConnected,
|
|
329
347
|
isLoading,
|
|
330
348
|
error,
|
|
331
349
|
capabilities,
|
|
@@ -335,6 +353,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
335
353
|
}, [
|
|
336
354
|
service,
|
|
337
355
|
client,
|
|
356
|
+
injectedClient,
|
|
338
357
|
isConnected,
|
|
339
358
|
isLoading,
|
|
340
359
|
error,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { MessagingShell } from '../components/MessagingShell'
|
|
5
|
+
import { MessagingProvider } from '../providers/MessagingProvider'
|
|
6
|
+
|
|
7
|
+
import { createMockMessagingClient } from './createMockMessagingClient'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exercises the real MessagingProvider → MessagingShell → ChannelView stack
|
|
11
|
+
* against an injected mock client (the `dev:mock-messaging` path). Use this to
|
|
12
|
+
* visually verify send-echo, which the unit tests can't cover because they stub
|
|
13
|
+
* stream-chat-react.
|
|
14
|
+
*/
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'testing/createMockMessagingClient',
|
|
17
|
+
parameters: { layout: 'fullscreen' },
|
|
18
|
+
}
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
const currentUser = { id: 'viewer-1', name: 'You' }
|
|
22
|
+
const participant = {
|
|
23
|
+
id: 'creator-1',
|
|
24
|
+
name: 'Ava Creator',
|
|
25
|
+
image: 'https://i.pravatar.cc/150?img=5',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Template: StoryFn = () => {
|
|
29
|
+
const [mock] = React.useState(() =>
|
|
30
|
+
createMockMessagingClient({
|
|
31
|
+
currentUser,
|
|
32
|
+
participant,
|
|
33
|
+
messages: [
|
|
34
|
+
{
|
|
35
|
+
id: 'w1',
|
|
36
|
+
from: 'them',
|
|
37
|
+
text: 'Welcome! Thanks for reaching out 👋',
|
|
38
|
+
metadata: { custom_type: 'MESSAGE_WELCOME' },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'l1',
|
|
42
|
+
from: 'them',
|
|
43
|
+
text: 'Check out my latest drop:',
|
|
44
|
+
attachments: [
|
|
45
|
+
{
|
|
46
|
+
type: 'image',
|
|
47
|
+
og_scrape_url: 'https://tr.ee/example',
|
|
48
|
+
title: 'My latest drop',
|
|
49
|
+
text: 'Limited edition',
|
|
50
|
+
image_url: 'https://picsum.photos/seed/mockdrop/560/360',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{ id: 'm1', from: 'me', text: 'Love it — just grabbed one!' },
|
|
55
|
+
],
|
|
56
|
+
onSend: (text) => `Thanks for saying "${text}"! 🎉`,
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<MessagingProvider client={mock.client} user={null}>
|
|
62
|
+
<div className="h-screen w-full bg-[#FBFAF9]">
|
|
63
|
+
<MessagingShell initialParticipantFilter={mock.participantFilterId} />
|
|
64
|
+
</div>
|
|
65
|
+
</MessagingProvider>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Default: StoryFn = Template.bind({})
|
|
70
|
+
Default.parameters = {
|
|
71
|
+
docs: {
|
|
72
|
+
description: {
|
|
73
|
+
story:
|
|
74
|
+
'Seeded direct conversation rendered through the real shell. Type a message and send it — the mock echoes a canned reply (send-echo).',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|