Add experimental scroll-based effects

- Scroll to top = refresh Home
- Scroll up/down = show/hide header and compose button
- Scroll near bottom = load next statuses
- Move Compose button to only at Home instead of 'App' level
This commit is contained in:
Lim Chee Aun 2023-01-02 21:36:24 +08:00
parent c2bf9eabc5
commit 39124ccc70
4 changed files with 112 additions and 32 deletions

View file

@ -90,6 +90,13 @@ a.mention span {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
align-items: center; align-items: center;
user-select: none; user-select: none;
transition: transform 0.5s ease-in-out;
}
.deck header[hidden] {
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
user-select: none;
} }
.deck header > .header-side:last-of-type { .deck header > .header-side:last-of-type {
text-align: right; text-align: right;
@ -350,6 +357,7 @@ a.mention span {
color: inherit; color: inherit;
transition: background-color 0.2s ease-out; transition: background-color 0.2s ease-out;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
animation: appear 0.2s ease-out;
} }
.status-link:is(:hover, :focus) { .status-link:is(:hover, :focus) {
background-color: var(--link-bg-hover-color); background-color: var(--link-bg-hover-color);
@ -600,7 +608,18 @@ button.carousel-dot[disabled].active {
z-index: 1; z-index: 1;
box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color), box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color),
0 10px 36px -4px var(--button-bg-blur-color); 0 10px 36px -4px var(--button-bg-blur-color);
transition: background-color 0.2s ease-in-out; transition: all 0.3s ease-in-out;
}
#compose-button[hidden] {
transform: translateY(150%);
pointer-events: none;
user-select: none;
}
#compose-button .icon {
transition: transform 0.3s ease-in-out;
}
#compose-button[hidden] .icon {
transform: rotate(90deg);
} }
#compose-button:is(:hover, :focus) { #compose-button:is(:hover, :focus) {
background-color: var(--button-bg-color); background-color: var(--button-bg-color);

View file

@ -177,31 +177,12 @@ function App() {
return ( return (
<> <>
{isLoggedIn && currentDeck && ( {isLoggedIn && currentDeck && (
<> <div class="decks">
<button {/* Home will never be unmounted */}
type="button" <Home hidden={currentDeck !== 'home'} />
id="compose-button" {/* Notifications can be unmounted */}
onClick={(e) => { {currentDeck === 'notifications' && <Notifications />}
if (e.shiftKey) { </div>
const newWin = openCompose();
if (!newWin) {
alert('Looks like your browser is blocking popups.');
states.showCompose = true;
}
} else {
states.showCompose = true;
}
}}
>
<Icon icon="quill" size="xxl" alt="Compose" />
</button>
<div class="decks">
{/* Home will never be unmounted */}
<Home hidden={currentDeck !== 'home'} />
{/* Notifications can be unmounted */}
{currentDeck === 'notifications' && <Notifications />}
</div>
</>
)} )}
{!isLoggedIn && uiState === 'loading' && <Loader />} {!isLoggedIn && uiState === 'loading' && <Loader />}
<Router <Router

View file

@ -8,6 +8,8 @@ import Icon from '../components/icon';
import Loader from '../components/loader'; import Loader from '../components/loader';
import Status from '../components/status'; import Status from '../components/status';
import states from '../utils/states'; import states from '../utils/states';
import useDebouncedCallback from '../utils/useDebouncedCallback';
import useScroll from '../utils/useScroll';
const LIMIT = 20; const LIMIT = 20;
@ -52,7 +54,10 @@ function Home({ hidden }) {
return allStatuses; return allStatuses;
} }
const loadStatuses = (firstLoad) => { const loadingStatuses = useRef(false);
const loadStatuses = useDebouncedCallback((firstLoad) => {
if (loadingStatuses.current) return;
loadingStatuses.current = true;
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
@ -62,9 +67,11 @@ function Home({ hidden }) {
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
setUIState('error'); setUIState('error');
} finally {
loadingStatuses.current = false;
} }
})(); })();
}; }, 1000);
useEffect(() => { useEffect(() => {
loadStatuses(true); loadStatuses(true);
@ -154,6 +161,25 @@ function Home({ hidden }) {
} }
}); });
const { scrollDirection, reachTop, nearReachTop, nearReachBottom } =
useScroll({
scrollableElement: scrollableRef.current,
distanceFromTop: window.innerHeight / 2,
distanceFromBottom: window.innerHeight,
});
useEffect(() => {
if (nearReachBottom && showMore) {
loadStatuses();
}
}, [nearReachBottom]);
useEffect(() => {
if (reachTop) {
loadStatuses(true);
}
}, [reachTop]);
return ( return (
<div <div
id="home-page" id="home-page"
@ -162,8 +188,27 @@ function Home({ hidden }) {
ref={scrollableRef} ref={scrollableRef}
tabIndex="-1" tabIndex="-1"
> >
<button
hidden={scrollDirection === 'down' && !nearReachTop}
type="button"
id="compose-button"
onClick={(e) => {
if (e.shiftKey) {
const newWin = openCompose();
if (!newWin) {
alert('Looks like your browser is blocking popups.');
states.showCompose = true;
}
} else {
states.showCompose = true;
}
}}
>
<Icon icon="quill" size="xxl" alt="Compose" />
</button>
<div class="timeline-deck deck"> <div class="timeline-deck deck">
<header <header
hidden={scrollDirection === 'down' && !nearReachTop}
onClick={() => { onClick={() => {
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
@ -240,7 +285,7 @@ function Home({ hidden }) {
})} })}
{showMore && ( {showMore && (
<> <>
<InView {/* <InView
as="li" as="li"
style={{ style={{
height: '20vh', height: '20vh',
@ -250,9 +295,9 @@ function Home({ hidden }) {
}} }}
root={scrollableRef.current} root={scrollableRef.current}
rootMargin="100px 0px" rootMargin="100px 0px"
> > */}
<Status skeleton /> <Status skeleton />
</InView> {/* </InView> */}
<li <li
style={{ style={{
height: '25vh', height: '25vh',

35
src/utils/useScroll.js Normal file
View file

@ -0,0 +1,35 @@
import { useEffect, useState } from 'preact/hooks';
export default function useScroll({
scrollableElement = window,
distanceFromTop = 0,
distanceFromBottom = 0,
} = {}) {
const [scrollDirection, setScrollDirection] = useState(null);
const [reachTop, setReachTop] = useState(false);
const [nearReachTop, setNearReachTop] = useState(false);
const [nearReachBottom, setNearReachBottom] = useState(false);
useEffect(() => {
let previousScrollTop = scrollableElement.scrollTop;
function onScroll() {
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
setScrollDirection(previousScrollTop < scrollTop ? 'down' : 'up');
previousScrollTop = scrollTop;
setReachTop(scrollTop === 0);
setNearReachTop(scrollTop <= distanceFromTop);
setNearReachBottom(
scrollTop + clientHeight >= scrollHeight - distanceFromBottom,
);
}
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
return () => scrollableElement.removeEventListener('scroll', onScroll);
}, [scrollableElement, distanceFromTop, distanceFromBottom]);
return { scrollDirection, reachTop, nearReachTop, nearReachBottom };
}