@linktr.ee/messaging-react 3.9.2 → 3.10.0-rc-1783605409
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-CVxFApFQ.cjs → Card-B8tKJ_Ax.cjs} +2 -2
- package/dist/{Card-CVxFApFQ.cjs.map → Card-B8tKJ_Ax.cjs.map} +1 -1
- package/dist/{Card-ckN-pJAf.js → Card-C_HNnA8B.js} +2 -2
- package/dist/{Card-ckN-pJAf.js.map → Card-C_HNnA8B.js.map} +1 -1
- package/dist/{Card-B83PIVO4.js → Card-CiuPUMBG.js} +2 -2
- package/dist/{Card-B83PIVO4.js.map → Card-CiuPUMBG.js.map} +1 -1
- package/dist/{Card-Cblg_OP2.js → Card-CpH1yJmm.js} +3 -3
- package/dist/{Card-Cblg_OP2.js.map → Card-CpH1yJmm.js.map} +1 -1
- package/dist/{Card-DyFTYx54.cjs → Card-DhXNjcpF.cjs} +2 -2
- package/dist/{Card-DyFTYx54.cjs.map → Card-DhXNjcpF.cjs.map} +1 -1
- package/dist/{Card-BcTjQXjg.cjs → Card-U-sU4Zv-.cjs} +2 -2
- package/dist/{Card-BcTjQXjg.cjs.map → Card-U-sU4Zv-.cjs.map} +1 -1
- package/dist/{LockedThumbnail-CZ8hj3VY.js → LockedThumbnail-BwSdYQrE.js} +2 -2
- package/dist/{LockedThumbnail-CZ8hj3VY.js.map → LockedThumbnail-BwSdYQrE.js.map} +1 -1
- package/dist/{LockedThumbnail-C-MNksZm.cjs → LockedThumbnail-CijnuXw_.cjs} +2 -2
- package/dist/{LockedThumbnail-C-MNksZm.cjs.map → LockedThumbnail-CijnuXw_.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-CPbf44kW.js → index-DSY7bIL-.js} +650 -572
- package/dist/index-DSY7bIL-.js.map +1 -0
- package/dist/{index-C2hIrXzf.cjs → index-Du7nTLh4.cjs} +2 -2
- package/dist/index-Du7nTLh4.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +55 -0
- package/dist/index.js +20 -19
- package/package.json +2 -2
- package/src/components/MessageBubble/MessageBubble.stories.tsx +98 -0
- package/src/components/MessageBubble/MessageBubble.test.tsx +71 -0
- package/src/components/MessageBubble/components/Received/Card.tsx +27 -0
- package/src/components/MessageBubble/components/Received/index.ts +1 -0
- package/src/components/MessageBubble/components/Sent/Card.tsx +23 -0
- package/src/components/MessageBubble/components/Sent/index.ts +1 -0
- package/src/components/MessageBubble/components/_shared/BubbleShell.tsx +77 -0
- package/src/components/MessageBubble/components/_shared/bubbleTail.tsx +48 -0
- package/src/components/MessageBubble/index.tsx +31 -0
- package/src/components/MessageBubble/types.ts +34 -0
- package/src/index.ts +7 -0
- package/src/styles.css +1 -1
- package/dist/index-C2hIrXzf.cjs.map +0 -1
- package/dist/index-CPbf44kW.js.map +0 -1
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-Du7nTLh4.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.MessageBubble=e.MessageBubble;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
|
@@ -1166,6 +1166,50 @@ export declare type MessageAttachmentVideoReceivedProps = VideoAttachmentSharedP
|
|
|
1166
1166
|
|
|
1167
1167
|
export declare type MessageAttachmentVideoSentProps = VideoAttachmentSharedProps;
|
|
1168
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* A dumb, context-free chat message bubble for rendering a single message row
|
|
1171
|
+
* *outside* a live channel — previews, digests, confirmation dialogs, the
|
|
1172
|
+
* Stacks "responded" summary, etc. Unlike `ChannelView`, it needs no Stream
|
|
1173
|
+
* client or provider.
|
|
1174
|
+
*
|
|
1175
|
+
* Render `MessageBubble.Sent` for the sender's own treatment (dark, right
|
|
1176
|
+
* tail) and `MessageBubble.Received` for the recipient's (light, left tail,
|
|
1177
|
+
* optional header). Surface colours, radius, typography, and the tail
|
|
1178
|
+
* silhouette are shared with the in-channel bubble drawn by `CustomMessage`,
|
|
1179
|
+
* so previews stay pixel-identical and re-theme from one source of truth.
|
|
1180
|
+
*
|
|
1181
|
+
* The variant is a **visual treatment, not an authorship claim**: to preview
|
|
1182
|
+
* an outgoing message as the recipient will see it, render
|
|
1183
|
+
* `MessageBubble.Received` with your own text.
|
|
1184
|
+
*/
|
|
1185
|
+
export declare const MessageBubble: {
|
|
1186
|
+
Sent: FC<SentBubbleProps>;
|
|
1187
|
+
Received: FC<ReceivedBubbleProps>;
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
export declare interface MessageBubbleBaseProps {
|
|
1191
|
+
/**
|
|
1192
|
+
* Message body. Rendered verbatim with `whitespace-pre-wrap` (newlines
|
|
1193
|
+
* preserved); this is a dumb component, so no markdown / link parsing.
|
|
1194
|
+
*/
|
|
1195
|
+
text: string;
|
|
1196
|
+
/**
|
|
1197
|
+
* Draw the bubble tail at the bottom corner. `sent` tails point
|
|
1198
|
+
* bottom-right, `received` bottom-left. Defaults to `true`.
|
|
1199
|
+
*/
|
|
1200
|
+
tail?: boolean;
|
|
1201
|
+
/** Extra classes on the outer bubble (e.g. `max-w-[520px]`). */
|
|
1202
|
+
className?: string;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Which visual treatment the bubble paints. This is decoupled from message
|
|
1207
|
+
* *authorship*: it selects a side + colour, not who wrote the text. To preview
|
|
1208
|
+
* an outgoing message as the recipient will see it (e.g. a bulk-reply
|
|
1209
|
+
* confirmation), render the `received` treatment with your own text.
|
|
1210
|
+
*/
|
|
1211
|
+
export declare type MessageBubbleVariant = 'sent' | 'received';
|
|
1212
|
+
|
|
1169
1213
|
declare type MessageCustomType = 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT' | 'MESSAGE_ATTACHMENT' | AgeSafetySystemType | DmAgentSystemType;
|
|
1170
1214
|
|
|
1171
1215
|
/**
|
|
@@ -1396,6 +1440,14 @@ declare interface PdfAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
|
1396
1440
|
onClick?: (index: number) => boolean | void;
|
|
1397
1441
|
}
|
|
1398
1442
|
|
|
1443
|
+
export declare interface ReceivedBubbleProps extends MessageBubbleBaseProps {
|
|
1444
|
+
/**
|
|
1445
|
+
* Optional header rendered above the text inside the bubble — used by the
|
|
1446
|
+
* Stacks message list to show the sender's name. Omit for a plain bubble.
|
|
1447
|
+
*/
|
|
1448
|
+
header?: default_2.ReactNode;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1399
1451
|
export declare interface ReceivedCardProps extends LockedAttachmentBaseProps {
|
|
1400
1452
|
/**
|
|
1401
1453
|
* Called when the recipient clicks Unlock on an unpaid attachment.
|
|
@@ -1443,6 +1495,9 @@ export declare function resolveMediaFromMessage(message: LocalMessage): MediaMes
|
|
|
1443
1495
|
*/
|
|
1444
1496
|
export declare function resolveParticipantDisplayName(user?: ParticipantDisplayUser | null): string;
|
|
1445
1497
|
|
|
1498
|
+
export declare interface SentBubbleProps extends MessageBubbleBaseProps {
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1446
1501
|
export declare interface SentCardProps extends LockedAttachmentBaseProps {
|
|
1447
1502
|
/** Placeholder shown in the title slot when no title is set. */
|
|
1448
1503
|
placeholderTitle?: string;
|
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
|
|
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 u, i as c, 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 b, u as P, v as f, w as x, x as y, y as B, z as S, B as q, D as z, E as D, G as E, H as V } from "./index-DSY7bIL-.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
|
@@ -10,27 +10,28 @@ export {
|
|
|
10
10
|
g as FaqListItem,
|
|
11
11
|
l as LinkAttachment,
|
|
12
12
|
M as LockedAttachment,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
L as
|
|
16
|
-
h as
|
|
17
|
-
d as
|
|
18
|
-
p as
|
|
19
|
-
v as
|
|
20
|
-
C as
|
|
21
|
-
A as
|
|
22
|
-
k as
|
|
23
|
-
F as
|
|
13
|
+
u as MediaMessage,
|
|
14
|
+
c as MessageAttachment,
|
|
15
|
+
L as MessageBubble,
|
|
16
|
+
h as MessageVoteButtons,
|
|
17
|
+
d as MessagingProvider,
|
|
18
|
+
p as MessagingShell,
|
|
19
|
+
v as buildCompactMetaLabel,
|
|
20
|
+
C as formatFileSize,
|
|
21
|
+
A as formatRelativeTime,
|
|
22
|
+
k as getFileExtensionLabel,
|
|
23
|
+
F as getMessageDisplayText,
|
|
24
|
+
b as isLinkAttachment,
|
|
24
25
|
P as isUuidLike,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
f as messageAttachmentGroupPositionFromStream,
|
|
27
|
+
x as normalizeLanguageCode,
|
|
28
|
+
y as resolveConversationParticipant,
|
|
29
|
+
B as resolveLinkAttachment,
|
|
29
30
|
S as resolveMediaFromMessage,
|
|
30
31
|
q as resolveParticipantDisplayName,
|
|
31
32
|
z as useComposerLocked,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
D as useCustomMessage,
|
|
34
|
+
E as useMessageVote,
|
|
35
|
+
V as useMessaging
|
|
35
36
|
};
|
|
36
37
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0-rc-1783605409",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@linktr.ee/component-library": "11.8.6",
|
|
53
|
-
"@linktr.ee/messaging-core": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1783605409",
|
|
54
54
|
"@phosphor-icons/react": "^2.1.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import MessageBubble from '.'
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'MessageBubble',
|
|
8
|
+
parameters: { layout: 'fullscreen' },
|
|
9
|
+
}
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
const Canvas = ({ children }: { children: React.ReactNode }) => (
|
|
13
|
+
<div className="min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const Row = ({
|
|
19
|
+
label,
|
|
20
|
+
align = 'start',
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
label: string
|
|
24
|
+
align?: 'start' | 'end'
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}) => (
|
|
27
|
+
<div>
|
|
28
|
+
<p className="mb-2 text-xs font-medium text-black/40">{label}</p>
|
|
29
|
+
<div
|
|
30
|
+
className={`flex ${align === 'end' ? 'justify-end' : 'justify-start'}`}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const SHORT = 'Hey! Thanks for reaching out 👋'
|
|
38
|
+
const LONG =
|
|
39
|
+
'Absolutely — I have a couple of slots free next week. Happy to send over the full brief and pricing so you can take a look before we lock anything in.'
|
|
40
|
+
const MULTILINE = 'Line one\nLine two\nLine three'
|
|
41
|
+
|
|
42
|
+
/** Design-review matrix: both variants across tail, length, and header. */
|
|
43
|
+
export const Gallery: StoryFn = () => (
|
|
44
|
+
<Canvas>
|
|
45
|
+
<Row label="Received — short" align="start">
|
|
46
|
+
<MessageBubble.Received text={SHORT} />
|
|
47
|
+
</Row>
|
|
48
|
+
<Row label="Received — with header (sender name)" align="start">
|
|
49
|
+
<MessageBubble.Received header="Brie" text={LONG} className="max-w-[520px]" />
|
|
50
|
+
</Row>
|
|
51
|
+
<Row label="Received — no tail" align="start">
|
|
52
|
+
<MessageBubble.Received text={SHORT} tail={false} />
|
|
53
|
+
</Row>
|
|
54
|
+
<Row label="Sent — short" align="end">
|
|
55
|
+
<MessageBubble.Sent text={SHORT} />
|
|
56
|
+
</Row>
|
|
57
|
+
<Row label="Sent — long" align="end">
|
|
58
|
+
<MessageBubble.Sent text={LONG} className="max-w-[520px]" />
|
|
59
|
+
</Row>
|
|
60
|
+
<Row label="Sent — multiline" align="end">
|
|
61
|
+
<MessageBubble.Sent text={MULTILINE} />
|
|
62
|
+
</Row>
|
|
63
|
+
<Row label="Sent — no tail" align="end">
|
|
64
|
+
<MessageBubble.Sent text={SHORT} tail={false} />
|
|
65
|
+
</Row>
|
|
66
|
+
</Canvas>
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stacks bulk-reply confirmation: preview an outgoing message as the *visitor*
|
|
71
|
+
* will receive it — the recipient (grey, left) treatment despite being your
|
|
72
|
+
* own text.
|
|
73
|
+
*/
|
|
74
|
+
export const PreviewToRecipient: StoryFn = () => (
|
|
75
|
+
<Canvas>
|
|
76
|
+
<Row label="How your reply will look to the visitor" align="start">
|
|
77
|
+
<MessageBubble.Received
|
|
78
|
+
text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
|
|
79
|
+
className="max-w-[520px]"
|
|
80
|
+
/>
|
|
81
|
+
</Row>
|
|
82
|
+
</Canvas>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stacks responded-summary: preview a sent reply as it looks *to you in the
|
|
87
|
+
* channel* — the sender (dark, right) treatment.
|
|
88
|
+
*/
|
|
89
|
+
export const PreviewInChannel: StoryFn = () => (
|
|
90
|
+
<Canvas>
|
|
91
|
+
<Row label="How your reply looks to you in the channel" align="end">
|
|
92
|
+
<MessageBubble.Sent
|
|
93
|
+
text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
|
|
94
|
+
className="max-w-[520px]"
|
|
95
|
+
/>
|
|
96
|
+
</Row>
|
|
97
|
+
</Canvas>
|
|
98
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders, screen } from '../../test/utils'
|
|
5
|
+
|
|
6
|
+
import MessageBubble from '.'
|
|
7
|
+
|
|
8
|
+
describe('MessageBubble', () => {
|
|
9
|
+
it('renders the message text', () => {
|
|
10
|
+
renderWithProviders(<MessageBubble.Sent text="Hello there" />)
|
|
11
|
+
expect(screen.getByText('Hello there')).toBeInTheDocument()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('preserves newlines with whitespace-pre-wrap', () => {
|
|
15
|
+
renderWithProviders(<MessageBubble.Sent text={'line one\nline two'} />)
|
|
16
|
+
const paragraph = screen.getByText(/line one/)
|
|
17
|
+
expect(paragraph.textContent).toBe('line one\nline two')
|
|
18
|
+
expect(paragraph.className).toContain('whitespace-pre-wrap')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('draws the tail by default and omits it when tail is false', () => {
|
|
22
|
+
const { rerender } = renderWithProviders(
|
|
23
|
+
<MessageBubble.Sent text="tailed" />
|
|
24
|
+
)
|
|
25
|
+
// The bubble box wraps the text; its last child is the tail <span>.
|
|
26
|
+
const bubble = screen.getByText('tailed').parentElement as HTMLElement
|
|
27
|
+
expect(bubble.querySelector('span[aria-hidden="true"]')).not.toBeNull()
|
|
28
|
+
|
|
29
|
+
rerender(<MessageBubble.Sent text="tailed" tail={false} />)
|
|
30
|
+
const bubbleNoTail = screen.getByText('tailed').parentElement as HTMLElement
|
|
31
|
+
expect(bubbleNoTail.querySelector('span[aria-hidden="true"]')).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders the optional header on the received variant', () => {
|
|
35
|
+
renderWithProviders(
|
|
36
|
+
<MessageBubble.Received header="Brie" text="How much for a feature?" />
|
|
37
|
+
)
|
|
38
|
+
expect(screen.getByTestId('message-bubble-header')).toHaveTextContent(
|
|
39
|
+
'Brie'
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not render a header slot when no header is provided', () => {
|
|
44
|
+
renderWithProviders(<MessageBubble.Received text="no header" />)
|
|
45
|
+
expect(screen.queryByTestId('message-bubble-header')).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('applies the sent and received surface treatments from theme vars', () => {
|
|
49
|
+
const { rerender } = renderWithProviders(
|
|
50
|
+
<MessageBubble.Sent text="mine" />
|
|
51
|
+
)
|
|
52
|
+
const sent = screen.getByText('mine').parentElement as HTMLElement
|
|
53
|
+
expect(sent.style.backgroundColor).toBe(
|
|
54
|
+
'var(--str-chat__own-message-bubble-background-color, #1e2330)'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
rerender(<MessageBubble.Received text="theirs" />)
|
|
58
|
+
const received = screen.getByText('theirs').parentElement as HTMLElement
|
|
59
|
+
expect(received.style.backgroundColor).toBe(
|
|
60
|
+
'var(--str-chat__message-bubble-background-color, #f1f0ee)'
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('forwards className onto the bubble box', () => {
|
|
65
|
+
renderWithProviders(
|
|
66
|
+
<MessageBubble.Sent text="wide" className="max-w-[520px]" />
|
|
67
|
+
)
|
|
68
|
+
const bubble = screen.getByText('wide').parentElement as HTMLElement
|
|
69
|
+
expect(bubble.className).toContain('max-w-[520px]')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ReceivedBubbleProps } from '../../types'
|
|
4
|
+
import BubbleShell from '../_shared/BubbleShell'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A message bubble in the recipient's treatment — light surface, left-side
|
|
8
|
+
* tail, with an optional header (e.g. sender name). Use to preview how a
|
|
9
|
+
* message looks *to the visitor* — including a preview of your own outgoing
|
|
10
|
+
* message as the recipient will receive it (e.g. a bulk-reply confirmation).
|
|
11
|
+
*/
|
|
12
|
+
const ReceivedCard: React.FC<ReceivedBubbleProps> = ({
|
|
13
|
+
text,
|
|
14
|
+
tail,
|
|
15
|
+
header,
|
|
16
|
+
className,
|
|
17
|
+
}) => (
|
|
18
|
+
<BubbleShell
|
|
19
|
+
variant="received"
|
|
20
|
+
text={text}
|
|
21
|
+
tail={tail}
|
|
22
|
+
header={header}
|
|
23
|
+
className={className}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export default ReceivedCard
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Card'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import type { SentBubbleProps } from '../../types'
|
|
4
|
+
import BubbleShell from '../_shared/BubbleShell'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A message bubble in the sender's own treatment — dark surface, right-side
|
|
8
|
+
* tail. Use to preview how a message looks *to you in the channel*.
|
|
9
|
+
*
|
|
10
|
+
* Note: the variant is a visual treatment, not an authorship claim. To preview
|
|
11
|
+
* an outgoing message as the *recipient* will see it, use
|
|
12
|
+
* `MessageBubble.Received` instead.
|
|
13
|
+
*/
|
|
14
|
+
const SentCard: React.FC<SentBubbleProps> = ({ text, tail, className }) => (
|
|
15
|
+
<BubbleShell
|
|
16
|
+
variant="sent"
|
|
17
|
+
text={text}
|
|
18
|
+
tail={tail}
|
|
19
|
+
className={className}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export default SentCard
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Card'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import type { MessageBubbleVariant } from '../../types'
|
|
5
|
+
|
|
6
|
+
import { BubbleTail } from './bubbleTail'
|
|
7
|
+
|
|
8
|
+
export interface BubbleShellProps {
|
|
9
|
+
variant: MessageBubbleVariant
|
|
10
|
+
text: string
|
|
11
|
+
/** Draw the bubble tail. Defaults to `true`. */
|
|
12
|
+
tail?: boolean
|
|
13
|
+
/** Optional header rendered above the text (e.g. sender name). */
|
|
14
|
+
header?: React.ReactNode
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Surface colours and metrics are pulled from the toolkit's Stream theme vars
|
|
20
|
+
* (see `styles.css`) so a standalone bubble is pixel-identical to the
|
|
21
|
+
* in-channel one and re-themes from the same single source of truth.
|
|
22
|
+
*/
|
|
23
|
+
const SURFACE_STYLE_BY_VARIANT: Record<
|
|
24
|
+
MessageBubbleVariant,
|
|
25
|
+
React.CSSProperties
|
|
26
|
+
> = {
|
|
27
|
+
sent: {
|
|
28
|
+
backgroundColor:
|
|
29
|
+
'var(--str-chat__own-message-bubble-background-color, #1e2330)',
|
|
30
|
+
color: 'var(--str-chat__own-message-bubble-color, #fff)',
|
|
31
|
+
},
|
|
32
|
+
received: {
|
|
33
|
+
backgroundColor:
|
|
34
|
+
'var(--str-chat__message-bubble-background-color, #f1f0ee)',
|
|
35
|
+
color: 'var(--str-chat__message-bubble-color, #000000)',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The rounded bubble box: theme-driven surface, channel-matched typography
|
|
41
|
+
* (`0.875rem / 1.3125rem`, `0.14px` tracking, `0.75rem 1rem` padding), an
|
|
42
|
+
* optional header slot, and an optional tail.
|
|
43
|
+
*
|
|
44
|
+
* `isolation: isolate` forms a stacking context so the `z-index: -1` tail
|
|
45
|
+
* paints behind the bubble's own opaque, same-colour background but in front
|
|
46
|
+
* of the surrounding page — matching the in-channel `::after` tail.
|
|
47
|
+
*/
|
|
48
|
+
const BubbleShell: React.FC<BubbleShellProps> = ({
|
|
49
|
+
variant,
|
|
50
|
+
text,
|
|
51
|
+
tail = true,
|
|
52
|
+
header,
|
|
53
|
+
className,
|
|
54
|
+
}) => (
|
|
55
|
+
<div
|
|
56
|
+
className={classNames(
|
|
57
|
+
'relative w-fit max-w-full px-4 py-3',
|
|
58
|
+
'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
style={{
|
|
62
|
+
...SURFACE_STYLE_BY_VARIANT[variant],
|
|
63
|
+
borderRadius: 'var(--str-chat__message-bubble-border-radius, 1.5rem)',
|
|
64
|
+
isolation: 'isolate',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{header != null && (
|
|
68
|
+
<div className="mb-1 font-medium" data-testid="message-bubble-header">
|
|
69
|
+
{header}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
<p className="m-0 whitespace-pre-wrap break-words">{text}</p>
|
|
73
|
+
{tail && <BubbleTail variant={variant} />}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
export default BubbleShell
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import type { MessageBubbleVariant } from '../../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Canonical chat-bubble tail (20×23). Kept identical to the in-channel tail
|
|
7
|
+
* drawn by `CustomMessage` in `styles.css` so standalone previews and real
|
|
8
|
+
* messages share one silhouette — the single source of truth for the shape.
|
|
9
|
+
*
|
|
10
|
+
* The path points to the bottom-*right*; the `received` variant mirrors it
|
|
11
|
+
* with `scaleX(-1)`, matching the in-channel convention.
|
|
12
|
+
*/
|
|
13
|
+
const TAIL_MASK =
|
|
14
|
+
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='23' fill='none'%3E%3Cpath fill='black' d='M0 0H15C15 10.1934 15.859 15.2345 19.168 21.4893C19.527 22.168 19.039 22.9917 18.274 22.9178C5.176 21.6506 0.32 6.0737 0 0Z'/%3E%3C/svg%3E\") no-repeat center / 100% 100%"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bubble surface colours, reused from the toolkit's Stream theme vars so the
|
|
18
|
+
* tail is always tinted the same colour as its bubble. Those vars are declared
|
|
19
|
+
* on `.str-chat`, so we supply the canonical values as `var()` fallbacks —
|
|
20
|
+
* that keeps the tail correct when rendered *outside* a channel (e.g. a
|
|
21
|
+
* standalone preview) while still honouring the var (or a consumer override)
|
|
22
|
+
* wherever it's defined.
|
|
23
|
+
*/
|
|
24
|
+
const BG_VAR_BY_VARIANT: Record<MessageBubbleVariant, string> = {
|
|
25
|
+
sent: 'var(--str-chat__own-message-bubble-background-color, #1e2330)',
|
|
26
|
+
received: 'var(--str-chat__message-bubble-background-color, #f1f0ee)',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The tail element. Absolutely positioned at the bubble's bottom corner and
|
|
31
|
+
* painted behind the bubble body (`z-index: -1`), so the parent bubble must be
|
|
32
|
+
* `position: relative` with `isolation: isolate` (see {@link BubbleShell}).
|
|
33
|
+
*/
|
|
34
|
+
export const BubbleTail = ({ variant }: { variant: MessageBubbleVariant }) => (
|
|
35
|
+
<span
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
className="pointer-events-none absolute bottom-0 z-[-1] h-[1.4375rem] w-5"
|
|
38
|
+
style={{
|
|
39
|
+
insetInlineEnd: variant === 'sent' ? 'calc(-1 * 0.25rem)' : undefined,
|
|
40
|
+
insetInlineStart: variant === 'received' ? 'calc(-1 * 0.25rem)' : undefined,
|
|
41
|
+
transform: variant === 'received' ? 'scaleX(-1)' : undefined,
|
|
42
|
+
transformOrigin: 'center',
|
|
43
|
+
backgroundColor: BG_VAR_BY_VARIANT[variant],
|
|
44
|
+
WebkitMask: TAIL_MASK,
|
|
45
|
+
mask: TAIL_MASK,
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import ReceivedCard from './components/Received/Card'
|
|
2
|
+
import SentCard from './components/Sent/Card'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A dumb, context-free chat message bubble for rendering a single message row
|
|
6
|
+
* *outside* a live channel — previews, digests, confirmation dialogs, the
|
|
7
|
+
* Stacks "responded" summary, etc. Unlike `ChannelView`, it needs no Stream
|
|
8
|
+
* client or provider.
|
|
9
|
+
*
|
|
10
|
+
* Render `MessageBubble.Sent` for the sender's own treatment (dark, right
|
|
11
|
+
* tail) and `MessageBubble.Received` for the recipient's (light, left tail,
|
|
12
|
+
* optional header). Surface colours, radius, typography, and the tail
|
|
13
|
+
* silhouette are shared with the in-channel bubble drawn by `CustomMessage`,
|
|
14
|
+
* so previews stay pixel-identical and re-theme from one source of truth.
|
|
15
|
+
*
|
|
16
|
+
* The variant is a **visual treatment, not an authorship claim**: to preview
|
|
17
|
+
* an outgoing message as the recipient will see it, render
|
|
18
|
+
* `MessageBubble.Received` with your own text.
|
|
19
|
+
*/
|
|
20
|
+
const MessageBubble = {
|
|
21
|
+
Sent: SentCard,
|
|
22
|
+
Received: ReceivedCard,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default MessageBubble
|
|
26
|
+
export type {
|
|
27
|
+
MessageBubbleBaseProps,
|
|
28
|
+
MessageBubbleVariant,
|
|
29
|
+
SentBubbleProps,
|
|
30
|
+
ReceivedBubbleProps,
|
|
31
|
+
} from './types'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which visual treatment the bubble paints. This is decoupled from message
|
|
5
|
+
* *authorship*: it selects a side + colour, not who wrote the text. To preview
|
|
6
|
+
* an outgoing message as the recipient will see it (e.g. a bulk-reply
|
|
7
|
+
* confirmation), render the `received` treatment with your own text.
|
|
8
|
+
*/
|
|
9
|
+
export type MessageBubbleVariant = 'sent' | 'received'
|
|
10
|
+
|
|
11
|
+
export interface MessageBubbleBaseProps {
|
|
12
|
+
/**
|
|
13
|
+
* Message body. Rendered verbatim with `whitespace-pre-wrap` (newlines
|
|
14
|
+
* preserved); this is a dumb component, so no markdown / link parsing.
|
|
15
|
+
*/
|
|
16
|
+
text: string
|
|
17
|
+
/**
|
|
18
|
+
* Draw the bubble tail at the bottom corner. `sent` tails point
|
|
19
|
+
* bottom-right, `received` bottom-left. Defaults to `true`.
|
|
20
|
+
*/
|
|
21
|
+
tail?: boolean
|
|
22
|
+
/** Extra classes on the outer bubble (e.g. `max-w-[520px]`). */
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SentBubbleProps extends MessageBubbleBaseProps {}
|
|
27
|
+
|
|
28
|
+
export interface ReceivedBubbleProps extends MessageBubbleBaseProps {
|
|
29
|
+
/**
|
|
30
|
+
* Optional header rendered above the text inside the bubble — used by the
|
|
31
|
+
* Stacks message list to show the sender's name. Omit for a plain bubble.
|
|
32
|
+
*/
|
|
33
|
+
header?: React.ReactNode
|
|
34
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { default as LockedAttachment } from './components/LockedAttachment'
|
|
|
10
10
|
export { default as LinkAttachment } from './components/LinkAttachment'
|
|
11
11
|
export { default as MessageAttachment } from './components/MessageAttachment'
|
|
12
12
|
export { Avatar } from './components/Avatar'
|
|
13
|
+
export { default as MessageBubble } from './components/MessageBubble'
|
|
13
14
|
export { FaqList } from './components/FaqList'
|
|
14
15
|
export { FaqListItem } from './components/FaqList/FaqListItem'
|
|
15
16
|
export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
|
|
@@ -67,6 +68,12 @@ export type {
|
|
|
67
68
|
OfficialCtaAttachment,
|
|
68
69
|
} from './stream-custom-data'
|
|
69
70
|
export type { AvatarProps } from './components/Avatar'
|
|
71
|
+
export type {
|
|
72
|
+
MessageBubbleBaseProps,
|
|
73
|
+
MessageBubbleVariant,
|
|
74
|
+
SentBubbleProps,
|
|
75
|
+
ReceivedBubbleProps,
|
|
76
|
+
} from './components/MessageBubble'
|
|
70
77
|
export type { ActionButtonProps } from './components/ActionButton'
|
|
71
78
|
export type {
|
|
72
79
|
ComposerCardProps,
|
package/src/styles.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--str-chat__font-family: inherit;
|
|
9
9
|
|
|
10
10
|
--str-chat__message-bubble-border-radius: 1.5rem;
|
|
11
|
-
--str-chat__message-bubble-background-color: #
|
|
11
|
+
--str-chat__message-bubble-background-color: #f1f0ee;
|
|
12
12
|
--str-chat__message-bubble-color: #000000;
|
|
13
13
|
|
|
14
14
|
--str-chat__own-message-bubble-background-color: #1e2330;
|