@linktr.ee/messaging-react 4.2.0-rc-1787083791 → 4.2.0-rc-1787181113
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/dist/{Card-B4lMv-0o.js → Card-BOudQBd6.js} +2 -2
- package/dist/{Card-B4lMv-0o.js.map → Card-BOudQBd6.js.map} +1 -1
- package/dist/{Card-DWL5I62S.cjs → Card-k2959zNX.cjs} +2 -2
- package/dist/{Card-DWL5I62S.cjs.map → Card-k2959zNX.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-C93LTGLc.js → index-6RdyZh6F.js} +1809 -1912
- package/dist/index-6RdyZh6F.js.map +1 -0
- package/dist/index-BcU18EjV.cjs +3 -0
- package/dist/index-BcU18EjV.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +78 -59
- package/dist/index.js +30 -26
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +346 -0
- package/src/components/AttachmentCard/Thumbnail.test.tsx +215 -0
- package/src/components/AttachmentCard/index.test.tsx +160 -0
- package/src/components/Avatar/Avatar.test.tsx +126 -0
- package/src/components/Avatar/getAvatarEmoji.test.ts +39 -0
- package/src/components/ChannelList/ChannelListContext.test.tsx +87 -0
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +410 -3
- package/src/components/ChannelList/index.test.tsx +132 -1
- package/src/components/CustomMessage/CustomMessage.stories.tsx +6 -314
- package/src/components/CustomMessage/CustomMessage.test.tsx +56 -2
- package/src/components/CustomMessage/Locked.stories.tsx +108 -0
- package/src/components/CustomMessage/MessageTag.stories.tsx +17 -53
- package/src/components/CustomMessage/MessageTag.test.tsx +7 -40
- package/src/components/CustomMessage/MessageTag.tsx +7 -39
- package/src/components/CustomMessage/PaidMessage.stories.tsx +42 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +8 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +2 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -33
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -6
- package/src/components/CustomMessage/Tip.stories.tsx +48 -0
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +51 -0
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +65 -0
- package/src/components/CustomMessage/TipMessage/index.tsx +27 -0
- package/src/components/CustomMessage/index.tsx +22 -19
- package/src/components/FaqList/FaqList.test.tsx +204 -0
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +327 -0
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +76 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +248 -0
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +123 -0
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +193 -0
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +242 -0
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +37 -0
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +72 -0
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +162 -0
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +58 -165
- package/src/components/MessageAttachment/Audio/index.tsx +77 -71
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +74 -176
- package/src/components/MessageAttachment/index.tsx +9 -25
- package/src/components/MessageAttachment/types.ts +9 -19
- package/src/components/MessagingShell/MessagingShell.test.tsx +20 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +4 -0
- package/src/logging/index.test.tsx +136 -0
- package/src/logging/index.tsx +48 -0
- package/src/providers/MessagingProvider.test.tsx +113 -1
- package/src/providers/MessagingProvider.tsx +25 -15
- package/src/stories/customMessageStoryTemplate.tsx +199 -0
- package/src/styles.css +10 -121
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +9 -0
- package/dist/index-B-9LkWep.cjs +0 -3
- package/dist/index-B-9LkWep.cjs.map +0 -1
- package/dist/index-C93LTGLc.js.map +0 -1
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +0 -335
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { StreamChatService } from '@linktr.ee/messaging-core'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Logger,
|
|
4
|
+
MessagingUser,
|
|
5
|
+
StreamChatServiceConfig,
|
|
6
|
+
} from '@linktr.ee/messaging-core'
|
|
3
7
|
import { render, waitFor, act } from '@testing-library/react'
|
|
4
8
|
import React from 'react'
|
|
5
9
|
import type { StreamChat } from 'stream-chat'
|
|
6
10
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
7
11
|
|
|
12
|
+
import { useMessagingLogger } from '../logging'
|
|
13
|
+
|
|
8
14
|
import { MessagingProvider, useMessagingContext } from './MessagingProvider'
|
|
9
15
|
|
|
10
16
|
// Stub stream-chat-react's <Chat /> so we don't need a real, fully-wired
|
|
@@ -486,4 +492,110 @@ describe('MessagingProvider', () => {
|
|
|
486
492
|
expect(last.hasClient).toBe(false)
|
|
487
493
|
expect(last.error).toBeNull()
|
|
488
494
|
})
|
|
495
|
+
|
|
496
|
+
describe('logger seam', () => {
|
|
497
|
+
const ServiceProbe: React.FC<{
|
|
498
|
+
onService: (service: StreamChatService | null) => void
|
|
499
|
+
}> = ({ onService }) => {
|
|
500
|
+
const { service } = useMessagingContext()
|
|
501
|
+
React.useEffect(() => {
|
|
502
|
+
onService(service)
|
|
503
|
+
}, [service, onService])
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const LoggerProbe: React.FC<{ onLogger: (logger: Logger) => void }> = ({
|
|
508
|
+
onLogger,
|
|
509
|
+
}) => {
|
|
510
|
+
const logger = useMessagingLogger()
|
|
511
|
+
React.useEffect(() => {
|
|
512
|
+
onLogger(logger)
|
|
513
|
+
}, [logger, onLogger])
|
|
514
|
+
return null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// The service stores its resolved sink privately; read it to prove which
|
|
518
|
+
// logger the provider handed to the constructor.
|
|
519
|
+
const serviceLogger = (service: StreamChatService) =>
|
|
520
|
+
(service as unknown as { logger: Logger }).logger
|
|
521
|
+
|
|
522
|
+
const renderWithLogger = async (
|
|
523
|
+
props: Partial<React.ComponentProps<typeof MessagingProvider>> & {
|
|
524
|
+
serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>
|
|
525
|
+
}
|
|
526
|
+
) => {
|
|
527
|
+
setupServiceMock()
|
|
528
|
+
const services: (StreamChatService | null)[] = []
|
|
529
|
+
const loggers: Logger[] = []
|
|
530
|
+
|
|
531
|
+
render(
|
|
532
|
+
<MessagingProvider
|
|
533
|
+
apiKey="mock-api-key"
|
|
534
|
+
user={
|
|
535
|
+
{ type: 'guest', id: 'guest-1', name: 'Guest' } as MessagingUser
|
|
536
|
+
}
|
|
537
|
+
{...props}
|
|
538
|
+
>
|
|
539
|
+
<ServiceProbe onService={(s) => services.push(s)} />
|
|
540
|
+
<LoggerProbe onLogger={(l) => loggers.push(l)} />
|
|
541
|
+
</MessagingProvider>
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
await waitFor(() => expect(services.filter(Boolean).length).toBe(1))
|
|
545
|
+
return {
|
|
546
|
+
service: services.filter(Boolean).pop() as StreamChatService,
|
|
547
|
+
logger: loggers[loggers.length - 1],
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const baseServiceConfig: Omit<StreamChatServiceConfig, 'apiKey'> = {
|
|
552
|
+
fetchToken: async () => 'token',
|
|
553
|
+
createChannel: async () => ({ channelId: 'ch-1' }),
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
it('forwards the logger prop into the service and to the subtree', async () => {
|
|
557
|
+
const sink: Logger = { error: vi.fn() }
|
|
558
|
+
const { service, logger } = await renderWithLogger({
|
|
559
|
+
serviceConfig: baseServiceConfig,
|
|
560
|
+
logger: sink,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
expect(serviceLogger(service)).toBe(sink)
|
|
564
|
+
|
|
565
|
+
const error = new Error('boom')
|
|
566
|
+
logger.error?.('[MessagingShell] Failed:', error)
|
|
567
|
+
expect(sink.error).toHaveBeenCalledWith('[MessagingShell] Failed:', error)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('lets an explicit serviceConfig.logger win over the logger prop', async () => {
|
|
571
|
+
const configSink: Logger = { error: vi.fn() }
|
|
572
|
+
const propSink: Logger = { error: vi.fn() }
|
|
573
|
+
const { service } = await renderWithLogger({
|
|
574
|
+
serviceConfig: { ...baseServiceConfig, logger: configSink },
|
|
575
|
+
logger: propSink,
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
expect(serviceLogger(service)).toBe(configSink)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('leaves the service on its console defaults when no logger is given', async () => {
|
|
582
|
+
const consoleError = vi
|
|
583
|
+
.spyOn(console, 'error')
|
|
584
|
+
.mockImplementation(() => {})
|
|
585
|
+
const { service, logger } = await renderWithLogger({
|
|
586
|
+
serviceConfig: baseServiceConfig,
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// Not the react seam object: the service fell back to its own defaults.
|
|
590
|
+
expect(serviceLogger(service)).not.toBe(logger)
|
|
591
|
+
|
|
592
|
+
const error = new Error('boom')
|
|
593
|
+
logger.error?.('[MessagingShell] Failed:', error)
|
|
594
|
+
serviceLogger(service).error?.('[StreamChatService] Failed:', error)
|
|
595
|
+
expect(consoleError.mock.calls).toEqual([
|
|
596
|
+
['[MessagingShell] Failed:', error],
|
|
597
|
+
['[StreamChatService] Failed:', error],
|
|
598
|
+
])
|
|
599
|
+
})
|
|
600
|
+
})
|
|
489
601
|
})
|
|
@@ -13,6 +13,7 @@ import React, {
|
|
|
13
13
|
import { StreamChat } from 'stream-chat'
|
|
14
14
|
import { Chat } from 'stream-chat-react'
|
|
15
15
|
|
|
16
|
+
import { MessagingLoggerProvider } from '../logging'
|
|
16
17
|
import type { MessagingProviderProps, MessagingCapabilities } from '../types'
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -56,6 +57,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
56
57
|
capabilities = {},
|
|
57
58
|
debug = false,
|
|
58
59
|
client: injectedClient,
|
|
60
|
+
logger,
|
|
59
61
|
}) => {
|
|
60
62
|
// Create debug logger that respects the debug prop
|
|
61
63
|
const debugLog = useCallback(
|
|
@@ -94,6 +96,8 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
94
96
|
serviceConfigRef.current = serviceConfig
|
|
95
97
|
const debugRef = useRef(debug)
|
|
96
98
|
debugRef.current = debug
|
|
99
|
+
const loggerRef = useRef(logger)
|
|
100
|
+
loggerRef.current = logger
|
|
97
101
|
|
|
98
102
|
// Track renders and prop changes
|
|
99
103
|
const prevPropsRef = useRef({
|
|
@@ -199,6 +203,10 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
199
203
|
apiKey,
|
|
200
204
|
debug: debugRef.current,
|
|
201
205
|
client: chatClient,
|
|
206
|
+
// An explicit serviceConfig.logger wins — existing injectors keep working.
|
|
207
|
+
...(currentConfig.logger || !loggerRef.current
|
|
208
|
+
? {}
|
|
209
|
+
: { logger: loggerRef.current }),
|
|
202
210
|
})
|
|
203
211
|
|
|
204
212
|
serviceRef.current = newService
|
|
@@ -404,20 +412,22 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
404
412
|
})
|
|
405
413
|
|
|
406
414
|
return (
|
|
407
|
-
<
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
415
|
+
<MessagingLoggerProvider logger={logger}>
|
|
416
|
+
<MessagingContext.Provider value={contextValue}>
|
|
417
|
+
{chatClient ? (
|
|
418
|
+
<Chat
|
|
419
|
+
client={chatClient}
|
|
420
|
+
customClasses={{
|
|
421
|
+
channelList:
|
|
422
|
+
'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A]',
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
{children}
|
|
426
|
+
</Chat>
|
|
427
|
+
) : (
|
|
428
|
+
children
|
|
429
|
+
)}
|
|
430
|
+
</MessagingContext.Provider>
|
|
431
|
+
</MessagingLoggerProvider>
|
|
422
432
|
)
|
|
423
433
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import '../stream-custom-data'
|
|
2
|
+
|
|
3
|
+
import type { StoryFn } from '@storybook/react'
|
|
4
|
+
import React, { useEffect } from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Channel as ChannelType,
|
|
7
|
+
QueryChannelAPIResponse,
|
|
8
|
+
StreamChat,
|
|
9
|
+
} from 'stream-chat'
|
|
10
|
+
import {
|
|
11
|
+
Channel,
|
|
12
|
+
Chat,
|
|
13
|
+
MessageList,
|
|
14
|
+
MessageUIComponentProps,
|
|
15
|
+
Window,
|
|
16
|
+
} from 'stream-chat-react'
|
|
17
|
+
|
|
18
|
+
import { CustomMessage } from '../components/CustomMessage'
|
|
19
|
+
import { OutboundContext, buildOutboundsByRecordId } from '../components/CustomMessage/OutboundContext'
|
|
20
|
+
import CustomTypingIndicator from '../components/CustomTypingIndicator'
|
|
21
|
+
import { createMockStreamChatClient } from '../testing/createMockStreamChatClient'
|
|
22
|
+
import type { OutboundResolver } from '../types'
|
|
23
|
+
|
|
24
|
+
import { minutesAgo } from './decorators/storyTime'
|
|
25
|
+
import { StoryUser, storyUsers } from './decorators/storyUser'
|
|
26
|
+
|
|
27
|
+
export const createMockChannel = async (
|
|
28
|
+
client: StreamChat,
|
|
29
|
+
messages: TemplateProps['messages']
|
|
30
|
+
) => {
|
|
31
|
+
const mockMessages = messages.map((msg, index) => ({
|
|
32
|
+
...msg,
|
|
33
|
+
type: msg.type ?? ('regular' as const),
|
|
34
|
+
created_at: minutesAgo(messages.length - index),
|
|
35
|
+
updated_at: minutesAgo(messages.length - index),
|
|
36
|
+
attachments: msg.attachments ?? [],
|
|
37
|
+
latest_reactions: [],
|
|
38
|
+
own_reactions: [],
|
|
39
|
+
reaction_counts: {},
|
|
40
|
+
reaction_scores: {},
|
|
41
|
+
reply_count: 0,
|
|
42
|
+
status: 'received',
|
|
43
|
+
cid: 'messaging:storybook-channel-1',
|
|
44
|
+
mentioned_users: msg.mentioned_users ?? [],
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const channelData = {
|
|
48
|
+
members: [storyUsers.creator.id, storyUsers.visitor.id],
|
|
49
|
+
// Required for useUserRole: canDelete needs delete-own-message, canFlag needs flag-message
|
|
50
|
+
own_capabilities: ['delete-own-message', 'flag-message'],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const channel = client.channel(
|
|
54
|
+
'messaging',
|
|
55
|
+
'storybook-channel-1',
|
|
56
|
+
channelData
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
channel.watch = async () => {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
channel.state.messages = mockMessages as unknown as any[]
|
|
62
|
+
channel.state.members = {
|
|
63
|
+
[storyUsers.creator.id]: {
|
|
64
|
+
user: storyUsers.creator,
|
|
65
|
+
user_id: storyUsers.creator.id,
|
|
66
|
+
},
|
|
67
|
+
[storyUsers.visitor.id]: {
|
|
68
|
+
user: storyUsers.visitor,
|
|
69
|
+
user_id: storyUsers.visitor.id,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
channel: channelData,
|
|
74
|
+
members: [],
|
|
75
|
+
messages: mockMessages,
|
|
76
|
+
watchers: [],
|
|
77
|
+
pinned_messages: [],
|
|
78
|
+
duration: '0ms',
|
|
79
|
+
} as unknown as QueryChannelAPIResponse
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await channel.watch()
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore errors in mock mode
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return channel
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TemplateProps {
|
|
92
|
+
currentUser: StoryUser
|
|
93
|
+
typingUser?: StoryUser
|
|
94
|
+
resolveOutbound?: OutboundResolver
|
|
95
|
+
messages: Array<{
|
|
96
|
+
id: string
|
|
97
|
+
text: string
|
|
98
|
+
user: StoryUser
|
|
99
|
+
mentioned_users?: StoryUser[]
|
|
100
|
+
type?: 'regular' | 'system'
|
|
101
|
+
attachments?: Array<Record<string, unknown>>
|
|
102
|
+
metadata?: {
|
|
103
|
+
custom_type?:
|
|
104
|
+
| 'MESSAGE_TIP'
|
|
105
|
+
| 'MESSAGE_PAID'
|
|
106
|
+
| 'MESSAGE_CHATBOT'
|
|
107
|
+
| 'MESSAGE_ATTACHMENT'
|
|
108
|
+
| 'MESSAGE_OUTBOUND'
|
|
109
|
+
payment_status?: 'pending' | 'paid' | 'failed' | 'refunded'
|
|
110
|
+
amount_text?: string
|
|
111
|
+
attachment_title?: string
|
|
112
|
+
attachment_mime_type?: string
|
|
113
|
+
attachment_thumbnail?: string
|
|
114
|
+
attachment_detail?: string
|
|
115
|
+
attachment_content_type?: 'text' | 'media'
|
|
116
|
+
outbound_id?: string
|
|
117
|
+
}
|
|
118
|
+
}>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const TemplateInner: React.FC<{
|
|
122
|
+
currentUser: StoryUser
|
|
123
|
+
typingUser?: StoryUser
|
|
124
|
+
messages: TemplateProps['messages']
|
|
125
|
+
resolveOutbound?: OutboundResolver
|
|
126
|
+
}> = ({ currentUser, typingUser, messages, resolveOutbound }) => {
|
|
127
|
+
const [client] = React.useState(() => createMockStreamChatClient(currentUser))
|
|
128
|
+
|
|
129
|
+
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
createMockChannel(client, messages).then(setChannel)
|
|
133
|
+
}, [client, messages])
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!channel || !typingUser) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Stream Channel UI only updates typing context from typing.start/typing.stop events.
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
client.dispatchEvent({
|
|
143
|
+
type: 'typing.start',
|
|
144
|
+
cid: channel.cid,
|
|
145
|
+
user: typingUser,
|
|
146
|
+
})
|
|
147
|
+
}, 0)
|
|
148
|
+
|
|
149
|
+
return () => clearTimeout(timer)
|
|
150
|
+
}, [channel, client, typingUser])
|
|
151
|
+
|
|
152
|
+
const outboundsByRecordId = React.useMemo(
|
|
153
|
+
() => buildOutboundsByRecordId(messages, resolveOutbound),
|
|
154
|
+
[messages, resolveOutbound]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const MessageComponent = React.useMemo(() => {
|
|
158
|
+
return function CustomMessageComponent(props: MessageUIComponentProps) {
|
|
159
|
+
return <CustomMessage {...props} onOutboundClick={() => undefined} />
|
|
160
|
+
}
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
if (!channel) {
|
|
164
|
+
return <div className="p-4">Loading...</div>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<OutboundContext.Provider value={outboundsByRecordId}>
|
|
169
|
+
<Chat client={client}>
|
|
170
|
+
<div className="h-screen w-full bg-white">
|
|
171
|
+
<Channel
|
|
172
|
+
channel={channel}
|
|
173
|
+
Message={MessageComponent}
|
|
174
|
+
TypingIndicator={CustomTypingIndicator}
|
|
175
|
+
>
|
|
176
|
+
<Window>
|
|
177
|
+
<MessageList />
|
|
178
|
+
</Window>
|
|
179
|
+
</Channel>
|
|
180
|
+
</div>
|
|
181
|
+
</Chat>
|
|
182
|
+
</OutboundContext.Provider>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const Template: StoryFn<TemplateProps> = ({
|
|
187
|
+
currentUser = storyUsers.creator,
|
|
188
|
+
typingUser,
|
|
189
|
+
messages,
|
|
190
|
+
resolveOutbound,
|
|
191
|
+
}) => (
|
|
192
|
+
<TemplateInner
|
|
193
|
+
key={currentUser.id}
|
|
194
|
+
currentUser={currentUser}
|
|
195
|
+
typingUser={typingUser}
|
|
196
|
+
messages={messages}
|
|
197
|
+
resolveOutbound={resolveOutbound}
|
|
198
|
+
/>
|
|
199
|
+
)
|
package/src/styles.css
CHANGED
|
@@ -924,12 +924,21 @@
|
|
|
924
924
|
align-items: flex-start;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
/* Gap between
|
|
927
|
+
/* Gap between a wrapper's stacked bubbles (e.g. StreamAttachmentMessage's
|
|
928
|
+
media + caption, MediaMessage's card + text) */
|
|
928
929
|
.str-chat__message-bubble-wrapper
|
|
929
930
|
> .str-chat__message-bubble:not(:first-child) {
|
|
930
931
|
margin-top: 4px;
|
|
931
932
|
}
|
|
932
933
|
|
|
934
|
+
/* Tighter gap for the locked-attachment card + text bubble and the tip
|
|
935
|
+
pill + text bubble — same margin-top mechanism, smaller value. Must come
|
|
936
|
+
after the rule above: equal specificity, so source order decides. */
|
|
937
|
+
.message-locked-attachment-wrapper > .str-chat__message-bubble:not(:first-child),
|
|
938
|
+
.message-tip-wrapper > .str-chat__message-bubble:not(:first-child) {
|
|
939
|
+
margin-top: 2px;
|
|
940
|
+
}
|
|
941
|
+
|
|
933
942
|
/** Remove padding from the text inside the locked attachment text shell,
|
|
934
943
|
and inherit TextShell's explicit colour — stream-chat-react's own
|
|
935
944
|
`MessageText` sets its own (black) colour that only gets overridden by
|
|
@@ -941,123 +950,3 @@
|
|
|
941
950
|
.message-locked-attachment-text .str-chat__message-text-inner {
|
|
942
951
|
color: inherit;
|
|
943
952
|
}
|
|
944
|
-
|
|
945
|
-
/* Audio attachment waveform — `MessageAttachment.Audio` (MES-1360).
|
|
946
|
-
|
|
947
|
-
Plain CSS rather than Tailwind utilities for two reasons: the per-bar
|
|
948
|
-
height is a floored custom property that has to be authored once
|
|
949
|
-
rather than repeated on 35 elements, and the played / unplayed fills
|
|
950
|
-
are the one design decision Figma left open (it draws the idle state
|
|
951
|
-
only), so keeping them in a single place is what makes them reviewable.
|
|
952
|
-
Follows the existing `.mes-media-viewer*` / `.mes-dialog` convention:
|
|
953
|
-
the package's shipped stylesheet is enough, with no Tailwind
|
|
954
|
-
dependency in the consumer.
|
|
955
|
-
|
|
956
|
-
Geometry is Figma `2754:17710` → `sound-wave` (`…;2754:8065`): a
|
|
957
|
-
138×30 track holding 35 bars at 2px with 2px gaps
|
|
958
|
-
(35 x 2 + 34 x 2 = 138). Bars flex from a zero basis, so the exact
|
|
959
|
-
2px falls out of the 138px track and a narrower container degrades
|
|
960
|
-
proportionally instead of clipping. */
|
|
961
|
-
.mes-audio-wave {
|
|
962
|
-
position: relative;
|
|
963
|
-
display: flex;
|
|
964
|
-
flex: 1 1 0;
|
|
965
|
-
min-width: 0;
|
|
966
|
-
align-items: center;
|
|
967
|
-
height: 30px;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.mes-audio-wave__track {
|
|
971
|
-
display: flex;
|
|
972
|
-
width: 100%;
|
|
973
|
-
height: 100%;
|
|
974
|
-
align-items: center;
|
|
975
|
-
gap: 2px;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/* Figma's bars run 4px..30px on a 30px track, so the amplitude
|
|
979
|
-
percentage is floored at that 4px minimum: a silent sample — or the
|
|
980
|
-
all-zero fallback used when no waveform could be produced — still
|
|
981
|
-
draws the shortest bar the design already contains, keeping the
|
|
982
|
-
card's silhouette identical instead of collapsing it. */
|
|
983
|
-
.mes-audio-wave__bar {
|
|
984
|
-
flex: 1 1 0;
|
|
985
|
-
min-width: 0;
|
|
986
|
-
height: max(4px, var(--mes-audio-wave-bar, 0%));
|
|
987
|
-
border-radius: 9999px;
|
|
988
|
-
background: rgb(255 255 255 / 0.4);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
.mes-audio-wave__bar--played {
|
|
992
|
-
background: #ffffff;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
.mes-audio-wave--light .mes-audio-wave__bar {
|
|
996
|
-
background: rgb(4 4 3 / 0.5);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
.mes-audio-wave--light .mes-audio-wave__bar--played {
|
|
1000
|
-
background: #040403;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/* Seek is a real `<input type="range">` stretched over the bars, which
|
|
1004
|
-
supplies click, drag, arrow keys, Home / End and a screen-reader
|
|
1005
|
-
slider role natively. The bars are the visible track, so the input
|
|
1006
|
-
itself must not paint — but it must stay focusable, which is why the
|
|
1007
|
-
thumb and track are cleared individually rather than the whole input
|
|
1008
|
-
being given `opacity: 0`. */
|
|
1009
|
-
.mes-audio-wave__seek {
|
|
1010
|
-
position: absolute;
|
|
1011
|
-
inset: 0;
|
|
1012
|
-
width: 100%;
|
|
1013
|
-
height: 100%;
|
|
1014
|
-
margin: 0;
|
|
1015
|
-
padding: 0;
|
|
1016
|
-
cursor: pointer;
|
|
1017
|
-
background: transparent;
|
|
1018
|
-
-webkit-appearance: none;
|
|
1019
|
-
appearance: none;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
.mes-audio-wave__seek:disabled {
|
|
1023
|
-
cursor: default;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
.mes-audio-wave__seek::-webkit-slider-runnable-track {
|
|
1027
|
-
background: transparent;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
.mes-audio-wave__seek::-webkit-slider-thumb {
|
|
1031
|
-
-webkit-appearance: none;
|
|
1032
|
-
appearance: none;
|
|
1033
|
-
width: 1px;
|
|
1034
|
-
height: 100%;
|
|
1035
|
-
opacity: 0;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
.mes-audio-wave__seek::-moz-range-track {
|
|
1039
|
-
background: transparent;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
.mes-audio-wave__seek::-moz-range-thumb {
|
|
1043
|
-
width: 1px;
|
|
1044
|
-
height: 100%;
|
|
1045
|
-
border: 0;
|
|
1046
|
-
opacity: 0;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
.mes-audio-wave__seek:focus {
|
|
1050
|
-
outline: none;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/* The input is transparent, so the focus ring has to render on the
|
|
1054
|
-
wrapper that actually has pixels. */
|
|
1055
|
-
.mes-audio-wave:has(.mes-audio-wave__seek:focus-visible) {
|
|
1056
|
-
outline: 2px solid rgb(255 255 255 / 0.6);
|
|
1057
|
-
outline-offset: 4px;
|
|
1058
|
-
border-radius: 4px;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
.mes-audio-wave--light:has(.mes-audio-wave__seek:focus-visible) {
|
|
1062
|
-
outline-color: rgb(4 4 3 / 0.6);
|
|
1063
|
-
}
|
|
@@ -141,11 +141,11 @@ export function createMockMessagingClient({
|
|
|
141
141
|
})
|
|
142
142
|
// Read/unread bookkeeping consults channel mute status; without a live
|
|
143
143
|
// connection the real method throws, so report "not muted".
|
|
144
|
-
channel.muteStatus = (
|
|
144
|
+
channel.muteStatus = () => ({
|
|
145
145
|
muted: false,
|
|
146
146
|
createdAt: null,
|
|
147
147
|
expiresAt: null,
|
|
148
|
-
})
|
|
148
|
+
})
|
|
149
149
|
|
|
150
150
|
// Neutralise the rest of the connection-dependent channel API that
|
|
151
151
|
// stream-chat-react calls during the chat lifecycle. Each of these POSTs to
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Logger,
|
|
2
3
|
MessagingUser,
|
|
3
4
|
StreamChatServiceConfig,
|
|
4
5
|
} from '@linktr.ee/messaging-core'
|
|
@@ -453,6 +454,7 @@ export type ChannelViewPassthroughProps = Pick<
|
|
|
453
454
|
| 'resolveOutbound'
|
|
454
455
|
| 'showChannelInfo'
|
|
455
456
|
| 'composerInput'
|
|
457
|
+
| 'attachmentPreviewList'
|
|
456
458
|
>
|
|
457
459
|
|
|
458
460
|
/**
|
|
@@ -500,6 +502,13 @@ interface MessagingProviderBaseProps {
|
|
|
500
502
|
user: MessagingUser | null
|
|
501
503
|
capabilities?: MessagingCapabilities
|
|
502
504
|
debug?: boolean
|
|
505
|
+
/**
|
|
506
|
+
* Optional sink for the package's own log output. Levels left undefined fall
|
|
507
|
+
* back to console, which is also the default when the prop is omitted. In
|
|
508
|
+
* apiKey mode it is forwarded to the underlying StreamChatService unless
|
|
509
|
+
* `serviceConfig.logger` is set, which wins.
|
|
510
|
+
*/
|
|
511
|
+
logger?: Logger
|
|
503
512
|
}
|
|
504
513
|
|
|
505
514
|
/**
|