Files
owncast/web/pages/embed/chat/readwrite/index.tsx
T
93b482871f Require authentication to participate in chat (#4762)
* feat(chat): require authentication to participate in chat

* fix: it's pretty much impossible to bypass the auth requirement, addressing review feedback anyway

* feat(chat): render chat text input as disabled if chat auth is required

* Commit updated API documentation

---------

Co-authored-by: Owncast <owncast@owncast.online>
2026-01-28 11:49:07 -08:00

100 lines
3.7 KiB
TypeScript

/* eslint-disable react/no-unknown-property */
import { useRecoilValue } from 'recoil';
import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'next-export-i18n';
import { ChatMessage } from '../../../../interfaces/chat-message.model';
import { ChatContainer } from '../../../../components/chat/ChatContainer/ChatContainer';
import {
ClientConfigStore,
currentUserAtom,
visibleChatMessagesSelector,
clientConfigStateAtom,
appStateAtom,
serverStatusState,
isChatAvailableSelector,
chatAuthenticatedAtom,
} from '../../../../components/stores/ClientConfigStore';
import Header from '../../../../components/ui/Header/Header';
import { ClientConfig } from '../../../../interfaces/client-config.model';
import { AppStateOptions } from '../../../../components/stores/application-state';
import { ServerStatus } from '../../../../interfaces/server-status.model';
import { Theme } from '../../../../components/theme/Theme';
import { ComponentError } from '../../../../components/ui/ComponentError/ComponentError';
import { Localization } from '../../../../types/localization';
export default function ReadWriteChatEmbed() {
const { t } = useTranslation();
const currentUser = useRecoilValue(currentUserAtom);
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const clientStatus = useRecoilValue<ServerStatus>(serverStatusState);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
const isUserAuthenticated = useRecoilValue<boolean>(chatAuthenticatedAtom);
const { name, chatDisabled, chatRequireAuthentication } = clientConfig;
// Determine if chat input should be enabled based on authentication requirements.
// Moderators bypass the authentication requirement.
const chatInputEnabled = !!(
isChatAvailable &&
(!chatRequireAuthentication || isUserAuthenticated || currentUser?.isModerator)
);
const chatInputDisabledMessage = chatRequireAuthentication
? t(Localization.Frontend.Chat.authenticateToChat)
: t(Localization.Frontend.chatDisabled);
const { videoAvailable } = appState;
const { streamTitle, online } = clientStatus;
const headerText = online ? streamTitle || name : name;
// This is a hack to force a specific body background color for just this page.
useEffect(() => {
document.body.classList.add('body-background');
}, []);
return (
<div>
<style jsx global>
{`
.body-background {
background: var(--theme-color-components-chat-background);
}
`}
</style>
<ErrorBoundary
// eslint-disable-next-line react/no-unstable-nested-components
fallbackRender={({ error }) => (
<ComponentError componentName="ReadWriteChatEmbed" message={error.message} />
)}
>
<ClientConfigStore />
<Theme />
<Header
name={headerText}
chatAvailable
chatDisabled={chatDisabled}
online={videoAvailable}
/>
{currentUser && (
<div id="chat-container">
<ChatContainer
messages={messages}
usernameToHighlight={currentUser.displayName}
chatUserId={currentUser.id}
isModerator={currentUser.isModerator}
showInput
height="92vh"
chatAvailable={isChatAvailable}
inputEnabled={chatInputEnabled}
inputDisabledPlaceholder={chatInputDisabledMessage}
/>
</div>
)}
</ErrorBoundary>
</div>
);
}