@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.
- package/dist/{Card-NDZLmjxS.js → Card-BAt0xifq.js} +2 -2
- package/dist/{Card-NDZLmjxS.js.map → Card-BAt0xifq.js.map} +1 -1
- package/dist/{Card-DSQclaiH.js → Card-BB34_MDl.js} +3 -3
- package/dist/{Card-DSQclaiH.js.map → Card-BB34_MDl.js.map} +1 -1
- package/dist/{Card-Jc1n2Iqu.cjs → Card-BDKGSydN.cjs} +2 -2
- package/dist/{Card-Jc1n2Iqu.cjs.map → Card-BDKGSydN.cjs.map} +1 -1
- package/dist/{Card-DEk34Twe.cjs → Card-BGJY5HWf.cjs} +2 -2
- package/dist/{Card-DEk34Twe.cjs.map → Card-BGJY5HWf.cjs.map} +1 -1
- package/dist/{Card-Cz8tanga.js → Card-C3-Ea5ho.js} +2 -2
- package/dist/{Card-Cz8tanga.js.map → Card-C3-Ea5ho.js.map} +1 -1
- package/dist/{Card-BThi6-oD.cjs → Card-DnFtpG1h.cjs} +2 -2
- package/dist/{Card-BThi6-oD.cjs.map → Card-DnFtpG1h.cjs.map} +1 -1
- package/dist/{LockedThumbnail-CqcnnPXB.cjs → LockedThumbnail-Bl9UC9sQ.cjs} +2 -2
- package/dist/{LockedThumbnail-CqcnnPXB.cjs.map → LockedThumbnail-Bl9UC9sQ.cjs.map} +1 -1
- package/dist/{LockedThumbnail-t9fLaFmy.js → LockedThumbnail-so7ftBnG.js} +2 -2
- package/dist/{LockedThumbnail-t9fLaFmy.js.map → LockedThumbnail-so7ftBnG.js.map} +1 -1
- package/dist/{index-bPlX3Oo5.js → index-2HMjfjM-.js} +1559 -1744
- package/dist/index-2HMjfjM-.js.map +1 -0
- package/dist/index-BgzqZorr.cjs +2 -0
- package/dist/index-BgzqZorr.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +116 -0
- package/dist/testing.js +107 -0
- package/dist/testing.js.map +1 -0
- package/package.json +14 -2
- package/src/components/ChannelView.stories.tsx +11 -68
- package/src/components/ChannelView.test.tsx +13 -54
- package/src/components/ChannelView.tsx +28 -74
- package/src/providers/MessagingProvider.test.tsx +30 -0
- package/src/providers/MessagingProvider.tsx +24 -5
- package/src/testing/createMockMessagingClient.stories.tsx +77 -0
- package/src/testing/createMockMessagingClient.ts +231 -0
- package/src/testing/index.ts +7 -0
- package/src/types.ts +15 -2
- package/dist/index-CPCX_7gP.cjs +0 -2
- package/dist/index-CPCX_7gP.cjs.map +0 -1
- package/dist/index-bPlX3Oo5.js.map +0 -1
- package/src/components/ChannelHeaderRedesign.tsx +0 -231
- /package/dist/assets/{index.css → messaging-react.css} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BgzqZorr.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1237,8 +1237,20 @@ export declare const MessagingProvider: default_2.FC<MessagingProviderProps>;
|
|
|
1237
1237
|
export declare interface MessagingProviderProps {
|
|
1238
1238
|
children: React.ReactNode;
|
|
1239
1239
|
user: MessagingUser | null;
|
|
1240
|
-
|
|
1241
|
-
|
|
1240
|
+
/**
|
|
1241
|
+
* An already-constructed Stream client. When provided it takes precedence
|
|
1242
|
+
* over `apiKey`: the provider renders `<Chat>` against this exact instance
|
|
1243
|
+
* and does NOT create a StreamChatService or call `connectUser` — the caller
|
|
1244
|
+
* owns the client's connection lifecycle. This is the seam for offline/dev
|
|
1245
|
+
* (`createMockMessagingClient` from `@linktr.ee/messaging-react/testing`) and
|
|
1246
|
+
* for tests, so consumers render the real UI instead of forking it.
|
|
1247
|
+
* `apiKey`/`serviceConfig` are ignored while `client` is set.
|
|
1248
|
+
*/
|
|
1249
|
+
client?: StreamChat;
|
|
1250
|
+
/** Required unless `client` is provided. */
|
|
1251
|
+
serviceConfig?: Omit<StreamChatServiceConfig, 'apiKey'>;
|
|
1252
|
+
/** Required unless `client` is provided. */
|
|
1253
|
+
apiKey?: string;
|
|
1242
1254
|
capabilities?: MessagingCapabilities;
|
|
1243
1255
|
debug?: boolean;
|
|
1244
1256
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-
|
|
1
|
+
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-2HMjfjM-.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("stream-chat"),w=600;function b({currentUser:e,participant:a,messages:u=[],channelId:h="mock-dm",onSend:d}){const s=new M.StreamChat("mock-api-key",{allowServerSideConnect:!0});s.connectUser=async()=>({me:e}),s.disconnectUser=async()=>{},s.userID=e.id,s.user=e;const m=`messaging:${h}`,l=(t,c)=>{const r=t.from==="me"?e:a,n=new Date(Date.now()-(u.length-c)*6e4).toISOString();return{id:t.id??`mock-msg-${c}`,text:t.text??"",type:"regular",html:t.text?`<p>${t.text}</p>`:"",user:r,attachments:t.attachments??[],latest_reactions:[],own_reactions:[],reaction_counts:{},reaction_scores:{},reply_count:0,status:"received",cid:m,created_at:n,updated_at:n,mentioned_users:[],...t.metadata?{metadata:t.metadata}:{}}},i=u.map(l),f={[e.id]:{user:{...e,is_account:!1},user_id:e.id,role:"owner",is_account:!1},[a.id]:{user:{...a,is_account:!0},user_id:a.id,role:"member",is_account:!0}},o=s.channel("messaging",h,{members:[e.id,a.id]});o.watch=async()=>(o.state.messages=i,o.state.members=f,{channel:{members:[e.id,a.id]},members:[],messages:i,watchers:[],pinned_messages:[],duration:"0ms"});const y=t=>{o.state.addMessageSorted(t),s.dispatchEvent({type:"message.new",cid:m,message:t,user:t.user??void 0})};let _=0;return o.sendMessage=async t=>{const c=typeof t=="string"?t:(t==null?void 0:t.text)??"",r={...l({text:c,from:"me",id:`mock-sent-${_++}`},i.length),...typeof t=="object"?t:{},user:e,cid:m};y(r);const n=d==null?void 0:d(c);if(n){const g=typeof n=="string"?n:n.text,p=typeof n=="string"?void 0:n.attachments;setTimeout(()=>{y(l({text:g,from:"them",attachments:p,id:`mock-reply-${_}`},i.length))},w)}return{message:r}},s.queryChannels=async()=>[o],o.watch(),{client:s,channel:o,participantFilterId:a.id}}exports.createMockMessagingClient=b;
|
|
2
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.cjs","sources":["../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n\n // Mark connected without opening a WebSocket. `MessagingProvider` renders\n // `<Chat client={client}>` directly for an injected client, so it never\n // calls connectUser itself.\n client.connectUser = async () => ({ me: currentUser }) as never\n client.disconnectUser = async () => undefined\n client.userID = currentUser.id\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.user = currentUser as any\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally.\n const createdAt = new Date(\n Date.now() - (messages.length - index) * 60_000\n ).toISOString()\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse.\n channel.watch = async () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: seededMessages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","client","StreamChat","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Channel } from 'stream-chat';
|
|
2
|
+
import { StreamChat } from 'stream-chat';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds an offline Stream client seeded with a single direct conversation, so
|
|
6
|
+
* consumers can render the **real** messaging UI (MessagingProvider →
|
|
7
|
+
* MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`
|
|
8
|
+
* surfaces and Storybook. It is a real `StreamChat` whose network calls
|
|
9
|
+
* (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are
|
|
10
|
+
* replaced with in-memory behaviour driven by Stream's own channel-state
|
|
11
|
+
* machinery, so rendering fidelity matches production.
|
|
12
|
+
*
|
|
13
|
+
* Ships from `@linktr.ee/messaging-react/testing` and must never be imported by
|
|
14
|
+
* production code.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createMockMessagingClient({ currentUser, participant, messages, channelId, onSend, }: CreateMockMessagingClientOptions): MockMessagingClient;
|
|
17
|
+
|
|
18
|
+
export declare interface CreateMockMessagingClientOptions {
|
|
19
|
+
/** The connected/viewing user. */
|
|
20
|
+
currentUser: MockMessagingUser;
|
|
21
|
+
/** The other party in the direct conversation (e.g. the creator/linker). */
|
|
22
|
+
participant: MockMessagingUser;
|
|
23
|
+
/** Seed conversation, oldest-first. */
|
|
24
|
+
messages?: MockMessage[];
|
|
25
|
+
/** Channel id; defaults to `mock-dm`. */
|
|
26
|
+
channelId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional canned reply. Called after the viewer sends a message; return the
|
|
29
|
+
* reply text (or a partial message) to have `participant` echo a response, or
|
|
30
|
+
* a falsy value for no reply. Drives the "send echo" in `dev:mock-messaging`.
|
|
31
|
+
*/
|
|
32
|
+
onSend?: (text: string) => string | {
|
|
33
|
+
text?: string;
|
|
34
|
+
attachments?: unknown[];
|
|
35
|
+
} | null | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A seed message, authored from the viewer's perspective. `from: 'me'` is the
|
|
40
|
+
* connected user (the viewer); `from: 'them'` is the other participant.
|
|
41
|
+
*/
|
|
42
|
+
export declare interface MockMessage {
|
|
43
|
+
id?: string;
|
|
44
|
+
text?: string;
|
|
45
|
+
from: 'me' | 'them';
|
|
46
|
+
/** Stream attachment payloads, rendered by the toolkit's attachment gate. */
|
|
47
|
+
attachments?: any[];
|
|
48
|
+
/** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */
|
|
49
|
+
metadata?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare interface MockMessagingClient {
|
|
53
|
+
/** Pass to `<MessagingProvider client={...}>`. */
|
|
54
|
+
client: StreamChat;
|
|
55
|
+
/** The seeded direct-conversation channel. */
|
|
56
|
+
channel: Channel;
|
|
57
|
+
/** Pass to `<MessagingShell initialParticipantFilter={...}>`. */
|
|
58
|
+
participantFilterId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A participant in the mock conversation. */
|
|
62
|
+
export declare interface MockMessagingUser {
|
|
63
|
+
id: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
image?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { }
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
declare module 'stream-chat' {
|
|
72
|
+
interface CustomAttachmentData {
|
|
73
|
+
action?: OfficialCtaActionPayload;
|
|
74
|
+
}
|
|
75
|
+
interface CustomChannelData {
|
|
76
|
+
/**
|
|
77
|
+
* Whether the channel has at least one visitor-originated message.
|
|
78
|
+
* Legacy channels may omit this field.
|
|
79
|
+
*/
|
|
80
|
+
has_visitor_message?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Priority score assigned by backend classification for spotlighting.
|
|
83
|
+
*/
|
|
84
|
+
priority_score?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Indicates a paid message is pending creator reply.
|
|
87
|
+
*/
|
|
88
|
+
pending_earn_message_reply?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Indicates DM agent has deferred and creator follow-up is needed.
|
|
91
|
+
*/
|
|
92
|
+
agent_deferred?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Whether DM agent auto-replies are paused for this channel.
|
|
95
|
+
*/
|
|
96
|
+
chatbot_paused?: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface CustomMessageData {
|
|
99
|
+
/**
|
|
100
|
+
* When true, hides the date timestamp in system messages.
|
|
101
|
+
* Used by CustomSystemMessage component.
|
|
102
|
+
*/
|
|
103
|
+
hide_date?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Message metadata from backend.
|
|
106
|
+
* Contains type and payment information.
|
|
107
|
+
*/
|
|
108
|
+
metadata?: MessageMetadata;
|
|
109
|
+
/**
|
|
110
|
+
* DM Agent-specific system message type.
|
|
111
|
+
* Used as a fallback when metadata.custom_type is not available.
|
|
112
|
+
*/
|
|
113
|
+
dm_agent_system_type?: DmAgentSystemType;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { StreamChat as x } from "stream-chat";
|
|
2
|
+
const M = 600;
|
|
3
|
+
function g({
|
|
4
|
+
currentUser: e,
|
|
5
|
+
participant: c,
|
|
6
|
+
messages: u = [],
|
|
7
|
+
channelId: h = "mock-dm",
|
|
8
|
+
onSend: r
|
|
9
|
+
}) {
|
|
10
|
+
const n = new x("mock-api-key", {
|
|
11
|
+
allowServerSideConnect: !0
|
|
12
|
+
});
|
|
13
|
+
n.connectUser = async () => ({ me: e }), n.disconnectUser = async () => {
|
|
14
|
+
}, n.userID = e.id, n.user = e;
|
|
15
|
+
const m = `messaging:${h}`, l = (t, a) => {
|
|
16
|
+
const d = t.from === "me" ? e : c, o = new Date(
|
|
17
|
+
Date.now() - (u.length - a) * 6e4
|
|
18
|
+
).toISOString();
|
|
19
|
+
return {
|
|
20
|
+
id: t.id ?? `mock-msg-${a}`,
|
|
21
|
+
text: t.text ?? "",
|
|
22
|
+
type: "regular",
|
|
23
|
+
html: t.text ? `<p>${t.text}</p>` : "",
|
|
24
|
+
user: d,
|
|
25
|
+
attachments: t.attachments ?? [],
|
|
26
|
+
latest_reactions: [],
|
|
27
|
+
own_reactions: [],
|
|
28
|
+
reaction_counts: {},
|
|
29
|
+
reaction_scores: {},
|
|
30
|
+
reply_count: 0,
|
|
31
|
+
status: "received",
|
|
32
|
+
cid: m,
|
|
33
|
+
created_at: o,
|
|
34
|
+
updated_at: o,
|
|
35
|
+
mentioned_users: [],
|
|
36
|
+
...t.metadata ? { metadata: t.metadata } : {}
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
};
|
|
39
|
+
}, i = u.map(l), f = {
|
|
40
|
+
[e.id]: {
|
|
41
|
+
user: { ...e, is_account: !1 },
|
|
42
|
+
user_id: e.id,
|
|
43
|
+
role: "owner",
|
|
44
|
+
is_account: !1
|
|
45
|
+
},
|
|
46
|
+
[c.id]: {
|
|
47
|
+
user: { ...c, is_account: !0 },
|
|
48
|
+
user_id: c.id,
|
|
49
|
+
role: "member",
|
|
50
|
+
is_account: !0
|
|
51
|
+
}
|
|
52
|
+
}, s = n.channel("messaging", h, {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
members: [e.id, c.id]
|
|
55
|
+
});
|
|
56
|
+
s.watch = async () => (s.state.messages = i, s.state.members = f, {
|
|
57
|
+
channel: { members: [e.id, c.id] },
|
|
58
|
+
members: [],
|
|
59
|
+
messages: i,
|
|
60
|
+
watchers: [],
|
|
61
|
+
pinned_messages: [],
|
|
62
|
+
duration: "0ms"
|
|
63
|
+
});
|
|
64
|
+
const y = (t) => {
|
|
65
|
+
s.state.addMessageSorted(t), n.dispatchEvent({
|
|
66
|
+
type: "message.new",
|
|
67
|
+
cid: m,
|
|
68
|
+
message: t,
|
|
69
|
+
user: t.user ?? void 0
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
let _ = 0;
|
|
74
|
+
return s.sendMessage = async (t) => {
|
|
75
|
+
const a = typeof t == "string" ? t : (t == null ? void 0 : t.text) ?? "", d = {
|
|
76
|
+
...l({ text: a, from: "me", id: `mock-sent-${_++}` }, i.length),
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
...typeof t == "object" ? t : {},
|
|
79
|
+
user: e,
|
|
80
|
+
cid: m
|
|
81
|
+
};
|
|
82
|
+
y(d);
|
|
83
|
+
const o = r == null ? void 0 : r(a);
|
|
84
|
+
if (o) {
|
|
85
|
+
const p = typeof o == "string" ? o : o.text, w = typeof o == "string" ? void 0 : o.attachments;
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
y(
|
|
88
|
+
l(
|
|
89
|
+
{
|
|
90
|
+
text: p,
|
|
91
|
+
from: "them",
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
attachments: w,
|
|
94
|
+
id: `mock-reply-${_}`
|
|
95
|
+
},
|
|
96
|
+
i.length
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}, M);
|
|
100
|
+
}
|
|
101
|
+
return { message: d };
|
|
102
|
+
}, n.queryChannels = async () => [s], s.watch(), { client: n, channel: s, participantFilterId: c.id };
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
g as createMockMessagingClient
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import type {\n Channel,\n MessageResponse,\n QueryChannelAPIResponse,\n SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n id: string\n name?: string\n image?: string\n}\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n id?: string\n text?: string\n from: 'me' | 'them'\n /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments?: any[]\n /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n /** The connected/viewing user. */\n currentUser: MockMessagingUser\n /** The other party in the direct conversation (e.g. the creator/linker). */\n participant: MockMessagingUser\n /** Seed conversation, oldest-first. */\n messages?: MockMessage[]\n /** Channel id; defaults to `mock-dm`. */\n channelId?: string\n /**\n * Optional canned reply. Called after the viewer sends a message; return the\n * reply text (or a partial message) to have `participant` echo a response, or\n * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n */\n onSend?: (\n text: string\n ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n /** Pass to `<MessagingProvider client={...}>`. */\n client: StreamChat\n /** The seeded direct-conversation channel. */\n channel: Channel\n /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n currentUser,\n participant,\n messages = [],\n channelId = 'mock-dm',\n onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n const client = new StreamChat('mock-api-key', {\n allowServerSideConnect: true,\n })\n\n // Mark connected without opening a WebSocket. `MessagingProvider` renders\n // `<Chat client={client}>` directly for an injected client, so it never\n // calls connectUser itself.\n client.connectUser = async () => ({ me: currentUser }) as never\n client.disconnectUser = async () => undefined\n client.userID = currentUser.id\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.user = currentUser as any\n\n const cid = `messaging:${channelId}`\n\n const toStreamMessage = (\n message: MockMessage,\n index: number\n ): MessageResponse => {\n const author = message.from === 'me' ? currentUser : participant\n // Stagger timestamps oldest-first so the list orders naturally.\n const createdAt = new Date(\n Date.now() - (messages.length - index) * 60_000\n ).toISOString()\n return {\n id: message.id ?? `mock-msg-${index}`,\n text: message.text ?? '',\n type: 'regular',\n html: message.text ? `<p>${message.text}</p>` : '',\n user: author,\n attachments: message.attachments ?? [],\n latest_reactions: [],\n own_reactions: [],\n reaction_counts: {},\n reaction_scores: {},\n reply_count: 0,\n status: 'received',\n cid,\n created_at: createdAt,\n updated_at: createdAt,\n mentioned_users: [],\n ...(message.metadata ? { metadata: message.metadata } : {}),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any\n }\n\n const seededMessages = messages.map(toStreamMessage)\n\n const memberState = {\n [currentUser.id]: {\n user: { ...currentUser, is_account: false },\n user_id: currentUser.id,\n role: 'owner',\n is_account: false,\n },\n [participant.id]: {\n user: { ...participant, is_account: true },\n user_id: participant.id,\n role: 'member',\n is_account: true,\n },\n }\n\n const channel = client.channel('messaging', channelId, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n members: [currentUser.id, participant.id] as any,\n })\n\n // Seed state locally instead of fetching. Mirrors the proven ChannelView\n // story mock: override `watch` to populate `state` and return a canned\n // QueryChannelAPIResponse.\n channel.watch = async () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.messages = seededMessages as unknown as any[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.members = memberState as unknown as any\n return {\n channel: { members: [currentUser.id, participant.id] },\n members: [],\n messages: seededMessages,\n watchers: [],\n pinned_messages: [],\n duration: '0ms',\n } as unknown as QueryChannelAPIResponse\n }\n\n const appendMessage = (message: MessageResponse) => {\n // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n // toolkit re-renders through the real channel-state path.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n channel.state.addMessageSorted(message as any)\n client.dispatchEvent({\n type: 'message.new',\n cid,\n message,\n user: message.user ?? undefined,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any)\n }\n\n let sentCount = 0\n channel.sendMessage = async (message) => {\n const text =\n typeof message === 'string'\n ? message\n : ((message as { text?: string })?.text ?? '')\n const sent = {\n ...toStreamMessage({ text, from: 'me', id: `mock-sent-${sentCount++}` }, seededMessages.length),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(typeof message === 'object' ? (message as any) : {}),\n user: currentUser,\n cid,\n } as MessageResponse\n\n // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n // dedupes by id, so confirming here is safe and idempotent.\n appendMessage(sent)\n\n const reply = onSend?.(text)\n if (reply) {\n const replyText = typeof reply === 'string' ? reply : reply.text\n const replyAttachments =\n typeof reply === 'string' ? undefined : reply.attachments\n setTimeout(() => {\n appendMessage(\n toStreamMessage(\n {\n text: replyText,\n from: 'them',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attachments: replyAttachments as any,\n id: `mock-reply-${sentCount}`,\n },\n seededMessages.length\n )\n )\n }, REPLY_DELAY_MS)\n }\n\n return { message: sent } as unknown as SendMessageAPIResponse\n }\n\n // MessagingShell finds the direct conversation via queryChannels; always\n // return the one seeded channel regardless of the filter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client.queryChannels = async () => [channel] as any\n\n // Initialise state up-front so first render already has the conversation.\n void channel.watch()\n\n return { client, channel, participantFilterId: participant.id }\n}\n"],"names":["REPLY_DELAY_MS","createMockMessagingClient","currentUser","participant","messages","channelId","onSend","client","StreamChat","cid","toStreamMessage","message","index","author","createdAt","seededMessages","memberState","channel","appendMessage","sentCount","text","sent","reply","replyText","replyAttachments"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0-rc-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": "
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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: (
|
|
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
|
|
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
|
-
|
|
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(
|
|
220
|
+
expect(buttons.length).toBeGreaterThanOrEqual(1)
|
|
221
|
+
expect(buttons[0]).toHaveClass(
|
|
226
222
|
'appearance-none',
|
|
227
223
|
'border-0',
|
|
228
224
|
'bg-transparent',
|
|
229
|
-
'
|
|
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(
|
|
235
|
-
expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
|
|
232
|
+
expect(avatars[0].closest('button')).toBe(buttons[0])
|
|
236
233
|
|
|
237
|
-
|
|
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
|
|