Files
owncast/web/pages/embed/chat/readwrite/index.tsx
T
20b1ac72a4 fix: prevent chat container from overflowing viewport in embedded view (#4713)
* fix: prevent chat container from overflowing viewport in embedded view

Add max-height: 100vh to .chatContainer to prevent the chat from getting
cut off vertically when used in the /embed/chat/readwrite view.

Fixes #4492

* fix: use flexbox layout for readwrite embed to prevent overflow in short viewports

Changes the readwrite embed to use a flexbox container with height: 100dvh
instead of a fixed 92vh height on the chat. This ensures the header takes
its natural height and the chat fills the remaining space without overflow.

- Add .embed-container with flex column layout and 100dvh height
- Set #chat-container to flex: 1 with min-height: 0 to fill remaining space
- Change ChatContainer height prop from "92vh" to "100%"

This addresses maintainer feedback that the chat was still getting cut off
in shorter viewports.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove max-height from global ChatContainer styles

Address Copilot review feedback: the max-height: 100vh on the global
.chatContainer class is no longer needed since the embed-container
flexbox layout properly constrains the height for the embedded chat.
Removing this avoids potential unintended side effects on other
ChatContainer instances (desktop sidebar, chat modal).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-23 15:40:27 -08:00

124 lines
4.4 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);
}
.embed-container {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.embed-container > #chat-container {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.embed-container #chat-container #chat-container {
flex: 1 1 0;
min-height: 0;
}
.embed-container #chat-container #virtuoso {
flex: 1;
min-height: 0;
height: auto !important;
}
`}
</style>
<ErrorBoundary
// eslint-disable-next-line react/no-unstable-nested-components
fallbackRender={({ error }) => (
<ComponentError componentName="ReadWriteChatEmbed" message={error.message} />
)}
>
<div className="embed-container">
<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="100%"
chatAvailable={isChatAvailable}
inputEnabled={chatInputEnabled}
inputDisabledPlaceholder={chatInputDisabledMessage}
/>
</div>
)}
</div>
</ErrorBoundary>
</div>
);
}