diff --git a/frontend/chat-plugin/src/components/chat/index.module.scss b/frontend/chat-plugin/src/components/chat/index.module.scss index 3b62d2368f..b8b8afb786 100644 --- a/frontend/chat-plugin/src/components/chat/index.module.scss +++ b/frontend/chat-plugin/src/components/chat/index.module.scss @@ -74,3 +74,25 @@ .messages::-webkit-scrollbar { display: none; } + +.connectedContainer { + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + position: relative; +} + +.disconnectedOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(1, 1, 1, 0.7); + display: flex; + justify-content: center; + align-items: center; + color: white; +} diff --git a/frontend/chat-plugin/src/components/chat/index.tsx b/frontend/chat-plugin/src/components/chat/index.tsx index 35bc45cb79..1b7e35a999 100644 --- a/frontend/chat-plugin/src/components/chat/index.tsx +++ b/frontend/chat-plugin/src/components/chat/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {useState, useEffect} from 'react'; import {IMessage} from '@stomp/stompjs'; -import WebSocket from '../../websocket'; +import WebSocket, {ConnectionState} from '../../websocket'; import MessageProp from '../../components/message'; import InputBarProp from '../../components/inputBar'; import AiryInputBar from '../../airyRenderProps/AiryInputBar'; @@ -16,7 +16,6 @@ import AiryBubble from '../../airyRenderProps/AiryBubble'; import {MessagePayload, SenderType, MessageState, isFromContact, Message} from 'httpclient'; import {SourceMessage, CommandUnion} from 'render'; import {MessageInfoWrapper} from 'render/components/MessageInfoWrapper'; -import {getResumeTokenFromStorage} from '../../storage'; /* eslint-disable @typescript-eslint/no-var-requires */ const camelcaseKeys = require('camelcase-keys'); @@ -42,9 +41,12 @@ const Chat = (props: Props) => { const [isChatHidden, setIsChatHidden] = useState(true); const [messages, setMessages] = useState([defaultWelcomeMessage]); const [messageString, setMessageString] = useState(''); + const [connectionState, setConnectionState] = useState(null); useEffect(() => { - ws = new WebSocket(props.channelId, onReceive, setInitialMessages, getResumeTokenFromStorage(props.channelId)); + ws = new WebSocket(props.channelId, onReceive, setInitialMessages, (state: ConnectionState) => { + setConnectionState(state); + }); ws.start().catch(error => { console.error(error); setInstallError(error.message); @@ -143,35 +145,40 @@ const Chat = (props: Props) => { {!isChatHidden && (
-
-
- {messages.map((message, index: number) => { - const nextMessage = messages[index + 1]; - const lastInGroup = nextMessage ? isFromContact(message) !== isFromContact(nextMessage) : true; - - return ( - props.airyMessageProp(ctrl) - : () => ( - - - - ) - } - /> - ); - })} +
+
+
+ {messages.map((message, index: number) => { + const nextMessage = messages[index + 1]; + const lastInGroup = nextMessage ? isFromContact(message) !== isFromContact(nextMessage) : true; + + return ( + props.airyMessageProp(ctrl) + : () => ( + + + + ) + } + /> + ); + })} +
+ + {connectionState === ConnectionState.Disconnected && ( +
Reconnecting...
+ )}
-
)} diff --git a/frontend/chat-plugin/src/websocket/index.ts b/frontend/chat-plugin/src/websocket/index.ts index 9e2d804e3c..74b1cba896 100644 --- a/frontend/chat-plugin/src/websocket/index.ts +++ b/frontend/chat-plugin/src/websocket/index.ts @@ -3,40 +3,51 @@ import 'regenerator-runtime/runtime'; import {start, getResumeToken, sendMessage} from '../api'; import {SuggestionResponse, TextContent} from 'render/providers/chatplugin/chatPluginModel'; import {Message} from 'httpclient'; -import {resetStorage} from '../storage'; +import {getResumeTokenFromStorage, resetStorage} from '../storage'; + /* eslint-disable @typescript-eslint/no-var-requires */ const camelcaseKeys = require('camelcase-keys'); -declare const window: { +declare global { + interface Window { airy: { host: string; channelId: string; noTLS: boolean; }; -}; +} +} const API_HOST = window.airy ? window.airy.host : 'chatplugin.airy'; // https: -> wss: and http: -> ws: const protocol = location.protocol.replace('http', 'ws'); +export enum ConnectionState{ + Connected = "CONNECTED", + Disconnected = "DISCONNECTED" +} + class WebSocket { client: Client; channelId: string; token: string; - resumeToken: string; setInitialMessages: (messages: Array) => void; onReceive: messageCallbackType; + reconnectTimeout: number; + isConnected: boolean; + updateConnectionState: (state: ConnectionState) => void; constructor( channelId: string, onReceive: messageCallbackType, setInitialMessages: (messages: Array) => void, - resumeToken?: string + updateConnectionState: (state: ConnectionState) => void ) { this.channelId = channelId; this.onReceive = onReceive; - this.resumeToken = resumeToken; this.setInitialMessages = setInitialMessages; + this.isConnected = false; + this.updateConnectionState = updateConnectionState; } connect = (token: string) => { @@ -50,12 +61,13 @@ class WebSocket { debug: function (str) { console.info(str); }, - reconnectDelay: 5000, + reconnectDelay: 0, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, }); this.client.onConnect = this.onConnect; + this.client.onWebSocketClose = this.onWebSocketClose; this.client.onStompError = function (frame: IFrame) { console.error('Broker reported error: ' + frame.headers['message']); @@ -68,7 +80,8 @@ class WebSocket { onSend = (message: TextContent | SuggestionResponse) => sendMessage(message, this.token); start = async () => { - const response = await start(this.channelId, this.resumeToken); + const resumeToken = getResumeTokenFromStorage(this.channelId) + const response = await start(this.channelId, resumeToken); if (response.token && response.messages) { this.connect(response.token); this.setInitialMessages( @@ -77,7 +90,7 @@ class WebSocket { sentAt: new Date(message.sent_at), })) ); - if (!this.resumeToken) { + if (!resumeToken) { await getResumeToken(this.channelId, this.token); } } else { @@ -87,7 +100,27 @@ class WebSocket { onConnect = () => { this.client.subscribe('/user/queue/message', this.onReceive); + this.isConnected = true; + clearTimeout(this.reconnectTimeout) + this.updateConnectionState(ConnectionState.Connected) }; + + tryReconnect = () => { + this.reconnectTimeout = window.setTimeout(this.reconnect, 5000) + } + + reconnect = () => { + if (!this.isConnected) { + this.reconnectTimeout = window.setTimeout(this.reconnect, 5000) + this.start(); + } + } + + onWebSocketClose = () => { + this.isConnected = false; + this.updateConnectionState(ConnectionState.Disconnected) + this.tryReconnect() + } } export default WebSocket;