Reworked mobile UI for some components

This commit is contained in:
t1enne 2022-07-08 22:20:22 +02:00
parent 37ad329072
commit efbe6907ac
15 changed files with 223 additions and 132 deletions

View File

@ -65,9 +65,9 @@ export default function ChatContainer(props: Props) {
const MessagesTable = useMemo( const MessagesTable = useMemo(
() => ( () => (
<> <div style={{ height: '100%' }}>
<Virtuoso <Virtuoso
style={{ height: isMobile ? 500 : '77vh', width: 'auto' }} style={{ height: '100%', width: 'auto' }}
ref={chatContainerRef} ref={chatContainerRef}
initialTopMostItemIndex={messages.length - 1} // Force alignment to bottom initialTopMostItemIndex={messages.length - 1} // Force alignment to bottom
data={messages} data={messages}
@ -92,7 +92,7 @@ export default function ChatContainer(props: Props) {
</Button> </Button>
</div> </div>
)} )}
</> </div>
), ),
[messages, usernameToHighlight, chatUserId, isModerator, atBottom, isMobile], [messages, usernameToHighlight, chatUserId, isModerator, atBottom, isMobile],
); );

View File

@ -1,36 +0,0 @@
import { useState } from 'react';
interface Props {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function ChatTextField(props: Props) {
const [value, setValue] = useState('');
const [showEmojis, setShowEmojis] = useState(false);
return (
<div>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder="Type a message here then hit ENTER"
/>
<button type="button" onClick={() => setShowEmojis(!showEmojis)}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
);
}

View File

@ -7,48 +7,62 @@
overflow-x: hidden; overflow-x: hidden;
div[role=textbox] { div[role=textbox] {
font-size: 0.9rem; font-size: 0.9rem;
border-radius: .2rem;
padding: .6rem; padding: .6rem;
padding-right: calc(0.6rem + 44px); padding-right: calc(0.6rem + 44px);
border-radius: .35rem;
background-color: var(--color-owncast-gray-700); background-color: var(--color-owncast-gray-700);
box-shadow: 0;
transition: box-shadow 50ms ease-in-out;
&:focus {
box-shadow: inset 0px 0px 0x 1px var(--color-owncast-purple-700);
outline: 1px solid var(--color-owncast-gray-500) !important;
}
& > p { & > p {
margin: 0px; margin: 0px;
} }
} }
.inputWrapper {
display: flex;
position: relative;
margin-right: .3rem;
border-radius: .2rem;
& > div {
transition: box-shadow .2s ease-in-out;
}
}
.emojiButton {
border: none;
background: none;
cursor: pointer;
padding: 0 1rem;
position: absolute;
right: 0px;
top: 50%;
transform: translateY(-50%);
svg {
fill: var(--color-owncast-gray-300);
}
}
.submitButtonWrapper {
display: flex;
padding: 6px 0;
justify-content: flex-end;
}
} }
.inputWrapper {
display: flex;
position: relative; .mobile {
border-radius: var(--theme-rounded-corners); &.root {
outline: 1px solid var(--color-owncast-gray-500); display: flex;
&:hover { .inputWrapper {
box-shadow: 0 0 1px 1px var(--color-owncast-gray-300); flex: 1;
} }
& > div { .submitButtonWrapper {
transition: box-shadow .2s ease-in-out; padding: 0px;
&:focus {
// box-shadow: 0 0 1px 1px var(--color-owncast-gray-300);
} }
} }
} }
.emojiButton {
border: none;
background: none;
cursor: pointer;
padding: 0 1rem;
position: absolute;
right: 0px;
top: 50%;
transform: translateY(-50%);
svg {
fill: var(--color-owncast-gray-300);
}
}
.submitButtonWrapper {
display: flex;
padding: 6px 0;
justify-content: flex-end;
}

View File

@ -4,9 +4,10 @@ import React, { useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Transforms, createEditor, BaseEditor, Text } from 'slate'; import { Transforms, createEditor, BaseEditor, Text } from 'slate';
import { Slate, Editable, withReact, ReactEditor } from 'slate-react'; import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
import cn from 'classnames';
import EmojiPicker from './EmojiPicker'; import EmojiPicker from './EmojiPicker';
import WebsocketService from '../../../services/websocket-service'; import WebsocketService from '../../../services/websocket-service';
import { websocketServiceAtom } from '../../stores/ClientConfigStore'; import { isMobileAtom, websocketServiceAtom } from '../../stores/ClientConfigStore';
import { MessageType } from '../../../interfaces/socket-events'; import { MessageType } from '../../../interfaces/socket-events';
import s from './ChatTextField.module.scss'; import s from './ChatTextField.module.scss';
@ -101,6 +102,7 @@ export default function ChatTextField(props: Props) {
// const { value: originalValue } = props; // const { value: originalValue } = props;
const [showEmojis, setShowEmojis] = useState(false); const [showEmojis, setShowEmojis] = useState(false);
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom); const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
const isMobile = useRecoilValue<boolean>(isMobileAtom);
const [editor] = useState(() => withImages(withReact(createEditor()))); const [editor] = useState(() => withImages(withReact(createEditor())));
const sendMessage = () => { const sendMessage = () => {
@ -120,7 +122,7 @@ export default function ChatTextField(props: Props) {
const handleChange = () => {}; const handleChange = () => {};
const handleEmojiSelect = e => { const handleEmojiSelect = (e: any) => {
ReactEditor.focus(editor); ReactEditor.focus(editor);
if (e.url) { if (e.url) {
@ -134,7 +136,7 @@ export default function ChatTextField(props: Props) {
} }
}; };
const onKeyDown = e => { const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
sendMessage(); sendMessage();
@ -142,7 +144,11 @@ export default function ChatTextField(props: Props) {
}; };
return ( return (
<div className={s.root}> <div
className={cn(s.root, {
[s.mobile]: isMobile,
})}
>
<div className={s.inputWrapper}> <div className={s.inputWrapper}>
<Slate <Slate
editor={editor} editor={editor}
@ -167,9 +173,13 @@ export default function ChatTextField(props: Props) {
</button> </button>
</div> </div>
<div className={s.submitButtonWrapper}> <div className={s.submitButtonWrapper}>
<Button size="middle" type="primary" icon={<SendOutlined />} onClick={sendMessage}> {isMobile ? (
Send <Button size="large" type="ghost" icon={<SendOutlined />} onClick={sendMessage} />
</Button> ) : (
<Button type="primary" icon={<SendOutlined />} onClick={sendMessage}>
Send
</Button>
)}
</div> </div>
<Popover <Popover
content={<EmojiPicker onEmojiSelect={handleEmojiSelect} />} content={<EmojiPicker onEmojiSelect={handleEmojiSelect} />}

View File

@ -1,4 +1,4 @@
.streamInfo { .root {
position: relative; position: relative;
display: grid; display: grid;
} }
@ -34,3 +34,44 @@
font-weight: bold; font-weight: bold;
} }
} }
.mobile {
&.root {
position: relative;
display: flex;
padding: 0 .3rem;
align-items: center;
justify-content: space-between;
.mobileInfo {
display: flex;
align-items: center;
.title {
font-size: 1.2rem;
font-weight: 600;
}
}
.mobileStatus {
display: flex;
font-weight: 600;
.viewerCount {
display: flex;
align-items: center;
gap: 4px;
}
.liveStatus {
display: flex;
align-items: center;
margin-left: .5rem;
font-size: .8rem;
gap: 4px;
.liveCircle {
border-radius: 50%;
background-color: red;
width: .5rem;
height: .5rem;
}
}
}
}
}

View File

@ -1,16 +1,56 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import cn from 'classnames';
import { EyeFilled } from '@ant-design/icons';
import { useEffect } from 'react';
import { ClientConfig } from '../../../interfaces/client-config.model'; import { ClientConfig } from '../../../interfaces/client-config.model';
import { clientConfigStateAtom } from '../../stores/ClientConfigStore'; import {
clientConfigStateAtom,
isOnlineSelector,
serverStatusState,
} from '../../stores/ClientConfigStore';
import { ServerLogo } from '../../ui'; import { ServerLogo } from '../../ui';
import StatusBar from '../../ui/Statusbar';
import CategoryIcon from '../../ui/CategoryIcon/CategoryIcon'; import CategoryIcon from '../../ui/CategoryIcon/CategoryIcon';
import SocialLinks from '../../ui/SocialLinks/SocialLinks'; import SocialLinks from '../../ui/SocialLinks/SocialLinks';
import s from './StreamInfo.module.scss'; import s from './StreamInfo.module.scss';
import { ServerStatus } from '../../../interfaces/server-status.model';
export default function StreamInfo() { interface Props {
isMobile: boolean;
}
export default function StreamInfo({ isMobile }: Props) {
const { socialHandles, name, title, tags } = useRecoilValue<ClientConfig>(clientConfigStateAtom); const { socialHandles, name, title, tags } = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const { viewerCount, lastConnectTime, lastDisconnectTime } =
useRecoilValue<ServerStatus>(serverStatusState);
const online = useRecoilValue<boolean>(isOnlineSelector);
return ( useEffect(() => {
<div className={s.streamInfo}> console.log({ online });
}, [online]);
return isMobile ? (
<div className={cn(s.root, s.mobile)}>
<div className={s.mobileInfo}>
<ServerLogo src="/logo" />
<div className={s.title}>{name}</div>
</div>
<div className={s.mobileStatus}>
<div className={s.viewerCount}>
{online && (
<>
<span>{viewerCount}</span>
<EyeFilled />
</>
)}
</div>
<div className={s.liveStatus}>
{online && <div className={s.liveCircle} />}
<span>{online ? 'LIVE' : 'OFFLINE'}</span>
</div>
</div>
</div>
) : (
<div className={s.root}>
<div className={s.logoTitleSection}> <div className={s.logoTitleSection}>
<ServerLogo src="/logo" /> <ServerLogo src="/logo" />
<div className={s.titleSection}> <div className={s.titleSection}>
@ -23,6 +63,12 @@ export default function StreamInfo() {
<SocialLinks links={socialHandles} /> <SocialLinks links={socialHandles} />
</div> </div>
</div> </div>
<StatusBar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
/>
</div> </div>
); );
} }

View File

@ -1,32 +1,8 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
&.mobile {
display: flex;
flex-direction: column;
height: calc(100vh - 64px);
overflow-y: hidden;
.topHalf {
border: 1px dashed white;
height: calc(40vh - 64px);
overflow: hidden;
}
.lowerHalf {
border: 1px dashed red;
height: 60vh;
}
}
} }
.mobileChat {
position: relative;
display: block;
// top: 0px;
width: 100%;
[data-virtuoso-scroller] {
height: 500px;
}
}
.leftCol { .leftCol {
display: flex; display: flex;
@ -40,3 +16,26 @@
z-index: 999999; z-index: 999999;
} }
.mobile {
&.root {
display: flex;
flex-direction: column;
height: calc(100vh - 64px);
overflow: hidden;
.topHalf {
display: grid;
grid-template-rows: 30vh 5vh 5vh;
height: 40vh;
// overflow: hidden;
}
.lowerHalf {
height: 60vh;
}
}
.mobileChat {
position: relative;
display: block;
height: 100%;
width: 100%;
}
}

View File

@ -11,7 +11,6 @@ import {
chatDisplayNameAtom, chatDisplayNameAtom,
chatUserIdAtom, chatUserIdAtom,
isChatVisibleSelector, isChatVisibleSelector,
serverStatusState,
appStateAtom, appStateAtom,
isOnlineSelector, isOnlineSelector,
isMobileAtom, isMobileAtom,
@ -28,8 +27,6 @@ import { ChatMessage } from '../../../interfaces/chat-message.model';
import ChatTextField from '../../chat/ChatTextField/ChatTextField'; import ChatTextField from '../../chat/ChatTextField/ChatTextField';
import ActionButtonRow from '../../action-buttons/ActionButtonRow'; import ActionButtonRow from '../../action-buttons/ActionButtonRow';
import ActionButton from '../../action-buttons/ActionButton'; import ActionButton from '../../action-buttons/ActionButton';
import Statusbar from '../Statusbar/Statusbar';
import { ServerStatus } from '../../../interfaces/server-status.model';
import { Follower } from '../../../interfaces/follower'; import { Follower } from '../../../interfaces/follower';
import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup'; import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup';
import OfflineBanner from '../OfflineBanner/OfflineBanner'; import OfflineBanner from '../OfflineBanner/OfflineBanner';
@ -38,13 +35,13 @@ import FollowButton from '../../action-buttons/FollowButton';
import NotifyButton from '../../action-buttons/NotifyButton'; import NotifyButton from '../../action-buttons/NotifyButton';
import Modal from '../Modal/Modal'; import Modal from '../Modal/Modal';
import BrowserNotifyModal from '../../modals/BrowserNotify/BrowserNotifyModal'; import BrowserNotifyModal from '../../modals/BrowserNotify/BrowserNotifyModal';
import StreamInfo from '../../common/StreamInfo';
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { Content } = Layout; const { Content } = Layout;
export default function ContentComponent() { export default function ContentComponent() {
const appState = useRecoilValue<AppStateOptions>(appStateAtom); const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const status = useRecoilValue<ServerStatus>(serverStatusState);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector); const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom); const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
@ -54,7 +51,6 @@ export default function ContentComponent() {
const chatUserId = useRecoilValue<string>(chatUserIdAtom); const chatUserId = useRecoilValue<string>(chatUserIdAtom);
const { extraPageContent, version, name, summary } = clientConfig; const { extraPageContent, version, name, summary } = clientConfig;
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
const [showNotifyReminder, setShowNotifyReminder] = useState(false); const [showNotifyReminder, setShowNotifyReminder] = useState(false);
const [showNotifyPopup, setShowNotifyPopup] = useState(false); const [showNotifyPopup, setShowNotifyPopup] = useState(false);
@ -129,13 +125,6 @@ export default function ContentComponent() {
text="Stream is offline text goes here. Will create a new form to set it in the Admin." text="Stream is offline text goes here. Will create a new form to set it in the Admin."
/> />
)} )}
<Statusbar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
/>
<div className={s.buttonsLogoTitleSection}> <div className={s.buttonsLogoTitleSection}>
<ActionButtonRow> <ActionButtonRow>
{externalActionButtons} {externalActionButtons}
@ -158,12 +147,18 @@ export default function ContentComponent() {
<BrowserNotifyModal /> <BrowserNotifyModal />
</Modal> </Modal>
</div> </div>
<StreamInfo isMobile={isMobile} />
</div> </div>
<div className={s.lowerHalf}> <div className={s.lowerHalf}>
<Tabs defaultActiveKey="0"> <Tabs defaultActiveKey="0">
{isChatVisible && isMobile && ( {isChatVisible && isMobile && (
<TabPane tab="Chat" key="0" className={s.pageContentSection}> <TabPane
<div style={{ position: 'relative' }}> tab="Chat"
key="0"
className={s.pageContentSection}
style={{ height: '100%' }}
>
<div style={{ position: 'relative', height: '100%' }}>
<div className={s.mobileChat}> <div className={s.mobileChat}>
<ChatContainer <ChatContainer
messages={messages} messages={messages}
@ -173,8 +168,8 @@ export default function ContentComponent() {
isModerator={false} isModerator={false}
isMobile={isMobile} isMobile={isMobile}
/> />
<ChatTextField />
</div> </div>
<ChatTextField />
</div> </div>
</TabPane> </TabPane>
)} )}
@ -186,7 +181,7 @@ export default function ContentComponent() {
<FollowerCollection total={total} followers={followers} /> <FollowerCollection total={total} followers={followers} />
</TabPane> </TabPane>
</Tabs> </Tabs>
<Footer version={version} /> {!isMobile && <Footer version={version} />}
</div> </div>
</div> </div>
{isChatVisible && !isMobile && <Sidebar />} {isChatVisible && !isMobile && <Sidebar />}

View File

@ -5,15 +5,22 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
z-index: 20; z-index: 20;
padding: 0.5rem 1rem; padding: 0.4rem .7rem;
background-color: var(--default-bg-color); background-color: var(--default-bg-color);
.logo { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
span { span {
margin-left: 1rem; margin-left: .5rem;
font-size: 1.7rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
} }
} }
} }
@media (max-width: 768px) {
.header {
line-height: 5vh;
height: 5vh;
}
}

View File

@ -4,8 +4,9 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
width: clamp(4rem, 10vw, 120px); margin-right: .5rem;
height: clamp(4rem, 10vw, 120px); width: clamp(2.5vh, 9vw, 120px);
height: clamp(2.5vh, 9vw, 120px);
border-radius: 50%; border-radius: 50%;
border-width: 3px; border-width: 3px;
border-style: solid; border-style: solid;
@ -27,3 +28,4 @@
background-position: center; background-position: center;
overflow: hidden; overflow: hidden;
} }

View File

@ -7,9 +7,8 @@
width: clamp(200px, 100%, 300px); width: clamp(200px, 100%, 300px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
;
background-color: var(--theme-background-secondary); background-color: var(--theme-background-secondary);
margin: auto; margin: 1rem auto;
border-radius: var(--theme-rounded-corners); border-radius: var(--theme-rounded-corners);
padding: 1rem; padding: 1rem;
} }

View File

@ -0,0 +1 @@
export { default } from './Statusbar';

View File

@ -4,3 +4,4 @@ export { default as Footer } from './Footer/index';
export { default as Content } from './Content/index'; export { default as Content } from './Content/index';
export { default as ModIcon } from './ModIcon'; export { default as ModIcon } from './ModIcon';
export { default as ServerLogo } from './Logo'; export { default as ServerLogo } from './Logo';
export { default as StatusBar } from './Statusbar';

View File

@ -10,3 +10,9 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (max-width: 768px) {
.player {
height: 30vh !important;
}
}

View File

@ -9,11 +9,17 @@ body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: var(--theme-font-family), var(--theme-header-font-family), sans-serif; font-family: var(--theme-font-family), var(--theme-header-font-family), sans-serif;
font-size: clamp(14px, 1.5vw, 17px); font-size: clamp(14px, 1vw, 17px);
background-color: var(--default-bg-color); background-color: var(--default-bg-color);
color: var(--default-text-color); color: var(--default-text-color);
} }
@media (max-width: 768px) {
body {
overflow: hidden;
}
}
// a { // a {
// color: inherit; // color: inherit;
// text-decoration: none; // text-decoration: none;