More code porting

This commit is contained in:
Lim Chee Aun 2023-02-08 19:11:33 +08:00
parent 9921e487e8
commit f511b0a5ab
9 changed files with 282 additions and 240 deletions

View file

@ -84,43 +84,46 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
} }
.deck > header { .deck > header {
min-height: 3em;
position: sticky; position: sticky;
top: 0; top: 0;
background-color: var(--bg-blur-color);
background-image: linear-gradient(to bottom, var(--bg-color), transparent);
backdrop-filter: saturate(180%) blur(20px);
border-bottom: var(--hairline-width) solid var(--divider-color);
z-index: 1; z-index: 1;
cursor: default; cursor: default;
z-index: 10; z-index: 10;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
user-select: none; user-select: none;
transition: transform 0.5s ease-in-out; transition: transform 0.5s ease-in-out;
user-select: none; user-select: none;
} }
.deck > header[hidden] { .deck > header[hidden] {
display: block;
transform: translateY(-100%); transform: translateY(-100%);
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
.deck > header > .header-side:last-of-type { .deck > header .header-grid {
background-color: var(--bg-blur-color);
background-image: linear-gradient(to bottom, var(--bg-color), transparent);
backdrop-filter: saturate(180%) blur(20px);
border-bottom: var(--hairline-width) solid var(--divider-color);
min-height: 3em;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
}
.deck > header .header-grid > .header-side:last-of-type {
text-align: right; text-align: right;
grid-column: 3; grid-column: 3;
} }
.deck > header :is(button, .button).plain { .deck > header .header-grid :is(button, .button).plain {
backdrop-filter: none; backdrop-filter: none;
} }
.deck > header h1 { .deck > header .header-grid h1 {
margin: 0 8px; margin: 0 8px;
padding: 0; padding: 0;
font-size: 1.2em; font-size: 1.2em;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
} }
.deck > header h1:first-child { .deck > header .header-grid h1:first-child {
text-align: left; text-align: left;
padding-left: 8px; padding-left: 8px;
} }
@ -1211,14 +1214,16 @@ meter.donut:is(.danger, .explode):after {
} }
.timeline-deck > header { .timeline-deck > header {
--margin-top: 8px; --margin-top: 8px;
min-height: 4em;
top: var(--margin-top); top: var(--margin-top);
border-bottom: 0; margin-inline: 8px;
background-color: var(--bg-faded-blur-color); }
background-image: none; .timeline-deck > header .header-grid {
border-bottom: 0; border-bottom: 0;
border-radius: 16px; border-radius: 16px;
margin-inline: 8px; background-color: var(--bg-faded-blur-color);
background-image: none;
border-radius: 16px;
min-height: 4em;
} }
.timeline-deck > header[hidden] { .timeline-deck > header[hidden] {
transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0); transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0);

View file

@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import useInterval from '../utils/useInterval';
import usePageVisibility from '../utils/usePageVisibility'; import usePageVisibility from '../utils/usePageVisibility';
import useScroll from '../utils/useScroll'; import useScroll from '../utils/useScroll';
@ -21,11 +22,13 @@ function Timeline({
boostsCarousel, boostsCarousel,
fetchItems = () => {}, fetchItems = () => {},
checkForUpdates = () => {}, checkForUpdates = () => {},
checkForUpdatesInterval = 60_000, // 1 minute
}) { }) {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const [showNew, setShowNew] = useState(false); const [showNew, setShowNew] = useState(false);
const [visible, setVisible] = useState(true);
const scrollableRef = useRef(); const scrollableRef = useRef();
const loadItems = useDebouncedCallback( const loadItems = useDebouncedCallback(
@ -185,26 +188,40 @@ function Timeline({
usePageVisibility( usePageVisibility(
(visible) => { (visible) => {
if (visible) { if (visible) {
if (lastHiddenTime.current) { const timeDiff = Date.now() - lastHiddenTime.current;
const timeDiff = Date.now() - lastHiddenTime.current; if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
if (timeDiff > 1000 * 60) { (async () => {
(async () => { console.log('✨ Check updates');
console.log('✨ Check updates'); const hasUpdate = await checkForUpdates();
const hasUpdate = await checkForUpdates(); if (hasUpdate) {
if (hasUpdate) { console.log('✨ Has new updates');
console.log('✨ Has new updates'); setShowNew(true);
setShowNew(true); }
} })();
})();
}
} }
} else { } else {
lastHiddenTime.current = Date.now(); lastHiddenTime.current = Date.now();
} }
setVisible(visible);
}, },
[checkForUpdates], [checkForUpdates],
); );
// checkForUpdates interval
useInterval(
() => {
(async () => {
console.log('✨ Check updates');
const hasUpdate = await checkForUpdates();
if (hasUpdate) {
console.log('✨ Has new updates');
setShowNew(true);
}
})();
},
visible && !showNew ? checkForUpdatesInterval : null,
);
const hiddenUI = scrollDirection === 'end' && !nearReachStart; const hiddenUI = scrollDirection === 'end' && !nearReachStart;
return ( return (
@ -231,14 +248,16 @@ function Timeline({
} }
}} }}
> >
<div class="header-side"> <div class="header-grid">
<Link to="/" class="button plain"> <div class="header-side">
<Icon icon="home" size="l" /> <Link to="/" class="button plain">
</Link> <Icon icon="home" size="l" />
</div> </Link>
{title && (titleComponent ? titleComponent : <h1>{title}</h1>)} </div>
<div class="header-side"> {title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
<Loader hidden={uiState !== 'loading'} /> <div class="header-side">
<Loader hidden={uiState !== 'loading'} />
</div>
</div> </div>
{items.length > 0 && {items.length > 0 &&
uiState !== 'loading' && uiState !== 'loading' &&

View file

@ -61,7 +61,8 @@ function Following() {
} }
const ws = useRef(); const ws = useRef();
async function streamUser() { const streamUser = async () => {
console.log('🎏 Start streaming user', ws.current);
if ( if (
ws.current && ws.current &&
(ws.current.readyState === WebSocket.CONNECTING || (ws.current.readyState === WebSocket.CONNECTING ||
@ -72,7 +73,8 @@ function Following() {
} }
const stream = await masto.v1.stream.streamUser(); const stream = await masto.v1.stream.streamUser();
ws.current = stream.ws; ws.current = stream.ws;
console.log('🎏 Streaming user'); ws.current.__id = Math.random();
console.log('🎏 Streaming user', ws.current);
stream.on('status.update', (status) => { stream.on('status.update', (status) => {
console.log(`🔄 Status ${status.id} updated`); console.log(`🔄 Status ${status.id} updated`);
@ -86,14 +88,20 @@ function Following() {
if (s) s._deleted = true; if (s) s._deleted = true;
}); });
stream.ws.onclose = () => {
console.log('🎏 Streaming user closed');
};
return stream; return stream;
} };
useEffect(() => { useEffect(() => {
streamUser(); let stream;
(async () => {
stream = await streamUser();
})();
return () => { return () => {
if (ws.current) { if (stream) {
console.log('🎏 Closing streaming user'); stream.ws.close();
ws.current.close();
ws.current = null; ws.current = null;
} }
}; };

View file

@ -320,63 +320,66 @@ function Home({ hidden }) {
loadStatuses(true); loadStatuses(true);
}} }}
> >
<div class="header-side"> <div class="header-grid">
<button <div class="header-side">
type="button" <button
class="plain" type="button"
onClick={(e) => { class="plain"
e.preventDefault(); onClick={(e) => {
e.stopPropagation(); e.preventDefault();
states.showSettings = true; e.stopPropagation();
}} states.showSettings = true;
> }}
<Icon icon="gear" size="l" alt="Settings" /> >
</button> <Icon icon="gear" size="l" alt="Settings" />
</button>
</div>
<h1>Home</h1>
<div class="header-side">
<Loader hidden={uiState !== 'loading'} />{' '}
<Link
to="/notifications"
class={`button plain ${
snapStates.notificationsNew.length > 0 ? 'has-badge' : ''
}`}
onClick={(e) => {
e.stopPropagation();
}}
>
<Icon icon="notification" size="l" alt="Notifications" />
</Link>
</div>
</div> </div>
<h1>Home</h1> {snapStates.homeNew.length > 0 &&
<div class="header-side"> uiState !== 'loading' &&
<Loader hidden={uiState !== 'loading'} />{' '} ((scrollDirection === 'start' &&
<Link !nearReachStart &&
to="/notifications" !nearReachEnd) ||
class={`button plain ${ showUpdatesButton) && (
snapStates.notificationsNew.length > 0 ? 'has-badge' : '' <button
}`} class="updates-button"
onClick={(e) => { type="button"
e.stopPropagation(); onClick={() => {
}} if (!snapStates.settings.boostsCarousel) {
> const uniqueHomeNew = snapStates.homeNew.filter(
<Icon icon="notification" size="l" alt="Notifications" /> (status) =>
</Link> !states.home.some((s) => s.id === status.id),
</div> );
</header> states.home.unshift(...uniqueHomeNew);
{snapStates.homeNew.length > 0 && }
uiState !== 'loading' && loadStatuses(true);
((scrollDirection === 'start' && states.homeNew = [];
!nearReachStart &&
!nearReachEnd) ||
showUpdatesButton) && (
<button
class="updates-button"
type="button"
onClick={() => {
if (!snapStates.settings.boostsCarousel) {
const uniqueHomeNew = snapStates.homeNew.filter(
(status) => !states.home.some((s) => s.id === status.id),
);
states.home.unshift(...uniqueHomeNew);
}
loadStatuses(true);
states.homeNew = [];
scrollableRef.current?.scrollTo({ scrollableRef.current?.scrollTo({
top: 0, top: 0,
behavior: 'smooth', behavior: 'smooth',
}); });
}} }}
> >
<Icon icon="arrow-up" /> New posts <Icon icon="arrow-up" /> New posts
</button> </button>
)} )}
</header>
{snapStates.home.length ? ( {snapStates.home.length ? (
<> <>
<ul class="timeline"> <ul class="timeline">

View file

@ -143,14 +143,16 @@ function Notifications() {
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
> >
<div class="header-side"> <div class="header-grid">
<Link to="/" class="button plain"> <div class="header-side">
<Icon icon="home" size="l" /> <Link to="/" class="button plain">
</Link> <Icon icon="home" size="l" />
</div> </Link>
<h1>Notifications</h1> </div>
<div class="header-side"> <h1>Notifications</h1>
<Loader hidden={uiState !== 'loading'} /> <div class="header-side">
<Loader hidden={uiState !== 'loading'} />
</div>
</div> </div>
</header> </header>
{snapStates.notificationsNew.length > 0 && uiState !== 'loading' && ( {snapStates.notificationsNew.length > 0 && uiState !== 'loading' && (

View file

@ -2,8 +2,6 @@
grid-column: 1 / 3; grid-column: 1 / 3;
} }
.status-deck header { .status-deck header {
display: flex;
align-items: center;
white-space: nowrap; white-space: nowrap;
} }
.status-deck header h1 { .status-deck header h1 {

View file

@ -469,132 +469,135 @@ function StatusPage() {
<Icon icon="chevron-left" size="xl" /> <Icon icon="chevron-left" size="xl" />
</Link> </Link>
</div> */} </div> */}
<h1> <div class="header-grid">
{!heroInView && heroStatus && uiState !== 'loading' ? ( <h1>
<> {!heroInView && heroStatus && uiState !== 'loading' ? (
<span class="hero-heading"> <>
<NameText <span class="hero-heading">
account={heroStatus.account} <NameText
instance={instance} account={heroStatus.account}
showAvatar instance={instance}
short showAvatar
/>{' '} short
<span class="insignificant"> />{' '}
&bull;{' '} <span class="insignificant">
<RelativeTime &bull;{' '}
datetime={heroStatus.createdAt} <RelativeTime
format="micro" datetime={heroStatus.createdAt}
format="micro"
/>
</span>
</span>{' '}
<button
type="button"
class="ancestors-indicator light small"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
heroStatusRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
>
<Icon
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
/> />
</span> </button>
</span>{' '} </>
<button ) : (
type="button" <>
class="ancestors-indicator light small" Status{' '}
onClick={(e) => { <button
e.preventDefault(); type="button"
e.stopPropagation(); class="ancestors-indicator light small"
heroStatusRef.current.scrollIntoView({ onClick={(e) => {
behavior: 'smooth', // Scroll to top
block: 'start', e.preventDefault();
}); e.stopPropagation();
}} scrollableRef.current.scrollTo({
> top: 0,
<Icon behavior: 'smooth',
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'} });
/> }}
</button> hidden={!ancestors.length || nearReachStart}
</> >
) : ( <Icon icon="arrow-up" />
<> <Icon icon="comment" />{' '}
Status{' '} <span class="insignificant">
<button {shortenNumber(ancestors.length)}
type="button" </span>
class="ancestors-indicator light small" </button>
onClick={(e) => { </>
// Scroll to top )}
e.preventDefault(); </h1>
e.stopPropagation(); <div class="header-side">
scrollableRef.current.scrollTo({ <Loader hidden={uiState !== 'loading'} />
top: 0, <Menu
behavior: 'smooth', align="end"
}); portal={{
}} // Need this, else the menu click will cause scroll jump
hidden={!ancestors.length || nearReachStart} target: scrollableRef.current,
>
<Icon icon="arrow-up" />
<Icon icon="comment" />{' '}
<span class="insignificant">
{shortenNumber(ancestors.length)}
</span>
</button>
</>
)}
</h1>
<div class="header-side">
<Loader hidden={uiState !== 'loading'} />
<Menu
align="end"
portal={{
// Need this, else the menu click will cause scroll jump
target: scrollableRef.current,
}}
menuButton={
<button type="button" class="button plain4">
<Icon icon="more" alt="Actions" size="xl" />
</button>
}
>
<MenuItem
onClick={() => {
// Click all buttons with class .spoiler but not .spoiling
const buttons = Array.from(
scrollableRef.current.querySelectorAll(
'button.spoiler:not(.spoiling)',
),
);
buttons.forEach((button) => {
button.click();
});
}} }}
menuButton={
<button type="button" class="button plain4">
<Icon icon="more" alt="Actions" size="xl" />
</button>
}
> >
<Icon icon="eye-open" /> <span>Show all sensitive content</span>
</MenuItem>
{import.meta.env.DEV && !authenticated && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
(async () => { // Click all buttons with class .spoiler but not .spoiling
try { const buttons = Array.from(
const { masto } = api(); scrollableRef.current.querySelectorAll(
const results = await masto.v2.search({ 'button.spoiler:not(.spoiling)',
q: heroStatus.url, ),
type: 'statuses', );
resolve: true, buttons.forEach((button) => {
limit: 1, button.click();
}); });
if (results.statuses.length) {
const status = results.statuses[0];
navigate(`/s/${status.id}`);
} else {
throw new Error('No results');
}
} catch (e) {
alert('Error: ' + e);
console.error(e);
}
})();
}} }}
> >
See post in currently logged-in instance <Icon icon="eye-open" />{' '}
<span>Show all sensitive content</span>
</MenuItem> </MenuItem>
)} {import.meta.env.DEV && !authenticated && (
</Menu> <MenuItem
<Link onClick={() => {
class="button plain deck-close" (async () => {
to={closeLink} try {
onClick={onClose} const { masto } = api();
> const results = await masto.v2.search({
<Icon icon="x" size="xl" /> q: heroStatus.url,
</Link> type: 'statuses',
resolve: true,
limit: 1,
});
if (results.statuses.length) {
const status = results.statuses[0];
navigate(`/s/${status.id}`);
} else {
throw new Error('No results');
}
} catch (e) {
alert('Error: ' + e);
console.error(e);
}
})();
}}
>
See post in currently logged-in instance
</MenuItem>
)}
</Menu>
<Link
class="button plain deck-close"
to={closeLink}
onClick={onClose}
>
<Icon icon="x" size="xl" />
</Link>
</div>
</div> </div>
</header> </header>
{!!statuses.length && heroStatus ? ( {!!statuses.length && heroStatus ? (

View file

@ -1,22 +1,25 @@
// useInterval with Preact
import { useEffect, useRef } from 'preact/hooks'; import { useEffect, useRef } from 'preact/hooks';
export default function useInterval(callback, delay) { const noop = () => {};
const savedCallback = useRef();
function useInterval(callback, delay, immediate) {
const savedCallback = useRef(noop);
// Remember the latest callback.
useEffect(() => { useEffect(() => {
savedCallback.current = callback; savedCallback.current = callback;
}, [callback]); }, []);
// Set up the interval.
useEffect(() => { useEffect(() => {
function tick() { if (!immediate || delay === null || delay === false) return;
savedCallback.current(); savedCallback.current();
} }, [immediate]);
if (delay !== null) {
let id = setInterval(tick, delay); useEffect(() => {
return () => clearInterval(id); if (delay === null || delay === false) return;
} const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]); }, [delay]);
} }
export default useInterval;

View file

@ -4,6 +4,7 @@ export default function usePageVisibility(fn = () => {}, deps = []) {
useEffect(() => { useEffect(() => {
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
const hidden = document.hidden || document.visibilityState === 'hidden'; const hidden = document.hidden || document.visibilityState === 'hidden';
console.log('👀 Page visibility changed', hidden);
fn(!hidden); fn(!hidden);
}; };