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