Polish up the initial loading experience
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { atom, selector, useRecoilState, useSetRecoilState } from 'recoil';
|
import { atom, selector, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { useMachine } from '@xstate/react';
|
import { useMachine } from '@xstate/react';
|
||||||
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
||||||
@@ -29,6 +29,8 @@ import { DisplayableError } from '../../types/displayable-error';
|
|||||||
const SERVER_STATUS_POLL_DURATION = 5000;
|
const SERVER_STATUS_POLL_DURATION = 5000;
|
||||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||||
|
|
||||||
|
let serverStatusRefreshPoll: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
// 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.
|
||||||
export const serverStatusState = atom<ServerStatus>({
|
export const serverStatusState = atom<ServerStatus>({
|
||||||
@@ -187,6 +189,8 @@ export const ClientConfigStore: FC = () => {
|
|||||||
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
||||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||||
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
|
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
|
||||||
|
const [hasLoadedStatus, setHasLoadedStatus] = useState(false);
|
||||||
|
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
|
||||||
|
|
||||||
let ws: WebsocketService;
|
let ws: WebsocketService;
|
||||||
|
|
||||||
@@ -205,12 +209,12 @@ export const ClientConfigStore: FC = () => {
|
|||||||
try {
|
try {
|
||||||
const config = await ClientConfigService.getConfig();
|
const config = await ClientConfigService.getConfig();
|
||||||
setClientConfig(config);
|
setClientConfig(config);
|
||||||
sendEvent('LOADED');
|
|
||||||
setGlobalFatalErrorMessage(null);
|
setGlobalFatalErrorMessage(null);
|
||||||
|
setHasLoadedConfig(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setGlobalFatalError(
|
setGlobalFatalError(
|
||||||
'Unable to reach Owncast server',
|
'Unable to reach Owncast server',
|
||||||
`Owncast cannot launch. Please make sure the Owncast server is running. ${error}`,
|
`Owncast cannot launch. Please make sure the Owncast server is running.`,
|
||||||
);
|
);
|
||||||
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
||||||
}
|
}
|
||||||
@@ -220,6 +224,7 @@ export const ClientConfigStore: FC = () => {
|
|||||||
try {
|
try {
|
||||||
const status = await ServerStatusService.getStatus();
|
const status = await ServerStatusService.getStatus();
|
||||||
setServerStatus(status);
|
setServerStatus(status);
|
||||||
|
setHasLoadedStatus(true);
|
||||||
const { serverTime } = status;
|
const { serverTime } = status;
|
||||||
|
|
||||||
const clockSkew = new Date(serverTime).getTime() - Date.now();
|
const clockSkew = new Date(serverTime).getTime() - Date.now();
|
||||||
@@ -332,12 +337,10 @@ export const ClientConfigStore: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startChat = async () => {
|
const startChat = async () => {
|
||||||
sendEvent(AppStateEvent.Loading);
|
|
||||||
try {
|
try {
|
||||||
ws = new WebsocketService(accessToken, '/ws');
|
ws = new WebsocketService(accessToken, '/ws');
|
||||||
ws.handleMessage = handleMessage;
|
ws.handleMessage = handleMessage;
|
||||||
setWebsocketService(ws);
|
setWebsocketService(ws);
|
||||||
sendEvent(AppStateEvent.Loaded);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`ChatService -> startChat() ERROR: \n${error}`);
|
console.error(`ChatService -> startChat() ERROR: \n${error}`);
|
||||||
}
|
}
|
||||||
@@ -366,11 +369,19 @@ export const ClientConfigStore: FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasLoadedStatus && hasLoadedConfig) {
|
||||||
|
sendEvent(AppStateEvent.Loaded);
|
||||||
|
}
|
||||||
|
}, [hasLoadedStatus, hasLoadedConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateClientConfig();
|
updateClientConfig();
|
||||||
handleUserRegistration();
|
handleUserRegistration();
|
||||||
updateServerStatus();
|
updateServerStatus();
|
||||||
setInterval(() => {
|
|
||||||
|
clearInterval(serverStatusRefreshPoll);
|
||||||
|
serverStatusRefreshPoll = setInterval(() => {
|
||||||
updateServerStatus();
|
updateServerStatus();
|
||||||
}, SERVER_STATUS_POLL_DURATION);
|
}, SERVER_STATUS_POLL_DURATION);
|
||||||
}, [appState]);
|
}, [appState]);
|
||||||
|
|||||||
@@ -170,79 +170,82 @@ export const Content: FC = () => {
|
|||||||
window.addEventListener('resize', checkIfMobile);
|
window.addEventListener('resize', checkIfMobile);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
let offlineMessageBody =
|
||||||
|
!appState.appLoading && 'Please follow and ask to get notified when the stream is live.';
|
||||||
|
if (offlineMessage && !appState.appLoading) {
|
||||||
|
offlineMessageBody = offlineMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offlineTitle = !appState.appLoading && `${name} is currently offline`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AntContent className={styles.root}>
|
<Spin className={styles.loadingSpinner} size="large" spinning={appState.appLoading}>
|
||||||
<div className={styles.leftContent}>
|
<AntContent className={styles.root}>
|
||||||
<Spin className={styles.loadingSpinner} size="large" spinning={appState.appLoading} />
|
<div className={styles.leftContent}>
|
||||||
|
<div className={styles.topSection}>
|
||||||
|
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
||||||
|
{!online && !appState.appLoading && (
|
||||||
|
<OfflineBanner title={offlineTitle} text={offlineMessageBody} />
|
||||||
|
)}
|
||||||
|
<Statusbar
|
||||||
|
online={online}
|
||||||
|
lastConnectTime={lastConnectTime}
|
||||||
|
lastDisconnectTime={lastDisconnectTime}
|
||||||
|
viewerCount={viewerCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.midSection}>
|
||||||
|
<div className={styles.buttonsLogoTitleSection}>
|
||||||
|
<ActionButtonRow>
|
||||||
|
{externalActionButtons}
|
||||||
|
<FollowButton size="small" />
|
||||||
|
<NotifyReminderPopup
|
||||||
|
visible={showNotifyReminder}
|
||||||
|
notificationClicked={() => setShowNotifyPopup(true)}
|
||||||
|
notificationClosed={() => disableNotifyReminderPopup()}
|
||||||
|
>
|
||||||
|
<NotifyButton onClick={() => setShowNotifyPopup(true)} />
|
||||||
|
</NotifyReminderPopup>
|
||||||
|
</ActionButtonRow>
|
||||||
|
|
||||||
<div className={styles.topSection}>
|
<Modal
|
||||||
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
title="Notify"
|
||||||
{!online && (
|
visible={showNotifyPopup}
|
||||||
<OfflineBanner
|
afterClose={() => disableNotifyReminderPopup()}
|
||||||
|
handleCancel={() => disableNotifyReminderPopup()}
|
||||||
|
>
|
||||||
|
<BrowserNotifyModal />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isMobile && isChatVisible ? (
|
||||||
|
<MobileContent
|
||||||
name={name}
|
name={name}
|
||||||
text={
|
streamTitle={streamTitle}
|
||||||
offlineMessage || 'Please follow and ask to get notified when the stream is live.'
|
summary={summary}
|
||||||
}
|
tags={tags}
|
||||||
|
socialHandles={socialHandles}
|
||||||
|
extraPageContent={extraPageContent}
|
||||||
|
messages={messages}
|
||||||
|
chatDisplayName={chatDisplayName}
|
||||||
|
chatUserId={chatUserId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DesktopContent
|
||||||
|
name={name}
|
||||||
|
streamTitle={streamTitle}
|
||||||
|
summary={summary}
|
||||||
|
tags={tags}
|
||||||
|
socialHandles={socialHandles}
|
||||||
|
extraPageContent={extraPageContent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Statusbar
|
|
||||||
online={online}
|
|
||||||
lastConnectTime={lastConnectTime}
|
|
||||||
lastDisconnectTime={lastDisconnectTime}
|
|
||||||
viewerCount={viewerCount}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.midSection}>
|
{isChatVisible && !isMobile && <Sidebar />}
|
||||||
<div className={styles.buttonsLogoTitleSection}>
|
</AntContent>
|
||||||
<ActionButtonRow>
|
{(!isMobile || !isChatVisible) && <Footer version={version} />}
|
||||||
{externalActionButtons}
|
</Spin>
|
||||||
<FollowButton size="small" />
|
|
||||||
<NotifyReminderPopup
|
|
||||||
visible={showNotifyReminder}
|
|
||||||
notificationClicked={() => setShowNotifyPopup(true)}
|
|
||||||
notificationClosed={() => disableNotifyReminderPopup()}
|
|
||||||
>
|
|
||||||
<NotifyButton onClick={() => setShowNotifyPopup(true)} />
|
|
||||||
</NotifyReminderPopup>
|
|
||||||
</ActionButtonRow>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="Notify"
|
|
||||||
visible={showNotifyPopup}
|
|
||||||
afterClose={() => disableNotifyReminderPopup()}
|
|
||||||
handleCancel={() => disableNotifyReminderPopup()}
|
|
||||||
>
|
|
||||||
<BrowserNotifyModal />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isMobile && isChatVisible ? (
|
|
||||||
<MobileContent
|
|
||||||
name={name}
|
|
||||||
streamTitle={streamTitle}
|
|
||||||
summary={summary}
|
|
||||||
tags={tags}
|
|
||||||
socialHandles={socialHandles}
|
|
||||||
extraPageContent={extraPageContent}
|
|
||||||
messages={messages}
|
|
||||||
chatDisplayName={chatDisplayName}
|
|
||||||
chatUserId={chatUserId}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DesktopContent
|
|
||||||
name={name}
|
|
||||||
streamTitle={streamTitle}
|
|
||||||
summary={summary}
|
|
||||||
tags={tags}
|
|
||||||
socialHandles={socialHandles}
|
|
||||||
extraPageContent={extraPageContent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isChatVisible && !isMobile && <Sidebar />}
|
|
||||||
</AntContent>
|
|
||||||
{(!isMobile || !isChatVisible) && <Footer version={version} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { FC } from 'react';
|
|||||||
import styles from './OfflineBanner.module.scss';
|
import styles from './OfflineBanner.module.scss';
|
||||||
|
|
||||||
export type OfflineBannerProps = {
|
export type OfflineBannerProps = {
|
||||||
name: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OfflineBanner: FC<OfflineBannerProps> = ({ name, text }) => (
|
export const OfflineBanner: FC<OfflineBannerProps> = ({ title, text }) => (
|
||||||
<div className={styles.outerContainer}>
|
<div className={styles.outerContainer}>
|
||||||
<div className={styles.innerContainer}>
|
<div className={styles.innerContainer}>
|
||||||
<div className={styles.header}>{name} is currently offline.</div>
|
<div className={styles.header}>{title}</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div>{text}</div>
|
<div>{text}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function VideoEmbed() {
|
|||||||
<ClientConfigStore />
|
<ClientConfigStore />
|
||||||
<div className="video-embed">
|
<div className="video-embed">
|
||||||
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
||||||
{!online && <OfflineBanner name={name} text="Stream is offline text goes here." />}{' '}
|
{!online && <OfflineBanner title={name} text="Stream is offline text goes here." />}{' '}
|
||||||
<Statusbar
|
<Statusbar
|
||||||
online={online}
|
online={online}
|
||||||
lastConnectTime={lastConnectTime}
|
lastConnectTime={lastConnectTime}
|
||||||
|
|||||||
Reference in New Issue
Block a user