@linktr.ee/messaging-react 1.22.0 → 1.22.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.22.0",
3
+ "version": "1.22.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,6 +47,9 @@ type BlockedUser = {
47
47
  blocked_user_id: string
48
48
  }
49
49
 
50
+ const ICON_BTN_CLASS =
51
+ 'size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150'
52
+
50
53
  /**
51
54
  * Custom channel header component
52
55
  */
@@ -118,9 +121,7 @@ const CustomChannelHeader: React.FC<{
118
121
  <div className="flex items-center gap-2">
119
122
  {showBackButton && (
120
123
  <button
121
- className={classNames(
122
- 'size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center'
123
- )}
124
+ className={ICON_BTN_CLASS}
124
125
  onClick={onBack || (() => {})}
125
126
  type="button"
126
127
  aria-label="Back to conversations"
@@ -143,7 +144,7 @@ const CustomChannelHeader: React.FC<{
143
144
  <div className="flex justify-end items-center gap-2">
144
145
  {showStarButton && (
145
146
  <button
146
- className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
147
+ className={ICON_BTN_CLASS}
147
148
  onClick={handleStarClick}
148
149
  type="button"
149
150
  aria-label={
@@ -160,7 +161,7 @@ const CustomChannelHeader: React.FC<{
160
161
  </button>
161
162
  )}
162
163
  <button
163
- className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
164
+ className={ICON_BTN_CLASS}
164
165
  onClick={onShowInfo}
165
166
  type="button"
166
167
  aria-label="Show info"
@@ -175,7 +176,7 @@ const CustomChannelHeader: React.FC<{
175
176
  <button
176
177
  type="button"
177
178
  onClick={onBack}
178
- className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
179
+ className={ICON_BTN_CLASS}
179
180
  aria-label="Back to conversations"
180
181
  >
181
182
  <ArrowLeftIcon className="size-5 text-black/90" />
@@ -197,7 +198,7 @@ const CustomChannelHeader: React.FC<{
197
198
  <div className="flex items-center gap-2">
198
199
  {showStarButton && (
199
200
  <button
200
- className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
201
+ className={ICON_BTN_CLASS}
201
202
  onClick={handleStarClick}
202
203
  type="button"
203
204
  aria-label={
@@ -215,7 +216,7 @@ const CustomChannelHeader: React.FC<{
215
216
  )}
216
217
  {canShowInfo && onShowInfo && (
217
218
  <button
218
- className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
219
+ className={ICON_BTN_CLASS}
219
220
  onClick={onShowInfo}
220
221
  type="button"
221
222
  aria-label="Show info"
@@ -38,7 +38,7 @@ const createMockChannel = async (
38
38
  created_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
39
39
  updated_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
40
40
  html: `<p>${msg.text}</p>`,
41
- attachments: [],
41
+ attachments: msg.attachments ?? [],
42
42
  latest_reactions: [],
43
43
  own_reactions: [],
44
44
  reaction_counts: {},
@@ -91,6 +91,7 @@ interface TemplateProps {
91
91
  text: string
92
92
  user: typeof mockUser | { id: string; name: string }
93
93
  type?: 'regular' | 'system'
94
+ attachments?: Array<Record<string, unknown>>
94
95
  metadata?: {
95
96
  custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
96
97
  amount_text?: string
@@ -206,3 +207,37 @@ MixedTags.args = {
206
207
  { id: 'msg-9', text: 'Thanks for letting me know.', user: mockUser },
207
208
  ],
208
209
  }
210
+
211
+ export const ChatbotVariants: StoryFn<TemplateProps> = Template.bind({})
212
+ ChatbotVariants.args = {
213
+ messages: [
214
+ {
215
+ id: 'msg-1',
216
+ text: 'Would you like to share some details that I can relay to Rupi Kaur?',
217
+ user: participant,
218
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
219
+ },
220
+ {
221
+ id: 'msg-2',
222
+ text: 'I’m sorry to hear that. Are you referring to the course you’ve recently purchased?',
223
+ user: mockUser,
224
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
225
+ },
226
+ {
227
+ id: 'msg-3',
228
+ text: '',
229
+ user: mockUser,
230
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
231
+ attachments: [
232
+ {
233
+ type: 'card',
234
+ title: 'The Perfect Yoga Course',
235
+ title_link: 'https://linktr.ee/rupikaur/learntoyoga',
236
+ text: 'linktr.ee/rupikaur/learntoyoga',
237
+ image_url:
238
+ 'https://images.unsplash.com/photo-1571019613914-85f342c1d4b5?auto=format&fit=crop&w=1024&q=80',
239
+ },
240
+ ],
241
+ },
242
+ ],
243
+ }
@@ -64,11 +64,34 @@ Paid.args = {
64
64
  }),
65
65
  }
66
66
 
67
- export const Chatbot: StoryFn<ComponentProps> = Template.bind({})
68
- Chatbot.args = {
67
+ export const ChatbotReceiverText: StoryFn<ComponentProps> = Template.bind({})
68
+ ChatbotReceiverText.args = {
69
69
  message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
70
70
  }
71
71
 
72
+ export const ChatbotSenderText: StoryFn<ComponentProps> = (args) => {
73
+ return (
74
+ <div className="p-12">
75
+ <div className="bg-[#121110] rounded-3xl px-4 py-3 w-[280px]">
76
+ <MessageTag {...args} />
77
+ </div>
78
+ </div>
79
+ )
80
+ }
81
+ ChatbotSenderText.args = {
82
+ message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
83
+ isMyMessage: true,
84
+ }
85
+
86
+ export const ChatbotSenderAttachment: StoryFn<ComponentProps> = Template.bind(
87
+ {}
88
+ )
89
+ ChatbotSenderAttachment.args = {
90
+ message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
91
+ isMyMessage: true,
92
+ hasAttachment: true,
93
+ }
94
+
72
95
  export const NoTag: StoryFn<ComponentProps> = Template.bind({})
73
96
  NoTag.args = {
74
97
  message: createMockMessage(),
@@ -104,11 +127,36 @@ export const AllVariants: StoryFn = () => {
104
127
  />
105
128
  </div>
106
129
  <div className="flex items-center gap-4">
107
- <span className="text-sm w-32">Chatbot:</span>
130
+ <span className="text-sm w-32">Chatbot (receiver):</span>
108
131
  <MessageTag
109
- message={createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } })}
132
+ message={createMockMessage({
133
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
134
+ })}
110
135
  />
111
136
  </div>
137
+ <div className="flex items-center gap-4">
138
+ <span className="text-sm w-32">Chatbot (sender):</span>
139
+ <div className="bg-[#121110] rounded-3xl px-4 py-3 w-[280px]">
140
+ <MessageTag
141
+ message={createMockMessage({
142
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
143
+ })}
144
+ isMyMessage
145
+ />
146
+ </div>
147
+ </div>
148
+ <div className="flex items-center gap-4">
149
+ <span className="text-sm w-32">Chatbot (attachment):</span>
150
+ <div className="w-[280px]">
151
+ <MessageTag
152
+ message={createMockMessage({
153
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
154
+ })}
155
+ isMyMessage
156
+ hasAttachment
157
+ />
158
+ </div>
159
+ </div>
112
160
  <div className="flex items-center gap-4">
113
161
  <span className="text-sm w-32">No tag:</span>
114
162
  <MessageTag message={createMockMessage()} />
@@ -0,0 +1,110 @@
1
+ import { LocalMessage } from 'stream-chat'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import { MessageTag } from './MessageTag'
7
+
8
+ interface MockMessageOptions {
9
+ metadata?: {
10
+ custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
11
+ amount_text?: string
12
+ }
13
+ text?: string
14
+ }
15
+
16
+ const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
17
+ ({
18
+ id: 'msg-1',
19
+ text: options?.text ?? 'Hello world',
20
+ type: 'regular',
21
+ created_at: new Date(),
22
+ updated_at: new Date(),
23
+ metadata: options?.metadata,
24
+ }) as LocalMessage
25
+
26
+ describe('MessageTag', () => {
27
+ it('renders receiver chatbot text variant with DM Agent copy and icon first', () => {
28
+ renderWithProviders(
29
+ <MessageTag
30
+ message={createMockMessage({
31
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
32
+ })}
33
+ />
34
+ )
35
+
36
+ const indicator = screen.getByTestId('message-chatbot-indicator')
37
+ expect(indicator).toHaveTextContent('Sent with DM Agent')
38
+ expect(indicator).toHaveClass('message-chatbot-indicator--receiver')
39
+ expect(indicator).toHaveClass('message-chatbot-indicator--text')
40
+
41
+ expect(indicator.children[0]).toHaveClass('message-chatbot-indicator__icon')
42
+ expect(indicator.children[1]).toHaveClass(
43
+ 'message-chatbot-indicator__label'
44
+ )
45
+ })
46
+
47
+ it('renders sender chatbot text variant with mirrored order', () => {
48
+ renderWithProviders(
49
+ <MessageTag
50
+ message={createMockMessage({
51
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
52
+ })}
53
+ isMyMessage
54
+ />
55
+ )
56
+
57
+ const indicator = screen.getByTestId('message-chatbot-indicator')
58
+ expect(indicator).toHaveTextContent('Sent with DM Agent')
59
+ expect(indicator).toHaveClass('message-chatbot-indicator--sender')
60
+ expect(indicator).toHaveClass('message-chatbot-indicator--text')
61
+
62
+ expect(indicator.children[0]).toHaveClass(
63
+ 'message-chatbot-indicator__label'
64
+ )
65
+ expect(indicator.children[1]).toHaveClass('message-chatbot-indicator__icon')
66
+ })
67
+
68
+ it('renders sender attachment chatbot variant outside-footer copy', () => {
69
+ renderWithProviders(
70
+ <MessageTag
71
+ message={createMockMessage({
72
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
73
+ })}
74
+ hasAttachment
75
+ isMyMessage
76
+ />
77
+ )
78
+
79
+ const indicator = screen.getByTestId('message-chatbot-indicator')
80
+ expect(indicator).toHaveTextContent('Sent with AI')
81
+ expect(indicator).toHaveClass('message-chatbot-indicator--sender')
82
+ expect(indicator).toHaveClass('message-chatbot-indicator--attachment')
83
+
84
+ expect(indicator.children[0]).toHaveClass('message-chatbot-indicator__icon')
85
+ expect(indicator.children[1]).toHaveClass(
86
+ 'message-chatbot-indicator__label'
87
+ )
88
+ })
89
+
90
+ it('keeps tip tag behavior unchanged', () => {
91
+ renderWithProviders(
92
+ <MessageTag
93
+ message={createMockMessage({
94
+ metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
95
+ })}
96
+ />
97
+ )
98
+
99
+ const tipTag = screen.getByText('Delivered with $5.50 tip')
100
+ expect(tipTag.closest('.message-tag--tip')).not.toBeNull()
101
+ })
102
+
103
+ it('returns null for non-custom messages', () => {
104
+ const { container } = renderWithProviders(
105
+ <MessageTag message={createMockMessage()} />
106
+ )
107
+
108
+ expect(container.firstChild).toBeNull()
109
+ })
110
+ })
@@ -5,13 +5,24 @@ interface MessageTagProps {
5
5
  message: LocalMessage
6
6
  /** When true, renders as a standalone bubble instead of a small tag */
7
7
  standalone?: boolean
8
+ /** When true, renders sender-side chatbot variants */
9
+ isMyMessage?: boolean
10
+ /** Whether the message includes an attachment */
11
+ hasAttachment?: boolean
8
12
  }
9
13
 
10
- const SparkleIcon = () => (
11
- <svg width="12" height="12" viewBox="0 0 10 10" fill="none">
14
+ const SparkleIcon = ({ size = 15 }: { size?: number }) => (
15
+ <svg
16
+ width={size}
17
+ height={size}
18
+ viewBox="0 0 15 15"
19
+ fill="none"
20
+ aria-hidden="true"
21
+ >
12
22
  <path
13
- d="M10.003 5a.705.705 0 0 1-.469.67L6.7 6.7 5.67 9.535a.715.715 0 0 1-1.34 0L3.3 6.7.466 5.67a.715.715 0 0 1 0-1.34L3.3 3.3 4.33.466a.715.715 0 0 1 1.34 0L6.7 3.3l2.834 1.03a.705.705 0 0 1 .469.67"
23
+ d="M12.003 9a.985.985 0 0 1-.652.934l-3.223 1.191-1.188 3.226a.995.995 0 0 1-1.867 0l-1.195-3.226L.65 9.937a.995.995 0 0 1 0-1.867l3.227-1.195 1.187-3.226a.995.995 0 0 1 1.868 0l1.195 3.226 3.226 1.187a.99.99 0 0 1 .649.938m3-5.83a.52.52 0 0 1-.344.492l-1.702.63-.627 1.703a.525.525 0 0 1-.986 0l-.63-1.704-1.704-.627a.525.525 0 0 1 0-.986l1.703-.63.627-1.704a.526.526 0 0 1 .986 0l.631 1.703 1.704.627a.52.52 0 0 1 .342.495"
14
24
  fill="currentColor"
25
+ fillOpacity={0.55}
15
26
  />
16
27
  </svg>
17
28
  )
@@ -44,6 +55,8 @@ export const isTipOnlyMessage = (message: LocalMessage): boolean => {
44
55
  export const MessageTag = ({
45
56
  message,
46
57
  standalone = false,
58
+ isMyMessage = false,
59
+ hasAttachment = false,
47
60
  }: MessageTagProps) => {
48
61
  const isTipOrPaid = isTipOrPaidMessage(message)
49
62
  const isChatbot = isChatbotMessage(message)
@@ -72,13 +85,44 @@ export const MessageTag = ({
72
85
  )
73
86
  }
74
87
 
75
- // Chatbot tag
88
+ const isSenderAttachmentVariant = isMyMessage && hasAttachment
89
+ const chatbotLabel = isSenderAttachmentVariant
90
+ ? 'Sent with AI'
91
+ : 'Sent with DM Agent'
92
+
93
+ const chatbotClassName = [
94
+ 'message-chatbot-indicator',
95
+ isMyMessage
96
+ ? 'message-chatbot-indicator--sender'
97
+ : 'message-chatbot-indicator--receiver',
98
+ isSenderAttachmentVariant
99
+ ? 'message-chatbot-indicator--attachment'
100
+ : 'message-chatbot-indicator--text',
101
+ ].join(' ')
102
+
103
+ const label = (
104
+ <span className="message-chatbot-indicator__label">{chatbotLabel}</span>
105
+ )
106
+ const icon = (
107
+ <span className="message-chatbot-indicator__icon">
108
+ <SparkleIcon size={isSenderAttachmentVariant ? 12 : 15} />
109
+ </span>
110
+ )
111
+
112
+ // Chatbot indicator variant
76
113
  return (
77
- <div className="message-tag message-tag--chatbot">
78
- <span className="message-tag__icon" style={{ marginTop: -1 }}>
79
- <SparkleIcon />
80
- </span>
81
- <span className="message-tag__label">Chatbot</span>
114
+ <div className={chatbotClassName} data-testid="message-chatbot-indicator">
115
+ {isMyMessage && !isSenderAttachmentVariant ? (
116
+ <>
117
+ {label}
118
+ {icon}
119
+ </>
120
+ ) : (
121
+ <>
122
+ {icon}
123
+ {label}
124
+ </>
125
+ )}
82
126
  </div>
83
127
  )
84
128
  }
@@ -117,11 +117,12 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
117
117
  handleClick = () => setIsBounceDialogOpen(true)
118
118
  }
119
119
 
120
+ const isMine = isMyMessage()
120
121
  const rootClassName = classNames(
121
122
  'str-chat__message str-chat__message-simple',
122
123
  `str-chat__message--${message.type}`,
123
124
  `str-chat__message--${message.status}`,
124
- isMyMessage()
125
+ isMine
125
126
  ? 'str-chat__message--me str-chat__message-simple--me'
126
127
  : 'str-chat__message--other',
127
128
  message.text ? 'str-chat__message--has-text' : 'has-no-text',
@@ -142,6 +143,12 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
142
143
 
143
144
  const poll = message.poll_id && client.polls.fromState(message.poll_id)
144
145
  const isTipOnly = isTipOnlyMessage(message)
146
+ const isChatbot = isChatbotMessage(message)
147
+ const hasRenderableAttachments = !!(
148
+ finalAttachments?.length && !message.quoted_message
149
+ )
150
+ const useAttachmentFooterChatbotTag =
151
+ isChatbot && isMine && hasRenderableAttachments
145
152
 
146
153
  return (
147
154
  <>
@@ -191,6 +198,13 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
191
198
  ) : (
192
199
  <div className="str-chat__message-bubble-wrapper">
193
200
  <div className="str-chat__message-bubble">
201
+ {isChatbot && !useAttachmentFooterChatbotTag && (
202
+ <MessageTag
203
+ message={message}
204
+ hasAttachment={hasRenderableAttachments}
205
+ isMyMessage={isMine}
206
+ />
207
+ )}
194
208
  {poll && <Poll poll={poll} />}
195
209
  {finalAttachments?.length && !message.quoted_message ? (
196
210
  <Attachment
@@ -208,9 +222,15 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
208
222
  )}
209
223
  <MessageErrorIcon />
210
224
  </div>
211
- {/* Tag positioned outside and below the bubble */}
212
- <MessageTag message={message} />
213
- {chatbotVotingEnabled && isChatbotMessage(message) && (
225
+ {/* Tip/paid tags stay outside; chatbot attachment indicator stays outside too */}
226
+ {(!isChatbot || useAttachmentFooterChatbotTag) && (
227
+ <MessageTag
228
+ message={message}
229
+ hasAttachment={hasRenderableAttachments}
230
+ isMyMessage={isMine}
231
+ />
232
+ )}
233
+ {chatbotVotingEnabled && isChatbot && (
214
234
  <MessageVoteButtons
215
235
  selected={voteState}
216
236
  onVoteUp={voteUp}
package/src/styles.css CHANGED
@@ -134,9 +134,51 @@
134
134
  color: #016630;
135
135
  }
136
136
 
137
- .message-tag--chatbot {
138
- background-color: transparent;
139
- color: #7f22fe;
137
+ .message-chatbot-indicator {
138
+ display: inline-flex;
139
+ align-items: center;
140
+ font-size: 10px;
141
+ font-weight: 400;
142
+ line-height: 16px;
143
+ letter-spacing: 0.2px;
144
+ }
145
+
146
+ .message-chatbot-indicator--text {
147
+ width: 100%;
148
+ gap: 6px;
149
+ margin-top: 12px;
150
+ padding-inline: 16px;
151
+ }
152
+
153
+ .message-chatbot-indicator--receiver {
154
+ justify-content: flex-start;
155
+ color: rgba(0, 0, 0, 0.3);
156
+ }
157
+
158
+ .message-chatbot-indicator--sender {
159
+ justify-content: flex-end;
160
+ }
161
+
162
+ .message-chatbot-indicator--sender.message-chatbot-indicator--text {
163
+ justify-content: flex-start;
164
+ color: rgba(255, 255, 255, 0.55);
165
+ }
166
+
167
+ .message-chatbot-indicator--attachment {
168
+ width: 100%;
169
+ gap: 8px;
170
+ margin-top: 4px;
171
+ color: rgba(0, 0, 0, 0.3);
172
+ }
173
+
174
+ .message-chatbot-indicator__icon {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ line-height: 1;
178
+ }
179
+
180
+ .message-chatbot-indicator__label {
181
+ text-align: left;
140
182
  }
141
183
 
142
184
  /* Message vote buttons (chatbot feedback) */
@@ -159,7 +201,9 @@
159
201
  background-color: transparent;
160
202
  color: rgba(0, 0, 0, 0.35);
161
203
  cursor: pointer;
162
- transition: background-color 0.15s, color 0.15s;
204
+ transition:
205
+ background-color 0.15s,
206
+ color 0.15s;
163
207
  }
164
208
 
165
209
  .message-vote-button:hover {