@linktr.ee/messaging-react 1.25.0 → 1.25.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.25.0",
3
+ "version": "1.25.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -129,6 +129,37 @@ describe('CustomChannelPreview', () => {
129
129
  ).not.toBeInTheDocument()
130
130
  })
131
131
 
132
+ it('ignores age safety system messages when resolving the channel preview text', () => {
133
+ const channel = createMockChannel([
134
+ {
135
+ id: 'msg-1',
136
+ text: 'Still available to chat',
137
+ type: 'regular',
138
+ created_at: new Date('2026-01-01T00:00:00.000Z'),
139
+ user: { id: 'participant-1', name: 'Alice' },
140
+ },
141
+ {
142
+ id: 'msg-2',
143
+ type: 'system',
144
+ created_at: new Date('2026-01-01T00:01:00.000Z'),
145
+ metadata: {
146
+ custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
147
+ },
148
+ },
149
+ ])
150
+
151
+ renderWithProviders(
152
+ <CustomChannelPreview {...defaultProps} channel={channel} />
153
+ )
154
+
155
+ expect(screen.getByText('Still available to chat')).toBeInTheDocument()
156
+ expect(
157
+ screen.queryByText(
158
+ 'This user isn’t able to reply because they don’t meet our age safety guidelines.'
159
+ )
160
+ ).not.toBeInTheDocument()
161
+ })
162
+
132
163
  it('shows fallback text when all messages are system messages', () => {
133
164
  const channel = createMockChannel([
134
165
  {
@@ -47,6 +47,14 @@ DmAgentResumed.args = createStoryProps({
47
47
  },
48
48
  })
49
49
 
50
+ export const AgeSafetyBlocked: StoryFn<EventComponentProps> = Template.bind({})
51
+ AgeSafetyBlocked.args = createStoryProps({
52
+ text: ' ',
53
+ metadata: {
54
+ custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
55
+ },
56
+ })
57
+
50
58
  export const GenericFallback: StoryFn<EventComponentProps> = Template.bind({})
51
59
  GenericFallback.args = createStoryProps({
52
60
  text: 'Message activity event',
@@ -60,6 +60,51 @@ describe('CustomSystemMessage', () => {
60
60
  expect(dmAgentSystemMessage).toHaveTextContent('DM Agent resumed')
61
61
  })
62
62
 
63
+ it('renders the age safety blocked subtype with fallback copy and warning treatment', () => {
64
+ const { container } = renderWithProviders(
65
+ <CustomSystemMessage
66
+ {...createProps({
67
+ text: ' ',
68
+ metadata: {
69
+ custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
70
+ },
71
+ })}
72
+ />
73
+ )
74
+
75
+ const ageSafetySystemMessage = screen.getByTestId(
76
+ 'age-safety-system-message'
77
+ )
78
+ expect(ageSafetySystemMessage).toHaveAttribute(
79
+ 'data-age-safety-system-type',
80
+ 'SYSTEM_AGE_SAFETY_BLOCKED'
81
+ )
82
+ expect(ageSafetySystemMessage).toHaveTextContent(
83
+ 'This user isn’t able to reply because they don’t meet our age safety guidelines.'
84
+ )
85
+ const emphasisLink = screen.getByRole('link', {
86
+ name: 'age safety guidelines.',
87
+ })
88
+ expect(emphasisLink).toHaveClass('mes-age-safety-system-message__emphasis')
89
+ expect(emphasisLink).toHaveAttribute(
90
+ 'href',
91
+ 'https://linktr.ee/s/about/contact'
92
+ )
93
+ expect(emphasisLink).toHaveAttribute('target', '_blank')
94
+ expect(emphasisLink).toHaveAttribute('rel', 'noopener noreferrer')
95
+ expect(
96
+ screen.getByTestId('age-safety-system-message-icon')
97
+ ).toBeInTheDocument()
98
+ expect(
99
+ container.querySelector('.mes-age-safety-system-message__icon')
100
+ ).toBeInTheDocument()
101
+ expect(
102
+ container.querySelector(
103
+ '[data-testid="age-safety-system-message-icon"] path[opacity="0.2"]'
104
+ )
105
+ ).toBeInTheDocument()
106
+ })
107
+
63
108
  it('falls back to legacy generic rendering when subtype is absent or unknown', () => {
64
109
  const { container } = renderWithProviders(
65
110
  <CustomSystemMessage
@@ -71,11 +116,12 @@ describe('CustomSystemMessage', () => {
71
116
  />
72
117
  )
73
118
 
74
- expect(screen.queryByTestId('dm-agent-system-message')).not.toBeInTheDocument()
119
+ expect(
120
+ screen.queryByTestId('dm-agent-system-message')
121
+ ).not.toBeInTheDocument()
75
122
  expect(screen.getByText('Legacy system message')).toBeInTheDocument()
76
123
  expect(
77
124
  container.querySelector('.str-chat__message--system__line')
78
125
  ).toBeInTheDocument()
79
126
  })
80
-
81
127
  })
@@ -1,7 +1,10 @@
1
- import { SparkleIcon } from '@phosphor-icons/react'
1
+ import { ProhibitIcon, SparkleIcon } from '@phosphor-icons/react'
2
2
  import { MessageTimestamp, type EventComponentProps } from 'stream-chat-react'
3
3
 
4
- import type { DmAgentSystemType } from '../../stream-custom-data'
4
+ import type {
5
+ AgeSafetySystemType,
6
+ DmAgentSystemType,
7
+ } from '../../stream-custom-data'
5
8
 
6
9
  const DM_AGENT_SYSTEM_TYPES: readonly DmAgentSystemType[] = [
7
10
  'SYSTEM_DM_AGENT_PAUSED',
@@ -14,51 +17,154 @@ const DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<DmAgentSystemType, string> =
14
17
  SYSTEM_DM_AGENT_RESUMED: 'DM Agent has rejoined the conversation',
15
18
  }
16
19
 
20
+ const AGE_SAFETY_SYSTEM_TYPES: readonly AgeSafetySystemType[] = [
21
+ 'SYSTEM_AGE_SAFETY_BLOCKED',
22
+ ]
23
+
24
+ const AGE_SAFETY_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<
25
+ AgeSafetySystemType,
26
+ string
27
+ > = {
28
+ SYSTEM_AGE_SAFETY_BLOCKED:
29
+ 'This user isn’t able to reply because they don’t meet our age safety guidelines.',
30
+ }
31
+
32
+ const AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS = 'age safety guidelines.'
33
+ const AGE_SAFETY_SYSTEM_MESSAGE_URL = 'https://linktr.ee/s/about/contact'
34
+
17
35
  const isDmAgentSystemType = (
18
36
  value: string | undefined
19
37
  ): value is DmAgentSystemType => {
20
38
  return DM_AGENT_SYSTEM_TYPES.includes(value as DmAgentSystemType)
21
39
  }
22
40
 
23
- const getDmAgentSystemType = (
41
+ const isAgeSafetySystemType = (
42
+ value: string | undefined
43
+ ): value is AgeSafetySystemType => {
44
+ return AGE_SAFETY_SYSTEM_TYPES.includes(value as AgeSafetySystemType)
45
+ }
46
+
47
+ type CustomSystemMessageVariant =
48
+ | {
49
+ kind: 'dm-agent'
50
+ type: DmAgentSystemType
51
+ }
52
+ | {
53
+ kind: 'age-safety'
54
+ type: AgeSafetySystemType
55
+ }
56
+
57
+ const getCustomSystemMessageVariant = (
24
58
  message: EventComponentProps['message']
25
- ): DmAgentSystemType | undefined => {
59
+ ): CustomSystemMessageVariant | undefined => {
26
60
  const metadataCustomType = message.metadata?.custom_type
27
61
  if (isDmAgentSystemType(metadataCustomType)) {
28
- return metadataCustomType
62
+ return {
63
+ kind: 'dm-agent',
64
+ type: metadataCustomType,
65
+ }
66
+ }
67
+
68
+ if (isAgeSafetySystemType(metadataCustomType)) {
69
+ return {
70
+ kind: 'age-safety',
71
+ type: metadataCustomType,
72
+ }
29
73
  }
30
74
 
31
75
  const fallbackType = message.dm_agent_system_type
32
76
  if (isDmAgentSystemType(fallbackType)) {
33
- return fallbackType
77
+ return {
78
+ kind: 'dm-agent',
79
+ type: fallbackType,
80
+ }
34
81
  }
35
82
 
36
83
  return undefined
37
84
  }
38
85
 
86
+ const renderAgeSafetyMessageText = (messageText: string): React.ReactNode => {
87
+ const emphasisIndex = messageText.indexOf(AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS)
88
+ if (emphasisIndex === -1) {
89
+ return messageText
90
+ }
91
+
92
+ const emphasisEndIndex =
93
+ emphasisIndex + AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS.length
94
+
95
+ return (
96
+ <>
97
+ {messageText.slice(0, emphasisIndex)}
98
+ <a
99
+ href={AGE_SAFETY_SYSTEM_MESSAGE_URL}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ className="mes-age-safety-system-message__emphasis font-medium text-inherit underline"
103
+ >
104
+ {AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS}
105
+ </a>
106
+ {messageText.slice(emphasisEndIndex)}
107
+ </>
108
+ )
109
+ }
110
+
39
111
  export const CustomSystemMessage: React.FC<EventComponentProps> = (props) => {
40
112
  const isDateHidden = props.message.hide_date === true
41
- const dmAgentSystemType = getDmAgentSystemType(props.message)
113
+ const customSystemMessageVariant = getCustomSystemMessageVariant(
114
+ props.message
115
+ )
42
116
 
43
- if (dmAgentSystemType) {
117
+ if (customSystemMessageVariant?.kind === 'dm-agent') {
44
118
  const messageText =
45
119
  props.message.text?.trim() ||
46
- DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT[dmAgentSystemType]
120
+ DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT[customSystemMessageVariant.type]
47
121
 
48
122
  return (
49
123
  <div className="str-chat__message--system" data-testid="message-system">
50
124
  <div
51
- className="mes-dm-agent-system-message"
125
+ className="mes-dm-agent-system-message mx-auto mb-2 inline-flex w-fit max-w-[min(100%,480px)] items-center justify-center gap-[10px] rounded-[12px] border border-[rgba(0,0,0,0.08)] p-3 text-[rgba(0,0,0,0.55)]"
52
126
  data-testid="dm-agent-system-message"
53
- data-dm-agent-system-type={dmAgentSystemType}
127
+ data-dm-agent-system-type={customSystemMessageVariant.type}
54
128
  >
55
129
  <SparkleIcon
56
130
  size={16}
57
131
  weight="regular"
58
132
  aria-hidden
59
- className="mes-dm-agent-system-message__sparkle"
133
+ className="mes-dm-agent-system-message__sparkle shrink-0"
134
+ />
135
+ <p className="mes-dm-agent-system-message__text m-0 text-center text-[14px] font-normal leading-5 tracking-[0.21px]">
136
+ {messageText}
137
+ </p>
138
+ </div>
139
+ {!isDateHidden && <MessageTimestamp message={props.message} />}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ if (customSystemMessageVariant?.kind === 'age-safety') {
145
+ const messageText =
146
+ props.message.text?.trim() ||
147
+ AGE_SAFETY_SYSTEM_MESSAGE_FALLBACK_TEXT[customSystemMessageVariant.type]
148
+
149
+ return (
150
+ <div className="str-chat__message--system" data-testid="message-system">
151
+ <div
152
+ className="mes-age-safety-system-message box-border mx-auto mb-2 flex w-full max-w-[329px] items-start justify-center gap-3 rounded-[12px] border border-[var(--border-secondary,rgba(0,0,0,0.08))] bg-[var(--bg-warning-subtle,#fef3c6)] px-2 py-4 pl-5 text-[color:var(--text-warning-on-warning,#894b00)]"
153
+ data-testid="age-safety-system-message"
154
+ data-age-safety-system-type={customSystemMessageVariant.type}
155
+ >
156
+ <ProhibitIcon
157
+ size={24}
158
+ weight="duotone"
159
+ aria-hidden
160
+ className="mes-age-safety-system-message__icon shrink-0 text-[color:var(--text-warning-on-warning,#894b00)]"
161
+ data-testid="age-safety-system-message-icon"
60
162
  />
61
- <p className="mes-dm-agent-system-message__text">{messageText}</p>
163
+ <div className="mes-age-safety-system-message__content min-w-0 flex-[1_0_0]">
164
+ <p className="m-0 text-balance text-left text-[12px] font-normal leading-4 tracking-[0.21px] text-[color:var(--text-warning-on-warning,#894b00)]">
165
+ {renderAgeSafetyMessageText(messageText)}
166
+ </p>
167
+ </div>
62
168
  </div>
63
169
  {!isDateHidden && <MessageTimestamp message={props.message} />}
64
170
  </div>
@@ -18,10 +18,13 @@ export type DmAgentSystemType =
18
18
  | 'SYSTEM_DM_AGENT_PAUSED'
19
19
  | 'SYSTEM_DM_AGENT_RESUMED'
20
20
 
21
+ export type AgeSafetySystemType = 'SYSTEM_AGE_SAFETY_BLOCKED'
22
+
21
23
  export type MessageCustomType =
22
24
  | 'MESSAGE_TIP'
23
25
  | 'MESSAGE_PAID'
24
26
  | 'MESSAGE_CHATBOT'
27
+ | AgeSafetySystemType
25
28
  | DmAgentSystemType
26
29
 
27
30
  /**
package/src/styles.css CHANGED
@@ -231,33 +231,3 @@
231
231
  color: #016e1a;
232
232
  background-color: #dbf0e0;
233
233
  }
234
-
235
- /* DM Agent pause/resume system message variants */
236
- .mes-dm-agent-system-message {
237
- display: inline-flex;
238
- align-items: center;
239
- justify-content: center;
240
- gap: 10px;
241
- margin-inline: auto;
242
- margin-bottom: 8px;
243
- width: fit-content;
244
- max-width: min(100%, 480px);
245
- padding: 12px;
246
- border: 1px solid rgba(0, 0, 0, 0.08);
247
- border-radius: 12px;
248
- color: rgba(0, 0, 0, 0.55);
249
- }
250
-
251
- .mes-dm-agent-system-message__sparkle {
252
- flex-shrink: 0;
253
- }
254
-
255
- .mes-dm-agent-system-message__text {
256
- margin: 0;
257
- font-size: 14px;
258
- font-weight: 400;
259
- line-height: 20px;
260
- letter-spacing: 0.21px;
261
- color: rgba(0, 0, 0, 0.55);
262
- text-align: center;
263
- }