@linktr.ee/messaging-react 1.33.1 → 1.33.2-rc-1777444067
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-Ddi8bg90.js → Card-CQYlmwhm.js} +2 -2
- package/dist/{Card-Ddi8bg90.js.map → Card-CQYlmwhm.js.map} +1 -1
- package/dist/{Card-DEe10CiS.js → Card-V-Et3tc4.js} +2 -2
- package/dist/{Card-DEe10CiS.js.map → Card-V-Et3tc4.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BePLvyvi.js → index-CahzrNJz.js} +1363 -1127
- package/dist/{index-BePLvyvi.js.map → index-CahzrNJz.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/LockedAttachment/components/MediaPlayer.tsx +10 -1
- package/src/components/MediaMessage/MediaMessage.stories.tsx +82 -19
- package/src/components/MediaMessage/MediaMessage.test.tsx +277 -18
- package/src/components/MediaMessage/index.tsx +388 -77
- package/src/providers/MessagingProvider.stories.tsx +214 -0
- package/src/providers/MessagingProvider.test.tsx +126 -0
- package/src/styles.css +53 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React, { useEffect, useState } from 'react'
|
|
3
|
+
import { StreamChat } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import { MessagingProvider, useMessagingContext } from './MessagingProvider'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Storybook coverage for guest-mode connections through MessagingProvider.
|
|
9
|
+
*
|
|
10
|
+
* The real Stream Chat backend is not reachable from Storybook, so we stub the
|
|
11
|
+
* pieces of `StreamChat.prototype` that `StreamChatService` exercises during
|
|
12
|
+
* `connectUser`/`disconnectUser`. This keeps the story focused on the new API
|
|
13
|
+
* surface (`user.type === 'guest'` + `fetchToken: undefined`) rather than on
|
|
14
|
+
* setting up a fake transport layer.
|
|
15
|
+
*/
|
|
16
|
+
const useStubbedStreamChat = () => {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const proto = StreamChat.prototype as unknown as Record<string, unknown>
|
|
19
|
+
const original = {
|
|
20
|
+
connectUser: proto.connectUser,
|
|
21
|
+
setGuestUser: proto.setGuestUser,
|
|
22
|
+
disconnectUser: proto.disconnectUser,
|
|
23
|
+
on: proto.on,
|
|
24
|
+
off: proto.off,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
proto.connectUser = function connectUser(this: StreamChat) {
|
|
28
|
+
this.userID = 'default-stub'
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
;(this as any).user = { id: 'default-stub', name: 'User' }
|
|
31
|
+
return Promise.resolve({ me: this.user }) as ReturnType<
|
|
32
|
+
typeof StreamChat.prototype.connectUser
|
|
33
|
+
>
|
|
34
|
+
} as typeof StreamChat.prototype.connectUser
|
|
35
|
+
|
|
36
|
+
proto.setGuestUser = function setGuestUser(
|
|
37
|
+
this: StreamChat,
|
|
38
|
+
user: { id: string; name?: string }
|
|
39
|
+
) {
|
|
40
|
+
this.userID = user.id
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
;(this as any).user = user
|
|
43
|
+
return Promise.resolve()
|
|
44
|
+
} as typeof StreamChat.prototype.setGuestUser
|
|
45
|
+
|
|
46
|
+
proto.disconnectUser = function disconnectUser(this: StreamChat) {
|
|
47
|
+
this.userID = undefined
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
;(this as any).user = undefined
|
|
50
|
+
return Promise.resolve()
|
|
51
|
+
} as typeof StreamChat.prototype.disconnectUser
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
Object.assign(proto, original)
|
|
55
|
+
}
|
|
56
|
+
}, [])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ConnectionStatus: React.FC = () => {
|
|
60
|
+
const { client, isConnected, isLoading, error } = useMessagingContext()
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex flex-col gap-1 rounded border border-gray-300 bg-white p-4 text-sm">
|
|
63
|
+
<div>
|
|
64
|
+
<strong>isLoading:</strong> {String(isLoading)}
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<strong>isConnected:</strong> {String(isConnected)}
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<strong>userID:</strong> {client?.userID ?? 'none'}
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<strong>error:</strong> {error ?? 'none'}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type StoryArgs = {
|
|
80
|
+
mode: 'guest' | 'default'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const meta: Meta<StoryArgs> = {
|
|
84
|
+
title: 'Providers/MessagingProvider',
|
|
85
|
+
parameters: {
|
|
86
|
+
layout: 'centered',
|
|
87
|
+
},
|
|
88
|
+
argTypes: {
|
|
89
|
+
mode: {
|
|
90
|
+
control: { type: 'inline-radio' },
|
|
91
|
+
options: ['guest', 'default'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
export default meta
|
|
96
|
+
|
|
97
|
+
const Template: StoryFn<StoryArgs> = ({ mode }) => {
|
|
98
|
+
useStubbedStreamChat()
|
|
99
|
+
|
|
100
|
+
// Force a remount when `mode` changes so MessagingProvider tears down its
|
|
101
|
+
// existing connection — the documented way to swap between default and
|
|
102
|
+
// guest identities.
|
|
103
|
+
const [providerKey, setProviderKey] = useState(0)
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setProviderKey((k) => k + 1)
|
|
106
|
+
}, [mode])
|
|
107
|
+
|
|
108
|
+
const user =
|
|
109
|
+
mode === 'guest'
|
|
110
|
+
? { type: 'guest' as const, id: 'guest-demo', name: 'Guest' }
|
|
111
|
+
: { id: 'user-demo', name: 'Authed User' }
|
|
112
|
+
|
|
113
|
+
// For guest mode `fetchToken` is intentionally omitted to demonstrate that
|
|
114
|
+
// `StreamChatServiceConfig.fetchToken` is no longer required.
|
|
115
|
+
const serviceConfig =
|
|
116
|
+
mode === 'guest'
|
|
117
|
+
? {
|
|
118
|
+
createChannel: async () => ({ channelId: 'noop' }),
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
fetchToken: async () => 'mock-token',
|
|
122
|
+
createChannel: async () => ({ channelId: 'noop' }),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<MessagingProvider
|
|
127
|
+
key={`${mode}-${providerKey}`}
|
|
128
|
+
apiKey="mock-api-key"
|
|
129
|
+
user={user}
|
|
130
|
+
serviceConfig={serviceConfig}
|
|
131
|
+
>
|
|
132
|
+
<ConnectionStatus />
|
|
133
|
+
</MessagingProvider>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const GuestMode = Template.bind({})
|
|
138
|
+
GuestMode.args = { mode: 'guest' }
|
|
139
|
+
GuestMode.parameters = {
|
|
140
|
+
docs: {
|
|
141
|
+
description: {
|
|
142
|
+
story:
|
|
143
|
+
'Connects through `MessagingProvider` with `user={{ type: "guest", ... }}` and a service config that omits `fetchToken`. The toolkit dispatches to `StreamChat.setGuestUser` internally; no token request is made.',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const DefaultMode = Template.bind({})
|
|
149
|
+
DefaultMode.args = { mode: 'default' }
|
|
150
|
+
DefaultMode.parameters = {
|
|
151
|
+
docs: {
|
|
152
|
+
description: {
|
|
153
|
+
story:
|
|
154
|
+
'Existing behavior: `user` without a `type` connects via `client.connectUser` using the configured `fetchToken` provider.',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const ModeSwap: StoryFn<StoryArgs> = () => {
|
|
160
|
+
useStubbedStreamChat()
|
|
161
|
+
const [mode, setMode] = useState<'guest' | 'default'>('guest')
|
|
162
|
+
|
|
163
|
+
const user =
|
|
164
|
+
mode === 'guest'
|
|
165
|
+
? { type: 'guest' as const, id: 'guest-demo', name: 'Guest' }
|
|
166
|
+
: { id: 'user-demo', name: 'Authed User' }
|
|
167
|
+
|
|
168
|
+
const serviceConfig =
|
|
169
|
+
mode === 'guest'
|
|
170
|
+
? {
|
|
171
|
+
createChannel: async () => ({ channelId: 'noop' }),
|
|
172
|
+
}
|
|
173
|
+
: {
|
|
174
|
+
fetchToken: async () => 'mock-token',
|
|
175
|
+
createChannel: async () => ({ channelId: 'noop' }),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex flex-col gap-3">
|
|
180
|
+
<div className="flex gap-2">
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className="rounded bg-gray-200 px-3 py-1"
|
|
184
|
+
onClick={() => setMode('guest')}
|
|
185
|
+
>
|
|
186
|
+
Guest
|
|
187
|
+
</button>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
className="rounded bg-gray-200 px-3 py-1"
|
|
191
|
+
onClick={() => setMode('default')}
|
|
192
|
+
>
|
|
193
|
+
Default
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
<MessagingProvider
|
|
197
|
+
key={mode}
|
|
198
|
+
apiKey="mock-api-key"
|
|
199
|
+
user={user}
|
|
200
|
+
serviceConfig={serviceConfig}
|
|
201
|
+
>
|
|
202
|
+
<ConnectionStatus />
|
|
203
|
+
</MessagingProvider>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
ModeSwap.parameters = {
|
|
208
|
+
docs: {
|
|
209
|
+
description: {
|
|
210
|
+
story:
|
|
211
|
+
'Mode swapping is consumer-driven: change the `key` on `<MessagingProvider>` so React unmounts the previous instance (which calls `disconnectUser`) and mounts a fresh one with the new identity.',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { StreamChatService } from '@linktr.ee/messaging-core'
|
|
2
|
+
import { render, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import type { StreamChat } from 'stream-chat'
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { MessagingProvider, useMessagingContext } from './MessagingProvider'
|
|
8
|
+
|
|
9
|
+
// Stub stream-chat-react's <Chat /> so we don't need a real, fully-wired
|
|
10
|
+
// StreamChat instance just to verify provider state.
|
|
11
|
+
vi.mock('stream-chat-react', () => ({
|
|
12
|
+
Chat: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const setupServiceMock = () => {
|
|
16
|
+
const connectUser = vi.fn(
|
|
17
|
+
async () => ({ userID: 'mock' }) as unknown as StreamChat
|
|
18
|
+
)
|
|
19
|
+
const disconnectUser = vi.fn(async () => undefined)
|
|
20
|
+
|
|
21
|
+
vi.spyOn(StreamChatService.prototype, 'connectUser').mockImplementation(
|
|
22
|
+
connectUser
|
|
23
|
+
)
|
|
24
|
+
vi.spyOn(StreamChatService.prototype, 'disconnectUser').mockImplementation(
|
|
25
|
+
disconnectUser
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return { connectUser, disconnectUser }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Probe: React.FC<{ onState: (state: unknown) => void }> = ({
|
|
32
|
+
onState,
|
|
33
|
+
}) => {
|
|
34
|
+
const ctx = useMessagingContext()
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
onState({
|
|
37
|
+
isConnected: ctx.isConnected,
|
|
38
|
+
isLoading: ctx.isLoading,
|
|
39
|
+
error: ctx.error,
|
|
40
|
+
hasClient: !!ctx.client,
|
|
41
|
+
})
|
|
42
|
+
}, [ctx, onState])
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('MessagingProvider', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.restoreAllMocks()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('connects a guest user through the underlying service', async () => {
|
|
52
|
+
const { connectUser } = setupServiceMock()
|
|
53
|
+
const states: unknown[] = []
|
|
54
|
+
|
|
55
|
+
render(
|
|
56
|
+
<MessagingProvider
|
|
57
|
+
apiKey="mock-api-key"
|
|
58
|
+
user={{ type: 'guest', id: 'guest-1', name: 'Guest' }}
|
|
59
|
+
serviceConfig={{
|
|
60
|
+
createChannel: async () => ({ channelId: 'ch-1' }),
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Probe onState={(s) => states.push(s)} />
|
|
64
|
+
</MessagingProvider>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
|
|
68
|
+
expect(connectUser).toHaveBeenCalledWith({
|
|
69
|
+
type: 'guest',
|
|
70
|
+
id: 'guest-1',
|
|
71
|
+
name: 'Guest',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
const last = states[states.length - 1] as { isConnected: boolean }
|
|
76
|
+
expect(last.isConnected).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('disconnects and reconnects when remounted with a new key', async () => {
|
|
81
|
+
const { connectUser, disconnectUser } = setupServiceMock()
|
|
82
|
+
|
|
83
|
+
const guestUser = {
|
|
84
|
+
type: 'guest' as const,
|
|
85
|
+
id: 'guest-1',
|
|
86
|
+
name: 'Guest',
|
|
87
|
+
}
|
|
88
|
+
const authedUser = { id: 'user-1', name: 'Authed User' }
|
|
89
|
+
|
|
90
|
+
const { rerender } = render(
|
|
91
|
+
<MessagingProvider
|
|
92
|
+
key="guest"
|
|
93
|
+
apiKey="mock-api-key"
|
|
94
|
+
user={guestUser}
|
|
95
|
+
serviceConfig={{
|
|
96
|
+
createChannel: async () => ({ channelId: 'ch-1' }),
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div />
|
|
100
|
+
</MessagingProvider>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
|
|
104
|
+
expect(connectUser).toHaveBeenLastCalledWith(guestUser)
|
|
105
|
+
|
|
106
|
+
await act(async () => {
|
|
107
|
+
rerender(
|
|
108
|
+
<MessagingProvider
|
|
109
|
+
key="authed"
|
|
110
|
+
apiKey="mock-api-key"
|
|
111
|
+
user={authedUser}
|
|
112
|
+
serviceConfig={{
|
|
113
|
+
fetchToken: async () => 'token',
|
|
114
|
+
createChannel: async () => ({ channelId: 'ch-1' }),
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<div />
|
|
118
|
+
</MessagingProvider>
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await waitFor(() => expect(disconnectUser).toHaveBeenCalled())
|
|
123
|
+
await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(2))
|
|
124
|
+
expect(connectUser).toHaveBeenLastCalledWith(authedUser)
|
|
125
|
+
})
|
|
126
|
+
})
|
package/src/styles.css
CHANGED
|
@@ -64,6 +64,55 @@
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Full-screen media viewer dialog */
|
|
68
|
+
.mes-media-viewer {
|
|
69
|
+
--transition-duration: 0.15s;
|
|
70
|
+
|
|
71
|
+
border: none;
|
|
72
|
+
padding: 0;
|
|
73
|
+
margin: 0;
|
|
74
|
+
background: transparent;
|
|
75
|
+
width: 100vw;
|
|
76
|
+
height: 100dvh;
|
|
77
|
+
max-width: 100vw;
|
|
78
|
+
max-height: 100dvh;
|
|
79
|
+
position: fixed;
|
|
80
|
+
inset: 0;
|
|
81
|
+
|
|
82
|
+
transition:
|
|
83
|
+
opacity var(--transition-duration) ease,
|
|
84
|
+
overlay var(--transition-duration) allow-discrete,
|
|
85
|
+
display var(--transition-duration) allow-discrete;
|
|
86
|
+
|
|
87
|
+
opacity: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mes-media-viewer[open] {
|
|
91
|
+
opacity: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.mes-media-viewer::backdrop {
|
|
95
|
+
transition:
|
|
96
|
+
display var(--transition-duration) allow-discrete,
|
|
97
|
+
overlay var(--transition-duration) allow-discrete,
|
|
98
|
+
background-color var(--transition-duration) linear;
|
|
99
|
+
background-color: rgba(0, 0, 0, 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mes-media-viewer[open]::backdrop {
|
|
103
|
+
background-color: rgba(0, 0, 0, 0.92);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@starting-style {
|
|
107
|
+
.mes-media-viewer[open] {
|
|
108
|
+
opacity: 0;
|
|
109
|
+
|
|
110
|
+
&::backdrop {
|
|
111
|
+
background-color: rgba(0, 0, 0, 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
67
116
|
/* Textarea composer focus styles */
|
|
68
117
|
.mes-textarea-composer-container:has(.mes-textarea-composer:focus) {
|
|
69
118
|
outline: 2px solid black;
|
|
@@ -128,6 +177,10 @@
|
|
|
128
177
|
color: #fff;
|
|
129
178
|
}
|
|
130
179
|
|
|
180
|
+
.str-chat__channel-list-react .str-chat__channel-list-messenger-react {
|
|
181
|
+
padding: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
131
184
|
/* Custom message tag styles */
|
|
132
185
|
.message-tag {
|
|
133
186
|
display: inline-flex;
|