@linktr.ee/messaging-react 1.19.3 → 1.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.19.3",
3
+ "version": "1.20.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -527,6 +527,7 @@ const ChannelViewInner: React.FC<{
527
527
  onBlockParticipantClick?: () => void
528
528
  onReportParticipantClick?: () => void
529
529
  showStarButton?: boolean
530
+ chatbotVotingEnabled?: boolean
530
531
  }> = ({
531
532
  onBack,
532
533
  showBackButton,
@@ -538,6 +539,7 @@ const ChannelViewInner: React.FC<{
538
539
  onBlockParticipantClick,
539
540
  onReportParticipantClick,
540
541
  showStarButton = false,
542
+ chatbotVotingEnabled = false,
541
543
  }) => {
542
544
  const { channel } = useChannelStateContext()
543
545
  const infoDialogRef = useRef<HTMLDialogElement>(null)
@@ -584,7 +586,7 @@ const ChannelViewInner: React.FC<{
584
586
  <WithComponents
585
587
  overrides={{
586
588
  Message: (props: MessageUIComponentProps) => (
587
- <CustomMessage {...props} />
589
+ <CustomMessage {...props} chatbotVotingEnabled={chatbotVotingEnabled} />
588
590
  ),
589
591
  }}
590
592
  >
@@ -655,6 +657,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
655
657
  messageMetadata,
656
658
  onMessageSent,
657
659
  showStarButton = false,
660
+ chatbotVotingEnabled = false,
658
661
  }) => {
659
662
  // Custom send message handler that:
660
663
  // 1. Applies messageMetadata if provided
@@ -727,6 +730,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
727
730
  onBlockParticipantClick={onBlockParticipantClick}
728
731
  onReportParticipantClick={onReportParticipantClick}
729
732
  showStarButton={showStarButton}
733
+ chatbotVotingEnabled={chatbotVotingEnabled}
730
734
  />
731
735
  </Channel>
732
736
  </div>
@@ -0,0 +1,62 @@
1
+ import React from 'react'
2
+
3
+ import type { VoteSelection } from '../../hooks/useMessageVote'
4
+
5
+ interface MessageVoteButtonsProps {
6
+ selected: VoteSelection
7
+ onVoteUp: () => void
8
+ onVoteDown: () => void
9
+ }
10
+
11
+ const ThumbUpIcon: React.FC<{ filled: boolean }> = ({ filled }) => (
12
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
13
+ <path
14
+ d="M4.667 7.333l2.666-6A1.333 1.333 0 018.667 2v2.667a.667.667 0 00.666.666h3.764a1.334 1.334 0 011.192 1.93l-2.333 4.666a1.333 1.333 0 01-1.193.738H4.667m0-5.334v5.334m0-5.334H2.667a1.333 1.333 0 00-1.334 1.334v2.666a1.333 1.333 0 001.334 1.334h2"
15
+ stroke="currentColor"
16
+ strokeWidth="1.33"
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ fill={filled ? 'currentColor' : 'none'}
20
+ />
21
+ </svg>
22
+ )
23
+
24
+ const ThumbDownIcon: React.FC<{ filled: boolean }> = ({ filled }) => (
25
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
26
+ <path
27
+ d="M11.333 8.667l-2.666 6A1.333 1.333 0 017.333 14v-2.667a.667.667 0 00-.666-.666H2.903a1.334 1.334 0 01-1.192-1.93l2.333-4.666a1.333 1.333 0 011.193-.738h6.096m0 5.334V3.333m0 5.334h2a1.333 1.333 0 001.334-1.334V4.667a1.333 1.333 0 00-1.334-1.334h-2"
28
+ stroke="currentColor"
29
+ strokeWidth="1.33"
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ fill={filled ? 'currentColor' : 'none'}
33
+ />
34
+ </svg>
35
+ )
36
+
37
+ export const MessageVoteButtons: React.FC<MessageVoteButtonsProps> = ({
38
+ selected,
39
+ onVoteUp,
40
+ onVoteDown,
41
+ }) => (
42
+ <div className="message-vote-buttons">
43
+ <button
44
+ type="button"
45
+ className={`message-vote-button${selected === 'up' ? ' message-vote-button--selected' : ''}`}
46
+ onClick={onVoteUp}
47
+ aria-label="Helpful"
48
+ aria-pressed={selected === 'up'}
49
+ >
50
+ <ThumbUpIcon filled={selected === 'up'} />
51
+ </button>
52
+ <button
53
+ type="button"
54
+ className={`message-vote-button${selected === 'down' ? ' message-vote-button--selected' : ''}`}
55
+ onClick={onVoteDown}
56
+ aria-label="Not helpful"
57
+ aria-pressed={selected === 'down'}
58
+ >
59
+ <ThumbDownIcon filled={selected === 'down'} />
60
+ </button>
61
+ </div>
62
+ )
@@ -28,15 +28,20 @@ import {
28
28
  type MessageUIComponentProps,
29
29
  } from 'stream-chat-react'
30
30
 
31
+ import { useMessageVote } from '../../hooks/useMessageVote'
31
32
  import { Avatar } from '../Avatar'
32
33
 
33
- import { MessageTag, isTipOnlyMessage } from './MessageTag'
34
+ import { MessageTag, isChatbotMessage, isTipOnlyMessage } from './MessageTag'
35
+ import { MessageVoteButtons } from './MessageVoteButtons'
34
36
 
35
- type CustomMessageWithContextProps = MessageContextValue
37
+ type CustomMessageWithContextProps = MessageContextValue & {
38
+ chatbotVotingEnabled?: boolean
39
+ }
36
40
 
37
41
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
38
42
  const {
39
43
  additionalMessageInputProps,
44
+ chatbotVotingEnabled,
40
45
  editing,
41
46
  endOfGroup,
42
47
  firstOfGroup,
@@ -55,6 +60,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
55
60
  const { client } = useChatContext('CustomMessage')
56
61
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
57
62
  const reminder = useMessageReminder(message.id)
63
+ const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
58
64
 
59
65
  const {
60
66
  Attachment = DefaultAttachment,
@@ -204,6 +210,13 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
204
210
  </div>
205
211
  {/* Tag positioned outside and below the bubble */}
206
212
  <MessageTag message={message} />
213
+ {chatbotVotingEnabled && isChatbotMessage(message) && (
214
+ <MessageVoteButtons
215
+ selected={voteState}
216
+ onVoteUp={voteUp}
217
+ onVoteDown={voteDown}
218
+ />
219
+ )}
207
220
  </div>
208
221
  )}
209
222
  </div>
@@ -226,7 +239,9 @@ const MemoizedCustomMessage = React.memo(
226
239
  areMessageUIPropsEqual
227
240
  ) as typeof CustomMessageWithContext
228
241
 
229
- export const CustomMessage = (props: MessageUIComponentProps) => {
242
+ export const CustomMessage = (
243
+ props: MessageUIComponentProps & { chatbotVotingEnabled?: boolean }
244
+ ) => {
230
245
  const messageContext = useMessageContext('CustomMessage')
231
246
  return <MemoizedCustomMessage {...messageContext} {...props} />
232
247
  }
@@ -34,6 +34,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
34
34
  messageMetadata,
35
35
  onMessageSent,
36
36
  showStarButton = false,
37
+ chatbotVotingEnabled = false,
37
38
  renderMessagePreview,
38
39
  }) => {
39
40
  const {
@@ -491,6 +492,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
491
492
  messageMetadata={messageMetadata}
492
493
  onMessageSent={onMessageSent}
493
494
  showStarButton={showStarButton}
495
+ chatbotVotingEnabled={chatbotVotingEnabled}
494
496
  />
495
497
  </div>
496
498
  ) : initialParticipantFilter ? (
@@ -0,0 +1,77 @@
1
+ import { useCallback, useMemo } from 'react'
2
+ import type { LocalMessage } from 'stream-chat'
3
+ import { useChannelStateContext, useChatContext } from 'stream-chat-react'
4
+
5
+ export type VoteSelection = 'up' | 'down' | null
6
+
7
+ const VOTE_UP = 'vote_up'
8
+ const VOTE_DOWN = 'vote_down'
9
+
10
+ function getVoteFromReactions(
11
+ ownReactions: LocalMessage['own_reactions']
12
+ ): VoteSelection {
13
+ if (!ownReactions?.length) return null
14
+ if (ownReactions.some((r) => r.type === VOTE_DOWN)) return 'down'
15
+ if (ownReactions.some((r) => r.type === VOTE_UP)) return 'up'
16
+ return null
17
+ }
18
+
19
+ interface UseMessageVoteResult {
20
+ selected: VoteSelection
21
+ voteUp: () => void
22
+ voteDown: () => void
23
+ }
24
+
25
+ /**
26
+ * Hook that wraps Stream Chat reactions to provide toggle-style
27
+ * upvote/downvote behavior on a message.
28
+ *
29
+ * Uses enforce_unique so sending a new reaction type automatically
30
+ * removes the previous one. Uses skip_push to avoid notifying the
31
+ * Linker on every vote.
32
+ */
33
+ export function useMessageVote(message: LocalMessage): UseMessageVoteResult {
34
+ const { channel } = useChannelStateContext()
35
+ const { client } = useChatContext('useMessageVote')
36
+
37
+ const selected = useMemo(
38
+ () => getVoteFromReactions(message.own_reactions),
39
+ [message.own_reactions]
40
+ )
41
+
42
+ const voteUp = useCallback(async () => {
43
+ if (!client?.userID) return
44
+ try {
45
+ if (selected === 'up') {
46
+ await channel.deleteReaction(message.id, VOTE_UP)
47
+ } else {
48
+ await channel.sendReaction(
49
+ message.id,
50
+ { type: VOTE_UP },
51
+ { enforce_unique: true, skip_push: true }
52
+ )
53
+ }
54
+ } catch {
55
+ // Silently fail — voting is non-critical
56
+ }
57
+ }, [channel, client?.userID, message.id, selected])
58
+
59
+ const voteDown = useCallback(async () => {
60
+ if (!client?.userID) return
61
+ try {
62
+ if (selected === 'down') {
63
+ await channel.deleteReaction(message.id, VOTE_DOWN)
64
+ } else {
65
+ await channel.sendReaction(
66
+ message.id,
67
+ { type: VOTE_DOWN },
68
+ { enforce_unique: true, skip_push: true }
69
+ )
70
+ }
71
+ } catch {
72
+ // Silently fail — voting is non-critical
73
+ }
74
+ }, [channel, client?.userID, message.id, selected])
75
+
76
+ return { selected, voteUp, voteDown }
77
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { Avatar } from './components/Avatar'
10
10
  export { FaqList } from './components/FaqList'
11
11
  export { FaqListItem } from './components/FaqList/FaqListItem'
12
12
  export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
13
+ export { MessageVoteButtons } from './components/CustomMessage/MessageVoteButtons'
13
14
 
14
15
  // Providers
15
16
  export { MessagingProvider } from './providers/MessagingProvider'
@@ -17,6 +18,7 @@ export { MessagingProvider } from './providers/MessagingProvider'
17
18
  // Hooks
18
19
  export { useMessaging } from './hooks/useMessaging'
19
20
  export { useParticipants } from './hooks/useParticipants'
21
+ export { useMessageVote } from './hooks/useMessageVote'
20
22
 
21
23
  // Utils
22
24
  export { formatRelativeTime } from './utils/formatRelativeTime'
@@ -36,3 +38,4 @@ export type { MessageMetadata } from './stream-custom-data'
36
38
  export type { AvatarProps } from './components/Avatar'
37
39
  export type { Faq, FaqListProps } from './components/FaqList'
38
40
  export type { FaqListItemProps } from './components/FaqList/FaqListItem'
41
+ export type { VoteSelection } from './hooks/useMessageVote'
package/src/styles.css CHANGED
@@ -139,6 +139,42 @@
139
139
  color: #7f22fe;
140
140
  }
141
141
 
142
+ /* Message vote buttons (chatbot feedback) */
143
+ .message-vote-buttons {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ gap: 2px;
147
+ margin-top: 4px;
148
+ }
149
+
150
+ .message-vote-button {
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ width: 28px;
155
+ height: 28px;
156
+ padding: 0;
157
+ border: none;
158
+ border-radius: 50%;
159
+ background-color: transparent;
160
+ color: rgba(0, 0, 0, 0.35);
161
+ cursor: pointer;
162
+ transition: background-color 0.15s, color 0.15s;
163
+ }
164
+
165
+ .message-vote-button:hover {
166
+ background-color: rgba(0, 0, 0, 0.06);
167
+ color: rgba(0, 0, 0, 0.55);
168
+ }
169
+
170
+ .message-vote-button--selected {
171
+ color: rgba(0, 0, 0, 0.7);
172
+ }
173
+
174
+ .message-vote-button--selected:hover {
175
+ color: rgba(0, 0, 0, 0.7);
176
+ }
177
+
142
178
  /* Standalone tip message (tip without text) */
143
179
  .message-tip-standalone {
144
180
  display: inline-flex;
package/src/types.ts CHANGED
@@ -139,6 +139,14 @@ export interface ChannelViewProps {
139
139
  * and filter by starred/pinned status.
140
140
  */
141
141
  showStarButton?: boolean
142
+
143
+ /**
144
+ * Enable thumbs up/down voting on chatbot messages.
145
+ * When true, vote buttons render below chatbot (DM Agent) messages.
146
+ * Votes are persisted as Stream Chat reactions (vote_up / vote_down).
147
+ * Defaults to false.
148
+ */
149
+ chatbotVotingEnabled?: boolean
142
150
  }
143
151
 
144
152
  /**
@@ -156,6 +164,7 @@ export type ChannelViewPassthroughProps = Pick<
156
164
  | 'messageMetadata'
157
165
  | 'onMessageSent'
158
166
  | 'showStarButton'
167
+ | 'chatbotVotingEnabled'
159
168
  >
160
169
 
161
170
  /**