mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
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:
parent
c2bf9eabc5
commit
39124ccc70
4 changed files with 112 additions and 32 deletions
21
src/app.css
21
src/app.css
|
@ -90,6 +90,13 @@ a.mention span {
|
|||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
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 {
|
||||
text-align: right;
|
||||
|
@ -350,6 +357,7 @@ a.mention span {
|
|||
color: inherit;
|
||||
transition: background-color 0.2s ease-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: appear 0.2s ease-out;
|
||||
}
|
||||
.status-link:is(:hover, :focus) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
|
@ -600,7 +608,18 @@ button.carousel-dot[disabled].active {
|
|||
z-index: 1;
|
||||
box-shadow: 0 3px 8px -1px var(--bg-faded-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) {
|
||||
background-color: var(--button-bg-color);
|
||||
|
|
31
src/app.jsx
31
src/app.jsx
|
@ -177,31 +177,12 @@ function App() {
|
|||
return (
|
||||
<>
|
||||
{isLoggedIn && currentDeck && (
|
||||
<>
|
||||
<button
|
||||
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="decks">
|
||||
{/* Home will never be unmounted */}
|
||||
<Home hidden={currentDeck !== 'home'} />
|
||||
{/* Notifications can be unmounted */}
|
||||
{currentDeck === 'notifications' && <Notifications />}
|
||||
</div>
|
||||
</>
|
||||
<div class="decks">
|
||||
{/* Home will never be unmounted */}
|
||||
<Home hidden={currentDeck !== 'home'} />
|
||||
{/* Notifications can be unmounted */}
|
||||
{currentDeck === 'notifications' && <Notifications />}
|
||||
</div>
|
||||
)}
|
||||
{!isLoggedIn && uiState === 'loading' && <Loader />}
|
||||
<Router
|
||||
|
|
|
@ -8,6 +8,8 @@ import Icon from '../components/icon';
|
|||
import Loader from '../components/loader';
|
||||
import Status from '../components/status';
|
||||
import states from '../utils/states';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||
import useScroll from '../utils/useScroll';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
|
@ -52,7 +54,10 @@ function Home({ hidden }) {
|
|||
return allStatuses;
|
||||
}
|
||||
|
||||
const loadStatuses = (firstLoad) => {
|
||||
const loadingStatuses = useRef(false);
|
||||
const loadStatuses = useDebouncedCallback((firstLoad) => {
|
||||
if (loadingStatuses.current) return;
|
||||
loadingStatuses.current = true;
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
|
@ -62,9 +67,11 @@ function Home({ hidden }) {
|
|||
} catch (e) {
|
||||
console.warn(e);
|
||||
setUIState('error');
|
||||
} finally {
|
||||
loadingStatuses.current = false;
|
||||
}
|
||||
})();
|
||||
};
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div
|
||||
id="home-page"
|
||||
|
@ -162,8 +188,27 @@ function Home({ hidden }) {
|
|||
ref={scrollableRef}
|
||||
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">
|
||||
<header
|
||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
||||
onClick={() => {
|
||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
|
@ -240,7 +285,7 @@ function Home({ hidden }) {
|
|||
})}
|
||||
{showMore && (
|
||||
<>
|
||||
<InView
|
||||
{/* <InView
|
||||
as="li"
|
||||
style={{
|
||||
height: '20vh',
|
||||
|
@ -250,9 +295,9 @@ function Home({ hidden }) {
|
|||
}}
|
||||
root={scrollableRef.current}
|
||||
rootMargin="100px 0px"
|
||||
>
|
||||
<Status skeleton />
|
||||
</InView>
|
||||
> */}
|
||||
<Status skeleton />
|
||||
{/* </InView> */}
|
||||
<li
|
||||
style={{
|
||||
height: '25vh',
|
||||
|
|
35
src/utils/useScroll.js
Normal file
35
src/utils/useScroll.js
Normal 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 };
|
||||
}
|
Loading…
Reference in a new issue