change chat from a sidebar to a column (#3113)

* change chat from a sidebar to a column

Using a 2-column layout prevents the chat scrollbar from overlapping the page
scrollbar. Also, it no longer needs to calculate extra padding for elements.

* remove unused Sidebar.tsx

* fix css for chat column

* re-center "Go to last message" button

* main content column always uses maximum height

* lint

* re-hide scrollbars in mainContent on chromium

* fix chat column width when input is over-full

* chat is only fixed-width in desktop

---------

Co-authored-by: janWilejan <>
This commit is contained in:
janWilejan 2023-07-09 21:07:35 +00:00 committed by GitHub
parent 60d6cda3a6
commit 2d72935564
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 196 deletions

View file

@ -2,8 +2,8 @@
// The button that is displayed to scroll to the bottom of the chat. // The button that is displayed to scroll to the bottom of the chat.
.toBottomWrap { .toBottomWrap {
align-self: center;
display: flex; display: flex;
width: 100%;
justify-content: center; justify-content: center;
position: absolute; position: absolute;
bottom: 75px; bottom: 75px;
@ -23,6 +23,7 @@
} }
.chatContainer { .chatContainer {
flex: 0 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--theme-color-components-chat-background); background-color: var(--theme-color-components-chat-background);

View file

@ -4,7 +4,8 @@
// this margin is for fixed header // this margin is for fixed header
padding-top: var(--header-height); padding-top: var(--header-height);
background-color: var(--theme-color-main-background); background-color: var(--theme-color-main-background);
min-height: 100vh; height: 100vh;
display: flex;
position: relative; position: relative;
// add some spacing between the last row of content and the footer // add some spacing between the last row of content and the footer

View file

@ -8,7 +8,6 @@ import { Layout } from 'antd';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Script from 'next/script'; import Script from 'next/script';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { Footer } from '../../ui/Footer/Footer';
import { import {
ClientConfigStore, ClientConfigStore,
isChatAvailableSelector, isChatAvailableSelector,
@ -16,9 +15,6 @@ import {
fatalErrorStateAtom, fatalErrorStateAtom,
appStateAtom, appStateAtom,
serverStatusState, serverStatusState,
isMobileAtom,
ChatState,
chatStateAtom,
} from '../../stores/ClientConfigStore'; } from '../../stores/ClientConfigStore';
import { Content } from '../../ui/Content/Content'; import { Content } from '../../ui/Content/Content';
import { Header } from '../../ui/Header/Header'; import { Header } from '../../ui/Header/Header';
@ -33,7 +29,6 @@ import { PushNotificationServiceWorker } from '../../workers/PushNotificationSer
import { AppStateOptions } from '../../stores/application-state'; import { AppStateOptions } from '../../stores/application-state';
import { Noscript } from '../../ui/Noscript/Noscript'; import { Noscript } from '../../ui/Noscript/Noscript';
import { ServerStatus } from '../../../interfaces/server-status.model'; import { ServerStatus } from '../../../interfaces/server-status.model';
import { DYNAMIC_PADDING_VALUE } from '../../../utils/constants';
// Lazy loaded components // Lazy loaded components
@ -54,17 +49,11 @@ export const Main: FC = () => {
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector); const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom); const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom); const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const isMobile = useRecoilValue<boolean | undefined>(isMobileAtom);
const chatState = useRecoilValue<ChatState>(chatStateAtom);
const layoutRef = useRef<HTMLDivElement>(null); const layoutRef = useRef<HTMLDivElement>(null);
const { chatDisabled } = clientConfig; const { chatDisabled } = clientConfig;
const { videoAvailable } = appState; const { videoAvailable } = appState;
const { online, streamTitle } = clientStatus; const { online, streamTitle } = clientStatus;
// accounts for sidebar width when online in desktop
const showChat = online && !chatDisabled && chatState === ChatState.VISIBLE;
const dynamicFooterPadding = showChat && !isMobile ? DYNAMIC_PADDING_VALUE : '';
useEffect(() => { useEffect(() => {
setupNoLinkReferrer(layoutRef.current); setupNoLinkReferrer(layoutRef.current);
}, []); }, []);
@ -171,10 +160,6 @@ export const Main: FC = () => {
{fatalError && ( {fatalError && (
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} /> <FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
)} )}
<div className={styles.footerContainer}>
<Footer dynamicPaddingValue={dynamicFooterPadding} />
</div>
</Layout> </Layout>
<Noscript /> <Noscript />
</> </>

View file

@ -1,5 +1,28 @@
@import '../../../styles/mixins'; @import '../../../styles/mixins';
.main {
display: flex;
flex-direction: row;
overflow: hidden;
height: 100%;
// I'm not quite sure why, but sass ignores `#chat-container` here
[id="chat-container"] {
width: var(--chat-col-width);
}
}
.mainColumn {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
}
.mainColumn::-webkit-scrollbar, .mainColumn::-webkit-scrollbar-thumb {
display: none;
}
.lowerSection { .lowerSection {
padding: var(--content-padding); padding: var(--content-padding);
} }
@ -59,6 +82,7 @@
.desktopActionButtons { .desktopActionButtons {
display: block; display: block;
width: 100%;
@include screen(tablet) { @include screen(tablet) {
display: none; display: none;

View file

@ -1,5 +1,5 @@
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { Skeleton, Col, Row, Button, Spin } from 'antd'; import { Skeleton, Row, Button, Spin } from 'antd';
import MessageFilled from '@ant-design/icons/MessageFilled'; import MessageFilled from '@ant-design/icons/MessageFilled';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -24,7 +24,6 @@ import { ClientConfig } from '../../../interfaces/client-config.model';
import styles from './Content.module.scss'; import styles from './Content.module.scss';
import desktopStyles from './DesktopContent.module.scss'; import desktopStyles from './DesktopContent.module.scss';
import { Sidebar } from '../Sidebar/Sidebar';
import { OfflineBanner } from '../OfflineBanner/OfflineBanner'; import { OfflineBanner } from '../OfflineBanner/OfflineBanner';
import { AppStateOptions } from '../../stores/application-state'; import { AppStateOptions } from '../../stores/application-state';
import { ServerStatus } from '../../../interfaces/server-status.model'; import { ServerStatus } from '../../../interfaces/server-status.model';
@ -35,8 +34,15 @@ import { Modal } from '../Modal/Modal';
import { DesktopContent } from './DesktopContent'; import { DesktopContent } from './DesktopContent';
import { MobileContent } from './MobileContent'; import { MobileContent } from './MobileContent';
import { ChatModal } from '../../modals/ChatModal/ChatModal'; import { ChatModal } from '../../modals/ChatModal/ChatModal';
import { Footer } from '../Footer/Footer';
// Lazy loaded components // Lazy loaded components
const ChatContainer = dynamic(
() => import('../../chat/ChatContainer/ChatContainer').then(mod => mod.ChatContainer),
{
ssr: false,
},
);
const FollowModal = dynamic( const FollowModal = dynamic(
() => import('../../modals/FollowModal/FollowModal').then(mod => mod.FollowModal), () => import('../../modals/FollowModal/FollowModal').then(mod => mod.FollowModal),
@ -208,12 +214,9 @@ export const Content: FC = () => {
const showChat = isChatAvailable && !chatDisabled && chatState === ChatState.VISIBLE; const showChat = isChatAvailable && !chatDisabled && chatState === ChatState.VISIBLE;
// accounts for sidebar width when online in desktop
const dynamicPadding = showChat && !isMobile ? '320px' : '0px';
return ( return (
<> <div className={styles.main}>
<> <div className={styles.mainColumn}>
{appState.appLoading && ( {appState.appLoading && (
<div <div
className={classnames([styles.topSectionElement, styles.centerSpinner])} className={classnames([styles.topSectionElement, styles.centerSpinner])}
@ -222,61 +225,54 @@ export const Content: FC = () => {
<Spin delay={2} size="large" tip="One moment..." /> <Spin delay={2} size="large" tip="One moment..." />
</div> </div>
)} )}
{showChat && !isMobile && <Sidebar />}
<Row> <Row>
<Col span={24} style={{ paddingRight: dynamicPadding }}> {online && (
{online && ( <OwncastPlayer
<OwncastPlayer source="/hls/stream.m3u8"
source="/hls/stream.m3u8" online={online}
online={online} title={streamTitle || name}
title={streamTitle || name} className={styles.topSectionElement}
className={styles.topSectionElement}
/>
)}
{!online && !appState.appLoading && (
<div id="offline-message">
<OfflineBanner
showsHeader={false}
streamName={name}
customText={offlineMessage}
notificationsEnabled={supportsBrowserNotifications}
fediverseAccount={fediverseAccount}
lastLive={lastDisconnectTime}
onNotifyClick={() => setShowNotifyModal(true)}
onFollowClick={() => setShowFollowModal(true)}
className={classnames([styles.topSectionElement, styles.offlineBanner])}
/>
</div>
)}
</Col>
</Row>
<Row>
<Col span={24} style={{ paddingRight: dynamicPadding }}>
{isStreamLive && (
<Statusbar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
className={classnames(styles.topSectionElement, styles.statusBar)}
/>
)}
</Col>
</Row>
<Row>
<Col span={24} style={{ paddingRight: dynamicPadding }}>
<ActionButtons
supportFediverseFeatures={supportFediverseFeatures}
supportsBrowserNotifications={supportsBrowserNotifications}
showNotifyReminder={showNotifyReminder}
setShowNotifyModal={setShowNotifyModal}
disableNotifyReminderPopup={disableNotifyReminderPopup}
externalActions={externalActions || []}
setExternalActionToDisplay={setExternalActionToDisplay}
setShowFollowModal={setShowFollowModal}
externalActionSelected={externalActionSelected}
/> />
</Col> )}
{!online && !appState.appLoading && (
<div id="offline-message" style={{ width: '100%' }}>
<OfflineBanner
showsHeader={false}
streamName={name}
customText={offlineMessage}
notificationsEnabled={supportsBrowserNotifications}
fediverseAccount={fediverseAccount}
lastLive={lastDisconnectTime}
onNotifyClick={() => setShowNotifyModal(true)}
onFollowClick={() => setShowFollowModal(true)}
className={classnames([styles.topSectionElement, styles.offlineBanner])}
/>
</div>
)}
</Row>
<Row>
{isStreamLive && (
<Statusbar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
className={classnames(styles.topSectionElement, styles.statusBar)}
/>
)}
</Row>
<Row>
<ActionButtons
supportFediverseFeatures={supportFediverseFeatures}
supportsBrowserNotifications={supportsBrowserNotifications}
showNotifyReminder={showNotifyReminder}
setShowNotifyModal={setShowNotifyModal}
disableNotifyReminderPopup={disableNotifyReminderPopup}
externalActions={externalActions || []}
setExternalActionToDisplay={setExternalActionToDisplay}
setShowFollowModal={setShowFollowModal}
externalActionSelected={externalActionSelected}
/>
</Row> </Row>
<Modal <Modal
@ -290,8 +286,19 @@ export const Content: FC = () => {
<Row> <Row>
{!name && <Skeleton active loading style={{ marginLeft: '10vw', marginRight: '10vw' }} />} {!name && <Skeleton active loading style={{ marginLeft: '10vw', marginRight: '10vw' }} />}
{isMobile ? ( {isMobile ? (
<Col span={24}> <MobileContent
<MobileContent name={name}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
online={online}
/>
) : (
<div className={desktopStyles.bottomSectionContent}>
<DesktopContent
name={name} name={name}
summary={summary} summary={summary}
tags={tags} tags={tags}
@ -299,26 +306,23 @@ export const Content: FC = () => {
extraPageContent={extraPageContent} extraPageContent={extraPageContent}
setShowFollowModal={setShowFollowModal} setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures} supportFediverseFeatures={supportFediverseFeatures}
online={online}
/> />
</Col> </div>
) : (
<Col span={24} style={{ paddingRight: dynamicPadding }}>
<div className={desktopStyles.bottomSectionContent}>
<DesktopContent
name={name}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
/>
</div>
</Col>
)} )}
</Row> </Row>
</> <div style={{ flex: '1 1' }} />
<Footer />
</div>
{showChat && !isMobile && currentUser && (
<ChatContainer
messages={messages}
usernameToHighlight={currentUser.displayName}
chatUserId={currentUser.id}
isModerator={currentUser.isModerator}
chatAvailable={isChatAvailable}
showInput={!!currentUser}
/>
)}
{externalActionToDisplay && ( {externalActionToDisplay && (
<ExternalModal <ExternalModal
externalActionToDisplay={externalActionToDisplay} externalActionToDisplay={externalActionToDisplay}
@ -354,6 +358,6 @@ export const Content: FC = () => {
Chat <MessageFilled /> Chat <MessageFilled />
</Button> </Button>
)} )}
</> </div>
); );
}; };

View file

@ -1,8 +1,6 @@
@import '../../../styles/mixins'; @import '../../../styles/mixins';
.footer { .footer {
position: absolute;
bottom: 0;
display: flex; display: flex;
align-items: center; align-items: center;
flex-flow: row wrap; flex-flow: row wrap;

View file

@ -4,18 +4,11 @@ import styles from './Footer.module.scss';
import { ServerStatus } from '../../../interfaces/server-status.model'; import { ServerStatus } from '../../../interfaces/server-status.model';
import { serverStatusState } from '../../stores/ClientConfigStore'; import { serverStatusState } from '../../stores/ClientConfigStore';
export type FooterProps = { export const Footer: FC = () => {
dynamicPaddingValue?: string;
};
export const Footer: FC<FooterProps> = ({ dynamicPaddingValue }) => {
const clientStatus = useRecoilValue<ServerStatus>(serverStatusState); const clientStatus = useRecoilValue<ServerStatus>(serverStatusState);
const { versionNumber } = clientStatus; const { versionNumber } = clientStatus;
const dynamicPaddingStyle = dynamicPaddingValue
? { paddingRight: `calc(${dynamicPaddingValue} + var(--footer-padding-x)` }
: null;
return ( return (
<footer className={styles.footer} id="footer" style={dynamicPaddingStyle}> <footer className={styles.footer} id="footer">
<span> <span>
Powered by <a href="https://owncast.online">Owncast v{versionNumber}</a> Powered by <a href="https://owncast.online">Owncast v{versionNumber}</a>
</span> </span>

View file

@ -1,30 +0,0 @@
@import '../../../styles/mixins';
.root {
background-color: var(--theme-color-components-chat-background);
@include screen(desktop) {
position: fixed;
right: 0;
bottom: 0;
top: 69px;
z-index:100;
}
@media screen and (width <= 1120px) and (width >= 768px) {
top: 65px;
}
}
/*
First div is .ant-layout-sider-children
Only way to target it apparently
*/
.root > div {
display: flex;
flex-flow: column nowrap;
-moz-box-flex: 1 !important;
flex-grow: 1 !important;
height: 100% !important;
width: 100%!important;
}

View file

@ -1,49 +0,0 @@
import Sider from 'antd/lib/layout/Sider';
import { useRecoilValue } from 'recoil';
import { FC } from 'react';
import dynamic from 'next/dynamic';
import { Spin } from 'antd';
import { ChatMessage } from '../../../interfaces/chat-message.model';
import styles from './Sidebar.module.scss';
import {
currentUserAtom,
visibleChatMessagesSelector,
isChatAvailableSelector,
} from '../../stores/ClientConfigStore';
// Lazy loaded components
const ChatContainer = dynamic(
() => import('../../chat/ChatContainer/ChatContainer').then(mod => mod.ChatContainer),
{
ssr: false,
},
);
export const Sidebar: FC = () => {
const currentUser = useRecoilValue(currentUserAtom);
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
if (!currentUser) {
return (
<Sider className={styles.root} collapsedWidth={0} width={320}>
<Spin spinning size="large" style={{ position: 'relative', top: '40vh' }} />
</Sider>
);
}
const { id, isModerator, displayName } = currentUser;
return (
<Sider className={styles.root} collapsedWidth={0} width={320}>
<ChatContainer
messages={messages}
usernameToHighlight={displayName}
chatUserId={id}
isModerator={isModerator}
chatAvailable={isChatAvailable}
showInput={!!currentUser}
/>
</Sider>
);
};

View file

@ -133,11 +133,3 @@ body {
margin-right: 5px; margin-right: 5px;
vertical-align: middle; vertical-align: middle;
} }
body::-webkit-scrollbar {
display: none;
}
body::-webkit-scrollbar-thumb {
display: none;
}

View file

@ -1 +0,0 @@
export const DYNAMIC_PADDING_VALUE = '320px';