@linktr.ee/messaging-react 3.9.1 → 3.9.2-rc-1783581484
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-Dau41drB.js → Card-0f6UozSj.js} +2 -2
- package/dist/{Card-Dau41drB.js.map → Card-0f6UozSj.js.map} +1 -1
- package/dist/{Card-D3QNullZ.js → Card-B4Rgburk.js} +2 -2
- package/dist/{Card-D3QNullZ.js.map → Card-B4Rgburk.js.map} +1 -1
- package/dist/{Card-BMau-mI5.cjs → Card-BTlJZXuO.cjs} +2 -2
- package/dist/{Card-BMau-mI5.cjs.map → Card-BTlJZXuO.cjs.map} +1 -1
- package/dist/{Card-BLLADEC_.js → Card-DBsTWA91.js} +3 -3
- package/dist/{Card-BLLADEC_.js.map → Card-DBsTWA91.js.map} +1 -1
- package/dist/{Card-MJ0x2roP.cjs → Card-DQSjcIX_.cjs} +2 -2
- package/dist/{Card-MJ0x2roP.cjs.map → Card-DQSjcIX_.cjs.map} +1 -1
- package/dist/{Card-C3rbCPM7.cjs → Card-QNGgvs9y.cjs} +2 -2
- package/dist/{Card-C3rbCPM7.cjs.map → Card-QNGgvs9y.cjs.map} +1 -1
- package/dist/{LockedThumbnail-Bq54AT8J.js → LockedThumbnail-CYzXhG5H.js} +2 -2
- package/dist/{LockedThumbnail-Bq54AT8J.js.map → LockedThumbnail-CYzXhG5H.js.map} +1 -1
- package/dist/{LockedThumbnail-a03GYGJz.cjs → LockedThumbnail-NlYrnJWD.cjs} +2 -2
- package/dist/{LockedThumbnail-a03GYGJz.cjs.map → LockedThumbnail-NlYrnJWD.cjs.map} +1 -1
- package/dist/index-D0lSJc9F.cjs +2 -0
- package/dist/index-D0lSJc9F.cjs.map +1 -0
- package/dist/{index-CBm9au-T.js → index-DYp35Tv3.js} +1177 -1195
- package/dist/index-DYp35Tv3.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/ChannelHeaderRedesign.tsx +1 -8
- package/src/components/ChannelView.tsx +9 -12
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +51 -11
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +39 -25
- package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +12 -19
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +26 -44
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +43 -37
- package/src/components/CustomMessage/index.tsx +21 -13
- package/dist/index-CBm9au-T.js.map +0 -1
- package/dist/index-CWopIWxB.cjs +0 -2
- package/dist/index-CWopIWxB.cjs.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-D0lSJc9F.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.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-DYp35Tv3.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.2-rc-1783581484",
|
|
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-1783581484",
|
|
54
54
|
"@phosphor-icons/react": "^2.1.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -42,7 +42,6 @@ const hasCustomerPaidTag = (participant?: ChannelMemberResponse) => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
interface ChannelHeaderRedesignProps {
|
|
45
|
-
className?: string
|
|
46
45
|
onBack?: () => void
|
|
47
46
|
showBackButton: boolean
|
|
48
47
|
showStarButton?: boolean
|
|
@@ -62,7 +61,6 @@ interface ChannelHeaderRedesignProps {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
|
|
65
|
-
className,
|
|
66
64
|
onBack,
|
|
67
65
|
showBackButton,
|
|
68
66
|
showStarButton = false,
|
|
@@ -114,12 +112,7 @@ export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
return (
|
|
117
|
-
<div
|
|
118
|
-
className={classNames(
|
|
119
|
-
'@container bg-[linear-gradient(180deg,#FBFAF9_0%,rgba(251,250,249,0.92)_58%,rgba(251,250,249,0)_100%)]',
|
|
120
|
-
className
|
|
121
|
-
)}
|
|
122
|
-
>
|
|
115
|
+
<div className="@container w-full bg-[linear-gradient(180deg,#FBFAF9_0%,rgba(251,250,249,0.92)_58%,rgba(251,250,249,0)_100%)]">
|
|
123
116
|
<div className="grid min-h-[100px] grid-cols-[minmax(40px,1fr)_auto_minmax(40px,1fr)] items-start pb-2 pt-5 @lg:min-h-[104px] @lg:pt-6">
|
|
124
117
|
<div className="flex items-start">
|
|
125
118
|
{showBackButton && (
|
|
@@ -475,7 +475,6 @@ const ChannelViewInner: React.FC<{
|
|
|
475
475
|
) : null
|
|
476
476
|
const redesignedHeader = onParticipantNameClick ? (
|
|
477
477
|
<ChannelHeaderRedesign
|
|
478
|
-
className="-mx-[var(--str-chat\_\_spacing-2)] px-[var(--str-chat\_\_spacing-2)] md:-mx-[min(var(--str-chat\_\_spacing-10),4%)] md:px-[min(var(--str-chat\_\_spacing-10),4%)]"
|
|
479
478
|
onBack={onBack}
|
|
480
479
|
showBackButton={showBackButton}
|
|
481
480
|
showChannelInfo={showChannelInfo}
|
|
@@ -553,22 +552,20 @@ const ChannelViewInner: React.FC<{
|
|
|
553
552
|
</>
|
|
554
553
|
)}
|
|
555
554
|
|
|
555
|
+
{/* Stream only renders MessageList.head when there are messages, so
|
|
556
|
+
empty redesigned channels need the header and banner in normal flow. */}
|
|
557
|
+
{onParticipantNameClick && !hasVisibleMessages && (
|
|
558
|
+
<div className="px-4 @lg:px-6">
|
|
559
|
+
{redesignedHeader}
|
|
560
|
+
{channelBanner}
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
|
|
556
564
|
{/* Message List */}
|
|
557
565
|
<div
|
|
558
566
|
key="lt-channel-message-list"
|
|
559
567
|
className="flex-1 overflow-hidden relative"
|
|
560
568
|
>
|
|
561
|
-
{/* Stream only renders MessageList.head when there are messages, so
|
|
562
|
-
empty redesigned channels need the header and banner in normal flow. */}
|
|
563
|
-
{onParticipantNameClick && !hasVisibleMessages && (
|
|
564
|
-
<div className="str-chat__list">
|
|
565
|
-
<div className="str-chat__message-list-scroll">
|
|
566
|
-
{redesignedHeader}
|
|
567
|
-
{channelBanner}
|
|
568
|
-
</div>
|
|
569
|
-
</div>
|
|
570
|
-
)}
|
|
571
|
-
|
|
572
569
|
<MessageList
|
|
573
570
|
head={
|
|
574
571
|
onParticipantNameClick && hasVisibleMessages ? (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
useChannelStateContext,
|
|
4
|
-
useChatContext,
|
|
5
4
|
useMessageContext,
|
|
6
5
|
} from 'stream-chat-react'
|
|
7
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -13,14 +12,13 @@ import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
|
|
|
13
12
|
// stream-chat-react's `MessageStatus` owns the outer gating (own message, not
|
|
14
13
|
// error, not in a thread) and only exposes a terminal state once a message is
|
|
15
14
|
// stored — Stream's tested code. What this component owns is the "Delivered"
|
|
16
|
-
// label wired into the terminal slots (sent/delivered/read), its "
|
|
17
|
-
// message" gate derived from live channel state, and suppressing
|
|
18
|
-
// spinner. Mock `MessageStatus` to render the slots it was handed,
|
|
19
|
-
// context hooks the label reads.
|
|
15
|
+
// label wired into the terminal slots (sent/delivered/read), its "latest
|
|
16
|
+
// message in the thread" gate derived from live channel state, and suppressing
|
|
17
|
+
// the sending spinner. Mock `MessageStatus` to render the slots it was handed,
|
|
18
|
+
// and the context hooks the label reads.
|
|
20
19
|
vi.mock('stream-chat-react', () => ({
|
|
21
20
|
useMessageContext: vi.fn(),
|
|
22
21
|
useChannelStateContext: vi.fn(),
|
|
23
|
-
useChatContext: vi.fn(),
|
|
24
22
|
MessageStatus: ({
|
|
25
23
|
MessageSentStatus,
|
|
26
24
|
MessageDeliveredStatus,
|
|
@@ -46,10 +44,10 @@ vi.mock('stream-chat-react', () => ({
|
|
|
46
44
|
|
|
47
45
|
const mockedUseMessageContext = vi.mocked(useMessageContext)
|
|
48
46
|
const mockedUseChannelStateContext = vi.mocked(useChannelStateContext)
|
|
49
|
-
const mockedUseChatContext = vi.mocked(useChatContext)
|
|
50
47
|
|
|
51
48
|
// A run where the viewer ('me') sent the first and last messages, with an
|
|
52
|
-
// incoming message in between — the
|
|
49
|
+
// incoming message in between — the label must sit on the last message ('own-2')
|
|
50
|
+
// and clear from the earlier one.
|
|
53
51
|
const CHANNEL_MESSAGES = [
|
|
54
52
|
{ id: 'own-1', user: { id: 'me' } },
|
|
55
53
|
{ id: 'their-1', user: { id: 'them' } },
|
|
@@ -60,13 +58,12 @@ const TERMINAL_SLOTS = ['sent-slot', 'delivered-slot', 'read-slot'] as const
|
|
|
60
58
|
|
|
61
59
|
describe('SentMessageDeliveryStatus', () => {
|
|
62
60
|
beforeEach(() => {
|
|
63
|
-
mockedUseChatContext.mockReturnValue({ client: { user: { id: 'me' } } } as never)
|
|
64
61
|
mockedUseChannelStateContext.mockReturnValue({
|
|
65
62
|
messages: CHANNEL_MESSAGES,
|
|
66
63
|
} as never)
|
|
67
64
|
})
|
|
68
65
|
|
|
69
|
-
it('renders "Delivered" in every terminal slot for the last
|
|
66
|
+
it('renders "Delivered" in every terminal slot for the last message in the thread', () => {
|
|
70
67
|
// delivered/read fire ahead of sent in receipt channels, so all three
|
|
71
68
|
// must show the label — never Stream's default delivered icon / read
|
|
72
69
|
// avatars.
|
|
@@ -83,7 +80,7 @@ describe('SentMessageDeliveryStatus', () => {
|
|
|
83
80
|
}
|
|
84
81
|
})
|
|
85
82
|
|
|
86
|
-
it('renders nothing for an earlier own message
|
|
83
|
+
it('renders nothing for an earlier own message once a later message exists', () => {
|
|
87
84
|
mockedUseMessageContext.mockReturnValue({
|
|
88
85
|
message: { id: 'own-1' },
|
|
89
86
|
} as never)
|
|
@@ -95,6 +92,49 @@ describe('SentMessageDeliveryStatus', () => {
|
|
|
95
92
|
}
|
|
96
93
|
})
|
|
97
94
|
|
|
95
|
+
it('renders nothing for an own message once a reply arrives after it', () => {
|
|
96
|
+
// 'own-2' was the viewer's last sent message, but 'their-1' is now the
|
|
97
|
+
// latest message in the thread — the label must clear from the bubble.
|
|
98
|
+
mockedUseChannelStateContext.mockReturnValue({
|
|
99
|
+
messages: [
|
|
100
|
+
{ id: 'own-1', user: { id: 'me' } },
|
|
101
|
+
{ id: 'own-2', user: { id: 'me' } },
|
|
102
|
+
{ id: 'their-1', user: { id: 'them' } },
|
|
103
|
+
],
|
|
104
|
+
} as never)
|
|
105
|
+
mockedUseMessageContext.mockReturnValue({
|
|
106
|
+
message: { id: 'own-2' },
|
|
107
|
+
} as never)
|
|
108
|
+
|
|
109
|
+
renderWithProviders(<SentMessageDeliveryStatus />)
|
|
110
|
+
|
|
111
|
+
for (const slot of TERMINAL_SLOTS) {
|
|
112
|
+
expect(screen.getByTestId(slot)).toBeEmptyDOMElement()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('ignores a trailing system row when the last real message is still the viewer’s own', () => {
|
|
117
|
+
mockedUseChannelStateContext.mockReturnValue({
|
|
118
|
+
messages: [
|
|
119
|
+
{ id: 'own-1', user: { id: 'me' } },
|
|
120
|
+
{ id: 'their-1', user: { id: 'them' } },
|
|
121
|
+
{ id: 'own-2', user: { id: 'me' } },
|
|
122
|
+
{ id: 'system-1', type: 'system' },
|
|
123
|
+
],
|
|
124
|
+
} as never)
|
|
125
|
+
mockedUseMessageContext.mockReturnValue({
|
|
126
|
+
message: { id: 'own-2' },
|
|
127
|
+
} as never)
|
|
128
|
+
|
|
129
|
+
renderWithProviders(<SentMessageDeliveryStatus />)
|
|
130
|
+
|
|
131
|
+
for (const slot of TERMINAL_SLOTS) {
|
|
132
|
+
expect(
|
|
133
|
+
within(screen.getByTestId(slot)).getByText('Delivered')
|
|
134
|
+
).toBeInTheDocument()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
98
138
|
it('always suppresses the sending spinner', () => {
|
|
99
139
|
mockedUseMessageContext.mockReturnValue({
|
|
100
140
|
message: { id: 'own-2' },
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { ChecksIcon } from '@phosphor-icons/react'
|
|
2
|
-
import { useMemo } from 'react'
|
|
3
2
|
import {
|
|
4
3
|
MessageStatus,
|
|
5
4
|
useChannelStateContext,
|
|
6
|
-
useChatContext,
|
|
7
5
|
useMessageContext,
|
|
8
6
|
} from 'stream-chat-react'
|
|
9
7
|
|
|
@@ -15,21 +13,25 @@ import {
|
|
|
15
13
|
*
|
|
16
14
|
* 1. `MessageStatus` picks its `delivered`/`read` branches ahead of `sent`
|
|
17
15
|
* whenever Stream has `deliveredTo`/`readBy`, and — unlike `sent` — those
|
|
18
|
-
* branches aren't gated to the last
|
|
16
|
+
* branches aren't gated to the last message. So we override all three
|
|
19
17
|
* terminal slots with this same label (never Stream's default delivered
|
|
20
|
-
* icon / read avatars — Read is out of scope) and re-apply the "
|
|
21
|
-
* message" gate ourselves
|
|
18
|
+
* icon / read avatars — Read is out of scope) and re-apply the "latest
|
|
19
|
+
* message in the thread" gate ourselves: the label only shows beneath the
|
|
20
|
+
* viewer's sent message while it is still the last message in the channel,
|
|
21
|
+
* and disappears as soon as a reply arrives after it.
|
|
22
22
|
*
|
|
23
|
-
* 2. We derive "
|
|
24
|
-
*
|
|
25
|
-
* `areMessagePropsEqual`, which does not compare
|
|
26
|
-
* previous own row whose message/grouping props are unchanged (e.g.
|
|
27
|
-
* an intervening incoming message) never re-renders, so
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
23
|
+
* 2. We derive "latest message" from **live channel state**, not the message
|
|
24
|
+
* context's `lastOwnMessage`/`lastReceivedId`. Stream's `Message` is
|
|
25
|
+
* memoised by `areMessagePropsEqual`, which does not compare those props;
|
|
26
|
+
* a previous own row whose message/grouping props are unchanged (e.g.
|
|
27
|
+
* after an intervening incoming message) never re-renders, so the old
|
|
28
|
+
* bubble would keep showing "Delivered" beside the new one.
|
|
29
|
+
* `useChannelStateContext` gets a fresh `messages` array on every new
|
|
30
|
+
* message and, being a context, re-renders this leaf straight through the
|
|
31
|
+
* stale `Message`/`MessageStatus` memos — so the label reliably clears
|
|
32
|
+
* once a reply lands. We still skip non-participant rows (`system`,
|
|
33
|
+
* `ephemeral`, `deleted`) when finding the last real message so a trailing
|
|
34
|
+
* system event does not clear the label.
|
|
33
35
|
*
|
|
34
36
|
* Rendered inside stream's `.str-chat__message-status` span, which supplies
|
|
35
37
|
* the flex layout; icon and copy are styled via that span in `styles.css`.
|
|
@@ -37,18 +39,30 @@ import {
|
|
|
37
39
|
const DeliveredStatus = () => {
|
|
38
40
|
const { message } = useMessageContext('SentMessageDeliveryStatus')
|
|
39
41
|
const { messages } = useChannelStateContext('SentMessageDeliveryStatus')
|
|
40
|
-
const { client } = useChatContext('SentMessageDeliveryStatus')
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
46
|
-
if (messages[i].user?.id === ownUserId) return messages[i].id
|
|
43
|
+
const lastMessageId = (() => {
|
|
44
|
+
if (!messages) {
|
|
45
|
+
return undefined
|
|
47
46
|
}
|
|
47
|
+
|
|
48
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const lastMessage = messages[index]
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
lastMessage.type === 'system' ||
|
|
53
|
+
lastMessage.type === 'ephemeral' ||
|
|
54
|
+
lastMessage.type === 'deleted'
|
|
55
|
+
) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lastMessage.id
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
return undefined
|
|
49
|
-
}
|
|
63
|
+
})()
|
|
50
64
|
|
|
51
|
-
if (message.id !==
|
|
65
|
+
if (message.id !== lastMessageId) {
|
|
52
66
|
return null
|
|
53
67
|
}
|
|
54
68
|
|
|
@@ -73,8 +87,8 @@ const NoStatus = () => null
|
|
|
73
87
|
* terminal (sending/sent/delivered/read) state once the message is stored.
|
|
74
88
|
* Scope is "Delivered" only: the sending spinner is suppressed and all
|
|
75
89
|
* terminal states render {@link DeliveredStatus}, which supplies the label
|
|
76
|
-
* and the
|
|
77
|
-
* live channel state rather than the context's
|
|
90
|
+
* and the latest-message-in-thread gate (see there for why the gate is
|
|
91
|
+
* computed from live channel state rather than the context's props).
|
|
78
92
|
*/
|
|
79
93
|
export const SentMessageDeliveryStatus = () => (
|
|
80
94
|
<MessageStatus
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import React from 'react'
|
|
4
|
-
import type { Attachment
|
|
4
|
+
import type { Attachment } from 'stream-chat'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
currentUserArgType,
|
|
@@ -23,29 +23,22 @@ import StreamAttachmentMessage from './StreamAttachmentMessage'
|
|
|
23
23
|
* Stream-attachment → `LinkAttachment` prop mapping, caption
|
|
24
24
|
* de-duplication (including emoji preservation), and media clustering.
|
|
25
25
|
*
|
|
26
|
-
* The component
|
|
27
|
-
* the
|
|
28
|
-
* ancestry that Stream bubble chrome and the MES-1031 tails are scoped under.
|
|
26
|
+
* The component renders bare rows; this wrapper just anchors the run to
|
|
27
|
+
* the correct chat edge so sent/received read the way they do in-app.
|
|
29
28
|
* ────────────────────────────────────────────────────────────────── */
|
|
30
29
|
|
|
31
30
|
const Bubbles: React.FC<{ isMyMessage: boolean; children: React.ReactNode }> = ({
|
|
32
31
|
isMyMessage,
|
|
33
32
|
children,
|
|
34
33
|
}) => (
|
|
35
|
-
<div className="
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
? 'str-chat__message--me str-chat__message-simple--me'
|
|
44
|
-
: 'str-chat__message--other'
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
47
|
-
<div className="str-chat__message-inner">{children}</div>
|
|
48
|
-
</div>
|
|
34
|
+
<div className="mx-auto max-w-2xl p-12">
|
|
35
|
+
<div
|
|
36
|
+
className={classNames(
|
|
37
|
+
'flex flex-col gap-[2px]',
|
|
38
|
+
isMyMessage ? 'items-end' : 'items-start'
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
49
42
|
</div>
|
|
50
43
|
</div>
|
|
51
44
|
)
|
|
@@ -58,7 +51,7 @@ const Render = (attachments: Attachment[], text?: string): StoryFn<StoryArgs> =>
|
|
|
58
51
|
<StreamAttachmentMessage
|
|
59
52
|
groupPosition="single"
|
|
60
53
|
isMyMessage={isMyMessage}
|
|
61
|
-
message={{ attachments, text }
|
|
54
|
+
message={{ attachments, text }}
|
|
62
55
|
/>
|
|
63
56
|
</Bubbles>
|
|
64
57
|
)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
3
2
|
import { describe, expect, it } from 'vitest'
|
|
4
3
|
|
|
5
4
|
import { renderWithProviders, screen } from '../../test/utils'
|
|
@@ -9,11 +8,6 @@ import StreamAttachmentMessage, {
|
|
|
9
8
|
linkCardPropsFromStreamAttachment,
|
|
10
9
|
} from './StreamAttachmentMessage'
|
|
11
10
|
|
|
12
|
-
const message = (
|
|
13
|
-
attachments: Attachment[],
|
|
14
|
-
text?: string
|
|
15
|
-
): LocalMessage => ({ attachments, text }) as LocalMessage
|
|
16
|
-
|
|
17
11
|
describe('linkCardPropsFromStreamAttachment', () => {
|
|
18
12
|
it('uses the featured layout when a thumbnail is present', () => {
|
|
19
13
|
expect(
|
|
@@ -72,8 +66,8 @@ describe('StreamAttachmentMessage', () => {
|
|
|
72
66
|
<StreamAttachmentMessage
|
|
73
67
|
groupPosition="single"
|
|
74
68
|
isMyMessage={true}
|
|
75
|
-
message={
|
|
76
|
-
[
|
|
69
|
+
message={{
|
|
70
|
+
attachments: [
|
|
77
71
|
{
|
|
78
72
|
type: 'link',
|
|
79
73
|
og_scrape_url: 'https://linktr.ee/someone',
|
|
@@ -82,8 +76,8 @@ describe('StreamAttachmentMessage', () => {
|
|
|
82
76
|
image_url: 'https://cdn.example.com/thumb.jpg',
|
|
83
77
|
},
|
|
84
78
|
],
|
|
85
|
-
'https://linktr.ee/someone !'
|
|
86
|
-
|
|
79
|
+
text: 'https://linktr.ee/someone !',
|
|
80
|
+
}}
|
|
87
81
|
/>
|
|
88
82
|
)
|
|
89
83
|
|
|
@@ -94,30 +88,24 @@ describe('StreamAttachmentMessage', () => {
|
|
|
94
88
|
})
|
|
95
89
|
|
|
96
90
|
it('keeps an emoji-only caption instead of dropping it', () => {
|
|
97
|
-
|
|
91
|
+
renderWithProviders(
|
|
98
92
|
<StreamAttachmentMessage
|
|
99
93
|
groupPosition="single"
|
|
100
94
|
isMyMessage={false}
|
|
101
|
-
message={
|
|
102
|
-
[
|
|
95
|
+
message={{
|
|
96
|
+
attachments: [
|
|
103
97
|
{
|
|
104
98
|
type: 'link',
|
|
105
99
|
og_scrape_url: 'https://linktr.ee/someone',
|
|
106
100
|
title: 'My Linktree',
|
|
107
101
|
},
|
|
108
102
|
],
|
|
109
|
-
'https://linktr.ee/someone 😎'
|
|
110
|
-
|
|
103
|
+
text: 'https://linktr.ee/someone 😎',
|
|
104
|
+
}}
|
|
111
105
|
/>
|
|
112
106
|
)
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
// caption is split across a link node and trailing emoji text.
|
|
116
|
-
const captionBubble = container.querySelector(
|
|
117
|
-
'.str-chat__message-bubble:not([data-attachment-bubble])'
|
|
118
|
-
)
|
|
119
|
-
expect(captionBubble).toHaveTextContent('linktr.ee/someone')
|
|
120
|
-
expect(captionBubble).toHaveTextContent('😎')
|
|
108
|
+
expect(screen.getByText('https://linktr.ee/someone 😎')).toBeInTheDocument()
|
|
121
109
|
})
|
|
122
110
|
|
|
123
111
|
it('renders mixed link and media attachments in order', () => {
|
|
@@ -125,8 +113,8 @@ describe('StreamAttachmentMessage', () => {
|
|
|
125
113
|
<StreamAttachmentMessage
|
|
126
114
|
groupPosition="single"
|
|
127
115
|
isMyMessage={true}
|
|
128
|
-
message={
|
|
129
|
-
[
|
|
116
|
+
message={{
|
|
117
|
+
attachments: [
|
|
130
118
|
{
|
|
131
119
|
type: 'link',
|
|
132
120
|
og_scrape_url: 'https://linktr.ee/someone',
|
|
@@ -138,8 +126,8 @@ describe('StreamAttachmentMessage', () => {
|
|
|
138
126
|
title: 'Vacation',
|
|
139
127
|
},
|
|
140
128
|
],
|
|
141
|
-
'caption'
|
|
142
|
-
|
|
129
|
+
text: 'caption',
|
|
130
|
+
}}
|
|
143
131
|
/>
|
|
144
132
|
)
|
|
145
133
|
|
|
@@ -152,28 +140,27 @@ describe('StreamAttachmentMessage', () => {
|
|
|
152
140
|
expect(container).toHaveTextContent('caption')
|
|
153
141
|
})
|
|
154
142
|
|
|
143
|
+
// The message text must never be dropped when a message carries an
|
|
144
|
+
// attachment — only genuine URL-only captions are suppressed.
|
|
155
145
|
it('renders the accompanying message text next to a link attachment', () => {
|
|
156
|
-
|
|
146
|
+
renderWithProviders(
|
|
157
147
|
<StreamAttachmentMessage
|
|
158
148
|
groupPosition="single"
|
|
159
149
|
isMyMessage={true}
|
|
160
|
-
message={
|
|
161
|
-
[
|
|
150
|
+
message={{
|
|
151
|
+
attachments: [
|
|
162
152
|
{
|
|
163
153
|
type: 'link',
|
|
164
154
|
og_scrape_url: 'https://linktr.ee/someone',
|
|
165
155
|
title: 'My Linktree',
|
|
166
156
|
},
|
|
167
157
|
],
|
|
168
|
-
'you have to see this'
|
|
169
|
-
|
|
158
|
+
text: 'you have to see this',
|
|
159
|
+
}}
|
|
170
160
|
/>
|
|
171
161
|
)
|
|
172
162
|
|
|
173
|
-
|
|
174
|
-
expect(bubbles).toHaveLength(2)
|
|
175
|
-
expect(bubbles[0]).toHaveAttribute('data-attachment-bubble', 'true')
|
|
176
|
-
expect(bubbles[1]).toHaveTextContent('you have to see this')
|
|
163
|
+
expect(screen.getByText('you have to see this')).toBeInTheDocument()
|
|
177
164
|
})
|
|
178
165
|
|
|
179
166
|
it('renders the accompanying message text next to a media attachment', () => {
|
|
@@ -181,24 +168,19 @@ describe('StreamAttachmentMessage', () => {
|
|
|
181
168
|
<StreamAttachmentMessage
|
|
182
169
|
groupPosition="single"
|
|
183
170
|
isMyMessage={false}
|
|
184
|
-
message={
|
|
185
|
-
[
|
|
171
|
+
message={{
|
|
172
|
+
attachments: [
|
|
186
173
|
{
|
|
187
174
|
type: 'image',
|
|
188
175
|
image_url: 'https://cdn.example.com/photo.jpg',
|
|
189
176
|
title: 'Vacation',
|
|
190
177
|
},
|
|
191
178
|
],
|
|
192
|
-
'from the trip'
|
|
193
|
-
|
|
179
|
+
text: 'from the trip',
|
|
180
|
+
}}
|
|
194
181
|
/>
|
|
195
182
|
)
|
|
196
183
|
|
|
197
184
|
expect(container).toHaveTextContent('from the trip')
|
|
198
|
-
expect(
|
|
199
|
-
container.querySelector(
|
|
200
|
-
'.str-chat__message-bubble:not([data-attachment-bubble])'
|
|
201
|
-
)
|
|
202
|
-
).toHaveTextContent('from the trip')
|
|
203
185
|
})
|
|
204
186
|
})
|