Banned and chat disconnected states will hide chat. Closes #2764

This commit is contained in:
Gabe Kangas 2023-03-13 15:23:14 -07:00
parent 2364293742
commit 0f58f8c0fe
No known key found for this signature in database
GPG key ID: 4345B2060657F330
7 changed files with 81 additions and 46 deletions

View file

@ -35,8 +35,9 @@ const ACCESS_TOKEN_KEY = 'accessToken';
let serverStatusRefreshPoll: ReturnType<typeof setInterval>; let serverStatusRefreshPoll: ReturnType<typeof setInterval>;
let hasBeenModeratorNotified = false; 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, // Server status is what gets updated such as viewer count, durations,
// stream title, online/offline state, etc. // stream title, online/offline state, etc.
@ -112,6 +113,15 @@ export const removedMessageIdsAtom = atom<string[]>({
default: [], 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 is visible if the user wishes it to be visible AND the required
// chat state is set. // chat state is set.
export const isChatVisibleSelector = selector({ export const isChatVisibleSelector = selector({
@ -119,17 +129,7 @@ export const isChatVisibleSelector = selector({
get: ({ get }) => { get: ({ get }) => {
const state: AppStateOptions = get(appStateAtom); const state: AppStateOptions = get(appStateAtom);
const userVisibleToggle: boolean = get(chatVisibleToggleAtom); const userVisibleToggle: boolean = get(chatVisibleToggleAtom);
const accessToken: string = get(accessTokenAtom); return state.chatAvailable && userVisibleToggle && !hasWebsocketDisconnected;
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;
}, },
}); });
@ -163,9 +163,9 @@ export const ClientConfigStore: FC = () => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom); const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom);
const setChatAuthenticated = useSetRecoilState<boolean>(chatAuthenticatedAtom); const setChatAuthenticated = useSetRecoilState<boolean>(chatAuthenticatedAtom);
const [clientConfig, setClientConfig] = useRecoilState<ClientConfig>(clientConfigStateAtom); const [clientConfig, setClientConfig] = useRecoilState<ClientConfig>(clientConfigStateAtom);
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState); const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
const setClockSkew = useSetRecoilState<Number>(clockSkewAtom); const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
const [chatMessages, setChatMessages] = useRecoilState<SocketEvent[]>(chatMessagesAtom); const setChatMessages = useSetRecoilState<SocketEvent[]>(chatMessagesAtom);
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom); const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom); const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom); const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
@ -281,6 +281,14 @@ export const ClientConfigStore: FC = () => {
} }
}; };
const handleSocketDisconnect = () => {
hasWebsocketDisconnected = true;
};
const handleSocketConnected = () => {
hasWebsocketDisconnected = false;
};
const handleMessage = (message: SocketEvent) => { const handleMessage = (message: SocketEvent) => {
switch (message.type) { switch (message.type) {
case MessageType.ERROR_NEEDS_REGISTRATION: case MessageType.ERROR_NEEDS_REGISTRATION:
@ -328,6 +336,10 @@ export const ClientConfigStore: FC = () => {
case MessageType.VISIBILITY_UPDATE: case MessageType.VISIBILITY_UPDATE:
handleMessageVisibilityChange(message as MessageVisibilityEvent); handleMessageVisibilityChange(message as MessageVisibilityEvent);
break; break;
case MessageType.ERROR_USER_DISABLED:
console.log('User has been disabled');
sendEvent([AppStateEvent.ChatUserDisabled]);
break;
default: default:
console.error('Unknown socket message type: ', message.type); console.error('Unknown socket message type: ', message.type);
} }
@ -336,7 +348,9 @@ export const ClientConfigStore: FC = () => {
const getChatHistory = async () => { const getChatHistory = async () => {
try { try {
const messages = await ChatService.getChatHistory(accessToken); const messages = await ChatService.getChatHistory(accessToken);
setChatMessages(currentState => [...currentState, ...messages]); if (messages) {
setChatMessages(currentState => [...currentState, ...messages]);
}
} catch (error) { } catch (error) {
console.error(`ChatService -> getChatHistory() ERROR: \n${error}`); console.error(`ChatService -> getChatHistory() ERROR: \n${error}`);
} }
@ -354,14 +368,15 @@ export const ClientConfigStore: FC = () => {
ws = new WebsocketService(accessToken, '/ws', host); ws = new WebsocketService(accessToken, '/ws', host);
ws.handleMessage = handleMessage; ws.handleMessage = handleMessage;
ws.socketDisconnected = handleSocketDisconnect;
ws.socketConnected = handleSocketConnected;
setWebsocketService(ws); setWebsocketService(ws);
} catch (error) { } catch (error) {
console.error(`ChatService -> startChat() ERROR: \n${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 // 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 // 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. // 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]); }, [hasLoadedConfig, accessToken]);
// Notify about chat activity when backgrounded.
useEffect(() => {
handleChatNotification();
}, [chatMessages]);
useEffect(() => { useEffect(() => {
updateClientConfig(); updateClientConfig();
handleUserRegistration(); handleUserRegistration();

View file

@ -63,6 +63,7 @@ export enum AppStateEvent {
Offline = 'OFFLINE', // Stream is not live Offline = 'OFFLINE', // Stream is not live
NeedsRegister = 'NEEDS_REGISTER', // Needs to register a chat user NeedsRegister = 'NEEDS_REGISTER', // Needs to register a chat user
Fail = 'FAIL', // Error Fail = 'FAIL', // Error
ChatUserDisabled = 'CHAT_USER_DISABLED', // Chat user is disabled
} }
const appStateModel = const appStateModel =
@ -97,6 +98,9 @@ const appStateModel =
OFFLINE: { OFFLINE: {
target: 'goodbye', target: 'goodbye',
}, },
CHAT_USER_DISABLED: {
target: 'chatUserDisabled',
},
}, },
}, },
offline: { offline: {
@ -124,6 +128,12 @@ const appStateModel =
}, },
}, },
}, },
chatUserDisabled: {
meta: {
...ONLINE_STATE,
chatAvailable: false,
},
},
}, },
}, },
serverFailure: { serverFailure: {

View file

@ -199,7 +199,7 @@ export const Content: FC = () => {
setSupportsBrowserNotifications(isPushNotificationSupported() && browserNotificationsEnabled); setSupportsBrowserNotifications(isPushNotificationSupported() && browserNotificationsEnabled);
}, [browserNotificationsEnabled]); }, [browserNotificationsEnabled]);
const showChat = !chatDisabled && isChatAvailable && isChatVisible; const showChat = online && !chatDisabled && isChatVisible;
return ( return (
<> <>

View file

@ -2,6 +2,7 @@ import Sider from 'antd/lib/layout/Sider';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { FC } from 'react'; import { FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { Spin } from 'antd';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
import styles from './Sidebar.module.scss'; import styles from './Sidebar.module.scss';
@ -25,7 +26,11 @@ export const Sidebar: FC = () => {
const isChatAvailable = useRecoilValue(isChatAvailableSelector); const isChatAvailable = useRecoilValue(isChatAvailableSelector);
if (!currentUser) { if (!currentUser) {
return <Sider className={styles.root} collapsedWidth={0} width={320} />; return (
<Sider className={styles.root} collapsedWidth={0} width={320}>
<Spin spinning size="large" />
</Sider>
);
} }
const { id, isModerator, displayName } = currentUser; const { id, isModerator, displayName } = currentUser;
@ -37,6 +42,7 @@ export const Sidebar: FC = () => {
chatUserId={id} chatUserId={id}
isModerator={isModerator} isModerator={isModerator}
chatAvailable={isChatAvailable} chatAvailable={isChatAvailable}
showInput={!!currentUser}
/> />
</Sider> </Sider>
); );

View file

@ -19,8 +19,12 @@ export interface ChatStaticService {
class ChatService { class ChatService {
public static async getChatHistory(accessToken: string): Promise<ChatMessage[]> { public static async getChatHistory(accessToken: string): Promise<ChatMessage[]> {
const response = await getUnauthedData(`${ENDPOINT}?accessToken=${accessToken}`); try {
return response; const response = await getUnauthedData(`${ENDPOINT}?accessToken=${accessToken}`);
return response;
} catch (e) {
return [];
}
} }
public static async registerUser(username: string): Promise<UserRegistrationResponse> { public static async registerUser(username: string): Promise<UserRegistrationResponse> {

View file

@ -10,6 +10,8 @@ export default class WebsocketService {
accessToken: string; accessToken: string;
host: string;
path: string; path: string;
websocketReconnectTimer: ReturnType<typeof setTimeout>; websocketReconnectTimer: ReturnType<typeof setTimeout>;
@ -20,20 +22,28 @@ export default class WebsocketService {
handleMessage?: (message: SocketEvent) => void; handleMessage?: (message: SocketEvent) => void;
socketConnected: () => void;
socketDisconnected: () => void;
constructor(accessToken, path, host) { constructor(accessToken, path, host) {
this.accessToken = accessToken; this.accessToken = accessToken;
this.path = path; this.path = path;
this.websocketReconnectTimer = null; this.websocketReconnectTimer = null;
this.isShutdown = false; this.isShutdown = false;
this.host = host;
this.createAndConnect = this.createAndConnect.bind(this); this.createAndConnect = this.createAndConnect.bind(this);
this.shutdown = this.shutdown.bind(this); this.shutdown = this.shutdown.bind(this);
this.createAndConnect(host); this.createAndConnect();
} }
createAndConnect(host) { createAndConnect() {
const url = new URL(host); if (!this.host) {
return;
}
const url = new URL(this.host);
url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
url.pathname = '/ws'; url.pathname = '/ws';
url.port = window.location.port === '3000' ? '8080' : window.location.port; url.port = window.location.port === '3000' ? '8080' : window.location.port;
@ -52,11 +62,13 @@ export default class WebsocketService {
if (this.websocketReconnectTimer) { if (this.websocketReconnectTimer) {
clearTimeout(this.websocketReconnectTimer); clearTimeout(this.websocketReconnectTimer);
} }
this.socketConnected();
} }
// On ws error just close the socket and let it re-connect again for now. // On ws error just close the socket and let it re-connect again for now.
onError(e) { onError() {
handleNetworkingError(`Socket error: ${e}`); handleNetworkingError();
this.socketDisconnected();
this.websocket.close(); this.websocket.close();
if (!this.isShutdown) { if (!this.isShutdown) {
this.scheduleReconnect(); this.scheduleReconnect();
@ -137,8 +149,8 @@ export default class WebsocketService {
} }
} }
function handleNetworkingError(error) { function handleNetworkingError() {
console.error( 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`,
); );
} }

View file

@ -150,21 +150,14 @@ export async function fetchData(url: string, options?: FetchOptions) {
requestOptions.credentials = 'include'; requestOptions.credentials = 'include';
} }
try { const response = await fetch(url, requestOptions);
const response = await fetch(url, requestOptions); const json = await response.json();
const json = await response.json();
if (!response.ok) { if (!response.ok) {
const message = json.message || `An error has occurred: ${response.status}`; const message = json.message || `An error has occurred: ${response.status}`;
throw new Error(message); throw new Error(message);
}
return json;
} catch (error) {
console.error(error);
return error;
// console.log(error)
// throw new Error(error)
} }
return json;
} }
export async function getUnauthedData(url: string, options?: FetchOptions) { export async function getUnauthedData(url: string, options?: FetchOptions) {