More accurately hide/show and track chat state

This commit is contained in:
Gabe Kangas 2022-05-13 14:44:16 -07:00
parent 4b2742739a
commit e0f8a1f702
No known key found for this signature in database
GPG key ID: 9A56337728BC81EA
9 changed files with 67 additions and 56 deletions

View file

@ -1,19 +1,19 @@
import { Menu, Dropdown, Button, Space } from 'antd'; import { Menu, Dropdown, Button, Space } from 'antd';
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import { useRecoilState } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { chatVisibilityAtom } from '../../stores/ClientConfigStore'; import { chatVisibilityAtom, chatDisplayNameAtom } from '../../stores/ClientConfigStore';
import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state'; import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state';
import s from './UserDropdown.module.scss'; import s from './UserDropdown.module.scss';
interface Props { interface Props {
username: string; username?: string;
chatState: ChatState; chatState: ChatState;
} }
export default function UserDropdown({ username = 'test-user', chatState }: Props) { export default function UserDropdown({ username: defaultUsername, chatState }: Props) {
const chatEnabled = chatState !== ChatState.NotAvailable;
const [chatVisibility, setChatVisibility] = const [chatVisibility, setChatVisibility] =
useRecoilState<ChatVisibilityState>(chatVisibilityAtom); useRecoilState<ChatVisibilityState>(chatVisibilityAtom);
const username = defaultUsername || useRecoilValue(chatDisplayNameAtom);
const toggleChatVisibility = () => { const toggleChatVisibility = () => {
if (chatVisibility === ChatVisibilityState.Hidden) { if (chatVisibility === ChatVisibilityState.Hidden) {
@ -27,7 +27,7 @@ export default function UserDropdown({ username = 'test-user', chatState }: Prop
<Menu> <Menu>
<Menu.Item key="0">Change name</Menu.Item> <Menu.Item key="0">Change name</Menu.Item>
<Menu.Item key="1">Authenticate</Menu.Item> <Menu.Item key="1">Authenticate</Menu.Item>
{chatEnabled && ( {chatState === ChatState.Available && (
<Menu.Item key="3" onClick={() => toggleChatVisibility()}> <Menu.Item key="3" onClick={() => toggleChatVisibility()}>
Toggle chat Toggle chat
</Menu.Item> </Menu.Item>
@ -48,3 +48,7 @@ export default function UserDropdown({ username = 'test-user', chatState }: Prop
</div> </div>
); );
} }
UserDropdown.defaultProps = {
username: undefined,
};

View file

@ -1,6 +1,5 @@
import { Layout } from 'antd'; import { Layout } from 'antd';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ServerStatusStore } from '../stores/ServerStatusStore';
import { ClientConfigStore, clientConfigStateAtom } from '../stores/ClientConfigStore'; import { ClientConfigStore, clientConfigStateAtom } from '../stores/ClientConfigStore';
import { Content, Header } from '../ui'; import { Content, Header } from '../ui';
import { ClientConfig } from '../../interfaces/client-config.model'; import { ClientConfig } from '../../interfaces/client-config.model';
@ -11,7 +10,6 @@ function Main() {
return ( return (
<> <>
<ServerStatusStore />
<ClientConfigStore /> <ClientConfigStore />
<Layout> <Layout>
<Header name={title || name} /> <Header name={title || name} />

View file

@ -1,11 +1,13 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import { useEffect, useLayoutEffect } from 'react'; import { useEffect } from 'react';
import { atom, useRecoilState, useSetRecoilState } 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 WebsocketService from '../../services/websocket-service';
import { ChatMessage } from '../../interfaces/chat-message.model'; import { ChatMessage } from '../../interfaces/chat-message.model';
import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model';
import { import {
AppState, AppState,
ChatState, ChatState,
@ -22,6 +24,14 @@ import {
} from '../../interfaces/socket-events'; } from '../../interfaces/socket-events';
import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo'; import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo';
import handleChatMessage from './eventhandlers/handleChatMessage'; import handleChatMessage from './eventhandlers/handleChatMessage';
import ServerStatusService from '../../services/status-service';
// Server status is what gets updated such as viewer count, durations,
// stream title, online/offline state, etc.
export const serverStatusState = atom<ServerStatus>({
key: 'serverStatusState',
default: makeEmptyServerStatus(),
});
// The config that comes from the API. // The config that comes from the API.
export const clientConfigStateAtom = atom({ export const clientConfigStateAtom = atom({
@ -71,6 +81,7 @@ export const websocketServiceAtom = atom<WebsocketService>({
export function ClientConfigStore() { export function ClientConfigStore() {
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom); const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom); const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom);
const setChatState = useSetRecoilState<ChatState>(chatStateAtom); const setChatState = useSetRecoilState<ChatState>(chatStateAtom);
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom); const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
@ -90,6 +101,22 @@ export function ClientConfigStore() {
} }
}; };
const updateServerStatus = async () => {
try {
const status = await ServerStatusService.getStatus();
setServerStatus(status);
if (status.online) {
setAppState(AppState.Online);
} else {
setAppState(AppState.Offline);
}
return status;
} catch (error) {
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
return null;
}
};
const handleUserRegistration = async (optionalDisplayName?: string) => { const handleUserRegistration = async (optionalDisplayName?: string) => {
try { try {
setAppState(AppState.Registering); setAppState(AppState.Registering);
@ -140,7 +167,6 @@ export function ClientConfigStore() {
} catch (error) { } catch (error) {
console.error(`ChatService -> startChat() ERROR: \n${error}`); console.error(`ChatService -> startChat() ERROR: \n${error}`);
} }
setChatState(ChatState.Available);
}; };
useEffect(() => { useEffect(() => {
@ -148,7 +174,14 @@ export function ClientConfigStore() {
handleUserRegistration(); handleUserRegistration();
}, []); }, []);
useLayoutEffect(() => { useEffect(() => {
setInterval(() => {
updateServerStatus();
}, 5000);
updateServerStatus();
}, []);
useEffect(() => {
if (!accessToken) { if (!accessToken) {
return; return;
} }
@ -159,6 +192,7 @@ export function ClientConfigStore() {
useEffect(() => { useEffect(() => {
const updatedChatState = getChatState(appState); const updatedChatState = getChatState(appState);
console.log('updatedChatState', updatedChatState);
setChatState(updatedChatState); setChatState(updatedChatState);
const updatedChatVisibility = getChatVisibilityState(appState); const updatedChatVisibility = getChatVisibilityState(appState);
console.log( console.log(

View file

@ -1,33 +0,0 @@
import { useEffect } from 'react';
import { atom, useRecoilState } from 'recoil';
import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model';
import ServerStatusService from '../../services/status-service';
export const serverStatusState = atom({
key: 'serverStatusState',
default: makeEmptyServerStatus(),
});
export function ServerStatusStore() {
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
const updateServerStatus = async () => {
try {
const status = await ServerStatusService.getStatus();
setServerStatus(status);
return status;
} catch (error) {
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
return null;
}
};
useEffect(() => {
setInterval(() => {
updateServerStatus();
}, 5000);
updateServerStatus();
}, []);
return null;
}

View file

@ -5,8 +5,8 @@ import {
clientConfigStateAtom, clientConfigStateAtom,
chatMessagesAtom, chatMessagesAtom,
chatStateAtom, chatStateAtom,
serverStatusState,
} from '../../stores/ClientConfigStore'; } from '../../stores/ClientConfigStore';
import { serverStatusState } from '../../stores/ServerStatusStore';
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';
@ -30,7 +30,7 @@ const { Content } = Layout;
export default function ContentComponent() { export default function ContentComponent() {
const status = useRecoilValue<ServerStatus>(serverStatusState); const status = useRecoilValue<ServerStatus>(serverStatusState);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const chatOpen = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom); const chatVisibility = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom); const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
const chatState = useRecoilValue<ChatState>(chatStateAtom); const chatState = useRecoilValue<ChatState>(chatStateAtom);
@ -41,6 +41,9 @@ export default function ContentComponent() {
const total = 0; const total = 0;
const isShowingChatColumn =
chatState === ChatState.Available && chatVisibility === ChatVisibilityState.Visible;
// This is example content. It should be removed. // This is example content. It should be removed.
const externalActions = [ const externalActions = [
{ {
@ -58,7 +61,7 @@ export default function ContentComponent() {
)); ));
return ( return (
<Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}> <Content className={`${s.root}`} data-columns={isShowingChatColumn ? 2 : 1}>
<div className={`${s.leftCol}`}> <div className={`${s.leftCol}`}>
<OwncastPlayer source="/hls/stream.m3u8" online={online} /> <OwncastPlayer source="/hls/stream.m3u8" online={online} />
<Statusbar <Statusbar
@ -72,6 +75,7 @@ export default function ContentComponent() {
<Button>Follow</Button> <Button>Follow</Button>
<Button>Notify</Button> <Button>Notify</Button>
</ActionButtonRow> </ActionButtonRow>
<div className={`${s.lowerRow}`}> <div className={`${s.lowerRow}`}>
<Tabs defaultActiveKey="1" type="card"> <Tabs defaultActiveKey="1" type="card">
<TabPane tab="About" key="1" className={`${s.pageContentSection}`}> <TabPane tab="About" key="1" className={`${s.pageContentSection}`}>
@ -82,16 +86,17 @@ export default function ContentComponent() {
</TabPane> </TabPane>
</Tabs> </Tabs>
{chatOpen && ( {chatVisibility && (
<div className={`${s.mobileChat}`}> <div className={`${s.mobileChat}`}>
<ChatContainer messages={messages} state={chatState} /> <ChatContainer messages={messages} state={chatState} />
<ChatTextField /> <ChatTextField />
</div> </div>
)} )}
<Footer version={version} /> <Footer version={version} />
</div> </div>
</div> </div>
{chatOpen && <Sidebar />} {isShowingChatColumn && <Sidebar />}
</Content> </Content>
); );
} }

View file

@ -9,5 +9,5 @@ interface Props {
export default function FooterComponent(props: Props) { export default function FooterComponent(props: Props) {
const { version } = props; const { version } = props;
return <Footer style={{ textAlign: 'center', height: '64px' }}>Footer: Owncast {version}</Footer>; return <Footer style={{ textAlign: 'center', height: '64px' }}>Owncast {version}</Footer>;
} }

View file

@ -1,6 +1,8 @@
import { Layout } from 'antd'; import { Layout } from 'antd';
import { useRecoilValue } from 'recoil';
import { ChatState } from '../../../interfaces/application-state'; import { ChatState } from '../../../interfaces/application-state';
import { OwncastLogo, UserDropdown } from '../../common'; import { OwncastLogo, UserDropdown } from '../../common';
import { chatStateAtom } from '../../stores/ClientConfigStore';
import s from './Header.module.scss'; import s from './Header.module.scss';
const { Header } = Layout; const { Header } = Layout;
@ -10,13 +12,15 @@ interface Props {
} }
export default function HeaderComponent({ name = 'Your stream title' }: Props) { export default function HeaderComponent({ name = 'Your stream title' }: Props) {
const chatState = useRecoilValue<ChatState>(chatStateAtom);
return ( return (
<Header className={`${s.header}`}> <Header className={`${s.header}`}>
<div className={`${s.logo}`}> <div className={`${s.logo}`}>
<OwncastLogo variant="contrast" /> <OwncastLogo variant="contrast" />
<span>{name}</span> <span>{name}</span>
</div> </div>
<UserDropdown username="fillmein" chatState={ChatState.Available} /> <UserDropdown chatState={chatState} />
</Header> </Header>
); );
} }

View file

@ -21,7 +21,6 @@ export default function Modal(props: Props) {
height: '80vh', height: '80vh',
}; };
console.log(url);
const iframe = url && ( const iframe = url && (
<iframe <iframe
title={title} title={title}

View file

@ -13,10 +13,10 @@ export enum ChatVisibilityState {
} }
export enum ChatState { export enum ChatState {
Available, // Normal state. Chat can be visible and used. Available = 'Available', // Normal state. Chat can be visible and used.
NotAvailable, // Chat features are not available. NotAvailable = 'NotAvailable', // Chat features are not available.
Loading, // Chat is connecting and loading history. Loading = 'Loading', // Chat is connecting and loading history.
Offline, // Chat is offline/disconnected for some reason but is visible. Offline = 'Offline', // Chat is offline/disconnected for some reason but is visible.
} }
export enum VideoState { export enum VideoState {