diff --git a/packages/chat-app/src/components/Chat/Conversations/Sidenav/ProfileHeader.tsx b/packages/chat-app/src/components/Chat/Conversations/Sidenav/NavHeader.tsx similarity index 56% rename from packages/chat-app/src/components/Chat/Conversations/Sidenav/ProfileHeader.tsx rename to packages/chat-app/src/components/Chat/Conversations/Sidenav/NavHeader.tsx index c17ebb85d..be5464597 100644 --- a/packages/chat-app/src/components/Chat/Conversations/Sidenav/ProfileHeader.tsx +++ b/packages/chat-app/src/components/Chat/Conversations/Sidenav/NavHeader.tsx @@ -1,10 +1,25 @@ import { ActionButton, ActionLink, Plus, Times, t } from '@youfoundation/common-app'; import { CHAT_ROOT } from '../../../../templates/Chat/ChatHome'; -export const ProfileHeader = ({ closeSideNav }: { closeSideNav: (() => void) | undefined }) => { +export const NavHeader = ({ + closeSideNav, + isOnline, +}: { + closeSideNav: (() => void) | undefined; + isOnline: boolean; +}) => { return (
-

Homebase Chat

+
+ + +

Homebase Chat

+
{t('New')} diff --git a/packages/chat-app/src/hooks/chat/useLiveChatProcessor.ts b/packages/chat-app/src/hooks/chat/useLiveChatProcessor.ts index 5047d2402..025c8c77a 100644 --- a/packages/chat-app/src/hooks/chat/useLiveChatProcessor.ts +++ b/packages/chat-app/src/hooks/chat/useLiveChatProcessor.ts @@ -25,17 +25,18 @@ import { processCommand } from '../../providers/ChatCommandProvider'; const MINUTE_IN_MS = 60000; -// We first setup the websocket, and then trigger processing of the inbox -// So that new message will be detected by the websocket; +// We first process the inbox, then we connect for live updates; export const useLiveChatProcessor = () => { - // Setup websocket, so that we get notified instantly when a new message is received - const connected = useChatWebsocket(true); + // Process the inbox on startup; As we want to cover the backlog of messages first + const { status: inboxStatus } = useInboxProcessor(true); - // Process the inbox on startup (once the socket is connected) - const { status: inboxStatus } = useInboxProcessor(connected); + // Only after the inbox is processed, we connect for live updates; So we avoid clearing the cache on each fileAdded update + const isOnline = useChatWebsocket(inboxStatus === 'success'); // Only after the inbox is processed, we process commands as new ones might have been added via the inbox useChatCommandProcessor(inboxStatus === 'success'); + + return isOnline; }; // Process the inbox on startup diff --git a/packages/chat-app/src/templates/Chat/ChatHome.tsx b/packages/chat-app/src/templates/Chat/ChatHome.tsx index 81b3b5e3d..98667c7b6 100644 --- a/packages/chat-app/src/templates/Chat/ChatHome.tsx +++ b/packages/chat-app/src/templates/Chat/ChatHome.tsx @@ -8,7 +8,7 @@ import { useState } from 'react'; import { NewConversation } from '../../components/Chat/Conversations/Sidenav/NewConversation'; import { NewConversationGroup } from '../../components/Chat/Conversations/Sidenav/NewConversationGroup'; import { ConversationsSidebar } from '../../components/Chat/Conversations/Sidenav/ConversationsSidenav'; -import { ProfileHeader } from '../../components/Chat/Conversations/Sidenav/ProfileHeader'; +import { NavHeader } from '../../components/Chat/Conversations/Sidenav/NavHeader'; import { CHAT_APP_ID, ExtendPermissionDialog, @@ -25,7 +25,7 @@ export const ChatHome = () => { const { conversationKey } = useParams(); const [isSidenavOpen, setIsSidenavOpen] = useState(false); - useLiveChatProcessor(); + const isOnline = useLiveChatProcessor(); useMarkAllAsRead({ appId: CHAT_APP_ID }); return ( @@ -42,7 +42,11 @@ export const ChatHome = () => { // needsAllConnected={true} />
- +
{ const ChatSideNav = ({ isOpen, setIsSidenavOpen, + isOnline, }: { isOpen: boolean; setIsSidenavOpen: (newIsOpen: boolean) => void; + isOnline: boolean; }) => { const { conversationKey } = useParams(); const { logout } = useAuth(); @@ -93,7 +99,10 @@ const ChatSideNav = ({ ) : ( <> - setIsSidenavOpen(false)} /> + setIsSidenavOpen(false)} + isOnline={isOnline} + /> { diff --git a/packages/common-app/src/hooks/transitProcessor/useNotificationSubscriber.ts b/packages/common-app/src/hooks/transitProcessor/useNotificationSubscriber.ts index 9795fc772..8b80ba477 100644 --- a/packages/common-app/src/hooks/transitProcessor/useNotificationSubscriber.ts +++ b/packages/common-app/src/hooks/transitProcessor/useNotificationSubscriber.ts @@ -15,14 +15,13 @@ export const useNotificationSubscriber = ( subscriber: ((notification: TypedConnectionNotification) => void) | undefined, types: NotificationType[], drives: TargetDrive[] = [BlogConfig.FeedDrive, BlogConfig.PublicChannelDrive], - onDisconnect?: () => void + onDisconnect?: () => void, + onReconnect?: () => void ) => { const [isActive, setIsActive] = useState(false); const isConnected = useRef(false); const dotYouClient = useDotYouClient().getDotYouClient(); - const [shouldReconnect, setShouldReconnect] = useState(false); - const localHandler = subscriber ? (notification: TypedConnectionNotification) => { if (types?.length >= 1 && !types.includes(notification.notificationType)) return; @@ -40,16 +39,20 @@ export const useNotificationSubscriber = ( if (!isConnected.current && localHandler) { isConnected.current = true; (async () => { - await Subscribe(dotYouClient, drives, localHandler, () => { - isConnected.current = false; - setIsActive(false); - - setShouldReconnect(true); - - onDisconnect && onDisconnect(); - }); + await Subscribe( + dotYouClient, + drives, + localHandler, + () => { + setIsActive(false); + onDisconnect && onDisconnect(); + }, + () => { + setIsActive(true); + onReconnect && onReconnect(); + } + ); setIsActive(true); - setShouldReconnect(false); })(); } @@ -64,7 +67,7 @@ export const useNotificationSubscriber = ( } } }; - }, [subscriber, shouldReconnect]); + }, [subscriber]); return isActive; }; diff --git a/packages/js-lib/src/core/WebsocketData/WebsocketProvider.ts b/packages/js-lib/src/core/WebsocketData/WebsocketProvider.ts index 823a72a6d..d58f87cdb 100644 --- a/packages/js-lib/src/core/WebsocketData/WebsocketProvider.ts +++ b/packages/js-lib/src/core/WebsocketData/WebsocketProvider.ts @@ -24,9 +24,12 @@ const PING_INTERVAL = 1000 * 5 * 1; let pingInterval: NodeJS.Timeout | undefined; let lastPong: number | undefined; +let reconnectTimeout: NodeJS.Timeout | undefined; + const subscribers: { handler: (data: TypedConnectionNotification) => void; onDisconnect?: () => void; + onReconnect?: () => void; }[] = []; interface RawClientNotification { @@ -120,9 +123,9 @@ const ConnectSocket = async ( lastPong = Date.now(); pingInterval = setInterval(() => { if (lastPong && Date.now() - lastPong > PING_INTERVAL * 2) { - // 2 ping intervals have passed without a pong, force disconnect + // 2 ping intervals have passed without a pong, reconnect if (isDebug) console.debug(`[NotificationProvider] Ping timeout`); - DisconnectSocket(); + ReconnectSocket(dotYouClient, drives, args); return; } Notify({ @@ -154,21 +157,42 @@ const ConnectSocket = async ( webSocketClient.onerror = (e) => { console.error('[NotificationProvider]', e); - DisconnectSocket(); }; webSocketClient.onclose = (e) => { if (isDebug) console.debug('[NotificationProvider] Connection closed', e); - DisconnectSocket(); + + subscribers.map((subscriber) => subscriber.onDisconnect && subscriber.onDisconnect()); + ReconnectSocket(dotYouClient, drives, args); }; }); }; -const DisconnectSocket = async () => { - if (!webSocketClient) throw new Error('No active client to disconnect'); +const ReconnectSocket = async ( + dotYouClient: DotYouClient, + drives: TargetDrive[], + args?: unknown // Extra parameters to pass to WebSocket constructor; Only applicable for React Native...; TODO: Remove this +) => { + if (reconnectTimeout) return; + + reconnectTimeout = setTimeout(async () => { + reconnectTimeout = undefined; + webSocketClient = undefined; + lastPong = undefined; + isConnected = false; + clearInterval(pingInterval); + + if (isDebug) console.debug('[NotificationProvider] Reconnecting'); + await ConnectSocket(dotYouClient, drives, args); + subscribers.map((subscriber) => subscriber.onReconnect && subscriber.onReconnect()); + }, 5000); +}; + +const DisconnectSocket = async () => { try { - webSocketClient.close(1000, 'Normal Disconnect'); + if (!webSocketClient) console.warn('No active client to disconnect'); + else webSocketClient.close(1000, 'Normal Disconnect'); } catch (e) { // Ignore any errors on close, as we always want to clean up } @@ -188,6 +212,7 @@ export const Subscribe = async ( drives: TargetDrive[], handler: (data: TypedConnectionNotification) => void, onDisconnect?: () => void, + onReconnect?: () => void, args?: unknown // Extra parameters to pass to WebSocket constructor; Only applicable for React Native...; TODO: Remove this ) => { const apiType = dotYouClient.getType(); @@ -197,7 +222,7 @@ export const Subscribe = async ( } activeSs = sharedSecret; - subscribers.push({ handler, onDisconnect }); + subscribers.push({ handler, onDisconnect, onReconnect }); if (isDebug) console.debug(`[NotificationProvider] New subscriber (${subscribers.length})`);