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;
|
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);
|
||||||
|
|
31
src/app.jsx
31
src/app.jsx
|
@ -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
|
||||||
|
|
|
@ -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
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