@linktr.ee/messaging-react 4.1.5-rc-1786947787 → 4.1.5

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/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-D0h8u2Iq.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.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-9elRVMIF.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.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -208,7 +208,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
208
208
  * Props that MessagingShell passes through to ChannelView.
209
209
  * ChannelViewProps is the source of truth for these props.
210
210
  */
211
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onOutboundClick' | 'resolveOutbound' | 'showChannelInfo' | 'composerInput' | 'attachmentPreviewList'>;
211
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onOutboundClick' | 'resolveOutbound' | 'showChannelInfo' | 'composerInput'>;
212
212
 
213
213
  /**
214
214
  * ChannelView component props
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-CBGh9Qyj.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-qKUd4Fz2.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": "4.1.5-rc-1786947787",
3
+ "version": "4.1.5",
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
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "2.4.0-rc-1786947787",
53
+ "@linktr.ee/messaging-core": "^2.4.0",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.10.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
@@ -60,6 +60,8 @@ vi.mock('./MessageTag', () => ({
60
60
  isMediaAttachmentMessage: () => false,
61
61
  isTextAttachmentMessage: () => false,
62
62
  isTipOnlyMessage: () => false,
63
+ isTipMessage: () => false,
64
+ hasPaidDeliveryTag: () => false,
63
65
  }))
64
66
 
65
67
  vi.mock('./LockedAttachment', () => ({
@@ -29,7 +29,7 @@ const SparkleIcon = ({ size = 15 }: { size?: number }) => (
29
29
  )
30
30
 
31
31
  /** Check if a message is a tip based on metadata */
32
- const isTipMessage = (message: LocalMessage): boolean => {
32
+ export const isTipMessage = (message: LocalMessage): boolean => {
33
33
  return message.metadata?.custom_type === MESSAGE_CUSTOM_TYPE.TIP
34
34
  }
35
35
 
@@ -67,6 +67,15 @@ const isTipOrPaidMessage = (message: LocalMessage): boolean => {
67
67
  return isTipMessage(message) || isPaidMessage(message)
68
68
  }
69
69
 
70
+ /** Check if the footer tag on this message already reads "Delivered with …" */
71
+ export const hasPaidDeliveryTag = (message: LocalMessage): boolean => {
72
+ return (
73
+ isTipOrPaidMessage(message) &&
74
+ !isTipOnlyMessage(message) &&
75
+ !!message.metadata?.amount_text
76
+ )
77
+ }
78
+
70
79
  /** Check if a message is a tip/paid-only message (no text) */
71
80
  export const isTipOnlyMessage = (message: LocalMessage): boolean => {
72
81
  return isTipOrPaidMessage(message) && !message.text?.trim()
@@ -391,6 +391,147 @@ describe('SentMessageDeliveryStatus', () => {
391
391
  expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
392
392
  })
393
393
 
394
+ it.each([
395
+ ['paid', 'MESSAGE_PAID'],
396
+ ['tip', 'MESSAGE_TIP'],
397
+ ])('does not render the delivery stamp on a %s message', (_label, type) => {
398
+ mockedUseMessageContext.mockReturnValue({
399
+ message: {
400
+ id: 'own-2',
401
+ text: 'thanks!',
402
+ metadata: { custom_type: type, amount_text: '$10.00' },
403
+ },
404
+ isMyMessage: () => true,
405
+ threadList: false,
406
+ } as never)
407
+
408
+ renderWithProviders(<SentMessageDeliveryStatus />)
409
+
410
+ for (const slot of TERMINAL_SLOTS) {
411
+ expect(screen.queryByTestId(slot)).not.toBeInTheDocument()
412
+ }
413
+ expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
414
+ })
415
+
416
+ it('keeps the delivery stamp on a textless paid message, which has no footer tag', () => {
417
+ mockedUseMessageContext.mockReturnValue({
418
+ message: {
419
+ id: 'own-2',
420
+ metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
421
+ },
422
+ isMyMessage: () => true,
423
+ threadList: false,
424
+ } as never)
425
+
426
+ renderWithProviders(<SentMessageDeliveryStatus />)
427
+
428
+ for (const slot of TERMINAL_SLOTS) {
429
+ expect(screen.getByTestId(slot)).toHaveTextContent(/delivered/i)
430
+ }
431
+ })
432
+
433
+ it('does not render the sending stamp on a paid message', () => {
434
+ mockedUseMessageContext.mockReturnValue({
435
+ message: {
436
+ id: 'own-2',
437
+ status: 'sending',
438
+ text: 'thanks!',
439
+ metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
440
+ },
441
+ isMyMessage: () => true,
442
+ threadList: false,
443
+ } as never)
444
+
445
+ renderWithProviders(<SentMessageDeliveryStatus />)
446
+
447
+ expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
448
+ expect(
449
+ screen.queryByTestId('sent-message-sending-status')
450
+ ).not.toBeInTheDocument()
451
+ })
452
+
453
+ it('renders "Delivered" instead of "Sending" on a pending textless tip', () => {
454
+ mockedUseMessageContext.mockReturnValue({
455
+ message: {
456
+ id: 'own-2',
457
+ status: 'sending',
458
+ metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
459
+ },
460
+ isMyMessage: () => true,
461
+ threadList: false,
462
+ } as never)
463
+
464
+ renderWithProviders(<SentMessageDeliveryStatus />)
465
+
466
+ expect(screen.getByTestId('sending-slot')).toHaveTextContent(/delivered/i)
467
+ expect(
468
+ screen.queryByTestId('sent-message-sending-status')
469
+ ).not.toBeInTheDocument()
470
+ })
471
+
472
+ it('keeps "Failed to send" on a tip that fails', () => {
473
+ mockedUseMessageContext.mockReturnValue({
474
+ message: {
475
+ id: 'own-2',
476
+ status: 'failed',
477
+ type: 'regular',
478
+ metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
479
+ },
480
+ isMyMessage: () => true,
481
+ threadList: false,
482
+ } as never)
483
+
484
+ renderWithProviders(<SentMessageDeliveryStatus />)
485
+
486
+ expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent(
487
+ /failed to send/i
488
+ )
489
+ })
490
+
491
+ it('keeps the failed and outbound stamps on a paid message', () => {
492
+ mockedUseMessageContext.mockReturnValue({
493
+ message: {
494
+ id: 'own-2',
495
+ status: 'failed',
496
+ type: 'regular',
497
+ text: 'thanks!',
498
+ metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
499
+ },
500
+ isMyMessage: () => true,
501
+ threadList: false,
502
+ } as never)
503
+
504
+ const { rerender } = renderWithProviders(<SentMessageDeliveryStatus />)
505
+
506
+ expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent(
507
+ /failed to send/i
508
+ )
509
+
510
+ mockedUseMessageContext.mockReturnValue({
511
+ message: {
512
+ id: 'own-2',
513
+ text: 'thanks!',
514
+ metadata: {
515
+ custom_type: 'MESSAGE_PAID',
516
+ amount_text: '$10.00',
517
+ outbound_id: 'outbound-1',
518
+ },
519
+ },
520
+ isMyMessage: () => true,
521
+ threadList: false,
522
+ } as never)
523
+ rerender(
524
+ <SentMessageDeliveryStatus
525
+ resolvedOutbound={{ id: 'automation-1', name: 'July 9th Class' }}
526
+ />
527
+ )
528
+
529
+ expect(
530
+ screen.getByTestId('sent-message-outbound-status')
531
+ ).toHaveTextContent('July 9th Class')
532
+ expect(screen.queryByText('•')).not.toBeInTheDocument()
533
+ })
534
+
394
535
  it('does not render failure status for a received message or thread message', () => {
395
536
  mockedUseMessageContext.mockReturnValue({
396
537
  message: { id: 'own-1', status: 'failed', type: 'regular' },
@@ -13,6 +13,8 @@ import {
13
13
 
14
14
  import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
15
15
 
16
+ import { hasPaidDeliveryTag, isTipMessage } from './MessageTag'
17
+
16
18
  /**
17
19
  * Message status stamps for sending and terminal `MessageStatus` states
18
20
  * (sent / delivered / read), plus failed messages.
@@ -159,6 +161,11 @@ const FailedStatus = () => (
159
161
  * latest-message-in-thread gate; failed messages bypass `MessageStatus`
160
162
  * because it has no failed slot and are shown on every own, non-thread
161
163
  * failed message.
164
+ *
165
+ * Messages whose footer tag already reads "Delivered with $10 message" skip
166
+ * this label so delivery is only claimed once (MES-1480); failed and broadcast
167
+ * stamps still apply.
168
+ * Tips read as delivered while still pending; a failed tip still shows Failed.
162
169
  */
163
170
  export const SentMessageDeliveryStatus = ({
164
171
  onOutboundClick,
@@ -199,12 +206,14 @@ export const SentMessageDeliveryStatus = ({
199
206
  />
200
207
  ) : null
201
208
 
202
- const messageStatus = (
209
+ const messageStatus = hasPaidDeliveryTag(message) ? null : (
203
210
  <MessageStatus
204
211
  MessageSentStatus={DeliveredStatus}
205
212
  MessageDeliveredStatus={DeliveredStatus}
206
213
  MessageReadStatus={DeliveredStatus}
207
- MessageSendingStatus={SendingStatus}
214
+ MessageSendingStatus={
215
+ isTipMessage(message) ? DeliveredStatus : SendingStatus
216
+ }
208
217
  />
209
218
  )
210
219
 
@@ -215,7 +224,7 @@ export const SentMessageDeliveryStatus = ({
215
224
  return (
216
225
  <span className="sent-message-status-row">
217
226
  {messageStatus}
218
- {isLatestRealMessage && (
227
+ {messageStatus && isLatestRealMessage && (
219
228
  <span className="sent-message-outbound-separator" aria-hidden="true">
220
229
  •
221
230
  </span>
@@ -125,26 +125,6 @@ describe('MessagingShell', () => {
125
125
  })
126
126
  })
127
127
 
128
- it('forwards attachmentPreviewList to the selected channel view', async () => {
129
- queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
130
- const AttachmentPreviewList = () => <div>staged attachment</div>
131
-
132
- renderWithProviders(
133
- <MessagingShell
134
- initialParticipantFilter="other-1"
135
- attachmentPreviewList={AttachmentPreviewList}
136
- />
137
- )
138
-
139
- await waitFor(() => {
140
- expect(screen.getByTestId('channel-view')).toBeInTheDocument()
141
- })
142
-
143
- expect(capturedChannelViewProps.attachmentPreviewList).toBe(
144
- AttachmentPreviewList
145
- )
146
- })
147
-
148
128
  it('calls onExitConversation and renders "Conversation ended" when the user leaves the conversation', async () => {
149
129
  queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
150
130
  const onExit = vi.fn()
@@ -43,7 +43,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
43
43
  onMessageLinkClick,
44
44
  showChannelInfo,
45
45
  composerInput,
46
- attachmentPreviewList,
47
46
  }) => {
48
47
  const {
49
48
  client,
@@ -270,7 +269,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
270
269
  onMessageLinkClick={onMessageLinkClick}
271
270
  showChannelInfo={showChannelInfo}
272
271
  composerInput={composerInput}
273
- attachmentPreviewList={attachmentPreviewList}
274
272
  />
275
273
  </div>
276
274
  </div>
package/src/types.ts CHANGED
@@ -453,7 +453,6 @@ export type ChannelViewPassthroughProps = Pick<
453
453
  | 'resolveOutbound'
454
454
  | 'showChannelInfo'
455
455
  | 'composerInput'
456
- | 'attachmentPreviewList'
457
456
  >
458
457
 
459
458
  /**