diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx index 81c0fe60b..32d7850fc 100644 --- a/web/components/stores/ClientConfigStore.tsx +++ b/web/components/stores/ClientConfigStore.tsx @@ -35,8 +35,9 @@ const ACCESS_TOKEN_KEY = 'accessToken'; let serverStatusRefreshPoll: ReturnType; let hasBeenModeratorNotified = false; +let hasWebsocketDisconnected = false; -const serverConnectivityError = `Cannot connect to the Owncast service. Please check your internet connection or if needed, double check this Owncast server is running.`; +const serverConnectivityError = `Cannot connect to the Owncast service. Please check your internet connection and verify this Owncast server is running.`; // Server status is what gets updated such as viewer count, durations, // stream title, online/offline state, etc. @@ -112,6 +113,15 @@ export const removedMessageIdsAtom = atom({ default: [], }); +export const isChatAvailableSelector = selector({ + key: 'isChatAvailableSelector', + get: ({ get }) => { + const state: AppStateOptions = get(appStateAtom); + const accessToken: string = get(accessTokenAtom); + return accessToken && state.chatAvailable && !hasWebsocketDisconnected; + }, +}); + // Chat is visible if the user wishes it to be visible AND the required // chat state is set. export const isChatVisibleSelector = selector({ @@ -119,17 +129,7 @@ export const isChatVisibleSelector = selector({ get: ({ get }) => { const state: AppStateOptions = get(appStateAtom); const userVisibleToggle: boolean = get(chatVisibleToggleAtom); - const accessToken: string = get(accessTokenAtom); - return accessToken && state.chatAvailable && userVisibleToggle; - }, -}); - -export const isChatAvailableSelector = selector({ - key: 'isChatAvailableSelector', - get: ({ get }) => { - const state: AppStateOptions = get(appStateAtom); - const accessToken: string = get(accessTokenAtom); - return accessToken && state.chatAvailable; + return state.chatAvailable && userVisibleToggle && !hasWebsocketDisconnected; }, }); @@ -163,9 +163,9 @@ export const ClientConfigStore: FC = () => { const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom); const setChatAuthenticated = useSetRecoilState(chatAuthenticatedAtom); const [clientConfig, setClientConfig] = useRecoilState(clientConfigStateAtom); - const [, setServerStatus] = useRecoilState(serverStatusState); + const setServerStatus = useSetRecoilState(serverStatusState); const setClockSkew = useSetRecoilState(clockSkewAtom); - const [chatMessages, setChatMessages] = useRecoilState(chatMessagesAtom); + const setChatMessages = useSetRecoilState(chatMessagesAtom); const [accessToken, setAccessToken] = useRecoilState(accessTokenAtom); const setAppState = useSetRecoilState(appStateAtom); const setGlobalFatalErrorMessage = useSetRecoilState(fatalErrorStateAtom); @@ -281,6 +281,14 @@ export const ClientConfigStore: FC = () => { } }; + const handleSocketDisconnect = () => { + hasWebsocketDisconnected = true; + }; + + const handleSocketConnected = () => { + hasWebsocketDisconnected = false; + }; + const handleMessage = (message: SocketEvent) => { switch (message.type) { case MessageType.ERROR_NEEDS_REGISTRATION: @@ -328,6 +336,10 @@ export const ClientConfigStore: FC = () => { case MessageType.VISIBILITY_UPDATE: handleMessageVisibilityChange(message as MessageVisibilityEvent); break; + case MessageType.ERROR_USER_DISABLED: + console.log('User has been disabled'); + sendEvent([AppStateEvent.ChatUserDisabled]); + break; default: console.error('Unknown socket message type: ', message.type); } @@ -336,7 +348,9 @@ export const ClientConfigStore: FC = () => { const getChatHistory = async () => { try { const messages = await ChatService.getChatHistory(accessToken); - setChatMessages(currentState => [...currentState, ...messages]); + if (messages) { + setChatMessages(currentState => [...currentState, ...messages]); + } } catch (error) { console.error(`ChatService -> getChatHistory() ERROR: \n${error}`); } @@ -354,14 +368,15 @@ export const ClientConfigStore: FC = () => { ws = new WebsocketService(accessToken, '/ws', host); ws.handleMessage = handleMessage; + ws.socketDisconnected = handleSocketDisconnect; + ws.socketConnected = handleSocketConnected; setWebsocketService(ws); } catch (error) { console.error(`ChatService -> startChat() ERROR: \n${error}`); + sendEvent([AppStateEvent.ChatUserDisabled]); } }; - const handleChatNotification = () => {}; - // Read the config and status on initial load from a JSON string that lives // in window. This is placed there server-side and allows for fast initial // load times because we don't have to wait for the API calls to complete. @@ -393,11 +408,6 @@ export const ClientConfigStore: FC = () => { } }, [hasLoadedConfig, accessToken]); - // Notify about chat activity when backgrounded. - useEffect(() => { - handleChatNotification(); - }, [chatMessages]); - useEffect(() => { updateClientConfig(); handleUserRegistration(); diff --git a/web/components/stores/application-state.ts b/web/components/stores/application-state.ts index f0df0d412..d43f14339 100644 --- a/web/components/stores/application-state.ts +++ b/web/components/stores/application-state.ts @@ -63,6 +63,7 @@ export enum AppStateEvent { Offline = 'OFFLINE', // Stream is not live NeedsRegister = 'NEEDS_REGISTER', // Needs to register a chat user Fail = 'FAIL', // Error + ChatUserDisabled = 'CHAT_USER_DISABLED', // Chat user is disabled } const appStateModel = @@ -97,6 +98,9 @@ const appStateModel = OFFLINE: { target: 'goodbye', }, + CHAT_USER_DISABLED: { + target: 'chatUserDisabled', + }, }, }, offline: { @@ -124,6 +128,12 @@ const appStateModel = }, }, }, + chatUserDisabled: { + meta: { + ...ONLINE_STATE, + chatAvailable: false, + }, + }, }, }, serverFailure: { diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx index 8f8b3c3b4..a5a95b0ed 100644 --- a/web/components/ui/Content/Content.tsx +++ b/web/components/ui/Content/Content.tsx @@ -199,7 +199,7 @@ export const Content: FC = () => { setSupportsBrowserNotifications(isPushNotificationSupported() && browserNotificationsEnabled); }, [browserNotificationsEnabled]); - const showChat = !chatDisabled && isChatAvailable && isChatVisible; + const showChat = online && !chatDisabled && isChatVisible; return ( <> diff --git a/web/components/ui/Sidebar/Sidebar.tsx b/web/components/ui/Sidebar/Sidebar.tsx index 1e492aa50..45453e17d 100644 --- a/web/components/ui/Sidebar/Sidebar.tsx +++ b/web/components/ui/Sidebar/Sidebar.tsx @@ -2,6 +2,7 @@ import Sider from 'antd/lib/layout/Sider'; import { useRecoilValue } from 'recoil'; import { FC } from 'react'; import dynamic from 'next/dynamic'; +import { Spin } from 'antd'; import { ChatMessage } from '../../../interfaces/chat-message.model'; import styles from './Sidebar.module.scss'; @@ -25,7 +26,11 @@ export const Sidebar: FC = () => { const isChatAvailable = useRecoilValue(isChatAvailableSelector); if (!currentUser) { - return ; + return ( + + + + ); } const { id, isModerator, displayName } = currentUser; @@ -37,6 +42,7 @@ export const Sidebar: FC = () => { chatUserId={id} isModerator={isModerator} chatAvailable={isChatAvailable} + showInput={!!currentUser} /> ); diff --git a/web/services/chat-service.ts b/web/services/chat-service.ts index edf433a55..2e5837b58 100644 --- a/web/services/chat-service.ts +++ b/web/services/chat-service.ts @@ -19,8 +19,12 @@ export interface ChatStaticService { class ChatService { public static async getChatHistory(accessToken: string): Promise { - const response = await getUnauthedData(`${ENDPOINT}?accessToken=${accessToken}`); - return response; + try { + const response = await getUnauthedData(`${ENDPOINT}?accessToken=${accessToken}`); + return response; + } catch (e) { + return []; + } } public static async registerUser(username: string): Promise { diff --git a/web/services/websocket-service.ts b/web/services/websocket-service.ts index d6fdca2cd..31e60a2b3 100644 --- a/web/services/websocket-service.ts +++ b/web/services/websocket-service.ts @@ -10,6 +10,8 @@ export default class WebsocketService { accessToken: string; + host: string; + path: string; websocketReconnectTimer: ReturnType; @@ -20,20 +22,28 @@ export default class WebsocketService { handleMessage?: (message: SocketEvent) => void; + socketConnected: () => void; + + socketDisconnected: () => void; + constructor(accessToken, path, host) { this.accessToken = accessToken; this.path = path; this.websocketReconnectTimer = null; this.isShutdown = false; + this.host = host; this.createAndConnect = this.createAndConnect.bind(this); this.shutdown = this.shutdown.bind(this); - this.createAndConnect(host); + this.createAndConnect(); } - createAndConnect(host) { - const url = new URL(host); + createAndConnect() { + if (!this.host) { + return; + } + const url = new URL(this.host); url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; url.pathname = '/ws'; url.port = window.location.port === '3000' ? '8080' : window.location.port; @@ -52,11 +62,13 @@ export default class WebsocketService { if (this.websocketReconnectTimer) { clearTimeout(this.websocketReconnectTimer); } + this.socketConnected(); } // On ws error just close the socket and let it re-connect again for now. - onError(e) { - handleNetworkingError(`Socket error: ${e}`); + onError() { + handleNetworkingError(); + this.socketDisconnected(); this.websocket.close(); if (!this.isShutdown) { this.scheduleReconnect(); @@ -137,8 +149,8 @@ export default class WebsocketService { } } -function handleNetworkingError(error) { +function handleNetworkingError() { console.error( - `Chat has been disconnected and is likely not working for you. It's possible you were removed from chat. If this is a server configuration issue, visit troubleshooting steps to resolve. https://owncast.online/docs/troubleshooting/#chat-is-disabled: ${error}`, + `Chat has been disconnected and is likely not working for you. It's possible you were removed from chat. If this is a server configuration issue, visit troubleshooting steps to resolve. https://owncast.online/docs/troubleshooting/#chat-is-disabled`, ); } diff --git a/web/utils/apis.ts b/web/utils/apis.ts index 097889144..b18148eb5 100644 --- a/web/utils/apis.ts +++ b/web/utils/apis.ts @@ -150,21 +150,14 @@ export async function fetchData(url: string, options?: FetchOptions) { requestOptions.credentials = 'include'; } - try { - const response = await fetch(url, requestOptions); - const json = await response.json(); + const response = await fetch(url, requestOptions); + const json = await response.json(); - if (!response.ok) { - const message = json.message || `An error has occurred: ${response.status}`; - throw new Error(message); - } - return json; - } catch (error) { - console.error(error); - return error; - // console.log(error) - // throw new Error(error) + if (!response.ok) { + const message = json.message || `An error has occurred: ${response.status}`; + throw new Error(message); } + return json; } export async function getUnauthedData(url: string, options?: FetchOptions) {