@linktr.ee/messaging-react 4.2.0-rc-1787325710 → 4.2.1-rc-1787325541
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-Be81K6su.cjs +2 -0
- package/dist/Card-Be81K6su.cjs.map +1 -0
- package/dist/Card-BzvDr-pU.js +112 -0
- package/dist/Card-BzvDr-pU.js.map +1 -0
- package/dist/Card-DEyEXhqc.cjs +2 -0
- package/dist/Card-DEyEXhqc.cjs.map +1 -0
- package/dist/Card-SKwZNuBP.js +701 -0
- package/dist/Card-SKwZNuBP.js.map +1 -0
- package/dist/PurchaseStatusBadge-BmqAQEV9.cjs +2 -0
- package/dist/PurchaseStatusBadge-BmqAQEV9.cjs.map +1 -0
- package/dist/PurchaseStatusBadge-D1RRsubZ.js +59 -0
- package/dist/PurchaseStatusBadge-D1RRsubZ.js.map +1 -0
- package/dist/assets/index.css +1 -2
- package/dist/index-BQCvCIsC.cjs +3 -0
- package/dist/index-BQCvCIsC.cjs.map +1 -0
- package/dist/index-Dp0HCBIW.js +5901 -0
- package/dist/index-Dp0HCBIW.js.map +1 -0
- package/dist/index.cjs +2 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +39 -4071
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +2 -2
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +118 -106
- package/dist/testing.js.map +1 -1
- package/package.json +5 -5
- package/src/components/ChannelView.stories.tsx +4 -0
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +28 -0
- package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +16 -8
- package/dist/AttachmentCard-C-X0jLZ2.cjs +0 -2
- package/dist/AttachmentCard-C-X0jLZ2.cjs.map +0 -1
- package/dist/AttachmentCard-D2IoHdcv.js +0 -410
- package/dist/AttachmentCard-D2IoHdcv.js.map +0 -1
- package/dist/Card-C7uLQbYz.js +0 -105
- package/dist/Card-C7uLQbYz.js.map +0 -1
- package/dist/Card-DENHpLIC.cjs +0 -2
- package/dist/Card-DENHpLIC.cjs.map +0 -1
- package/dist/Card-Ds1FMcUj.cjs +0 -2
- package/dist/Card-Ds1FMcUj.cjs.map +0 -1
- package/dist/Card-EKD9NkTa.js +0 -498
- package/dist/Card-EKD9NkTa.js.map +0 -1
- package/dist/LoadingDots-BSk_UeS0.cjs +0 -2
- package/dist/LoadingDots-BSk_UeS0.cjs.map +0 -1
- package/dist/LoadingDots-B_WiXBeQ.js +0 -73
- package/dist/LoadingDots-B_WiXBeQ.js.map +0 -1
- package/dist/rolldown-runtime-BocRIvOZ.cjs +0 -1
package/dist/testing.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value
|
|
2
|
-
//# sourceMappingURL=testing.cjs.map
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("stream-chat");function v(s,{stubConnection:n=!1}={}){const a=new b.StreamChat("mock-api-key",{allowServerSideConnect:!0});return a.userID=s.id,a.user=s,n&&(a.connectUser=async()=>({me:s}),a.disconnectUser=async()=>{},a.userMuteStatus=(()=>!1)),a}const w=600;function x({currentUser:s,participant:n,messages:a=[],channelId:y="mock-dm",onSend:d}){const r=v(s,{stubConnection:!0}),u=`messaging:${y}`,l=(t,c)=>{const i=t.from==="me"?s:n,o=new Date(Date.now()-(a.length-c)*6e4);return{id:t.id??`mock-msg-${c}`,text:t.text??"",type:"regular",html:t.text?`<p>${t.text}</p>`:"",user:i,attachments:t.attachments??[],latest_reactions:[],own_reactions:[],reaction_counts:{},reaction_scores:{},reply_count:0,status:"received",cid:u,created_at:o,updated_at:o,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},m=a.map(l),_={[s.id]:{user:{...s,is_account:!1},user_id:s.id,role:"owner",is_account:!1},[n.id]:{user:{...n,is_account:!0},user_id:n.id,role:"member",is_account:!0}},e=r.channel("messaging",y,{members:[s.id,n.id]});e.muteStatus=()=>({muted:!1,createdAt:null,expiresAt:null}),e.markRead=(async()=>({})),e.keystroke=(async()=>{}),e.stopTyping=(async()=>{}),e.sendReaction=(async()=>({})),e.deleteReaction=(async()=>({})),e.countUnread=(()=>0),e.query=(async()=>({messages:[]})),e.hide=(async()=>({}));let h=!1;e.watch=async()=>(h||(e.state.messages=m,h=!0),e.state.members=_,{channel:{members:[s.id,n.id]},members:[],messages:e.state.messages,watchers:[],pinned_messages:[],duration:"0ms"});const f=t=>{e.state.addMessageSorted(t),r.dispatchEvent({type:"message.new",cid:u,message:t,user:t.user??void 0})};let p=0,g=0;return e.sendMessage=async t=>{const c=typeof t=="string"?t:(t==null?void 0:t.text)??"",i={...l({text:c,from:"me",id:`mock-sent-${p++}`},m.length),...typeof t=="object"?t:{},user:s,cid:u};f(i);const o=d==null?void 0:d(c);if(o){const M=typeof o=="string"?o:o.text,k=typeof o=="string"?void 0:o.attachments,C=`mock-reply-${g++}`;setTimeout(()=>{f(l({text:M,from:"them",attachments:k,id:C},m.length))},w)}return{message:i}},r.queryChannels=async()=>[e],e.watch(),{client:r,channel:e,participantFilterId:n.id}}exports.createMockMessagingClient=x;
|
|
2
|
+
//# sourceMappingURL=testing.cjs.map
|
package/dist/testing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.cjs","names":[],"sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport {\n createMockStreamChatClient,\n type MockMessagingUser,\n} from './createMockStreamChatClient'\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n channel.muteStatus = () => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n let replyCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: replyId,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.cjs","sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport {\n createMockStreamChatClient,\n type MockMessagingUser,\n} from './createMockStreamChatClient'\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n channel.muteStatus = () => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n let replyCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: replyId,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["createMockStreamChatClient","user","stubConnection","client","StreamChat","REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","replyCount","text","sent","reply","replyText","replyAttachments","replyId"],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -1,108 +1,120 @@
|
|
|
1
|
-
import { StreamChat as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { StreamChat as g } from "stream-chat";
|
|
2
|
+
function v(s, { stubConnection: n = !1 } = {}) {
|
|
3
|
+
const a = new g("mock-api-key", {
|
|
4
|
+
allowServerSideConnect: !0
|
|
5
|
+
});
|
|
6
|
+
return a.userID = s.id, a.user = s, n && (a.connectUser = async () => ({ me: s }), a.disconnectUser = async () => {
|
|
7
|
+
}, a.userMuteStatus = (() => !1)), a;
|
|
6
8
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
9
|
+
const C = 600;
|
|
10
|
+
function S({
|
|
11
|
+
currentUser: s,
|
|
12
|
+
participant: n,
|
|
13
|
+
messages: a = [],
|
|
14
|
+
channelId: y = "mock-dm",
|
|
15
|
+
onSend: d
|
|
16
|
+
}) {
|
|
17
|
+
const r = v(s, {
|
|
18
|
+
stubConnection: !0
|
|
19
|
+
}), u = `messaging:${y}`, l = (t, c) => {
|
|
20
|
+
const i = t.from === "me" ? s : n, o = new Date(Date.now() - (a.length - c) * 6e4);
|
|
21
|
+
return {
|
|
22
|
+
id: t.id ?? `mock-msg-${c}`,
|
|
23
|
+
text: t.text ?? "",
|
|
24
|
+
type: "regular",
|
|
25
|
+
html: t.text ? `<p>${t.text}</p>` : "",
|
|
26
|
+
user: i,
|
|
27
|
+
attachments: t.attachments ?? [],
|
|
28
|
+
latest_reactions: [],
|
|
29
|
+
own_reactions: [],
|
|
30
|
+
reaction_counts: {},
|
|
31
|
+
reaction_scores: {},
|
|
32
|
+
reply_count: 0,
|
|
33
|
+
status: "received",
|
|
34
|
+
cid: u,
|
|
35
|
+
created_at: o,
|
|
36
|
+
updated_at: o,
|
|
37
|
+
mentioned_users: [],
|
|
38
|
+
...t.metadata ? { metadata: t.metadata } : {}
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
};
|
|
41
|
+
}, m = a.map(l), p = {
|
|
42
|
+
[s.id]: {
|
|
43
|
+
user: { ...s, is_account: !1 },
|
|
44
|
+
user_id: s.id,
|
|
45
|
+
role: "owner",
|
|
46
|
+
is_account: !1
|
|
47
|
+
},
|
|
48
|
+
[n.id]: {
|
|
49
|
+
user: { ...n, is_account: !0 },
|
|
50
|
+
user_id: n.id,
|
|
51
|
+
role: "member",
|
|
52
|
+
is_account: !0
|
|
53
|
+
}
|
|
54
|
+
}, e = r.channel("messaging", y, {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
members: [s.id, n.id]
|
|
57
|
+
});
|
|
58
|
+
e.muteStatus = () => ({
|
|
59
|
+
muted: !1,
|
|
60
|
+
createdAt: null,
|
|
61
|
+
expiresAt: null
|
|
62
|
+
}), e.markRead = (async () => ({})), e.keystroke = (async () => {
|
|
63
|
+
}), e.stopTyping = (async () => {
|
|
64
|
+
}), e.sendReaction = (async () => ({})), e.deleteReaction = (async () => ({})), e.countUnread = (() => 0), e.query = (async () => ({ messages: [] })), e.hide = (async () => ({}));
|
|
65
|
+
let h = !1;
|
|
66
|
+
e.watch = async () => (h || (e.state.messages = m, h = !0), e.state.members = p, {
|
|
67
|
+
channel: { members: [s.id, n.id] },
|
|
68
|
+
members: [],
|
|
69
|
+
messages: e.state.messages,
|
|
70
|
+
watchers: [],
|
|
71
|
+
pinned_messages: [],
|
|
72
|
+
duration: "0ms"
|
|
73
|
+
});
|
|
74
|
+
const f = (t) => {
|
|
75
|
+
e.state.addMessageSorted(t), r.dispatchEvent({
|
|
76
|
+
type: "message.new",
|
|
77
|
+
cid: u,
|
|
78
|
+
message: t,
|
|
79
|
+
user: t.user ?? void 0
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
let _ = 0, k = 0;
|
|
84
|
+
return e.sendMessage = async (t) => {
|
|
85
|
+
const c = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", i = {
|
|
86
|
+
...l(
|
|
87
|
+
{ text: c, from: "me", id: `mock-sent-${_++}` },
|
|
88
|
+
m.length
|
|
89
|
+
),
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
...typeof t == "object" ? t : {},
|
|
92
|
+
user: s,
|
|
93
|
+
cid: u
|
|
94
|
+
};
|
|
95
|
+
f(i);
|
|
96
|
+
const o = d == null ? void 0 : d(c);
|
|
97
|
+
if (o) {
|
|
98
|
+
const w = typeof o == "string" ? o : o.text, x = typeof o == "string" ? void 0 : o.attachments, M = `mock-reply-${k++}`;
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
f(
|
|
101
|
+
l(
|
|
102
|
+
{
|
|
103
|
+
text: w,
|
|
104
|
+
from: "them",
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
attachments: x,
|
|
107
|
+
id: M
|
|
108
|
+
},
|
|
109
|
+
m.length
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}, C);
|
|
113
|
+
}
|
|
114
|
+
return { message: i };
|
|
115
|
+
}, r.queryChannels = async () => [e], e.watch(), { client: r, channel: e, participantFilterId: n.id };
|
|
104
116
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//# sourceMappingURL=testing.js.map
|
|
117
|
+
export {
|
|
118
|
+
S as createMockMessagingClient
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport {\n createMockStreamChatClient,\n type MockMessagingUser,\n} from './createMockStreamChatClient'\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n channel.muteStatus = () => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n let replyCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: replyId,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"mappings":";;AA6BA,SAAgB,EACd,GACA,EAAE,oBAAiB,OAA6C,CAAC,GACrD;CACZ,IAAM,IAAS,IAAI,EAAW,gBAAgB,EAC5C,wBAAwB,GAC1B,CAAC;CAUD,OATA,EAAO,SAAS,EAAK,IACrB,EAAO,OAAO,GAEV,MACF,EAAO,cAAc,aAAa,EAAE,IAAI,EAAK,IAC7C,EAAO,iBAAiB,YAAY,KAAA,GACpC,EAAO,wBAAwB,MAG1B;AACT;;;ACUA,IAAM,IAAiB;AAcvB,SAAgB,EAA0B,EACxC,gBACA,gBACA,cAAW,CAAC,GACZ,eAAY,WACZ,aACwD;CAMxD,IAAM,IAAS,EAA2B,GAAa,EACrD,gBAAgB,GAClB,CAAC,GAEK,IAAM,aAAa,KAEnB,KACJ,GACA,MACoB;EACpB,IAAM,IAAS,EAAQ,SAAS,OAAO,IAAc,GAK/C,oBAAY,IAAI,KAAK,KAAK,IAAI,KAAK,EAAS,SAAS,KAAS,GAAM;EAC1E,OAAO;GACL,IAAI,EAAQ,MAAM,YAAY;GAC9B,MAAM,EAAQ,QAAQ;GACtB,MAAM;GACN,MAAM,EAAQ,OAAO,MAAM,EAAQ,KAAK,QAAQ;GAChD,MAAM;GACN,aAAa,EAAQ,eAAe,CAAC;GACrC,kBAAkB,CAAC;GACnB,eAAe,CAAC;GAChB,iBAAiB,CAAC;GAClB,iBAAiB,CAAC;GAClB,aAAa;GACb,QAAQ;GACR;GACA,YAAY;GACZ,YAAY;GACZ,iBAAiB,CAAC;GAClB,GAAI,EAAQ,WAAW,EAAE,UAAU,EAAQ,SAAS,IAAI,CAAC;EAE3D;CACF,GAEM,IAAiB,EAAS,IAAI,CAAe,GAE7C,IAAc;GACjB,EAAY,KAAK;GAChB,MAAM;IAAE,GAAG;IAAa,YAAY;GAAM;GAC1C,SAAS,EAAY;GACrB,MAAM;GACN,YAAY;EACd;GACC,EAAY,KAAK;GAChB,MAAM;IAAE,GAAG;IAAa,YAAY;GAAK;GACzC,SAAS,EAAY;GACrB,MAAM;GACN,YAAY;EACd;CACF,GAEM,IAAU,EAAO,QAAQ,aAAa,GAAW,EAErD,SAAS,CAAC,EAAY,IAAI,EAAY,EAAE,EAC1C,CAAC;CAyBD,AAtBA,EAAQ,oBAAoB;EAC1B,OAAO;EACP,WAAW;EACX,WAAW;CACb,IAQA,EAAQ,YAAY,aAAa,CAAC,KAClC,EAAQ,aAAa,YAAY,KAAA,IACjC,EAAQ,cAAc,YAAY,KAAA,IAClC,EAAQ,gBAAgB,aAAa,CAAC,KACtC,EAAQ,kBAAkB,aAAa,CAAC,KACxC,EAAQ,qBAAqB,IAE7B,EAAQ,SAAS,aAAa,EAAE,UAAU,CAAC,EAAE,KAG7C,EAAQ,QAAQ,aAAa,CAAC;CAQ9B,IAAI,IAAY;CAChB,EAAQ,QAAQ,aACd,AAGE,OADA,EAAQ,MAAM,WAAW,GACb,KAGd,EAAQ,MAAM,UAAU,GACjB;EACL,SAAS,EAAE,SAAS,CAAC,EAAY,IAAI,EAAY,EAAE,EAAE;EACrD,SAAS,CAAC;EACV,UAAU,EAAQ,MAAM;EACxB,UAAU,CAAC;EACX,iBAAiB,CAAC;EAClB,UAAU;CACZ;CAGF,IAAM,KAAiB,MAA6B;EAKlD,AADA,EAAQ,MAAM,iBAAiB,CAAc,GAC7C,EAAO,cAAc;GACnB,MAAM;GACN;GACA;GACA,MAAM,EAAQ,QAAQ,KAAA;EAExB,CAAQ;CACV,GAEI,IAAY,GACZ,IAAa;CAyDjB,OAxDA,EAAQ,cAAc,OAAO,MAAY;EACvC,IAAM,IACJ,OAAO,KAAY,WACf,IACE,GAA+B,QAAQ,IACzC,IAAO;GACX,GAAG,EACD;IAAE;IAAM,MAAM;IAAM,IAAI,aAAa;GAAc,GACnD,EAAe,MACjB;GAEA,GAAI,OAAO,KAAY,WAAY,IAAkB,CAAC;GACtD,MAAM;GACN;EACF;EAIA,EAAc,CAAI;EAElB,IAAM,IAAQ,IAAS,CAAI;EAC3B,IAAI,GAAO;GACT,IAAM,IAAY,OAAO,KAAU,WAAW,IAAQ,EAAM,MACtD,IACJ,OAAO,KAAU,WAAW,KAAA,IAAY,EAAM,aAI1C,IAAU,cAAc;GAC9B,iBAAiB;IACf,EACE,EACE;KACE,MAAM;KACN,MAAM;KAEN,aAAa;KACb,IAAI;IACN,GACA,EAAe,MACjB,CACF;GACF,GAAG,CAAc;EACnB;EAEA,OAAO,EAAE,SAAS,EAAK;CACzB,GAKA,EAAO,gBAAgB,YAAY,CAAC,CAAO,GAG3C,EAAa,MAAM,GAEZ;EAAE;EAAQ;EAAS,qBAAqB,EAAY;CAAG;AAChE"}
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\nexport interface CreateMockStreamChatClientOptions {\n /**\n * Also replace the connection-dependent client methods (`connectUser`,\n * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n * client drives a flow that would otherwise open a WebSocket or throw\n * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n * rendering (e.g. component stories) does not need this.\n */\n stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n user: MockMessagingUser,\n { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n client.userID = user.id\n client.user = user as any\n\n if (stubConnection) {\n client.connectUser = async () => ({ me: user }) as any\n client.disconnectUser = async () => undefined\n client.userMuteStatus = (() => false) as any\n }\n\n return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport {\n createMockStreamChatClient,\n type MockMessagingUser,\n} from './createMockStreamChatClient'\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n // Offline client marked connected without a WebSocket. `stubConnection` also\n // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n // renders `<Chat client={client}>` directly so it never calls connectUser,\n // and handling the `message.new` events dispatched on send + echo below runs\n // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n const client = createMockStreamChatClient(currentUser, {\n stubConnection: true,\n })\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally. Must be a\n // Date, not an ISO string: we seed `channel.state.messages` directly\n // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n // Read/unread bookkeeping consults channel mute status; without a live\n // connection the real method throws, so report \"not muted\".\n channel.muteStatus = () => ({\n muted: false,\n createdAt: null,\n expiresAt: null,\n })\n\n // Neutralise the rest of the connection-dependent channel API that\n // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n // Stream or checks for a live WebSocket and would otherwise throw\n // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n channel.markRead = (async () => ({}) as any) as any\n channel.keystroke = (async () => undefined) as any\n channel.stopTyping = (async () => undefined) as any\n channel.sendReaction = (async () => ({}) as any) as any\n channel.deleteReaction = (async () => ({}) as any) as any\n channel.countUnread = (() => 0) as any\n // Pagination: report no older pages so \"load more\" resolves to a no-op.\n channel.query = (async () => ({ messages: [] }) as any) as any\n // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n // Stream, so no-op it to keep the action working offline.\n channel.hide = (async () => ({}) as any) as any\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n // remount/navigation, so seed only once — otherwise a re-watch would wipe\n // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n let hasSeeded = false\n channel.watch = async () => {\n if (!hasSeeded) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n hasSeeded = true\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: channel.state.messages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n let replyCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage(\n { text, from: 'me', id: `mock-sent-${sentCount++}` },\n seededMessages.length\n ),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n // would otherwise both read the same later `sentCount` at fire time and\n // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n const replyId = `mock-reply-${replyCount++}`\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: replyId,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["createMockStreamChatClient","user","stubConnection","client","StreamChat","REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","hasSeeded","appendMessage","sentCount","replyCount","text","sent","reply","replyText","replyAttachments","replyId"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1-rc-1787325541",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"chromatic:local": "yarn storybook:build && chromatic"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@linktr.ee/messaging-core": "2.
|
|
54
|
-
"@linktr.ee/messaging-taxonomy": "^0.
|
|
53
|
+
"@linktr.ee/messaging-core": "2.5.0-rc-1787325541",
|
|
54
|
+
"@linktr.ee/messaging-taxonomy": "^0.10.0",
|
|
55
55
|
"@phosphor-icons/react": "^2.1.10"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@testing-library/user-event": "^14.5.1",
|
|
69
69
|
"@types/react": "^18.3.24",
|
|
70
70
|
"@types/react-dom": "^18.3.7",
|
|
71
|
-
"@vitejs/plugin-react": "^
|
|
71
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
72
72
|
"@vitest/ui": "^4.1.10",
|
|
73
73
|
"autoprefixer": "^10.4.20",
|
|
74
74
|
"chromatic": "^17.4.0",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"tailwindcss": "^3.4.17",
|
|
84
84
|
"tailwindcss-animate": "^1.0.7",
|
|
85
85
|
"typescript": "5.9.2",
|
|
86
|
-
"vite": "^
|
|
86
|
+
"vite": "^6.0.7",
|
|
87
87
|
"vite-plugin-dts": "^3.7.0",
|
|
88
88
|
"vitest": "^4.1.10"
|
|
89
89
|
},
|
|
@@ -682,6 +682,10 @@ WithTypingIndicator.args = {
|
|
|
682
682
|
},
|
|
683
683
|
}
|
|
684
684
|
WithTypingIndicator.parameters = {
|
|
685
|
+
// The typing indicator is animation-dominant, so a snapshot mostly captures
|
|
686
|
+
// whichever frame the animation happened to pause at — a false-positive
|
|
687
|
+
// source. Skip it in Chromatic; the story stays browsable in Storybook.
|
|
688
|
+
chromatic: { disableSnapshot: true },
|
|
685
689
|
docs: {
|
|
686
690
|
description: {
|
|
687
691
|
story:
|
|
@@ -47,6 +47,34 @@ describe('MediaStackGrid', () => {
|
|
|
47
47
|
})
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
it.each([
|
|
51
|
+
[2, '+3', 'tile 2'],
|
|
52
|
+
[3, '+2', 'tile 3'],
|
|
53
|
+
])(
|
|
54
|
+
'shows overflow on the last visible tile when maxVisible is %s',
|
|
55
|
+
(maxVisible, badge, lastTile) => {
|
|
56
|
+
const { container } = renderWithProviders(
|
|
57
|
+
<MediaStackGrid tiles={tiles(5)} maxVisible={maxVisible} />
|
|
58
|
+
)
|
|
59
|
+
expect(container.querySelectorAll('span')).toHaveLength(maxVisible)
|
|
60
|
+
expect(screen.getByText(badge)).toBeInTheDocument()
|
|
61
|
+
expect(screen.getByText(badge).parentElement).toHaveTextContent(lastTile)
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
it('shows overflow on the single visible tile', () => {
|
|
66
|
+
const { container } = renderWithProviders(
|
|
67
|
+
<MediaStackGrid tiles={tiles(3)} maxVisible={1} />
|
|
68
|
+
)
|
|
69
|
+
expect(container.querySelectorAll('span')).toHaveLength(1)
|
|
70
|
+
expect(screen.getByText('+2').parentElement).toHaveTextContent('tile 1')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does not show overflow when all tiles are visible', () => {
|
|
74
|
+
renderWithProviders(<MediaStackGrid tiles={tiles(3)} maxVisible={3} />)
|
|
75
|
+
expect(screen.queryByText(/^\+/)).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
50
78
|
it('renders activatable buttons with labels and indexes', () => {
|
|
51
79
|
const onActivate = vi.fn()
|
|
52
80
|
renderWithProviders(
|
|
@@ -118,6 +118,12 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
118
118
|
|
|
119
119
|
const visible = tiles.slice(0, Math.min(total, maxVisible))
|
|
120
120
|
const overflow = total - visible.length
|
|
121
|
+
const overflowBadge =
|
|
122
|
+
overflow > 0 ? (
|
|
123
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/55 text-2xl font-semibold text-white">
|
|
124
|
+
+{overflow}
|
|
125
|
+
</div>
|
|
126
|
+
) : null
|
|
121
127
|
|
|
122
128
|
const renderTile = (
|
|
123
129
|
tile: MediaStackTile,
|
|
@@ -174,7 +180,7 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
174
180
|
)}
|
|
175
181
|
style={singleTileStyle}
|
|
176
182
|
>
|
|
177
|
-
{renderTile(visible[0], 0)}
|
|
183
|
+
{renderTile(visible[0], 0, overflowBadge)}
|
|
178
184
|
</div>
|
|
179
185
|
)
|
|
180
186
|
}
|
|
@@ -187,7 +193,13 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
187
193
|
className
|
|
188
194
|
)}
|
|
189
195
|
>
|
|
190
|
-
{visible.map((tile, index) =>
|
|
196
|
+
{visible.map((tile, index) =>
|
|
197
|
+
renderTile(
|
|
198
|
+
tile,
|
|
199
|
+
index,
|
|
200
|
+
index === visible.length - 1 ? overflowBadge : null
|
|
201
|
+
)
|
|
202
|
+
)}
|
|
191
203
|
</div>
|
|
192
204
|
)
|
|
193
205
|
}
|
|
@@ -202,7 +214,7 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
202
214
|
>
|
|
203
215
|
<div className="row-span-2">{renderTile(visible[0], 0)}</div>
|
|
204
216
|
{renderTile(visible[1], 1)}
|
|
205
|
-
{renderTile(visible[2], 2)}
|
|
217
|
+
{renderTile(visible[2], 2, overflowBadge)}
|
|
206
218
|
</div>
|
|
207
219
|
)
|
|
208
220
|
}
|
|
@@ -227,11 +239,7 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
227
239
|
return renderTile(
|
|
228
240
|
tile,
|
|
229
241
|
index,
|
|
230
|
-
isLastWithOverflow ?
|
|
231
|
-
<div className="absolute inset-0 flex items-center justify-center bg-black/55 text-2xl font-semibold text-white">
|
|
232
|
-
+{overflow}
|
|
233
|
-
</div>
|
|
234
|
-
) : null
|
|
242
|
+
isLastWithOverflow ? overflowBadge : null
|
|
235
243
|
)
|
|
236
244
|
})}
|
|
237
245
|
</div>
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./rolldown-runtime-BocRIvOZ.cjs");let t=require("react");t=e.t(t,1);let n=require("react/jsx-runtime"),r=require("classnames");r=e.t(r,1);let i=require("@phosphor-icons/react");var a=[`linktr.ee`],o=`messaging-attachment-v1_0`;function s(e){try{return new URL(e).hostname.toLowerCase()}catch{return null}}function c(e){return a.some(t=>e===t||e.endsWith(`.${t}`))}function l(e){let t=e?.trim();if(!t)return;let n=s(t);if(!n||!c(n))return t;let r=new URL(t);return r.searchParams.get(`io`)===`true`&&r.searchParams.get(`size`)===o?t:(r.searchParams.set(`io`,`true`),r.searchParams.set(`size`,o),r.toString())}var u=[[/pdf/,`pdf`],[/wordprocessingml|msword|\.doc/,`doc`],[/spreadsheetml|ms-excel|\.xls/,`xls`],[/csv/,`csv`],[/presentationml|ms-powerpoint|\.ppt/,`ppt`],[/zip|x-rar|x-7z|x-tar|x-gzip/,`zip`],[/plain|rtf/,`text`],[/markdown/,`markdown`]];function d(e){return e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e.startsWith(`image/`)?`image`:`document`}function f(e){let t=u.find(([t])=>t.test(e));return t?t[1]:`generic`}var p={video:i.VideoCameraIcon,audio:i.SpeakerHighIcon,image:i.ImageIcon,document:i.FileIcon},m={pdf:i.FilePdfIcon,doc:i.FileDocIcon,xls:i.FileXlsIcon,csv:i.FileCsvIcon,ppt:i.FilePptIcon,zip:i.FileZipIcon,text:i.FileTextIcon,markdown:i.FileMdIcon,generic:i.FileIcon};function h(e){let t=d(e);return t===`document`?m[f(e)]:p[t]}function g(e,n){return t.default.createElement(h(e),n)}function _(){return!1}var v=e=>`touches`in e?e.touches[0]?.clientX??e.changedTouches[0]?.clientX??0:e.clientX,y=({source:e,mimeType:r,poster:a,autoPlay:o=!1,playing:s,loop:c=!1,controls:l=!0,showProgress:u=!1,muted:f=!1,onContainerClick:p})=>{let m=d(r),h=(0,t.useRef)(null),y=(0,t.useRef)(null),b=(0,t.useRef)(null),x=(0,t.useRef)(s),[S,C]=(0,t.useState)(o),[w,T]=(0,t.useState)(0),[E,D]=(0,t.useState)(!1),[O,k]=(0,t.useState)(!1),[A,j]=(0,t.useState)(!1),[M,N]=(0,t.useState)(!1),[P,F]=(0,t.useState)(!0),[I,L]=(0,t.useState)(null),R=(0,t.useCallback)(()=>{j(!1),C(!0)},[]),z=(0,t.useCallback)(e=>{let t=y.current;if(!t)return 0;let n=t.getBoundingClientRect();return Math.max(0,Math.min(1,(v(e)-n.left)/n.width))},[]),B=(0,t.useCallback)(e=>{let t=h.current;t&&t.duration&&(t.currentTime=e*t.duration)},[]),V=e=>{e.stopPropagation(),D(!0);let t=z(e);T(t),B(t)},H=(0,t.useCallback)(e=>{e.key===`ArrowRight`&&B(Math.min(1,w+.05)),e.key===`ArrowLeft`&&B(Math.max(0,w-.05))},[B,w]);(0,t.useEffect)(()=>{s!==void 0&&s!==x.current&&(x.current=s,C(s))},[s]),(0,t.useEffect)(()=>{if(!S){b.current!==null&&(cancelAnimationFrame(b.current),b.current=null);return}let e=()=>{let t=h.current;t&&t.duration&&!E&&T(t.currentTime/t.duration),b.current=requestAnimationFrame(e)};return b.current=requestAnimationFrame(e),()=>{b.current!==null&&cancelAnimationFrame(b.current)}},[S,E]),(0,t.useEffect)(()=>{let e=h.current;e&&(S?e.play().catch(e=>{C(!1),j(!0),_()&&console.debug(`[MediaPlayer] play() failed`,e)}):e.pause())},[S]),(0,t.useEffect)(()=>{if(!E)return;let e=e=>T(z(e)),t=e=>{D(!1),B(z(e))};return window.addEventListener(`mousemove`,e),window.addEventListener(`mouseup`,t),window.addEventListener(`touchmove`,e,{passive:!0}),window.addEventListener(`touchend`,t),()=>{window.removeEventListener(`mousemove`,e),window.removeEventListener(`mouseup`,t),window.removeEventListener(`touchmove`,e),window.removeEventListener(`touchend`,t)}},[E,z,B]);let U=I?{aspectRatio:String(I)}:void 0,W=I?``:` aspect-video`,G=Math.round(w*100);return(0,n.jsxs)(`div`,{role:`button`,tabIndex:0,className:`relative cursor-pointer overflow-hidden bg-black ${W}`,style:U,onClick:e=>{if(p){p(e);return}A||l&&C(e=>!e)},onKeyDown:e=>{if(e.key===`Enter`||e.key===` `){if(e.preventDefault(),p){p(e);return}A||l&&C(e=>!e)}},children:[a&&(m===`audio`||P)&&(0,n.jsx)(`img`,{src:a,alt:``,className:`absolute inset-0 h-full w-full object-cover`}),!a&&(m===`audio`||P)&&(0,n.jsx)(`div`,{className:`absolute inset-0 flex items-center justify-center`,children:g(r,{className:`size-12 text-black/20`,weight:`regular`})}),(0,n.jsx)(`div`,{className:`absolute inset-0`,children:m===`audio`?(0,n.jsx)(`audio`,{ref:h,src:e,loop:c,muted:f,style:{width:`100%`,height:`100%`},onLoadStart:()=>N(!0),onCanPlay:()=>{N(!1),F(!1)},onWaiting:()=>N(!0),onPlay:()=>j(!1),onEnded:()=>{c||(C(!1),T(0))},children:(0,n.jsx)(`track`,{kind:`captions`})}):(0,n.jsx)(`video`,{ref:h,src:e,loop:c,muted:f,playsInline:!0,style:{width:`100%`,height:`100%`},onLoadStart:()=>N(!0),onCanPlay:()=>{N(!1),F(!1)},onWaiting:()=>N(!0),onPlay:()=>j(!1),onLoadedMetadata:()=>{let e=h.current;e instanceof HTMLVideoElement&&e.videoWidth&&e.videoHeight&&L(e.videoWidth/e.videoHeight)},onEnded:()=>{c||(C(!1),T(0))},children:(0,n.jsx)(`track`,{kind:`captions`})})}),M&&!A&&(0,n.jsx)(`div`,{className:`absolute inset-0 z-10 flex items-center justify-center`,children:(0,n.jsx)(i.CircleNotchIcon,{className:`size-8 animate-spin text-white/80`,weight:`bold`})}),A&&!l&&(0,n.jsx)(`div`,{className:`absolute inset-0 z-30 flex cursor-pointer items-center justify-center bg-black/35`,role:`button`,tabIndex:0,"aria-label":`Play preview`,onClick:e=>{e.stopPropagation(),R()},onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),e.stopPropagation(),R())},children:(0,n.jsx)(`span`,{className:`flex size-16 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm`,children:(0,n.jsx)(i.PlayIcon,{className:`size-9 translate-x-0.5`,weight:`fill`})})}),u&&!l&&(0,n.jsx)(`div`,{className:`absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent`,children:(0,n.jsx)(`div`,{role:`slider`,"aria-label":`Playback position`,"aria-valuenow":G,"aria-valuemin":0,"aria-valuemax":100,tabIndex:0,ref:y,className:`relative flex h-4 w-full cursor-pointer items-center`,onMouseDown:V,onTouchStart:V,onClick:e=>e.stopPropagation(),onKeyDown:H,children:(0,n.jsx)(`div`,{className:`w-full overflow-hidden rounded-full bg-white/30 h-1`,children:(0,n.jsx)(`div`,{className:`h-full rounded-full bg-white`,style:{width:`${G}%`}})})})}),l&&(0,n.jsxs)(`div`,{className:`absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6 transition-all duration-200`,children:[(0,n.jsx)(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),C(e=>!e)},className:`shrink-0 text-white`,"aria-label":S?`Pause`:`Play`,children:S?(0,n.jsx)(i.PauseIcon,{className:`size-5`,weight:`fill`}):(0,n.jsx)(i.PlayIcon,{className:`size-5 translate-x-px`,weight:`fill`})}),(0,n.jsxs)(`div`,{role:`slider`,"aria-label":`Playback position`,"aria-valuenow":G,"aria-valuemin":0,"aria-valuemax":100,tabIndex:0,ref:y,className:`relative flex h-4 w-full cursor-pointer items-center`,onMouseDown:V,onTouchStart:V,onClick:e=>e.stopPropagation(),onMouseEnter:()=>k(!0),onMouseLeave:()=>k(!1),onKeyDown:H,children:[(0,n.jsx)(`div`,{className:`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${O||E?`h-1.5`:`h-1`}`,children:(0,n.jsx)(`div`,{className:`h-full rounded-full bg-white`,style:{width:`${G}%`}})}),(0,n.jsx)(`div`,{className:`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${O||E?`scale-100 opacity-100`:`scale-0 opacity-0`}`,style:{left:`${G}%`}})]})]})]})},b=e=>e===`dark`?`size-12 text-white/20`:`size-12 text-black/20`,x=e=>e===`dark`?`aspect-video overflow-hidden bg-white/10`:`aspect-video overflow-hidden bg-black/5`,S=({mimeType:e,sourceUrl:r,thumbnailUrl:i,title:a,variant:o,mediaPlayerProps:s,containedImage:c=!1})=>{let u=d(e),[f,p]=(0,t.useState)(!1),m=l(r),h=l(i);return r&&(u===`video`||u===`audio`)?(0,n.jsx)(y,{source:r,mimeType:e,poster:h,controls:!0,...s}):r&&u===`image`?c?(0,n.jsx)(`div`,{className:`relative aspect-video overflow-hidden bg-black/5`,children:(0,n.jsx)(`img`,{src:m??r,alt:a??``,className:`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${f?`opacity-100`:`opacity-0`}`,draggable:!1,onLoad:()=>p(!0)})}):(0,n.jsx)(`img`,{src:m??r,alt:a??``,className:`block w-full`,draggable:!1}):r&&u===`document`?i?c?(0,n.jsx)(`div`,{className:`relative aspect-video overflow-hidden bg-black/5`,children:(0,n.jsx)(`img`,{src:h??i,alt:a??``,className:`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${f?`opacity-100`:`opacity-0`}`,draggable:!1,onLoad:()=>p(!0)})}):(0,n.jsx)(`img`,{src:h??i,alt:``,className:`block w-full`,draggable:!1}):(0,n.jsx)(`div`,{className:`flex aspect-video w-full items-center justify-center ${o===`dark`?`bg-white/10`:`bg-black/5`}`,children:g(e,{className:b(o),weight:`regular`})}):i?(0,n.jsx)(`div`,{className:`relative ${x(o)}`,children:(0,n.jsx)(`img`,{src:h??i,alt:a??``,draggable:!1,className:`absolute inset-0 h-full w-full object-cover`})}):(0,n.jsx)(`div`,{className:`flex aspect-video w-full items-center justify-center ${o===`dark`?`bg-white/10`:`bg-black/5`}`,children:g(e,{className:b(o),weight:`regular`})})},C=({variant:e,thumbnail:t,title:i,placeholderTitle:a=`Attachment title`,mimeType:o,detail:s,statusBadge:c,action:l,topLeft:u,topRight:d,rootRef:f,"data-testid":p})=>{let m=e===`dark`,h=m?i??a:i??``,_=m&&!i;return(0,n.jsxs)(`div`,{ref:f,"data-testid":p,className:(0,r.default)(`relative w-[280px] select-none overflow-hidden rounded-[24px] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]`,m?`bg-[#1e2330]`:`bg-white`),children:[u?(0,n.jsx)(`div`,{className:`pointer-events-auto absolute left-3 top-3 z-50`,children:u}):null,d?(0,n.jsx)(`div`,{className:`pointer-events-auto absolute right-3 top-3 z-50`,children:d}):null,t,(0,n.jsxs)(`div`,{className:`px-4 pb-3 pt-3`,children:[h.trim()!==``&&(0,n.jsx)(`p`,{className:(0,r.default)(`mb-0.5 truncate text-base font-medium`,{"text-black":!m,"text-white/30":m&&_,"text-white":m&&!_}),children:h}),(0,n.jsxs)(`div`,{className:`flex flex-wrap items-center gap-1`,children:[g(o,{className:(0,r.default)(`size-5 shrink-0`,m?`text-white/55`:`text-black/55`),weight:`regular`}),s!=null&&s!==``&&(0,n.jsx)(`span`,{className:(0,r.default)(`text-xs font-medium`,m?`text-white/55`:`text-black/55`),children:s}),c]}),l]})]})};Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return C}});
|
|
2
|
-
//# sourceMappingURL=AttachmentCard-C-X0jLZ2.cjs.map
|