mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-04 05:17:27 +08:00 
			
		
		
		
	Handle centralized app state and registration + chat history
This commit is contained in:
		@ -18,6 +18,7 @@ func ExternalGetChatMessages(integration user.ExternalAPIUser, w http.ResponseWr
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// GetChatMessages gets all of the chat messages.
 | 
					// GetChatMessages gets all of the chat messages.
 | 
				
			||||||
func GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) {
 | 
					func GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						middleware.EnableCors(w)
 | 
				
			||||||
	getChatMessages(w, r)
 | 
						getChatMessages(w, r)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,7 +42,16 @@ func getChatMessages(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// RegisterAnonymousChatUser will register a new user.
 | 
					// RegisterAnonymousChatUser will register a new user.
 | 
				
			||||||
func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
 | 
					func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if r.Method != POST {
 | 
						middleware.EnableCors(w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if r.Method == "OPTIONS" {
 | 
				
			||||||
 | 
							// All OPTIONS requests should have a wildcard CORS header.
 | 
				
			||||||
 | 
							w.Header().Set("Access-Control-Allow-Origin", "*")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusNoContent)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if r.Method != http.MethodPost {
 | 
				
			||||||
		WriteSimpleResponse(w, false, r.Method+" not supported")
 | 
							WriteSimpleResponse(w, false, r.Method+" not supported")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,8 @@ module.exports = {
 | 
				
			|||||||
    'no-console': 'off',
 | 
					    'no-console': 'off',
 | 
				
			||||||
    'no-use-before-define': [0],
 | 
					    'no-use-before-define': [0],
 | 
				
			||||||
    '@typescript-eslint/no-use-before-define': [1],
 | 
					    '@typescript-eslint/no-use-before-define': [1],
 | 
				
			||||||
 | 
					    'no-shadow': 'off',
 | 
				
			||||||
 | 
					    '@typescript-eslint/no-shadow': ['error'],
 | 
				
			||||||
    'react/jsx-no-target-blank': [
 | 
					    'react/jsx-no-target-blank': [
 | 
				
			||||||
      1,
 | 
					      1,
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { Menu, Dropdown } from 'antd';
 | 
				
			|||||||
import { DownOutlined } from '@ant-design/icons';
 | 
					import { DownOutlined } from '@ant-design/icons';
 | 
				
			||||||
import { useRecoilState } from 'recoil';
 | 
					import { useRecoilState } from 'recoil';
 | 
				
			||||||
import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
 | 
					import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
 | 
				
			||||||
import { chatVisibility as chatVisibilityAtom } from './stores/ClientConfigStore';
 | 
					import { chatVisibilityAtom as chatVisibilityAtom } from './stores/ClientConfigStore';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  username: string;
 | 
					  username: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,7 @@ export default function ChatContainer(props: Props) {
 | 
				
			|||||||
      <Spin spinning={loading} />
 | 
					      <Spin spinning={loading} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Virtuoso
 | 
					      <Virtuoso
 | 
				
			||||||
        style={{ height: 400 }}
 | 
					        style={{ height: '400px' }}
 | 
				
			||||||
        ref={chatContainerRef}
 | 
					        ref={chatContainerRef}
 | 
				
			||||||
        initialTopMostItemIndex={999}
 | 
					        initialTopMostItemIndex={999}
 | 
				
			||||||
        data={messages}
 | 
					        data={messages}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,79 +1,104 @@
 | 
				
			|||||||
import { useEffect } from 'react';
 | 
					import { useEffect, useLayoutEffect } from 'react';
 | 
				
			||||||
import { atom, useRecoilState } from 'recoil';
 | 
					import { atom, useRecoilState, useSetRecoilState } from 'recoil';
 | 
				
			||||||
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
 | 
					import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
 | 
				
			||||||
import ClientConfigService from '../../services/client-config-service';
 | 
					import ClientConfigService from '../../services/client-config-service';
 | 
				
			||||||
import ChatService from '../../services/chat-service';
 | 
					import ChatService from '../../services/chat-service';
 | 
				
			||||||
 | 
					import WebsocketService from '../../services/websocket-service';
 | 
				
			||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../../interfaces/chat-message.model';
 | 
				
			||||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
 | 
					import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
 | 
				
			||||||
import { ChatVisibilityState } from '../../interfaces/application-state';
 | 
					import {
 | 
				
			||||||
 | 
					  AppState,
 | 
				
			||||||
 | 
					  ChatState,
 | 
				
			||||||
 | 
					  ChatVisibilityState,
 | 
				
			||||||
 | 
					  getChatState,
 | 
				
			||||||
 | 
					  getChatVisibilityState,
 | 
				
			||||||
 | 
					} from '../../interfaces/application-state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// The config that comes from the API.
 | 
					// The config that comes from the API.
 | 
				
			||||||
export const clientConfigState = atom({
 | 
					export const clientConfigStateAtom = atom({
 | 
				
			||||||
  key: 'clientConfigState',
 | 
					  key: 'clientConfigState',
 | 
				
			||||||
  default: makeEmptyClientConfig(),
 | 
					  default: makeEmptyClientConfig(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const chatVisibility = atom<ChatVisibilityState>({
 | 
					export const appStateAtom = atom<AppState>({
 | 
				
			||||||
  key: 'chatVisibility',
 | 
					  key: 'appStateAtom',
 | 
				
			||||||
  default: ChatVisibilityState.Hidden,
 | 
					  default: AppState.Loading,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const chatDisplayName = atom({
 | 
					export const chatStateAtom = atom<ChatState>({
 | 
				
			||||||
 | 
					  key: 'chatStateAtom',
 | 
				
			||||||
 | 
					  default: ChatState.Offline,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const chatVisibilityAtom = atom<ChatVisibilityState>({
 | 
				
			||||||
 | 
					  key: 'chatVisibility',
 | 
				
			||||||
 | 
					  default: ChatVisibilityState.Visible,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const chatDisplayNameAtom = atom<string>({
 | 
				
			||||||
  key: 'chatDisplayName',
 | 
					  key: 'chatDisplayName',
 | 
				
			||||||
  default: null,
 | 
					  default: null,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const accessTokenAtom = atom({
 | 
					export const accessTokenAtom = atom<string>({
 | 
				
			||||||
  key: 'accessToken',
 | 
					  key: 'accessTokenAtom',
 | 
				
			||||||
  default: null,
 | 
					  default: null,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const chatMessages = atom({
 | 
					export const chatMessagesAtom = atom<ChatMessage[]>({
 | 
				
			||||||
  key: 'chatMessages',
 | 
					  key: 'chatMessages',
 | 
				
			||||||
  default: [] as ChatMessage[],
 | 
					  default: [] as ChatMessage[],
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ClientConfigStore() {
 | 
					export function ClientConfigStore() {
 | 
				
			||||||
  const [, setClientConfig] = useRecoilState<ClientConfig>(clientConfigState);
 | 
					  const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
 | 
				
			||||||
  const [, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessages);
 | 
					  const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
 | 
				
			||||||
 | 
					  const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom);
 | 
				
			||||||
 | 
					  const [chatState, setChatState] = useRecoilState<ChatState>(chatStateAtom);
 | 
				
			||||||
 | 
					  const setChatMessages = useSetRecoilState<ChatMessage[]>(chatMessagesAtom);
 | 
				
			||||||
  const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
 | 
					  const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
 | 
				
			||||||
  const [, setChatDisplayName] = useRecoilState<string>(chatDisplayName);
 | 
					  const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateClientConfig = async () => {
 | 
					  const updateClientConfig = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const config = await ClientConfigService.getConfig();
 | 
					      const config = await ClientConfigService.getConfig();
 | 
				
			||||||
      console.log(`ClientConfig: ${JSON.stringify(config)}`);
 | 
					      // console.log(`ClientConfig: ${JSON.stringify(config)}`);
 | 
				
			||||||
      setClientConfig(config);
 | 
					      setClientConfig(config);
 | 
				
			||||||
 | 
					      setAppState(AppState.Online);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
 | 
					      console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleUserRegistration = async (optionalDisplayName: string) => {
 | 
					  const handleUserRegistration = async (optionalDisplayName?: string) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      setAppState(AppState.Registering);
 | 
				
			||||||
      const response = await ChatService.registerUser(optionalDisplayName);
 | 
					      const response = await ChatService.registerUser(optionalDisplayName);
 | 
				
			||||||
      console.log(`ChatService -> registerUser() response: \n${JSON.stringify(response)}`);
 | 
					      console.log(`ChatService -> registerUser() response: \n${response}`);
 | 
				
			||||||
      const { accessToken: newAccessToken, displayName } = response;
 | 
					      const { accessToken: newAccessToken, displayName: newDisplayName } = response;
 | 
				
			||||||
      if (!newAccessToken) {
 | 
					      if (!newAccessToken) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      setAccessToken(accessToken);
 | 
					      console.log('setting access token', newAccessToken);
 | 
				
			||||||
      setLocalStorage('accessToken', newAccessToken);
 | 
					      setAccessToken(newAccessToken);
 | 
				
			||||||
      setChatDisplayName(displayName);
 | 
					      // setLocalStorage('accessToken', newAccessToken);
 | 
				
			||||||
 | 
					      setChatDisplayName(newDisplayName);
 | 
				
			||||||
 | 
					      setAppState(AppState.Online);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.error(`ChatService -> registerUser() ERROR: \n${e}`);
 | 
					      console.error(`ChatService -> registerUser() ERROR: \n${e}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO: Requires access token.
 | 
					 | 
				
			||||||
  const getChatHistory = async () => {
 | 
					  const getChatHistory = async () => {
 | 
				
			||||||
 | 
					    setChatState(ChatState.Loading);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const messages = await ChatService.getChatHistory(accessToken);
 | 
					      const messages = await ChatService.getChatHistory(accessToken);
 | 
				
			||||||
      console.log(`ChatService -> getChatHistory() messages: \n${JSON.stringify(messages)}`);
 | 
					      // console.log(`ChatService -> getChatHistory() messages: \n${JSON.stringify(messages)}`);
 | 
				
			||||||
      setChatMessages(messages);
 | 
					      setChatMessages(messages);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error(`ChatService -> getChatHistory() ERROR: \n${error}`);
 | 
					      console.error(`ChatService -> getChatHistory() ERROR: \n${error}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    setChatState(ChatState.Available);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@ -81,9 +106,29 @@ export function ClientConfigStore() {
 | 
				
			|||||||
    handleUserRegistration();
 | 
					    handleUserRegistration();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    if (!accessToken) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('access token changed', accessToken);
 | 
				
			||||||
    getChatHistory();
 | 
					    getChatHistory();
 | 
				
			||||||
  }, [accessToken]);
 | 
					  }, [accessToken]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const updatedChatState = getChatState(appState);
 | 
				
			||||||
 | 
					    setChatState(updatedChatState);
 | 
				
			||||||
 | 
					    const updatedChatVisibility = getChatVisibilityState(appState);
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      'app state: ',
 | 
				
			||||||
 | 
					      AppState[appState],
 | 
				
			||||||
 | 
					      'chat state:',
 | 
				
			||||||
 | 
					      ChatState[updatedChatState],
 | 
				
			||||||
 | 
					      'chat visibility:',
 | 
				
			||||||
 | 
					      ChatVisibilityState[updatedChatVisibility],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    setChatVisibility(updatedChatVisibility);
 | 
				
			||||||
 | 
					  }, [appState]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { useRecoilValue } from 'recoil';
 | 
					import { useRecoilValue } from 'recoil';
 | 
				
			||||||
import { Layout, Row, Col, Tabs } from 'antd';
 | 
					import { Layout, Row, Col, Tabs } from 'antd';
 | 
				
			||||||
import { clientConfigState } from '../../stores/ClientConfigStore';
 | 
					import { clientConfigStateAtom } from '../../stores/ClientConfigStore';
 | 
				
			||||||
import { ClientConfig } from '../../../interfaces/client-config.model';
 | 
					import { ClientConfig } from '../../../interfaces/client-config.model';
 | 
				
			||||||
import CustomPageContent from '../../CustomPageContent';
 | 
					import CustomPageContent from '../../CustomPageContent';
 | 
				
			||||||
import OwncastPlayer from '../../video/OwncastPlayer';
 | 
					import OwncastPlayer from '../../video/OwncastPlayer';
 | 
				
			||||||
@ -11,7 +11,7 @@ const { TabPane } = Tabs;
 | 
				
			|||||||
const { Content } = Layout;
 | 
					const { Content } = Layout;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function FooterComponent() {
 | 
					export default function FooterComponent() {
 | 
				
			||||||
  const clientConfig = useRecoilValue<ClientConfig>(clientConfigState);
 | 
					  const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
 | 
				
			||||||
  const { extraPageContent } = clientConfig;
 | 
					  const { extraPageContent } = clientConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,25 @@
 | 
				
			|||||||
import Sider from 'antd/lib/layout/Sider';
 | 
					import Sider from 'antd/lib/layout/Sider';
 | 
				
			||||||
import { useRecoilValue } from 'recoil';
 | 
					import { useRecoilValue } from 'recoil';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			||||||
import ChatContainer from '../../chat/ChatContainer';
 | 
					import ChatContainer from '../../chat/ChatContainer';
 | 
				
			||||||
import { chatMessages, chatVisibility as chatVisibilityAtom } from '../../stores/ClientConfigStore';
 | 
					import {
 | 
				
			||||||
import { ChatVisibilityState } from '../../../interfaces/application-state';
 | 
					  chatMessagesAtom,
 | 
				
			||||||
 | 
					  chatVisibilityAtom,
 | 
				
			||||||
 | 
					  chatStateAtom,
 | 
				
			||||||
 | 
					} from '../../stores/ClientConfigStore';
 | 
				
			||||||
 | 
					import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state';
 | 
				
			||||||
import ChatTextField from '../../chat/ChatTextField';
 | 
					import ChatTextField from '../../chat/ChatTextField';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Sidebar() {
 | 
					export default function Sidebar() {
 | 
				
			||||||
  const messages = useRecoilValue<ChatMessage[]>(chatMessages);
 | 
					  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
				
			||||||
  const chatVisibility = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
 | 
					  const chatVisibility = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
 | 
				
			||||||
 | 
					  const chatState = useRecoilValue<ChatState>(chatStateAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Sider
 | 
					    <Sider
 | 
				
			||||||
      collapsed={chatVisibility === ChatVisibilityState.Hidden}
 | 
					      collapsed={chatVisibility === ChatVisibilityState.Hidden}
 | 
				
			||||||
 | 
					      collapsedWidth={0}
 | 
				
			||||||
      width={300}
 | 
					      width={300}
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        position: 'fixed',
 | 
					        position: 'fixed',
 | 
				
			||||||
@ -21,7 +28,7 @@ export default function Sidebar() {
 | 
				
			|||||||
        bottom: 0,
 | 
					        bottom: 0,
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <ChatContainer messages={messages} />
 | 
					      <ChatContainer messages={messages} state={chatState} />
 | 
				
			||||||
      <ChatTextField />
 | 
					      <ChatTextField />
 | 
				
			||||||
    </Sider>
 | 
					    </Sider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
export enum AppState {
 | 
					export enum AppState {
 | 
				
			||||||
  Loading, // Initial loading state as config + status is loading.
 | 
					  Loading, // Initial loading state as config + status is loading.
 | 
				
			||||||
 | 
					  Registering, // Creating a default anonymous chat account.
 | 
				
			||||||
  Online, // Stream is active.
 | 
					  Online, // Stream is active.
 | 
				
			||||||
  Offline, // Stream is not active.
 | 
					  Offline, // Stream is not active.
 | 
				
			||||||
  OfflineWaiting, // Period of time after going offline chat is still available.
 | 
					  OfflineWaiting, // Period of time after going offline chat is still available.
 | 
				
			||||||
@ -30,6 +31,8 @@ export function getChatState(state: AppState): ChatState {
 | 
				
			|||||||
      return ChatState.NotAvailable;
 | 
					      return ChatState.NotAvailable;
 | 
				
			||||||
    case AppState.OfflineWaiting:
 | 
					    case AppState.OfflineWaiting:
 | 
				
			||||||
      return ChatState.Available;
 | 
					      return ChatState.Available;
 | 
				
			||||||
 | 
					    case AppState.Registering:
 | 
				
			||||||
 | 
					      return ChatState.Loading;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return ChatState.Offline;
 | 
					      return ChatState.Offline;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -47,6 +50,8 @@ export function getChatVisibilityState(state: AppState): ChatVisibilityState {
 | 
				
			|||||||
      return ChatVisibilityState.Hidden;
 | 
					      return ChatVisibilityState.Hidden;
 | 
				
			||||||
    case AppState.OfflineWaiting:
 | 
					    case AppState.OfflineWaiting:
 | 
				
			||||||
      return ChatVisibilityState.Visible;
 | 
					      return ChatVisibilityState.Visible;
 | 
				
			||||||
 | 
					    case AppState.Registering:
 | 
				
			||||||
 | 
					      return ChatVisibilityState.Visible;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return ChatVisibilityState.Hidden;
 | 
					      return ChatVisibilityState.Hidden;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { ChatMessage } from '../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../interfaces/chat-message.model';
 | 
				
			||||||
 | 
					import { getUnauthedData } from '../utils/apis';
 | 
				
			||||||
const ENDPOINT = `http://localhost:8080/api/chat`;
 | 
					const ENDPOINT = `http://localhost:8080/api/chat`;
 | 
				
			||||||
const URL_CHAT_REGISTRATION = `http://localhost:8080/api/chat/register`;
 | 
					const URL_CHAT_REGISTRATION = `http://localhost:8080/api/chat/register`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,9 +11,8 @@ interface UserRegistrationResponse {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class ChatService {
 | 
					class ChatService {
 | 
				
			||||||
  public static async getChatHistory(accessToken: string): Promise<ChatMessage[]> {
 | 
					  public static async getChatHistory(accessToken: string): Promise<ChatMessage[]> {
 | 
				
			||||||
    const response = await fetch(`${ENDPOINT}?accessToken=${accessToken}`);
 | 
					    const response = await getUnauthedData(`${ENDPOINT}?accessToken=${accessToken}`);
 | 
				
			||||||
    const status = await response.json();
 | 
					    return response;
 | 
				
			||||||
    return status;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static async registerUser(username: string): Promise<UserRegistrationResponse> {
 | 
					  public static async registerUser(username: string): Promise<UserRegistrationResponse> {
 | 
				
			||||||
@ -24,15 +24,8 @@ class ChatService {
 | 
				
			|||||||
      body: JSON.stringify({ displayName: username }),
 | 
					      body: JSON.stringify({ displayName: username }),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					      const response = await getUnauthedData(URL_CHAT_REGISTRATION, options);
 | 
				
			||||||
      const response = await fetch(URL_CHAT_REGISTRATION, options);
 | 
					      return response;
 | 
				
			||||||
      const result = await response.json();
 | 
					 | 
				
			||||||
      return result;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										138
									
								
								web/services/websocket-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								web/services/websocket-service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					import { message } from "antd";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SocketMessageType {
 | 
				
			||||||
 | 
					  CHAT = 'CHAT',
 | 
				
			||||||
 | 
					  PING = 'PING',
 | 
				
			||||||
 | 
					  NAME_CHANGE = 'NAME_CHANGE',
 | 
				
			||||||
 | 
					  PONG = 'PONG',
 | 
				
			||||||
 | 
					  SYSTEM = 'SYSTEM',
 | 
				
			||||||
 | 
					  USER_JOINED = 'USER_JOINED',
 | 
				
			||||||
 | 
					  CHAT_ACTION = 'CHAT_ACTION',
 | 
				
			||||||
 | 
					  FEDIVERSE_ENGAGEMENT_FOLLOW = 'FEDIVERSE_ENGAGEMENT_FOLLOW',
 | 
				
			||||||
 | 
					  FEDIVERSE_ENGAGEMENT_LIKE = 'FEDIVERSE_ENGAGEMENT_LIKE',
 | 
				
			||||||
 | 
					  FEDIVERSE_ENGAGEMENT_REPOST = 'FEDIVERSE_ENGAGEMENT_REPOST',
 | 
				
			||||||
 | 
					  CONNECTED_USER_INFO = 'CONNECTED_USER_INFO',
 | 
				
			||||||
 | 
					  ERROR_USER_DISABLED = 'ERROR_USER_DISABLED',
 | 
				
			||||||
 | 
					  ERROR_NEEDS_REGISTRATION = 'ERROR_NEEDS_REGISTRATION',
 | 
				
			||||||
 | 
					  ERROR_MAX_CONNECTIONS_EXCEEDED = 'ERROR_MAX_CONNECTIONS_EXCEEDED',
 | 
				
			||||||
 | 
					  VISIBILITY_UPDATE = 'VISIBILITY-UPDATE',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SocketMessage {
 | 
				
			||||||
 | 
					  type: SocketMessageType;
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class WebsocketService {
 | 
				
			||||||
 | 
					  websocket: WebSocket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  accessToken: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  websocketReconnectTimer: ReturnType<typeof setTimeout>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(accessToken, path) {
 | 
				
			||||||
 | 
					    this.accessToken = accessToken;
 | 
				
			||||||
 | 
					    this.path = 'http://localhost:8080/ws';
 | 
				
			||||||
 | 
					    // this.websocketReconnectTimer = null;
 | 
				
			||||||
 | 
					    // this.accessToken = accessToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // this.websocketConnectedListeners = [];
 | 
				
			||||||
 | 
					    // this.websocketDisconnectListeners = [];
 | 
				
			||||||
 | 
					    // this.rawMessageListeners = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // this.send = this.send.bind(this);
 | 
				
			||||||
 | 
					    // this.createAndConnect = this.createAndConnect.bind(this);
 | 
				
			||||||
 | 
					    // this.scheduleReconnect = this.scheduleReconnect.bind(this);
 | 
				
			||||||
 | 
					    // this.shutdown = this.shutdown.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // this.isShutdown = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.createAndConnect();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createAndConnect() {
 | 
				
			||||||
 | 
					    const url = new URL(this.path);
 | 
				
			||||||
 | 
					    url.searchParams.append('accessToken', this.accessToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const ws = new WebSocket(url.toString());
 | 
				
			||||||
 | 
					    ws.onopen = this.onOpen.bind(this);
 | 
				
			||||||
 | 
					    // ws.onclose = this.onClose.bind(this);
 | 
				
			||||||
 | 
					    ws.onerror = this.onError.bind(this);
 | 
				
			||||||
 | 
					    ws.onmessage = this.onMessage.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.websocket = ws;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onOpen() {
 | 
				
			||||||
 | 
					    if (this.websocketReconnectTimer) {
 | 
				
			||||||
 | 
					      clearTimeout(this.websocketReconnectTimer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // On ws error just close the socket and let it re-connect again for now.
 | 
				
			||||||
 | 
					  onError(e) {
 | 
				
			||||||
 | 
					    handleNetworkingError(`Socket error: ${JSON.parse(e)}`);
 | 
				
			||||||
 | 
					    this.websocket.close();
 | 
				
			||||||
 | 
					    // if (!this.isShutdown) {
 | 
				
			||||||
 | 
					    //   this.scheduleReconnect();
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /*
 | 
				
			||||||
 | 
					  onMessage is fired when an inbound object comes across the websocket.
 | 
				
			||||||
 | 
					  If the message is of type `PING` we send a `PONG` back and do not
 | 
				
			||||||
 | 
					  pass it along to listeners.
 | 
				
			||||||
 | 
					  */
 | 
				
			||||||
 | 
					  onMessage(e: SocketMessage) {
 | 
				
			||||||
 | 
					    // Optimization where multiple events can be sent within a
 | 
				
			||||||
 | 
					    // single websocket message. So split them if needed.
 | 
				
			||||||
 | 
					    const messages = e.data.split('\n');
 | 
				
			||||||
 | 
					    let message: SocketMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-plusplus
 | 
				
			||||||
 | 
					    for (let i = 0; i < messages.length; i++) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        message = JSON.parse(messages[i]);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error(e, e.data);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!message.type) {
 | 
				
			||||||
 | 
					        console.error('No type provided', message);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Send PONGs
 | 
				
			||||||
 | 
					      if (message.type === SocketMessageType.PING) {
 | 
				
			||||||
 | 
					        this.sendPong();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Outbound: Other components can pass an object to `send`.
 | 
				
			||||||
 | 
					  send(message: any) {
 | 
				
			||||||
 | 
					    // Sanity check that what we're sending is a valid type.
 | 
				
			||||||
 | 
					    if (!message.type || !SocketMessageType[message.type]) {
 | 
				
			||||||
 | 
					      console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const messageJSON = JSON.stringify(message);
 | 
				
			||||||
 | 
					    this.websocket.send(messageJSON);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Reply to a PING as a keep alive.
 | 
				
			||||||
 | 
					  sendPong() {
 | 
				
			||||||
 | 
					    const pong = { type: SocketMessageType.PONG };
 | 
				
			||||||
 | 
					    this.send(pong);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleNetworkingError(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}`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -120,6 +120,8 @@ interface FetchOptions {
 | 
				
			|||||||
  auth?: boolean;
 | 
					  auth?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function fetchData(url: string, options?: FetchOptions) {
 | 
					export async function fetchData(url: string, options?: FetchOptions) {
 | 
				
			||||||
  const { data, method = 'GET', auth = true } = options || {};
 | 
					  const { data, method = 'GET', auth = true } = options || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -151,12 +153,22 @@ export async function fetchData(url: string, options?: FetchOptions) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return json;
 | 
					    return json;
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(error);
 | 
				
			||||||
    return error;
 | 
					    return error;
 | 
				
			||||||
    // console.log(error)
 | 
					    // console.log(error)
 | 
				
			||||||
    // throw new Error(error)
 | 
					    // throw new Error(error)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getUnauthedData(url: string, options?: FetchOptions) {
 | 
				
			||||||
 | 
					  const opts = {
 | 
				
			||||||
 | 
					    method: 'GET',
 | 
				
			||||||
 | 
					    auth: false,
 | 
				
			||||||
 | 
					    ...options,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return fetchData(url, opts);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function fetchExternalData(url: string) {
 | 
					export async function fetchExternalData(url: string) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const response = await fetch(url, {
 | 
					    const response = await fetch(url, {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user