Skip to content

Commit

Permalink
Refactor live chat messages (#61)
Browse files Browse the repository at this point in the history
* Added ExtendPermissionDialog;

* Added more safer checks on the public channel id;

* Add support to request allConnectedCircle;

* Add support to request allConnectedCircle;

* Small ui fixes; Added better message on "noErrorCode"

* Added follow homebase.id CTA on the social feed;

* Hide follow link fully;

* Added clear of the cache on logout;

* Moved all isDebug checks to use the helper for it;

* Moved all isDebug checks to use the helper for it;

* Changed order of liveChatMessages;

* Show online state within chat;

* Better websocket handling; Reconnects are done form the WebsocketProvider now;
  • Loading branch information
stef-coenen authored Jan 18, 2024
1 parent ca80d41 commit 6a7d125
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-row items-center gap-2 p-2 lg:p-5">
<p className="text-2xl dark:text-white">Homebase Chat</p>
<div className="flex flex-row items-center gap-2">
<span
className={`h-3 w-3 rounded-full transition-colors ${
isOnline ? 'bg-green-400' : 'bg-red-400'
}`}
title={isOnline ? t('Connected') : t('Offline')}
/>

<p className="text-2xl dark:text-white">Homebase Chat</p>
</div>
<div className="ml-auto flex flex-row items-center gap-2">
<ActionLink href={`${CHAT_ROOT}/new`} icon={Plus} type="secondary">
{t('New')}
Expand Down
13 changes: 7 additions & 6 deletions packages/chat-app/src/hooks/chat/useLiveChatProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions packages/chat-app/src/templates/Chat/ChatHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand All @@ -42,7 +42,11 @@ export const ChatHome = () => {
// needsAllConnected={true}
/>
<div className={`flex h-[100dvh] w-full flex-row overflow-hidden`}>
<ChatSideNav isOpen={isSidenavOpen} setIsSidenavOpen={setIsSidenavOpen} />
<ChatSideNav
isOpen={isSidenavOpen}
setIsSidenavOpen={setIsSidenavOpen}
isOnline={isOnline}
/>

<div className="h-full w-full flex-grow bg-background">
<ChatDetail
Expand All @@ -58,9 +62,11 @@ export const ChatHome = () => {
const ChatSideNav = ({
isOpen,
setIsSidenavOpen,
isOnline,
}: {
isOpen: boolean;
setIsSidenavOpen: (newIsOpen: boolean) => void;
isOnline: boolean;
}) => {
const { conversationKey } = useParams();
const { logout } = useAuth();
Expand Down Expand Up @@ -93,7 +99,10 @@ const ChatSideNav = ({
<NewConversationGroup />
) : (
<>
<ProfileHeader closeSideNav={isRoot ? undefined : () => setIsSidenavOpen(false)} />
<NavHeader
closeSideNav={isRoot ? undefined : () => setIsSidenavOpen(false)}
isOnline={isOnline}
/>
<ConversationsSidebar
activeConversationId={conversationKey}
openConversation={(newId) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(false);
const isConnected = useRef<boolean>(false);
const dotYouClient = useDotYouClient().getDotYouClient();

const [shouldReconnect, setShouldReconnect] = useState<boolean>(false);

const localHandler = subscriber
? (notification: TypedConnectionNotification) => {
if (types?.length >= 1 && !types.includes(notification.notificationType)) return;
Expand All @@ -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);
})();
}

Expand All @@ -64,7 +67,7 @@ export const useNotificationSubscriber = (
}
}
};
}, [subscriber, shouldReconnect]);
}, [subscriber]);

return isActive;
};
41 changes: 33 additions & 8 deletions packages/js-lib/src/core/WebsocketData/WebsocketProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
}
Expand All @@ -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();
Expand All @@ -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})`);

Expand Down

0 comments on commit 6a7d125

Please sign in to comment.