@seamly/web-ui 22.3.4 → 22.3.5

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.
Files changed (34) hide show
  1. package/build/dist/lib/components.js +1484 -14
  2. package/build/dist/lib/components.js.map +1 -1
  3. package/build/dist/lib/components.min.js +1 -1
  4. package/build/dist/lib/components.min.js.map +1 -1
  5. package/build/dist/lib/hooks.js +1519 -9
  6. package/build/dist/lib/hooks.js.map +1 -1
  7. package/build/dist/lib/hooks.min.js +1 -1
  8. package/build/dist/lib/hooks.min.js.map +1 -1
  9. package/build/dist/lib/index.debug.js +15 -4
  10. package/build/dist/lib/index.debug.js.map +1 -1
  11. package/build/dist/lib/index.debug.min.js +1 -1
  12. package/build/dist/lib/index.debug.min.js.LICENSE.txt +4 -0
  13. package/build/dist/lib/index.debug.min.js.map +1 -1
  14. package/build/dist/lib/index.js +258 -244
  15. package/build/dist/lib/index.js.map +1 -1
  16. package/build/dist/lib/index.min.js +1 -1
  17. package/build/dist/lib/index.min.js.map +1 -1
  18. package/build/dist/lib/standalone.js +536 -245
  19. package/build/dist/lib/standalone.js.map +1 -1
  20. package/build/dist/lib/standalone.min.js +1 -1
  21. package/build/dist/lib/standalone.min.js.map +1 -1
  22. package/build/dist/lib/style-guide.js +265 -251
  23. package/build/dist/lib/style-guide.js.map +1 -1
  24. package/build/dist/lib/style-guide.min.js +1 -1
  25. package/build/dist/lib/style-guide.min.js.map +1 -1
  26. package/build/dist/lib/utils.js +8497 -8427
  27. package/build/dist/lib/utils.js.map +1 -1
  28. package/build/dist/lib/utils.min.js +1 -1
  29. package/build/dist/lib/utils.min.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/javascripts/api/conversation-connector.ts +48 -32
  32. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +197 -218
  33. package/src/javascripts/ui/hooks/seamly-hooks.js +5 -5
  34. package/src/javascripts/ui/hooks/use-seamly-conversation.ts +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamly/web-ui",
3
- "version": "22.3.4",
3
+ "version": "22.3.5",
4
4
  "main": "build/dist/lib/index.js",
5
5
  "types": "build/src/javascripts/index.d.ts",
6
6
  "module": "",
@@ -18,6 +18,17 @@ type ConnectionState = {
18
18
  ready: boolean
19
19
  currentState: CurrentConnectionState
20
20
  }
21
+
22
+ // Subscribers set which are used to subscribe to changes in the conversation.
23
+ // Needs to be outside of the ConversationConnector class so its not recreated for each instance.
24
+ const subscribers = new Set<Function>()
25
+
26
+ // Syncs the lifecycle of the conversation with Preact. Each subscriber will fetch the latest value from the conversation if needed.
27
+ const emitChange = () => {
28
+ // Call the callback function for each subscriber
29
+ subscribers.forEach((callback) => callback())
30
+ }
31
+
21
32
  export default class ConversationConnector {
22
33
  #connectionListeners: ((_payload: ConnectionState) => boolean | void)[] = []
23
34
 
@@ -46,32 +57,17 @@ export default class ConversationConnector {
46
57
  this.channelTopic = channelTopic
47
58
  const { url: splittedUrl, params } = splitUrlParams(this.url)
48
59
 
49
- if (!this.socket) {
50
- this.socket = new Socket(splittedUrl, {
51
- params: { ...params, v: apiVersion },
52
- reconnectAfterMs: (tries) => {
53
- // Calculate the backoff time based on the number of tries.
54
- const backoff = 2 ** tries * 250
55
-
56
- // Limit the backoff time to 10 seconds.
57
- return Math.min(backoff, 10000)
58
- },
59
- })
60
- }
60
+ this.socket = new Socket(splittedUrl, {
61
+ params: { ...params, v: apiVersion },
62
+ reconnectAfterMs: (tries) => {
63
+ // Calculate the backoff time based on the number of tries.
64
+ const backoff = 2 ** tries * 250
61
65
 
62
- // Checks if the socket is not already connected before attempting a connection.
63
- // This prevents multiple connection attempts during server reconnects, network switches or SPA route changes.
64
- if (!this.socket.isConnected()) {
65
- this.socket.connect()
66
- }
67
-
68
- this.channel = this.socket.channel(this.channelTopic, {
69
- authorization: `Bearer ${this.accessToken}`,
70
- channelName: this.channelName,
66
+ // Limit the backoff time to 10 seconds.
67
+ return Math.min(backoff, 10000)
68
+ },
71
69
  })
72
70
 
73
- this.start()
74
-
75
71
  this.socket.onError((err) => {
76
72
  log('[SOCKET][ERROR]', err)
77
73
  })
@@ -80,6 +76,24 @@ export default class ConversationConnector {
80
76
  log('[SOCKET]OPEN')
81
77
  })
82
78
 
79
+ this.socket.onClose(() => {
80
+ log('[SOCKET]CLOSE')
81
+ this.#emitConnectionState({
82
+ connected: false,
83
+ ready: false,
84
+ currentState: 'socket_closed',
85
+ })
86
+ })
87
+
88
+ this.socket.connect()
89
+
90
+ this.channel = this.socket.channel(this.channelTopic, {
91
+ authorization: `Bearer ${this.accessToken}`,
92
+ channelName: this.channelName,
93
+ })
94
+
95
+ this.start()
96
+
83
97
  this.channel.on('system', (msg) => {
84
98
  switch (msg.type) {
85
99
  case 'attach_channel_succeeded':
@@ -92,15 +106,6 @@ export default class ConversationConnector {
92
106
  }
93
107
  })
94
108
 
95
- this.socket.onClose(() => {
96
- log('[SOCKET]CLOSE')
97
- this.#emitConnectionState({
98
- connected: false,
99
- ready: false,
100
- currentState: 'socket_closed',
101
- })
102
- })
103
-
104
109
  this.channel.onClose(() => {
105
110
  log('[CHANNEL]CLOSE')
106
111
  this.#emitConnectionState({
@@ -184,6 +189,8 @@ export default class ConversationConnector {
184
189
  // If we only want to execute the callback once, remove it from the listener
185
190
  return !complete
186
191
  })
192
+
193
+ emitChange()
187
194
  }
188
195
 
189
196
  pushToChannel(
@@ -193,4 +200,13 @@ export default class ConversationConnector {
193
200
  ) {
194
201
  this.channel.push(command, payload, timeout)
195
202
  }
203
+
204
+ // Adds a callback function to the subscribers set and returns a method to unsubscribe from the store.
205
+ // This method is used to sync the state with the lifecycle of Preact.
206
+ static subscribe(callback: Function) {
207
+ subscribers.add(callback)
208
+
209
+ // Returns a function that removes the callback from the subscribers set.
210
+ return () => subscribers.delete(callback)
211
+ }
196
212
  }
@@ -4,10 +4,10 @@ import SeamlyOfflineError from 'api/errors/seamly-offline-error'
4
4
  import SeamlySessionExpiredError from 'api/errors/seamly-session-expired-error'
5
5
  import { SeamlyEventBusContext } from 'ui/components/core/seamly-api-context'
6
6
  import {
7
- useSeamlyApiContext,
8
7
  useSeamlyCommands,
9
8
  useSeamlyIdleDetachCountdown,
10
9
  } from 'ui/hooks/seamly-hooks'
10
+ import useSeamlyConversation from 'ui/hooks/use-seamly-conversation'
11
11
  import { featureKeys } from 'ui/utils/seamly-utils'
12
12
  import { setHasResponded } from 'domains/app/slice'
13
13
  import { clearInterrupt, setInterrupt } from 'domains/interrupt/slice'
@@ -38,7 +38,7 @@ import { setTranslationProposalPrompt } from 'domains/translations/slice'
38
38
  const EMITTABLE_MESSAGE_TYPES = ['text', 'choice_prompt', 'image', 'video']
39
39
 
40
40
  const SeamlyEventSubscriber = () => {
41
- const api = useSeamlyApiContext()
41
+ const conversation = useSeamlyConversation()
42
42
  const syncChannelRef = useRef<number>()
43
43
  const messageChannelRef = useRef<number>()
44
44
  const dispatch = useAppDispatch()
@@ -48,8 +48,8 @@ const SeamlyEventSubscriber = () => {
48
48
  const { emitEvent } = useSeamlyCommands()
49
49
 
50
50
  useEffect(() => {
51
- if (api.connectionInfo && api.conversation.socket) {
52
- const { socket } = api.conversation
51
+ if (conversation.socket) {
52
+ const { socket } = conversation
53
53
  socket.onError((err) => {
54
54
  const seamlyOfflineError = new SeamlyOfflineError(err)
55
55
  dispatch(
@@ -61,6 +61,7 @@ const SeamlyEventSubscriber = () => {
61
61
  originalError: seamlyOfflineError.originalError,
62
62
  }),
63
63
  )
64
+
64
65
  dispatch(clearEvents())
65
66
  })
66
67
 
@@ -68,11 +69,11 @@ const SeamlyEventSubscriber = () => {
68
69
  dispatch(clearInterrupt())
69
70
  })
70
71
  }
71
- }, [api, api.connectionInfo, api.conversation.socket, dispatch])
72
+ }, [conversation, dispatch])
72
73
 
73
74
  useEffect(() => {
74
- if (api.connectionInfo && api.conversation.socket) {
75
- api.conversation.onConnection(({ currentState }) => {
75
+ if (conversation.socket) {
76
+ conversation.onConnection(({ currentState }) => {
76
77
  if (currentState === 'join_channel_erred') {
77
78
  const seamlyGeneralError = new SeamlyGeneralError()
78
79
  dispatch(
@@ -91,251 +92,229 @@ const SeamlyEventSubscriber = () => {
91
92
  return false
92
93
  })
93
94
  }
94
- }, [api, api.connectionInfo, api.conversation.channel, dispatch])
95
+ }, [conversation, dispatch])
95
96
 
96
97
  useEffect(() => {
97
- if (api.connectionInfo) {
98
- const updateParticipant = (event: ParticipantEvent) => {
99
- if (event.type !== 'participant') {
100
- return
101
- }
102
- const { payload } = event
98
+ const updateParticipant = (event: ParticipantEvent) => {
99
+ if (event.type !== 'participant') {
100
+ return
101
+ }
102
+ const { payload } = event
103
103
 
104
- if (!payload || !payload.participant) {
105
- return
106
- }
104
+ if (!payload || !payload.participant) {
105
+ return
106
+ }
107
107
 
108
- const { fromClient, participant } = payload
108
+ const { fromClient, participant } = payload
109
109
 
110
- if (
111
- !fromClient &&
112
- typeof participant !== 'string' &&
113
- participant?.name
114
- ) {
115
- dispatch(setHeaderSubTitle(participant.name))
116
- }
110
+ if (!fromClient && typeof participant !== 'string' && participant?.name) {
111
+ dispatch(setHeaderSubTitle(participant.name))
112
+ }
117
113
 
118
- dispatch(
119
- setParticipant({
120
- participant,
121
- fromClient,
122
- }),
123
- )
114
+ dispatch(
115
+ setParticipant({
116
+ participant,
117
+ fromClient,
118
+ }),
119
+ )
124
120
 
125
- if (typeof participant !== 'string' && participant.introduction) {
126
- dispatch(addEvent(event))
127
- }
121
+ if (typeof participant !== 'string' && participant.introduction) {
122
+ dispatch(addEvent(event))
128
123
  }
124
+ }
129
125
 
130
- api.conversation.onConnection(({ connected }) => {
131
- if (!connected) return false
126
+ conversation.onConnection(({ connected }) => {
127
+ if (!connected) return false
132
128
 
133
- const { channel } = api.conversation
129
+ const { channel } = conversation
134
130
 
135
- channel.onMessage = (type, payload) => {
136
- const event = { type, payload }
131
+ channel.onMessage = (type, payload) => {
132
+ const event = { type, payload }
137
133
 
138
- switch (type) {
139
- case 'ui':
140
- if (payload.state && payload.state.hasOwnProperty('loading')) {
141
- dispatch(setIsLoading(payload.state.loading))
142
- }
143
- switch (payload.type) {
144
- case 'idle_detach_countdown':
145
- initCountdown(payload.body.duration)
146
- break
147
- case 'idle_detach_countdown_elapsed':
148
- endCountdown(undefined, true)
149
- break
150
- case 'resume_conversation_prompt':
151
- dispatch(initResumeConversationPrompt())
152
- break
153
- case 'translation_proposal':
154
- dispatch(setTranslationProposalPrompt(payload.body))
155
- break
156
- case 'user_first_response':
157
- dispatch(setHasResponded(true))
158
- // @ts-ignore
159
- eventBus.emit('system.userFirstResponse', payload.body)
160
- break
161
- }
162
- break
163
- case 'message':
164
- updateParticipant(payload)
165
- switch (payload.type) {
166
- case 'text':
167
- case 'choice_prompt':
168
- case 'splash':
169
- case 'image':
170
- case 'upload':
171
- case 'video':
172
- case 'cta':
173
- case 'custom':
174
- case 'carousel':
175
- case 'card':
176
- if (payload.service && payload.service.serviceSessionId) {
177
- dispatch(setActiveService(payload.service.serviceSessionId))
178
- }
179
-
180
- dispatch(addEvent(event as ChannelEvent))
181
- break
182
- }
183
- break
184
- case 'participant':
185
- updateParticipant(event as ParticipantEvent)
186
- break
187
- case 'service_data':
188
- if (payload.persist) {
189
- dispatch(setServiceDataItem(payload))
134
+ switch (type) {
135
+ case 'ui':
136
+ if (payload.state && payload.state.hasOwnProperty('loading')) {
137
+ dispatch(setIsLoading(payload.state.loading))
138
+ }
139
+ switch (payload.type) {
140
+ case 'idle_detach_countdown':
141
+ initCountdown(payload.body.duration)
142
+ break
143
+ case 'idle_detach_countdown_elapsed':
144
+ endCountdown(undefined, true)
145
+ break
146
+ case 'resume_conversation_prompt':
147
+ dispatch(initResumeConversationPrompt())
148
+ break
149
+ case 'translation_proposal':
150
+ dispatch(setTranslationProposalPrompt(payload.body))
151
+ break
152
+ case 'user_first_response':
153
+ dispatch(setHasResponded(true))
154
+ // @ts-ignore
155
+ eventBus.emit('system.userFirstResponse', payload.body)
156
+ break
157
+ }
158
+ break
159
+ case 'message':
160
+ updateParticipant(payload)
161
+ switch (payload.type) {
162
+ case 'text':
163
+ case 'choice_prompt':
164
+ case 'splash':
165
+ case 'image':
166
+ case 'upload':
167
+ case 'video':
168
+ case 'cta':
169
+ case 'custom':
170
+ case 'carousel':
171
+ case 'card':
172
+ if (payload.service && payload.service.serviceSessionId) {
173
+ dispatch(setActiveService(payload.service.serviceSessionId))
174
+ }
175
+
176
+ dispatch(addEvent(event as ChannelEvent))
177
+ break
178
+ }
179
+ break
180
+ case 'participant':
181
+ updateParticipant(event as ParticipantEvent)
182
+ break
183
+ case 'service_data':
184
+ if (payload.persist) {
185
+ dispatch(setServiceDataItem(payload))
186
+ }
187
+ break
188
+ case 'ack':
189
+ dispatch(ackEvent(event as AckEvent))
190
+ break
191
+ case 'system':
192
+ if (payload.type === 'service_changed') {
193
+ const { serviceSettings, ...eventPayload } = payload
194
+ const { entry, proactiveMessages } = serviceSettings
195
+ const { upload } = entry.options
196
+
197
+ dispatch(
198
+ setFeatureEnabledState({
199
+ key: featureKeys.uploads,
200
+ enabled: !!(upload && upload.enabled),
201
+ }),
202
+ )
203
+
204
+ dispatch(setProactiveMessages(proactiveMessages.enabled || false))
205
+
206
+ dispatch(setServiceEntryMetadata(entry))
207
+ if (payload.serviceSessionId) {
208
+ dispatch(setActiveService(payload.serviceSessionId))
190
209
  }
191
- break
192
- case 'ack':
193
- dispatch(ackEvent(event as AckEvent))
194
- break
195
- case 'system':
196
- if (payload.type === 'service_changed') {
197
- const { serviceSettings, ...eventPayload } = payload
198
- const { entry, proactiveMessages } = serviceSettings
199
- const { upload } = entry.options
200
210
 
211
+ emitEvent('system.serviceChanged', eventPayload)
212
+ }
213
+ break
214
+ case 'info':
215
+ if (
216
+ payload.type === 'divider' ||
217
+ payload.type === 'text' ||
218
+ payload.type === 'translation'
219
+ ) {
220
+ dispatch(addEvent(event as ChannelEvent))
221
+ }
222
+ break
223
+ case 'error':
224
+ switch (payload.type) {
225
+ case 'find_conversation_erred':
226
+ const seamlySessionExpiredError = new SeamlySessionExpiredError(
227
+ event,
228
+ )
201
229
  dispatch(
202
- setFeatureEnabledState({
203
- key: featureKeys.uploads,
204
- enabled: !!(upload && upload.enabled),
230
+ setInterrupt({
231
+ name: seamlySessionExpiredError.name,
232
+ action: seamlySessionExpiredError.action,
233
+ message: seamlySessionExpiredError.message,
234
+ originalEvent: seamlySessionExpiredError.originalEvent,
235
+ originalError: seamlySessionExpiredError.originalError,
205
236
  }),
206
237
  )
207
-
238
+ break
239
+ case 'conversation_erred':
240
+ case 'attach_channel_erred':
241
+ const seamlyGeneralError = new SeamlyGeneralError(event)
208
242
  dispatch(
209
- setProactiveMessages(proactiveMessages.enabled || false),
243
+ setInterrupt({
244
+ name: seamlyGeneralError.name,
245
+ message: seamlyGeneralError.message,
246
+ langKey: seamlyGeneralError.langKey,
247
+ originalEvent: seamlyGeneralError.originalEvent,
248
+ originalError: seamlyGeneralError.originalError,
249
+ action: seamlyGeneralError.action,
250
+ }),
210
251
  )
211
-
212
- dispatch(setServiceEntryMetadata(entry))
213
- if (payload.serviceSessionId) {
214
- dispatch(setActiveService(payload.serviceSessionId))
215
- }
216
-
217
- emitEvent('system.serviceChanged', eventPayload)
218
- }
219
- break
220
- case 'info':
221
- if (
222
- payload.type === 'divider' ||
223
- payload.type === 'text' ||
224
- payload.type === 'translation'
225
- ) {
226
- dispatch(addEvent(event as ChannelEvent))
227
- }
228
- break
229
- case 'error':
230
- switch (payload.type) {
231
- case 'find_conversation_erred':
232
- const seamlySessionExpiredError =
233
- new SeamlySessionExpiredError(event)
234
- dispatch(
235
- setInterrupt({
236
- name: seamlySessionExpiredError.name,
237
- action: seamlySessionExpiredError.action,
238
- message: seamlySessionExpiredError.message,
239
- originalEvent: seamlySessionExpiredError.originalEvent,
240
- originalError: seamlySessionExpiredError.originalError,
241
- }),
242
- )
243
- break
244
- case 'conversation_erred':
245
- case 'attach_channel_erred':
246
- const seamlyGeneralError = new SeamlyGeneralError(event)
247
- dispatch(
248
- setInterrupt({
249
- name: seamlyGeneralError.name,
250
- message: seamlyGeneralError.message,
251
- langKey: seamlyGeneralError.langKey,
252
- originalEvent: seamlyGeneralError.originalEvent,
253
- originalError: seamlyGeneralError.originalError,
254
- action: seamlyGeneralError.action,
255
- }),
256
- )
257
- break
258
- }
259
- }
260
-
261
- return payload
252
+ break
253
+ }
262
254
  }
263
255
 
264
- return false
265
- })
266
- }
267
- }, [
268
- api,
269
- api.connectionInfo,
270
- api.conversation.channel,
271
- dispatch,
272
- emitEvent,
273
- endCountdown,
274
- eventBus,
275
- initCountdown,
276
- ])
256
+ return payload
257
+ }
277
258
 
278
- useEffect(() => {
279
- if (api.connectionInfo) {
280
- api.conversation.onConnection(({ connected }) => {
281
- if (!connected) return false
259
+ return false
260
+ })
261
+ }, [conversation, dispatch, emitEvent, endCountdown, eventBus, initCountdown])
282
262
 
283
- const { channel } = api.conversation
263
+ useEffect(() => {
264
+ conversation.onConnection(({ connected }) => {
265
+ if (!connected) return false
284
266
 
285
- if (messageChannelRef.current) {
286
- channel?.off('message', messageChannelRef.current)
287
- }
267
+ const { channel } = conversation
288
268
 
289
- messageChannelRef.current = channel.on('message', (payload) => {
290
- if (!EMITTABLE_MESSAGE_TYPES.includes(payload.type)) {
291
- return payload
292
- }
269
+ if (messageChannelRef.current) {
270
+ channel.off('message', messageChannelRef.current)
271
+ }
293
272
 
294
- // This check dedupes the sending of messages via
295
- // the bus if a duplicate connection exists in an
296
- // error situation.
297
- if (payload.id !== prevEmittedEventId.current) {
298
- // @ts-ignore
299
- eventBus.emit('message', payload)
300
- }
301
- prevEmittedEventId.current = payload.id
273
+ messageChannelRef.current = channel.on('message', (payload) => {
274
+ if (!EMITTABLE_MESSAGE_TYPES.includes(payload.type)) {
302
275
  return payload
303
- })
276
+ }
304
277
 
305
- return true
278
+ // This check dedupes the sending of messages via
279
+ // the bus if a duplicate connection exists in an
280
+ // error situation.
281
+ if (payload.id !== prevEmittedEventId.current) {
282
+ // @ts-ignore
283
+ eventBus.emit('message', payload)
284
+ }
285
+ prevEmittedEventId.current = payload.id
286
+ return payload
306
287
  })
307
- }
308
- }, [api, api.connectionInfo, api.conversation.channel, eventBus])
288
+
289
+ return true
290
+ })
291
+ }, [conversation, eventBus])
309
292
 
310
293
  useEffect(() => {
311
- if (api.connectionInfo) {
312
- api.conversation.onConnection(({ connected }) => {
313
- if (!connected) return false
294
+ conversation.onConnection(({ connected }) => {
295
+ if (!connected) return false
314
296
 
315
- if (syncChannelRef.current) {
316
- api.conversation.channel?.off('sync', syncChannelRef.current)
317
- }
297
+ if (syncChannelRef.current) {
298
+ conversation.channel.off('sync', syncChannelRef.current)
299
+ }
318
300
 
319
- syncChannelRef.current = api.conversation.channel.on(
320
- 'sync',
321
- async (payload: {
322
- lastEvent: { id: string; occurredAt: number }
323
- }) => {
324
- try {
325
- const history = await dispatch(getConversation(payload)).unwrap()
326
- if (!history) return
327
-
328
- dispatch(setHistory(history))
329
- } catch (_e) {
330
- // nothing to do, the error is handled in the thunk
331
- }
332
- },
333
- )
301
+ syncChannelRef.current = conversation.channel.on(
302
+ 'sync',
303
+ async (payload: { lastEvent: { id: string; occurredAt: number } }) => {
304
+ try {
305
+ const history = await dispatch(getConversation(payload)).unwrap()
306
+ if (!history) return
334
307
 
335
- return true
336
- })
337
- }
338
- }, [api, api.connectionInfo, api.conversation.channel, dispatch])
308
+ dispatch(setHistory(history))
309
+ } catch (_e) {
310
+ // nothing to do, the error is handled in the thunk
311
+ }
312
+ },
313
+ )
314
+
315
+ return true
316
+ })
317
+ }, [conversation, dispatch])
339
318
 
340
319
  return null
341
320
  }
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'preact/hooks'
2
2
  // Import extracted hooks here for use inside this file
3
- import { useSeamlyApiContext } from './seamly-api-hooks'
3
+ import useSeamlyConversation from './use-seamly-conversation'
4
4
 
5
5
  // Export extracted hooks here,
6
6
  // although this is a redundant, it prevents a bazillion code changes for now.
@@ -54,11 +54,11 @@ export {
54
54
  // and imported directly from this file
55
55
  // Please do not remove
56
56
  export const useSeamlyEventStream = (nextFn, filterFn) => {
57
- const api = useSeamlyApiContext()
57
+ const conversation = useSeamlyConversation()
58
58
 
59
59
  useEffect(() => {
60
- if (api.connectionInfo && api.conversation?.channel) {
61
- const { channel } = api.conversation
60
+ if (conversation.channel) {
61
+ const { channel } = conversation
62
62
 
63
63
  channel.onMessage = (type, payload) => {
64
64
  if (!filterFn || filterFn({ type, payload })) {
@@ -68,5 +68,5 @@ export const useSeamlyEventStream = (nextFn, filterFn) => {
68
68
  return payload
69
69
  }
70
70
  }
71
- }, [nextFn, filterFn, api.connectionInfo, api.conversation])
71
+ }, [nextFn, filterFn, conversation])
72
72
  }
@@ -0,0 +1,13 @@
1
+ import { useSyncExternalStore } from 'preact/compat'
2
+ import ConversationConnector from 'api/conversation-connector'
3
+ import { useSeamlyApiContext } from './seamly-api-hooks'
4
+
5
+ const useSeamlyConversation = () => {
6
+ const api = useSeamlyApiContext()
7
+
8
+ const getSnapshot = () => api.conversation
9
+
10
+ return useSyncExternalStore(ConversationConnector.subscribe, getSnapshot)
11
+ }
12
+
13
+ export default useSeamlyConversation