@@ -20,7 +20,8 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
2020import { computed , inject , onBeforeUnmount , provide , ref , watch } from 'vue'
2121import { START_LOCATION , useRoute } from 'vue-router'
2222import { useStore } from 'vuex'
23- import { CHAT , MESSAGE } from '../constants.ts'
23+ import { CHAT , CONFIG , MESSAGE } from '../constants.ts'
24+ import { getTalkConfig } from '../services/CapabilitiesManager.ts'
2425import { EventBus } from '../services/EventBus.ts'
2526import { useChatStore } from '../stores/chat.ts'
2627import { useChatExtrasStore } from '../stores/chatExtras.ts'
@@ -41,6 +42,7 @@ type GetMessagesContext = {
4142}
4243
4344const GET_MESSAGES_CONTEXT_KEY : InjectionKey < GetMessagesContext > = Symbol . for ( 'GET_MESSAGES_CONTEXT' )
45+ const experimentalChatRelay = ( getTalkConfig ( 'local' , 'experiments' , 'enabled' ) ?? 0 ) & CONFIG . EXPERIMENTAL . CHAT_RELAY
4446
4547/**
4648 * Check whether caught error is from OCS API
@@ -54,6 +56,7 @@ function isAxiosErrorResponse(exception: unknown): exception is AxiosError<strin
5456let pollingTimeout : NodeJS . Timeout | undefined
5557let expirationInterval : NodeJS . Timeout | undefined
5658let pollingErrorTimeout = 1_000
59+ let chatRelaySupported = false
5760
5861/**
5962 * Composable to provide control logic for fetching messages list
@@ -74,6 +77,7 @@ export function useGetMessagesProvider() {
7477 const loadingNewMessages = ref ( false )
7578 const isInitialisingMessages = ref ( true )
7679 const stopFetchingOldMessages = ref ( false )
80+ let chatRelayEnabled = false
7781
7882 /**
7983 * Returns whether the current participant is a participant of current conversation.
@@ -144,12 +148,14 @@ export function useGetMessagesProvider() {
144148 }
145149 if ( oldToken && oldToken !== newToken ) {
146150 store . dispatch ( 'cancelPollNewMessages' , { requestId : oldToken } )
151+ stopChatRelay ( )
147152 }
148153
149154 if ( newToken && canGetMessages ) {
150155 handleStartGettingMessagesPreconditions ( newToken )
151156 } else {
152157 store . dispatch ( 'cancelPollNewMessages' , { requestId : newToken } )
158+ stopChatRelay ( )
153159 }
154160
155161 /** Remove expired messages when joining a room */
@@ -162,6 +168,7 @@ export function useGetMessagesProvider() {
162168 subscribe ( 'networkOnline' , handleNetworkOnline )
163169 EventBus . on ( 'route-change' , onRouteChange )
164170 EventBus . on ( 'set-context-id-to-bottom' , setContextIdToBottom )
171+ EventBus . on ( 'signaling-supported-features' , checkChatRelaySupport )
165172
166173 /** Every 30 seconds we remove expired messages from the store */
167174 expirationInterval = setInterval ( ( ) => {
@@ -173,8 +180,11 @@ export function useGetMessagesProvider() {
173180 unsubscribe ( 'networkOnline' , handleNetworkOnline )
174181 EventBus . off ( 'route-change' , onRouteChange )
175182 EventBus . off ( 'set-context-id-to-bottom' , setContextIdToBottom )
183+ EventBus . off ( 'signaling-message-received' , addMessageFromChatRelay )
184+ EventBus . off ( 'signaling-supported-features' , checkChatRelaySupport )
176185
177186 store . dispatch ( 'cancelPollNewMessages' , { requestId : currentToken . value } )
187+ stopChatRelay ( )
178188 clearInterval ( pollingTimeout )
179189 clearInterval ( expirationInterval )
180190 } )
@@ -195,6 +205,7 @@ export function useGetMessagesProvider() {
195205 if ( currentToken . value ) {
196206 console . debug ( 'Canceling message request as we are offline' )
197207 store . dispatch ( 'cancelPollNewMessages' , { requestId : currentToken . value } )
208+ stopChatRelay ( )
198209 }
199210 }
200211
@@ -482,6 +493,10 @@ export function useGetMessagesProvider() {
482493 * @param token token of conversation where a method was called
483494 */
484495 async function pollNewMessages ( token : string ) {
496+ if ( chatRelayEnabled ) {
497+ // Stop polling if chat relay is supported
498+ return
499+ }
485500 // Check that the token has not changed
486501 if ( currentToken . value !== token ) {
487502 console . debug ( `token has changed to ${ currentToken . value } , breaking the loop for ${ token } ` )
@@ -499,6 +514,7 @@ export function useGetMessagesProvider() {
499514 requestId : token ,
500515 } )
501516 debugTimer . end ( `${ token } | long polling` , 'status 200' )
517+ tryChatRelay ( )
502518 } catch ( exception ) {
503519 if ( Axios . isCancel ( exception ) ) {
504520 debugTimer . end ( `${ token } | long polling` , 'cancelled' )
@@ -512,6 +528,7 @@ export function useGetMessagesProvider() {
512528 // This is not an error, so reset error timeout and poll again
513529 pollingErrorTimeout = 1_000
514530 clearTimeout ( pollingTimeout )
531+ tryChatRelay ( { force : true } )
515532 pollingTimeout = setTimeout ( ( ) => {
516533 pollNewMessages ( token )
517534 } , 500 )
@@ -539,6 +556,70 @@ export function useGetMessagesProvider() {
539556 } , 500 )
540557 }
541558
559+ /**
560+ * Try to start chat relay
561+ *
562+ * @param options
563+ * @param options.force - to skip end reached check when it is guaranteed
564+ */
565+ function tryChatRelay ( options ?: { force : boolean } ) {
566+ if ( chatRelaySupported && ( isChatEndReached . value || options ?. force ) ) {
567+ startChatRelay ( )
568+ }
569+ }
570+
571+ /**
572+ * Check whether chat relay is supported
573+ *
574+ * @param features
575+ */
576+ function checkChatRelaySupport ( features : string [ ] ) {
577+ if ( experimentalChatRelay && features . includes ( 'chat-relay' ) ) {
578+ chatRelaySupported = true
579+ }
580+ }
581+
582+ /**
583+ * Initialize chat relay support by stopping polling and listening to chat relay messages
584+ */
585+ function startChatRelay ( ) {
586+ if ( currentToken . value ) {
587+ // it might have been set already, ensure we cancel it
588+ store . dispatch ( 'cancelPollNewMessages' , { requestId : currentToken . value } )
589+ stopChatRelay ( )
590+ }
591+ chatRelayEnabled = true
592+ EventBus . on ( 'signaling-message-received' , addMessageFromChatRelay )
593+ }
594+
595+ /**
596+ *
597+ * Chat relay sends one message at a time, we update our stores directly
598+ *
599+ * @param payload
600+ * @param payload.token
601+ * @param payload.message
602+ */
603+ function addMessageFromChatRelay ( payload : { token : string , message : ChatMessage } ) {
604+ const { token, message } = payload
605+ if ( token !== currentToken . value ) {
606+ // Guard: Message is for another conversation
607+ // e.g., user switched conversation while messages were in-flight
608+ return
609+ }
610+
611+ chatStore . processChatBlocks ( token , [ message ] , { mergeBy : chatStore . getLastKnownId ( token ) } )
612+ store . dispatch ( 'processMessage' , { token, message } )
613+ }
614+
615+ /**
616+ * Stop chat relay and remove listener
617+ */
618+ function stopChatRelay ( ) {
619+ chatRelayEnabled = false
620+ EventBus . off ( 'signaling-message-received' , addMessageFromChatRelay )
621+ }
622+
542623 provide ( GET_MESSAGES_CONTEXT_KEY , {
543624 contextMessageId,
544625 loadingOldMessages,
0 commit comments