feat: provide and consume scroll offsets

This commit is contained in:
Paul Makles 2021-12-30 18:15:31 +00:00
parent 9387575372
commit bad7458560
17 changed files with 113 additions and 97 deletions

View file

@ -10,7 +10,6 @@ import { Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import Header from "../ui/Header";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
@ -20,7 +19,7 @@ interface Props {
background?: boolean; background?: boolean;
} }
const ServerBanner = styled.div<Props>` const ServerBanner = styled.div<Omit<Props, "server">>`
background-color: var(--secondary-header); background-color: var(--secondary-header);
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@ -44,7 +43,8 @@ const ServerBanner = styled.div<Props>`
`} `}
.container { .container {
height: 48px; height: var(--header-height);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 14px; padding: 0 14px;
@ -54,12 +54,6 @@ const ServerBanner = styled.div<Props>`
text-overflow: ellipsis; text-overflow: ellipsis;
gap: 8px; gap: 8px;
${() =>
isTouchscreenDevice &&
css`
height: 56px;
`}
.title { .title {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;

View file

@ -16,12 +16,12 @@ import { Children } from "../../types/Preact";
interface Props { interface Props {
borders?: boolean; borders?: boolean;
background?: boolean; background?: boolean;
transparent?: boolean;
placement: "primary" | "secondary"; placement: "primary" | "secondary";
} }
const Header = styled.div<Props>` const Header = styled.div<Props>`
gap: 10px; gap: 10px;
height: 48px;
flex: 0 auto; flex: 0 auto;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@ -29,16 +29,11 @@ const Header = styled.div<Props>`
font-weight: 600; font-weight: 600;
user-select: none; user-select: none;
align-items: center; align-items: center;
height: var(--header-height);
background-size: cover !important; background-size: cover !important;
background-position: center !important; background-position: center !important;
background-color: rgba(
var(--primary-header-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
z-index: 20;
position: absolute;
width: 100%;
svg { svg {
flex-shrink: 0; flex-shrink: 0;
@ -49,11 +44,21 @@ const Header = styled.div<Props>`
color: var(--secondary-foreground); color: var(--secondary-foreground);
} }
${() => ${(props) =>
isTouchscreenDevice && props.transparent
css` ? css`
height: 56px; background-color: rgba(
`} var(--primary-header-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
z-index: 20;
position: absolute;
width: 100%;
`
: css`
background-color: var(--primary-header);
`}
${(props) => ${(props) =>
props.background && props.background &&
@ -99,19 +104,19 @@ const IconContainer = styled.div`
`} `}
`; `;
interface PageHeaderProps { type PageHeaderProps = Omit<Props, "placement" | "borders"> & {
noBurger?: boolean; noBurger?: boolean;
children: Children; children: Children;
icon: Children; icon: Children;
} };
export const PageHeader = observer( export const PageHeader = observer(
({ children, icon, noBurger }: PageHeaderProps) => { ({ children, icon, noBurger, ...props }: PageHeaderProps) => {
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
const visible = layout.getSectionState(SIDEBAR_CHANNELS, true); const visible = layout.getSectionState(SIDEBAR_CHANNELS, true);
return ( return (
<Header placement="primary" borders={!visible}> <Header {...props} placement="primary" borders={!visible}>
{!noBurger && <HamburgerAction />} {!noBurger && <HamburgerAction />}
<IconContainer <IconContainer
onClick={() => onClick={() =>
@ -136,7 +141,7 @@ export function HamburgerAction() {
function openSidebar() { function openSidebar() {
document document
.querySelector("#app > div > div") .querySelector("#app > div > div > div")
?.scrollTo({ behavior: "smooth", left: 0 }); ?.scrollTo({ behavior: "smooth", left: 0 });
} }

View file

@ -72,10 +72,14 @@ export type Theme = Overrides & {
font?: Fonts; font?: Fonts;
css?: string; css?: string;
monospaceFont?: MonospaceFonts; monospaceFont?: MonospaceFonts;
"min-opacity"?: number; "min-opacity"?: number;
}; };
export type ComputedVariables = Theme & {
"header-height"?: string;
"effective-bottom-offset"?: string;
};
export interface ThemeOptions { export interface ThemeOptions {
base?: string; base?: string;
ligatures?: boolean; ligatures?: boolean;

View file

@ -138,6 +138,16 @@ export default class Layout implements Store, Persistent<Data> {
return this.lastHomePath; return this.lastHomePath;
} }
/**
* Get the last path the user had open.
* @returns Last path
*/
@computed getLastPath() {
return this.lastSection === "home"
? this.lastHomePath
: this.getLastOpened(this.lastSection);
}
/** /**
* Set the current path open in the home tab. * Set the current path open in the home tab.
* @param path Pathname * @param path Pathname

View file

@ -1,6 +1,8 @@
import rgba from "color-rgba"; import rgba from "color-rgba";
import { makeAutoObservable, computed, action } from "mobx"; import { makeAutoObservable, computed, action } from "mobx";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
Theme, Theme,
PRESETS, PRESETS,
@ -9,6 +11,7 @@ import {
DEFAULT_MONO_FONT, DEFAULT_MONO_FONT,
Fonts, Fonts,
MonospaceFonts, MonospaceFonts,
ComputedVariables,
} from "../../../context/Theme"; } from "../../../context/Theme";
import Settings from "../Settings"; import Settings from "../Settings";
@ -94,14 +97,10 @@ export default class STheme {
...PRESETS[this.getBase()], ...PRESETS[this.getBase()],
...this.settings.get("appearance:theme:overrides"), ...this.settings.get("appearance:theme:overrides"),
light: this.isLight(), light: this.isLight(),
"min-opacity": this.settings.get("appearance:transparency", true)
? 0
: 1,
}; };
} }
@computed computeVariables(): Theme { @computed computeVariables(): ComputedVariables {
const variables = this.getVariables() as Record< const variables = this.getVariables() as Record<
string, string,
string | boolean | number string | boolean | number
@ -114,7 +113,16 @@ export default class STheme {
} }
} }
return variables as unknown as Theme; return {
...(variables as unknown as Theme),
"min-opacity": this.settings.get("appearance:transparency", true)
? 0
: 1,
"header-height": isTouchscreenDevice ? "56px" : "48px",
"effective-bottom-offset": isTouchscreenDevice
? "var(--bottom-navigation-height)"
: "0px",
};
} }
@action setVariable(key: Variables, value: string) { @action setVariable(key: Variables, value: string) {

View file

@ -45,7 +45,7 @@ const StatusBar = styled.div`
} }
`; `;
const Routes = styled.div` const Routes = styled.div.attrs({ "data-component": "routes" })`
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -29,20 +29,17 @@ import ChannelHeader from "./ChannelHeader";
import { MessageArea } from "./messaging/MessageArea"; import { MessageArea } from "./messaging/MessageArea";
import VoiceHeader from "./voice/VoiceHeader"; import VoiceHeader from "./voice/VoiceHeader";
const ChannelMain = styled.div` const ChannelMain = styled.div.attrs({ "data-component": "channel" })`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
flex-direction: row; flex-direction: row;
> * > ::-webkit-scrollbar-thumb {
background-clip: content-box;
border-top: 48px solid transparent;
}
`; `;
const ChannelContent = styled.div` const ChannelContent = styled.div.attrs({
"data-component": "content",
})`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
overflow: hidden; overflow: hidden;

View file

@ -78,7 +78,6 @@ const Info = styled.div`
export default observer(({ channel }: ChannelHeaderProps) => { export default observer(({ channel }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const layout = useApplicationState().layout;
const name = getChannelName(channel); const name = getChannelName(channel);
let icon, recipient: User | undefined; let icon, recipient: User | undefined;
@ -99,7 +98,7 @@ export default observer(({ channel }: ChannelHeaderProps) => {
} }
return ( return (
<PageHeader icon={icon}> <PageHeader icon={icon} transparent>
<Info> <Info>
<span className="name">{name}</span> <span className="name">{name}</span>
{isTouchscreenDevice && {isTouchscreenDevice &&

View file

@ -30,7 +30,7 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
const history = useHistory(); const history = useHistory();
function openRightSidebar() { function openRightSidebar() {
const panels = document.querySelector("#app > div > div"); const panels = document.querySelector("#app > div > div > div");
panels?.scrollTo({ panels?.scrollTo({
behavior: "smooth", behavior: "smooth",
left: panels.clientWidth * 3, left: panels.clientWidth * 3,
@ -84,7 +84,11 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
)} )}
{(channel.channel_type === "Group" || {(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") && ( channel.channel_type === "TextChannel") && (
<IconButton onClick={openSidebar}> <IconButton
onClick={() => {
internalEmit("RightSidebar", "open", undefined);
openRightSidebar();
}}>
<Group size={25} /> <Group size={25} />
</IconButton> </IconButton>
)} )}

View file

@ -33,14 +33,18 @@ import Preloader from "../../../components/ui/Preloader";
import ConversationStart from "./ConversationStart"; import ConversationStart from "./ConversationStart";
import MessageRenderer from "./MessageRenderer"; import MessageRenderer from "./MessageRenderer";
const Area = styled.div` const Area = styled.div.attrs({ "data-scroll-offset": "with-padding" })`
height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
word-break: break-word;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
padding-top: 48px;
word-break: break-word; &::-webkit-scrollbar-thumb {
min-height: 150px;
}
> div { > div {
display: flex; display: flex;

View file

@ -29,7 +29,7 @@ export default function Developer() {
return ( return (
<div> <div>
<PageHeader icon={<Wrench size="24" />}>Developer Tab</PageHeader> <PageHeader icon={<Wrench size="24" />}>Developer Tab</PageHeader>
<div style={{ padding: "16px", paddingTop: "56px" }}> <div style={{ padding: "16px" }}>
<PaintCounter always /> <PaintCounter always />
</div> </div>
<div style={{ padding: "16px" }}> <div style={{ padding: "16px" }}>

View file

@ -12,9 +12,7 @@
.list { .list {
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
padding-top: 48px;
user-select: none; user-select: none;
/*overflow-y: scroll;*/
&[data-empty="true"] { &[data-empty="true"] {
img { img {
@ -186,12 +184,9 @@
} }
} }
// Hide the remove friend button on smaller screens.
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.list { .list .remove {
padding: 56px 8px 8px 8px; display: none;
.remove {
display: none;
}
} }
} }

View file

@ -6,6 +6,7 @@ import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import classNames from "classnames";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { TextReact } from "../../lib/i18n"; import { TextReact } from "../../lib/i18n";
@ -17,32 +18,12 @@ import { useClient } from "../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../components/common/CollapsibleSection"; import CollapsibleSection from "../../components/common/CollapsibleSection";
import Tooltip from "../../components/common/Tooltip"; import Tooltip from "../../components/common/Tooltip";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import Header, { PageHeader } from "../../components/ui/Header"; import { PageHeader } from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton"; import IconButton from "../../components/ui/IconButton";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import { Friend } from "./Friend"; import { Friend } from "./Friend";
const Container = styled.div`
overflow-y: scroll;
::-webkit-scrollbar-thumb {
min-height: 100px;
width: 4px;
background-clip: content-box;
border-top: 48px solid transparent;
}
${() =>
isTouchscreenDevice &&
css`
::-webkit-scrollbar-thumb {
min-height: 150px;
border-top: 56px solid transparent;
border-bottom: var(--bottom-navigation-height) solid transparent;
}
`}
`;
export default observer(() => { export default observer(() => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
@ -93,7 +74,7 @@ export default observer(() => {
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0; const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
return ( return (
<> <>
<PageHeader icon={<UserDetail size={24} />} noBurger> <PageHeader icon={<UserDetail size={24} />} transparent noBurger>
<div className={styles.title}> <div className={styles.title}>
<Text id="app.navigation.tabs.friends" /> <Text id="app.navigation.tabs.friends" />
</div> </div>
@ -136,9 +117,9 @@ export default observer(() => {
*/} */}
</div> </div>
</PageHeader> </PageHeader>
<Container> <div data-scroll-offset="true" data-avoids-navigation="true">
<div <div
className={styles.list} className={classNames(styles.list, "with-padding")}
data-empty={isEmpty} data-empty={isEmpty}
data-mobile={isTouchscreenDevice}> data-mobile={isTouchscreenDevice}>
{isEmpty && ( {isEmpty && (
@ -234,7 +215,7 @@ export default observer(() => {
); );
})} })}
</div> </div>
</Container> </div>
</> </>
); );
}); });

View file

@ -83,7 +83,7 @@ export default observer(() => {
</div> </div>
)} )}
<div className="content"> <div className="content">
<PageHeader icon={<HomeIcon size={24} />}> <PageHeader icon={<HomeIcon size={24} />} transparent>
<Text id="app.navigation.tabs.home" /> <Text id="app.navigation.tabs.home" />
</PageHeader> </PageHeader>
<div className={styles.homeScreen}> <div className={styles.homeScreen}>

View file

@ -102,7 +102,7 @@ export function GenericSettings({
/> />
</Helmet> </Helmet>
{isTouchscreenDevice && ( {isTouchscreenDevice && (
<Header placement="primary"> <Header placement="primary" transparent>
{typeof page === "undefined" ? ( {typeof page === "undefined" ? (
<> <>
{showExitButton && ( {showExitButton && (
@ -168,6 +168,9 @@ export function GenericSettings({
<div className={styles.content}> <div className={styles.content}>
<div <div
className={styles.scrollbox} className={styles.scrollbox}
data-scroll-offset={
isTouchscreenDevice ? "with-padding" : undefined
}
ref={(ref) => { ref={(ref) => {
// Force scroll to top if page changes. // Force scroll to top if page changes.
if (ref) { if (ref) {

View file

@ -43,20 +43,13 @@
background: var(--primary-background); background: var(--primary-background);
} }
.scrollbox {
&::-webkit-scrollbar-thumb {
min-height: 150px;
border-top: 56px solid transparent;
//border-bottom: var(--bottom-navigation-height) solid transparent;
}
}
/* Sidebar */ /* Sidebar */
.sidebar { .sidebar {
overflow-y: auto; overflow-y: auto;
.container { .container {
padding: 76px 8px calc(var(--bottom-navigation-height) + 30px); padding: calc(var(--header-height) + 4px) 8px
calc(var(--bottom-navigation-height) + 30px);
min-width: 218px; min-width: 218px;
} }

View file

@ -13,13 +13,32 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
min-height: 15px; min-height: 30px;
min-width: 15px; min-width: 30px;
background-clip: content-box; background-clip: content-box;
background: var(--scrollbar-thumb); background: var(--scrollbar-thumb);
} }
[data-scroll-offset] {
overflow-y: scroll;
}
[data-scroll-offset="with-padding"],
[data-scroll-offset] .with-padding {
padding-top: var(--header-height);
}
[data-scroll-offset]::-webkit-scrollbar-thumb {
background-clip: content-box;
border-top: var(--header-height) solid transparent;
}
[data-avoids-navigation]::-webkit-scrollbar-thumb {
background-clip: content-box;
border-bottom: var(--effective-bottom-offset) solid transparent;
}
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background: transparent; background: transparent;
} }