@linktr.ee/messaging-react 1.12.7 → 1.12.8-rc-1765974383

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,260 @@
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 } 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
+
160
+ return (
161
+ <>
162
+ {editing && (
163
+ <EditMessageModal
164
+ additionalMessageInputProps={additionalMessageInputProps}
165
+ />
166
+ )}
167
+ {isBounceDialogOpen && (
168
+ <MessageBounceModal
169
+ MessageBouncePrompt={MessageBouncePrompt}
170
+ onClose={() => setIsBounceDialogOpen(false)}
171
+ open={isBounceDialogOpen}
172
+ />
173
+ )}
174
+ <div className={rootClassName} key={message.id}>
175
+ {PinIndicator && <PinIndicator />}
176
+ {!!reminder && <ReminderNotification reminder={reminder} />}
177
+ {message.user && (
178
+ <Avatar
179
+ image={message.user.image}
180
+ name={message.user.name || message.user.id}
181
+ onClick={onUserClick}
182
+ onMouseOver={onUserHover}
183
+ user={message.user}
184
+ />
185
+ )}
186
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
187
+ <div
188
+ className={classNames('str-chat__message-inner', {
189
+ 'str-chat__simple-message--error-failed': allowRetry || isBounced,
190
+ })}
191
+ data-testid="message-inner"
192
+ onClick={handleClick}
193
+ onKeyDown={handleClick}
194
+ role={handleClick ? 'button' : undefined}
195
+ tabIndex={handleClick ? 0 : undefined}
196
+ >
197
+ <MessageActions />
198
+ <div className="str-chat__message-reactions-host">
199
+ {hasReactions && <ReactionsList reverse />}
200
+ </div>
201
+ <div className="str-chat__message-bubble">
202
+ {poll && <Poll poll={poll} />}
203
+ {finalAttachments?.length && !message.quoted_message ? (
204
+ <Attachment
205
+ actionHandler={handleAction}
206
+ attachments={finalAttachments}
207
+ />
208
+ ) : null}
209
+ {/* Custom tag component - positioned above message text */}
210
+ <MessageTag message={message} />
211
+ {isAIGenerated ? (
212
+ <StreamedMessageText message={message} renderText={renderText} />
213
+ ) : (
214
+ <MessageText message={message} renderText={renderText} />
215
+ )}
216
+ <MessageErrorIcon />
217
+ </div>
218
+ </div>
219
+ {showReplyCountButton && (
220
+ <MessageRepliesCountButton
221
+ onClick={handleOpenThread}
222
+ reply_count={message.reply_count}
223
+ />
224
+ )}
225
+ {showIsReplyInChannel && (
226
+ <MessageIsThreadReplyInChannelButtonIndicator />
227
+ )}
228
+ {showMetadata && (
229
+ <div className="str-chat__message-metadata">
230
+ <MessageStatus />
231
+ {!isMyMessage() && !!message.user && (
232
+ <span className="str-chat__message-simple-name">
233
+ {message.user.name || message.user.id}
234
+ </span>
235
+ )}
236
+ <MessageTimestamp customClass="str-chat__message-simple-timestamp" />
237
+ {isEdited && (
238
+ <span className="str-chat__mesage-simple-edited">
239
+ {t('Edited')}
240
+ </span>
241
+ )}
242
+ {isEdited && (
243
+ <MessageEditedTimestamp calendar open={isEditedTimestampOpen} />
244
+ )}
245
+ </div>
246
+ )}
247
+ </div>
248
+ </>
249
+ )
250
+ }
251
+
252
+ const MemoizedCustomMessage = React.memo(
253
+ CustomMessageWithContext,
254
+ areMessageUIPropsEqual
255
+ ) as typeof CustomMessageWithContext
256
+
257
+ export const CustomMessage = (props: MessageUIComponentProps) => {
258
+ const messageContext = useMessageContext('CustomMessage')
259
+ return <MemoizedCustomMessage {...messageContext} {...props} />
260
+ }
@@ -1,10 +1,19 @@
1
+ import { StarFourIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
1
3
  import { MessageTimestamp, type EventComponentProps } from 'stream-chat-react'
2
4
 
3
5
  export const CustomSystemMessage: React.FC<EventComponentProps> = (props) => {
4
6
  const isDateHidden = props.message.hide_date === true
7
+ const isPriority = props.message.system_type === 'priority'
5
8
 
6
9
  return (
7
- <div className="str-chat__message--system" data-testid="message-system">
10
+ <div
11
+ className={classNames('str-chat__message--system', {
12
+ 'str-chat__message--system--priority': isPriority,
13
+ })}
14
+ data-testid="message-system"
15
+ >
16
+ {isPriority && <StarFourIcon size={18} className="mx-auto mb-2 mt-3" />}
8
17
  <div className="str-chat__message--system__text">
9
18
  <div className="str-chat__message--system__line"></div>
10
19
  <p>{props.message.text}</p>
@@ -21,6 +21,26 @@ declare module 'stream-chat' {
21
21
  * Used by CustomSystemMessage component.
22
22
  */
23
23
  hide_date?: boolean
24
+ /**
25
+ * Type of system message for special styling.
26
+ * Used by CustomSystemMessage component.
27
+ */
28
+ system_type?: 'priority'
29
+ /**
30
+ * Type of tag to display on the message.
31
+ * Used by MessageTag component.
32
+ */
33
+ tag_type?: 'paid-priority' | 'tip'
34
+ /**
35
+ * Monetary amount for tip messages.
36
+ * Used by MessageTag component.
37
+ */
38
+ amount?: number
39
+ /**
40
+ * Currency code (e.g. 'USD', 'EUR') for tip messages.
41
+ * Used by MessageTag component.
42
+ */
43
+ currency?: string
24
44
  }
25
45
  }
26
46
 
package/src/styles.css CHANGED
@@ -102,3 +102,37 @@
102
102
  .str-chat__list {
103
103
  background: transparent;
104
104
  }
105
+
106
+ /* Custom message tag styles */
107
+ .message-tag {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ gap: 4px;
111
+ margin: var(--str-chat__spacing-4);
112
+ margin-bottom: 0;
113
+ padding: 7px 8px;
114
+ border-radius: 20px;
115
+ font-size: 12px;
116
+ font-weight: 500;
117
+ color: white;
118
+ }
119
+
120
+ .message-tag__icon {
121
+ display: flex;
122
+ align-items: center;
123
+ }
124
+
125
+ .message-tag--tip {
126
+ background-color: #008236;
127
+ }
128
+
129
+ .message-tag--paid-priority {
130
+ background-color: #7f22fe;
131
+ }
132
+
133
+ .str-chat__message--system--priority {
134
+ margin-top: var(--str-chat__spacing-3);
135
+ margin-bottom: var(--str-chat__spacing-2);
136
+ font-size: 12px;
137
+ color: var(--text-secondary, #0000008c);
138
+ }