@linktr.ee/messaging-react 3.6.2 → 3.7.0-rc-1783305846

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{Card-NDZLmjxS.js → Card-BAt0xifq.js} +2 -2
  2. package/dist/{Card-NDZLmjxS.js.map → Card-BAt0xifq.js.map} +1 -1
  3. package/dist/{Card-DSQclaiH.js → Card-BB34_MDl.js} +3 -3
  4. package/dist/{Card-DSQclaiH.js.map → Card-BB34_MDl.js.map} +1 -1
  5. package/dist/{Card-Jc1n2Iqu.cjs → Card-BDKGSydN.cjs} +2 -2
  6. package/dist/{Card-Jc1n2Iqu.cjs.map → Card-BDKGSydN.cjs.map} +1 -1
  7. package/dist/{Card-DEk34Twe.cjs → Card-BGJY5HWf.cjs} +2 -2
  8. package/dist/{Card-DEk34Twe.cjs.map → Card-BGJY5HWf.cjs.map} +1 -1
  9. package/dist/{Card-Cz8tanga.js → Card-C3-Ea5ho.js} +2 -2
  10. package/dist/{Card-Cz8tanga.js.map → Card-C3-Ea5ho.js.map} +1 -1
  11. package/dist/{Card-BThi6-oD.cjs → Card-DnFtpG1h.cjs} +2 -2
  12. package/dist/{Card-BThi6-oD.cjs.map → Card-DnFtpG1h.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-CqcnnPXB.cjs → LockedThumbnail-Bl9UC9sQ.cjs} +2 -2
  14. package/dist/{LockedThumbnail-CqcnnPXB.cjs.map → LockedThumbnail-Bl9UC9sQ.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-t9fLaFmy.js → LockedThumbnail-so7ftBnG.js} +2 -2
  16. package/dist/{LockedThumbnail-t9fLaFmy.js.map → LockedThumbnail-so7ftBnG.js.map} +1 -1
  17. package/dist/{index-bPlX3Oo5.js → index-2HMjfjM-.js} +1559 -1744
  18. package/dist/index-2HMjfjM-.js.map +1 -0
  19. package/dist/index-BgzqZorr.cjs +2 -0
  20. package/dist/index-BgzqZorr.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +14 -2
  23. package/dist/index.js +1 -1
  24. package/dist/testing.cjs +2 -0
  25. package/dist/testing.cjs.map +1 -0
  26. package/dist/testing.d.ts +116 -0
  27. package/dist/testing.js +107 -0
  28. package/dist/testing.js.map +1 -0
  29. package/package.json +14 -2
  30. package/src/components/ChannelView.stories.tsx +11 -68
  31. package/src/components/ChannelView.test.tsx +13 -54
  32. package/src/components/ChannelView.tsx +28 -74
  33. package/src/providers/MessagingProvider.test.tsx +30 -0
  34. package/src/providers/MessagingProvider.tsx +24 -5
  35. package/src/testing/createMockMessagingClient.stories.tsx +77 -0
  36. package/src/testing/createMockMessagingClient.ts +231 -0
  37. package/src/testing/index.ts +7 -0
  38. package/src/types.ts +15 -2
  39. package/dist/index-CPCX_7gP.cjs +0 -2
  40. package/dist/index-CPCX_7gP.cjs.map +0 -1
  41. package/dist/index-bPlX3Oo5.js.map +0 -1
  42. package/src/components/ChannelHeaderRedesign.tsx +0 -231
  43. /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-CPCX_7gP.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;
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
- serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>;
1241
- apiKey: string;
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-bPlX3Oo5.js";
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,
@@ -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
+
@@ -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.6.2",
3
+ "version": "3.7.0-rc-1783305846",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -12,8 +12,20 @@
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
  },
22
+ "typesVersions": {
23
+ "*": {
24
+ "testing": [
25
+ "dist/testing.d.ts"
26
+ ]
27
+ }
28
+ },
17
29
  "license": "UNLICENSED",
18
30
  "files": [
19
31
  "dist",
@@ -38,7 +50,7 @@
38
50
  },
39
51
  "dependencies": {
40
52
  "@linktr.ee/component-library": "11.8.6",
41
- "@linktr.ee/messaging-core": "^2.2.0",
53
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783305846",
42
54
  "@phosphor-icons/react": "^2.1.10"
43
55
  },
44
56
  "devDependencies": {
@@ -16,7 +16,6 @@ import { ChannelView } from './ChannelView'
16
16
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
17
17
 
18
18
  type ComponentProps = React.ComponentProps<typeof ChannelView>
19
- const SCROLLABLE_MESSAGE_COUNT = 10
20
19
 
21
20
  const meta: Meta<ComponentProps> = {
22
21
  title: 'ChannelView',
@@ -42,9 +41,7 @@ const createMockChannel = async (
42
41
  client: StreamChat,
43
42
  hasMessages = true,
44
43
  followerStatus?: string | boolean,
45
- isFrozen = false,
46
- participantCustomerTag?: string,
47
- hasScrollableMessages = false
44
+ isFrozen = false
48
45
  ) => {
49
46
  const participant = mockParticipants[0]
50
47
 
@@ -104,34 +101,6 @@ const createMockChannel = async (
104
101
  cid: 'messaging:storybook-channel-1',
105
102
  mentioned_users: [],
106
103
  },
107
- ...(hasScrollableMessages
108
- ? Array.from({ length: SCROLLABLE_MESSAGE_COUNT }, (_, index) => {
109
- const isParticipantMessage = index % 2 === 0
110
- const user = isParticipantMessage ? participant : mockUser
111
- const text = isParticipantMessage
112
- ? `Here is another message to show how the redesigned header floats above the thread (${index + 1}).`
113
- : `Replying so the conversation has enough content to scroll under the header (${index + 1}).`
114
-
115
- return {
116
- id: `msg-extra-${index + 1}`,
117
- text,
118
- type: 'regular' as const,
119
- created_at: minutesAgo(Math.max(1, 25 - index)),
120
- updated_at: minutesAgo(Math.max(1, 25 - index)),
121
- user,
122
- html: `<p>${text}</p>`,
123
- attachments: [],
124
- latest_reactions: [],
125
- own_reactions: [],
126
- reaction_counts: {},
127
- reaction_scores: {},
128
- reply_count: 0,
129
- status: 'received',
130
- cid: 'messaging:storybook-channel-1',
131
- mentioned_users: [],
132
- }
133
- })
134
- : []),
135
104
  ]
136
105
  : []
137
106
 
@@ -149,11 +118,7 @@ const createMockChannel = async (
149
118
  }
150
119
 
151
120
  // Create a real channel using the client
152
- const channel = client.channel(
153
- 'messaging',
154
- 'storybook-channel-1',
155
- channelData
156
- )
121
+ const channel = client.channel('messaging', 'storybook-channel-1', channelData)
157
122
 
158
123
  // Mock the watch method to return mocked data
159
124
  channel.watch = async () => {
@@ -177,7 +142,6 @@ const createMockChannel = async (
177
142
  user_id: participant.id,
178
143
  role: 'member',
179
144
  is_account: true,
180
- customerTag: participantCustomerTag,
181
145
  },
182
146
  }
183
147
  return {
@@ -205,7 +169,6 @@ const createMockChannel = async (
205
169
  }
206
170
 
207
171
  type TemplateProps = ComponentProps & {
208
- hasMessages?: boolean
209
172
  followerStatus?: string | boolean
210
173
  isFrozen?: boolean
211
174
  typingUser?: {
@@ -213,17 +176,12 @@ type TemplateProps = ComponentProps & {
213
176
  name?: string
214
177
  image?: string
215
178
  }
216
- participantCustomerTag?: string
217
- hasScrollableMessages?: boolean
218
179
  }
219
180
 
220
181
  const Template: StoryFn<TemplateProps> = (args) => {
221
182
  const {
222
- hasMessages = true,
223
183
  followerStatus,
224
184
  isFrozen = false,
225
- participantCustomerTag,
226
- hasScrollableMessages,
227
185
  typingUser,
228
186
  ...channelViewProps
229
187
  } = args
@@ -235,28 +193,16 @@ const Template: StoryFn<TemplateProps> = (args) => {
235
193
  client.user = mockUser
236
194
  return client
237
195
  })
238
-
196
+
239
197
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
240
198
 
241
199
  useEffect(() => {
242
- createMockChannel(
243
- client,
244
- hasMessages,
245
- followerStatus,
246
- isFrozen,
247
- participantCustomerTag,
248
- hasScrollableMessages
249
- ).then((mockChannel) => {
250
- setChannel(mockChannel)
251
- })
252
- }, [
253
- client,
254
- hasMessages,
255
- followerStatus,
256
- isFrozen,
257
- participantCustomerTag,
258
- hasScrollableMessages,
259
- ])
200
+ createMockChannel(client, true, followerStatus, isFrozen).then(
201
+ (mockChannel) => {
202
+ setChannel(mockChannel)
203
+ }
204
+ )
205
+ }, [client, followerStatus, isFrozen])
260
206
 
261
207
  useEffect(() => {
262
208
  if (!channel || !typingUser) {
@@ -310,9 +256,6 @@ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
310
256
  )
311
257
  ParticipantIdentityTrigger.args = {
312
258
  showBackButton: false,
313
- showStarButton: true,
314
- participantCustomerTag: 'CUSTOMER_PAID',
315
- hasScrollableMessages: true,
316
259
  onParticipantNameClick: () =>
317
260
  // eslint-disable-next-line no-alert
318
261
  window.alert('Participant details clicked — open Thread content'),
@@ -321,7 +264,7 @@ ParticipantIdentityTrigger.parameters = {
321
264
  docs: {
322
265
  description: {
323
266
  story:
324
- 'Header with `onParticipantNameClick` set: the redesigned floating header centers the avatar and name pill, shows paid/star badges when present, and fires the callback when clicked (shows an alert here). With the prop omitted (see Default) the previous header remains in use.',
267
+ 'Header with `onParticipantNameClick` set: the participant avatar and name render as one button with a trailing caret in both the mobile and desktop header layouts. Click the identity block to fire the callback (shows an alert here). With the prop omitted (see Default) the header remains non-interactive.',
325
268
  },
326
269
  },
327
270
  }
@@ -599,7 +542,7 @@ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
599
542
  client.user = mockUser
600
543
  return client
601
544
  })
602
-
545
+
603
546
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
604
547
 
605
548
  useEffect(() => {
@@ -22,9 +22,7 @@ vi.mock('stream-chat-react', () => ({
22
22
  Window: ({ children }: { children: React.ReactNode }) => (
23
23
  <div data-testid="window">{children}</div>
24
24
  ),
25
- MessageList: ({ head }: { head?: React.ReactElement }) => (
26
- <div data-testid="message-list">{head}</div>
27
- ),
25
+ MessageList: () => <div data-testid="message-list" />,
28
26
  WithComponents: ({ children }: { children: React.ReactNode }) => (
29
27
  <>{children}</>
30
28
  ),
@@ -96,19 +94,15 @@ vi.mock('../hooks/useChannelStar', () => ({
96
94
  const createChannel = ({
97
95
  currentUserId = 'visitor-1',
98
96
  currentUserRole = 'owner',
99
- messages = [],
100
97
  otherUser = {
101
98
  id: 'linker-1' as const,
102
99
  name: 'Linker' as string | undefined,
103
100
  username: undefined as string | undefined,
104
101
  is_account: true,
105
102
  },
106
- otherMemberCustomerTag,
107
103
  }: {
108
104
  currentUserId?: 'visitor-1' | 'linker-1'
109
105
  currentUserRole?: 'owner' | 'member'
110
- messages?: Channel['state']['messages']
111
- otherMemberCustomerTag?: string
112
106
  otherUser?: {
113
107
  id: string
114
108
  name?: string
@@ -140,12 +134,11 @@ const createChannel = ({
140
134
  is_account:
141
135
  otherUser.is_account ?? currentUserId === 'visitor-1',
142
136
  },
143
- customerTag: otherMemberCustomerTag,
144
137
  role: currentUserRole === 'owner' ? 'member' : 'owner',
145
138
  },
146
139
  },
147
140
  membership: {},
148
- messages,
141
+ messages: [],
149
142
  },
150
143
  on: vi.fn(),
151
144
  off: vi.fn(),
@@ -210,7 +203,7 @@ describe('ChannelView', () => {
210
203
  )
211
204
  })
212
205
 
213
- it('renders the redesigned header button when onParticipantNameClick is provided', () => {
206
+ it('renders the participant name as a button that fires onParticipantNameClick', () => {
214
207
  const onParticipantNameClick = vi.fn()
215
208
  renderWithProviders(
216
209
  <ChannelView
@@ -219,63 +212,29 @@ describe('ChannelView', () => {
219
212
  />
220
213
  )
221
214
 
222
- const button = screen.getByRole('button', {
215
+ // Both the mobile and desktop header layouts render the button (one is
216
+ // hidden via CSS), so there is at least one match.
217
+ const buttons = screen.getAllByRole('button', {
223
218
  name: /view details for linker/i,
224
219
  })
225
- expect(button).toHaveClass(
220
+ expect(buttons.length).toBeGreaterThanOrEqual(1)
221
+ expect(buttons[0]).toHaveClass(
226
222
  'appearance-none',
227
223
  'border-0',
228
224
  'bg-transparent',
229
- 'flex-col'
225
+ 'rounded-[12px]',
226
+ 'px-1',
227
+ 'py-0.5'
230
228
  )
231
229
 
232
230
  const avatars = screen.getAllByTestId('avatar')
233
231
  expect(avatars.length).toBeGreaterThanOrEqual(1)
234
- expect(avatars[0].closest('button')).toBe(button)
235
- expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
232
+ expect(avatars[0].closest('button')).toBe(buttons[0])
236
233
 
237
- button.click()
234
+ buttons[0].click()
238
235
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
239
236
  })
240
237
 
241
- it('renders redesigned header badges for paid and starred conversations', () => {
242
- mockIsStarred = true
243
-
244
- renderWithProviders(
245
- <ChannelView
246
- channel={createChannel({
247
- otherMemberCustomerTag: 'CUSTOMER_PAID',
248
- })}
249
- onParticipantNameClick={vi.fn()}
250
- showStarButton={true}
251
- />
252
- )
253
-
254
- expect(screen.getByLabelText('Paid customer')).toBeInTheDocument()
255
- expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
256
- })
257
-
258
- it('renders the redesigned header outside MessageList head when all messages are deleted', () => {
259
- renderWithProviders(
260
- <ChannelView
261
- channel={createChannel({
262
- messages: [
263
- {
264
- id: 'deleted-message-1',
265
- type: 'deleted',
266
- } as Channel['state']['messages'][number],
267
- ],
268
- })}
269
- onParticipantNameClick={vi.fn()}
270
- />
271
- )
272
-
273
- const button = screen.getByRole('button', {
274
- name: /view details for linker/i,
275
- })
276
- expect(button.closest('[data-testid="message-list"]')).toBeNull()
277
- })
278
-
279
238
  it('renders the actions menu by default', () => {
280
239
  renderWithProviders(<ChannelView channel={createChannel()} />)
281
240