@linktr.ee/messaging-react 1.13.0 → 1.14.0

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.
@@ -0,0 +1,271 @@
1
+ import classNames from 'classnames'
2
+ import React, { useMemo, useState } from 'react'
3
+ import {
4
+ Attachment as DefaultAttachment,
5
+ Avatar as DefaultAvatar,
6
+ EditMessageModal as DefaultEditMessageModal,
7
+ MessageBounceModal,
8
+ MessageBouncePrompt as DefaultMessageBouncePrompt,
9
+ MessageBlocked as DefaultMessageBlocked,
10
+ MessageDeleted as DefaultMessageDeleted,
11
+ MessageEditedTimestamp,
12
+ MessageErrorIcon,
13
+ MessageIsThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator,
14
+ MessageOptions as DefaultMessageOptions,
15
+ MessageRepliesCountButton as DefaultMessageRepliesCountButton,
16
+ MessageStatus as DefaultMessageStatus,
17
+ MessageText,
18
+ MessageTimestamp as DefaultMessageTimestamp,
19
+ Poll,
20
+ ReactionsList as DefaultReactionList,
21
+ ReminderNotification as DefaultReminderNotification,
22
+ StreamedMessageText as DefaultStreamedMessageText,
23
+ areMessageUIPropsEqual,
24
+ isDateSeparatorMessage,
25
+ isMessageBlocked,
26
+ isMessageBounced,
27
+ isMessageEdited,
28
+ messageHasAttachments,
29
+ messageHasReactions,
30
+ useComponentContext,
31
+ useChatContext,
32
+ useMessageContext,
33
+ useMessageReminder,
34
+ useTranslationContext,
35
+ type MessageContextValue,
36
+ type MessageUIComponentProps,
37
+ } from 'stream-chat-react'
38
+
39
+ import { MessageTag, isTipOnlyMessage } from './MessageTag'
40
+
41
+ type CustomMessageWithContextProps = MessageContextValue
42
+
43
+ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
44
+ const {
45
+ additionalMessageInputProps,
46
+ editing,
47
+ endOfGroup,
48
+ firstOfGroup,
49
+ groupedByUser,
50
+ handleAction,
51
+ handleOpenThread,
52
+ handleRetry,
53
+ highlighted,
54
+ isMessageAIGenerated,
55
+ isMyMessage,
56
+ message,
57
+ onUserClick,
58
+ onUserHover,
59
+ renderText,
60
+ threadList,
61
+ } = props
62
+
63
+ const { client } = useChatContext('CustomMessage')
64
+ const { t } = useTranslationContext('CustomMessage')
65
+ const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
66
+ const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false)
67
+ const reminder = useMessageReminder(message.id)
68
+
69
+ const {
70
+ Attachment = DefaultAttachment,
71
+ Avatar = DefaultAvatar,
72
+ EditMessageModal = DefaultEditMessageModal,
73
+ MessageOptions = DefaultMessageOptions,
74
+ MessageActions = MessageOptions,
75
+ MessageBlocked = DefaultMessageBlocked,
76
+ MessageBouncePrompt = DefaultMessageBouncePrompt,
77
+ MessageDeleted = DefaultMessageDeleted,
78
+ MessageIsThreadReplyInChannelButtonIndicator = DefaultMessageIsThreadReplyInChannelButtonIndicator,
79
+ MessageRepliesCountButton = DefaultMessageRepliesCountButton,
80
+ MessageStatus = DefaultMessageStatus,
81
+ MessageTimestamp = DefaultMessageTimestamp,
82
+ ReactionsList = DefaultReactionList,
83
+ ReminderNotification = DefaultReminderNotification,
84
+ StreamedMessageText = DefaultStreamedMessageText,
85
+ PinIndicator,
86
+ } = useComponentContext('CustomMessage')
87
+
88
+ const hasAttachment = messageHasAttachments(message)
89
+ const hasReactions = messageHasReactions(message)
90
+ const isAIGenerated = useMemo(
91
+ () => isMessageAIGenerated?.(message),
92
+ [isMessageAIGenerated, message]
93
+ )
94
+ const finalAttachments = useMemo(
95
+ () =>
96
+ !message.shared_location && !message.attachments
97
+ ? []
98
+ : !message.shared_location
99
+ ? message.attachments
100
+ : [message.shared_location, ...(message.attachments ?? [])],
101
+ [message]
102
+ )
103
+
104
+ if (isDateSeparatorMessage(message)) {
105
+ return null
106
+ }
107
+
108
+ if (message.deleted_at || message.type === 'deleted') {
109
+ return <MessageDeleted message={message} />
110
+ }
111
+
112
+ if (isMessageBlocked(message)) {
113
+ return <MessageBlocked />
114
+ }
115
+
116
+ const showMetadata = !groupedByUser || endOfGroup
117
+ const showReplyCountButton = !threadList && !!message.reply_count
118
+ const showIsReplyInChannel =
119
+ !threadList && message.show_in_channel && message.parent_id
120
+ const allowRetry =
121
+ message.status === 'failed' && message.error?.status !== 403
122
+ const isBounced = isMessageBounced(message)
123
+ const isEdited = isMessageEdited(message) && !isAIGenerated
124
+
125
+ let handleClick: (() => void) | undefined = undefined
126
+
127
+ if (allowRetry) {
128
+ handleClick = () => handleRetry(message)
129
+ } else if (isBounced) {
130
+ handleClick = () => setIsBounceDialogOpen(true)
131
+ } else if (isEdited) {
132
+ handleClick = () => setEditedTimestampOpen((prev) => !prev)
133
+ }
134
+
135
+ const rootClassName = classNames(
136
+ 'str-chat__message str-chat__message-simple',
137
+ `str-chat__message--${message.type}`,
138
+ `str-chat__message--${message.status}`,
139
+ isMyMessage()
140
+ ? 'str-chat__message--me str-chat__message-simple--me'
141
+ : 'str-chat__message--other',
142
+ message.text ? 'str-chat__message--has-text' : 'has-no-text',
143
+ {
144
+ 'str-chat__message--has-attachment': hasAttachment,
145
+ 'str-chat__message--highlighted': highlighted,
146
+ 'str-chat__message--pinned pinned-message': message.pinned,
147
+ 'str-chat__message--with-reactions': hasReactions,
148
+ 'str-chat__message-send-can-be-retried':
149
+ message?.status === 'failed' && message?.error?.status !== 403,
150
+ 'str-chat__message-with-thread-link':
151
+ showReplyCountButton || showIsReplyInChannel,
152
+ 'str-chat__virtual-message__wrapper--end': endOfGroup,
153
+ 'str-chat__virtual-message__wrapper--first': firstOfGroup,
154
+ 'str-chat__virtual-message__wrapper--group': groupedByUser,
155
+ }
156
+ )
157
+
158
+ const poll = message.poll_id && client.polls.fromState(message.poll_id)
159
+ const isTipOnly = isTipOnlyMessage(message)
160
+
161
+ return (
162
+ <>
163
+ {editing && (
164
+ <EditMessageModal
165
+ additionalMessageInputProps={additionalMessageInputProps}
166
+ />
167
+ )}
168
+ {isBounceDialogOpen && (
169
+ <MessageBounceModal
170
+ MessageBouncePrompt={MessageBouncePrompt}
171
+ onClose={() => setIsBounceDialogOpen(false)}
172
+ open={isBounceDialogOpen}
173
+ />
174
+ )}
175
+ <div className={rootClassName} key={message.id}>
176
+ {PinIndicator && <PinIndicator />}
177
+ {!!reminder && <ReminderNotification reminder={reminder} />}
178
+ {message.user && (
179
+ <Avatar
180
+ image={message.user.image}
181
+ name={message.user.name || message.user.id}
182
+ onClick={onUserClick}
183
+ onMouseOver={onUserHover}
184
+ user={message.user}
185
+ />
186
+ )}
187
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
188
+ <div
189
+ className={classNames('str-chat__message-inner', {
190
+ 'str-chat__simple-message--error-failed': allowRetry || isBounced,
191
+ })}
192
+ data-testid="message-inner"
193
+ onClick={handleClick}
194
+ onKeyDown={handleClick}
195
+ role={handleClick ? 'button' : undefined}
196
+ tabIndex={handleClick ? 0 : undefined}
197
+ >
198
+ <MessageActions />
199
+ <div className="str-chat__message-reactions-host">
200
+ {hasReactions && <ReactionsList reverse />}
201
+ </div>
202
+ {isTipOnly ? (
203
+ /* Tip-only messages render as a standalone bubble */
204
+ <MessageTag message={message} standalone />
205
+ ) : (
206
+ <div className="str-chat__message-bubble-wrapper">
207
+ <div className="str-chat__message-bubble">
208
+ {poll && <Poll poll={poll} />}
209
+ {finalAttachments?.length && !message.quoted_message ? (
210
+ <Attachment
211
+ actionHandler={handleAction}
212
+ attachments={finalAttachments}
213
+ />
214
+ ) : null}
215
+ {isAIGenerated ? (
216
+ <StreamedMessageText
217
+ message={message}
218
+ renderText={renderText}
219
+ />
220
+ ) : (
221
+ <MessageText message={message} renderText={renderText} />
222
+ )}
223
+ <MessageErrorIcon />
224
+ </div>
225
+ {/* Tag positioned outside and below the bubble */}
226
+ <MessageTag message={message} />
227
+ </div>
228
+ )}
229
+ </div>
230
+ {showReplyCountButton && (
231
+ <MessageRepliesCountButton
232
+ onClick={handleOpenThread}
233
+ reply_count={message.reply_count}
234
+ />
235
+ )}
236
+ {showIsReplyInChannel && (
237
+ <MessageIsThreadReplyInChannelButtonIndicator />
238
+ )}
239
+ {showMetadata && (
240
+ <div className="str-chat__message-metadata">
241
+ <MessageStatus />
242
+ {!isMyMessage() && !!message.user && (
243
+ <span className="str-chat__message-simple-name">
244
+ {message.user.name || message.user.id}
245
+ </span>
246
+ )}
247
+ <MessageTimestamp customClass="str-chat__message-simple-timestamp" />
248
+ {isEdited && (
249
+ <span className="str-chat__mesage-simple-edited">
250
+ {t('Edited')}
251
+ </span>
252
+ )}
253
+ {isEdited && (
254
+ <MessageEditedTimestamp calendar open={isEditedTimestampOpen} />
255
+ )}
256
+ </div>
257
+ )}
258
+ </div>
259
+ </>
260
+ )
261
+ }
262
+
263
+ const MemoizedCustomMessage = React.memo(
264
+ CustomMessageWithContext,
265
+ areMessageUIPropsEqual
266
+ ) as typeof CustomMessageWithContext
267
+
268
+ export const CustomMessage = (props: MessageUIComponentProps) => {
269
+ const messageContext = useMessageContext('CustomMessage')
270
+ return <MemoizedCustomMessage {...messageContext} {...props} />
271
+ }
@@ -14,3 +14,4 @@ export const CustomSystemMessage: React.FC<EventComponentProps> = (props) => {
14
14
  </div>
15
15
  )
16
16
  }
17
+
@@ -21,6 +21,17 @@ declare module 'stream-chat' {
21
21
  * Used by CustomSystemMessage component.
22
22
  */
23
23
  hide_date?: boolean
24
+ /**
25
+ * Message metadata from backend.
26
+ * Contains type and payment information.
27
+ */
28
+ metadata?: {
29
+ custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
30
+ amount_text?: string
31
+ payment_status?: string
32
+ payment_intent_id?: string
33
+ listing_id?: string
34
+ }
24
35
  }
25
36
  }
26
37
 
package/src/styles.css CHANGED
@@ -102,3 +102,48 @@
102
102
  .str-chat__list {
103
103
  background: transparent;
104
104
  }
105
+
106
+ /* Wrapper to ensure tag stays below bubble */
107
+ .str-chat__message-bubble-wrapper {
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+
112
+ /* Custom message tag styles */
113
+ .message-tag {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ gap: 4px;
117
+ margin-top: 4px;
118
+ padding: 0;
119
+ font-size: 12px;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .message-tag__icon {
124
+ display: flex;
125
+ align-items: center;
126
+ }
127
+
128
+ .message-tag--tip {
129
+ background-color: transparent;
130
+ color: #016630;
131
+ }
132
+
133
+ .message-tag--chatbot {
134
+ background-color: transparent;
135
+ color: #7f22fe;
136
+ }
137
+
138
+ /* Standalone tip message (tip without text) */
139
+ .message-tip-standalone {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 6px;
143
+ padding: 10px 14px;
144
+ border-radius: 18px;
145
+ font-size: 14px;
146
+ font-weight: 500;
147
+ color: #016e1a;
148
+ background-color: #dbf0e0;
149
+ }