mirror of
https://github.com/owncast/owncast.git
synced 2024-11-29 03:29:03 +03:00
Banned and chat disconnected states will hide chat. Closes #2764
This commit is contained in:
parent
2364293742
commit
0f58f8c0fe
7 changed files with 81 additions and 46 deletions
|
@ -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();
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue