Minimum viable Home → Following port
This commit is contained in:
parent
c6c18aae09
commit
9921e487e8
7 changed files with 147 additions and 8 deletions
|
@ -701,9 +701,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.updates-button {
|
.updates-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
top: 3em;
|
||||||
animation: fade-from-top 0.3s ease-out;
|
animation: fade-from-top 0.3s ease-out;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-top: 8px;
|
margin-top: 16px;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
|
|
|
@ -94,8 +94,10 @@ function App() {
|
||||||
if (account) {
|
if (account) {
|
||||||
store.session.set('currentAccount', account.info.id);
|
store.session.set('currentAccount', account.info.id);
|
||||||
const { masto } = api({ account });
|
const { masto } = api({ account });
|
||||||
initInstance(masto);
|
(async () => {
|
||||||
setIsLoggedIn(true);
|
await initInstance(masto);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
|
|
@ -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 usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -19,14 +20,17 @@ function Timeline({
|
||||||
useItemID, // use statusID instead of status object, assuming it's already in states
|
useItemID, // use statusID instead of status object, assuming it's already in states
|
||||||
boostsCarousel,
|
boostsCarousel,
|
||||||
fetchItems = () => {},
|
fetchItems = () => {},
|
||||||
|
checkForUpdates = () => {},
|
||||||
}) {
|
}) {
|
||||||
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 scrollableRef = useRef();
|
const scrollableRef = useRef();
|
||||||
|
|
||||||
const loadItems = useDebouncedCallback(
|
const loadItems = useDebouncedCallback(
|
||||||
(firstLoad) => {
|
(firstLoad) => {
|
||||||
|
setShowNew(false);
|
||||||
if (uiState === 'loading') return;
|
if (uiState === 'loading') return;
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -148,9 +152,16 @@ function Timeline({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nearReachEnd, reachStart, reachEnd } = useScroll({
|
const {
|
||||||
|
scrollDirection,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
reachStart,
|
||||||
|
reachEnd,
|
||||||
|
} = useScroll({
|
||||||
scrollableElement: scrollableRef.current,
|
scrollableElement: scrollableRef.current,
|
||||||
distanceFromEnd: 1,
|
distanceFromEnd: 2,
|
||||||
|
scrollThresholdStart: 44,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -170,6 +181,32 @@ function Timeline({
|
||||||
}
|
}
|
||||||
}, [nearReachEnd, showMore]);
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
|
const lastHiddenTime = useRef();
|
||||||
|
usePageVisibility(
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
if (lastHiddenTime.current) {
|
||||||
|
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||||
|
if (timeDiff > 1000 * 60) {
|
||||||
|
(async () => {
|
||||||
|
console.log('✨ Check updates');
|
||||||
|
const hasUpdate = await checkForUpdates();
|
||||||
|
if (hasUpdate) {
|
||||||
|
console.log('✨ Has new updates');
|
||||||
|
setShowNew(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastHiddenTime.current = Date.now();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[checkForUpdates],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`${id}-page`}
|
id={`${id}-page`}
|
||||||
|
@ -184,6 +221,7 @@ function Timeline({
|
||||||
>
|
>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header
|
<header
|
||||||
|
hidden={hiddenUI}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
scrollableRef.current?.scrollTo({
|
scrollableRef.current?.scrollTo({
|
||||||
|
@ -202,6 +240,24 @@ function Timeline({
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
</div>
|
</div>
|
||||||
|
{items.length > 0 &&
|
||||||
|
uiState !== 'loading' &&
|
||||||
|
!hiddenUI &&
|
||||||
|
showNew && (
|
||||||
|
<button
|
||||||
|
class="updates-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
loadItems(true);
|
||||||
|
scrollableRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up" /> New posts
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
{!!items.length ? (
|
{!!items.length ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useRef } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { saveStatus } from '../utils/states';
|
import { getStatus, saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -14,6 +14,8 @@ function Following() {
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const homeIterator = useRef();
|
const homeIterator = useRef();
|
||||||
|
const latestItem = useRef();
|
||||||
|
|
||||||
async function fetchHome(firstLoad) {
|
async function fetchHome(firstLoad) {
|
||||||
if (firstLoad || !homeIterator.current) {
|
if (firstLoad || !homeIterator.current) {
|
||||||
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
||||||
|
@ -21,6 +23,10 @@ function Following() {
|
||||||
const results = await homeIterator.current.next();
|
const results = await homeIterator.current.next();
|
||||||
const { value } = results;
|
const { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
if (firstLoad) {
|
||||||
|
latestItem.current = value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
saveStatus(item, instance);
|
saveStatus(item, instance);
|
||||||
});
|
});
|
||||||
|
@ -35,6 +41,64 @@ function Following() {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const results = await masto.v1.timelines
|
||||||
|
.listHome({
|
||||||
|
limit: 5,
|
||||||
|
since_id: latestItem.current,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
const { value } = results;
|
||||||
|
console.log('checkForUpdates', value);
|
||||||
|
if (value?.some((item) => !item.reblog)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = useRef();
|
||||||
|
async function streamUser() {
|
||||||
|
if (
|
||||||
|
ws.current &&
|
||||||
|
(ws.current.readyState === WebSocket.CONNECTING ||
|
||||||
|
ws.current.readyState === WebSocket.OPEN)
|
||||||
|
) {
|
||||||
|
console.log('🎏 Streaming user already open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stream = await masto.v1.stream.streamUser();
|
||||||
|
ws.current = stream.ws;
|
||||||
|
console.log('🎏 Streaming user');
|
||||||
|
|
||||||
|
stream.on('status.update', (status) => {
|
||||||
|
console.log(`🔄 Status ${status.id} updated`);
|
||||||
|
saveStatus(status, instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('delete', (statusID) => {
|
||||||
|
console.log(`❌ Status ${statusID} deleted`);
|
||||||
|
// delete states.statuses[statusID];
|
||||||
|
const s = getStatus(statusID, instance);
|
||||||
|
if (s) s._deleted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
streamUser();
|
||||||
|
return () => {
|
||||||
|
if (ws.current) {
|
||||||
|
console.log('🎏 Closing streaming user');
|
||||||
|
ws.current.close();
|
||||||
|
ws.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
title="Following"
|
title="Following"
|
||||||
|
@ -42,6 +106,7 @@ function Following() {
|
||||||
emptyText="Nothing to see here."
|
emptyText="Nothing to see here."
|
||||||
errorText="Unable to load posts."
|
errorText="Unable to load posts."
|
||||||
fetchItems={fetchHome}
|
fetchItems={fetchHome}
|
||||||
|
checkForUpdates={checkForUpdates}
|
||||||
useItemID
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -294,7 +294,7 @@ function Home({ hidden }) {
|
||||||
reachStart,
|
reachStart,
|
||||||
);
|
);
|
||||||
setShowUpdatesButton(isNewAndTop);
|
setShowUpdatesButton(isNewAndTop);
|
||||||
}, [snapStates.homeNew.length]);
|
}, [snapStates.homeNew.length, reachStart]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -83,6 +83,7 @@ export async function initInstance(client) {
|
||||||
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
||||||
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
||||||
if (streamingApi || streaming) {
|
if (streamingApi || streaming) {
|
||||||
|
console.log('🎏 Streaming API URL:', streaming || streamingApi);
|
||||||
masto.config.props.streamingApiUrl = streaming || streamingApi;
|
masto.config.props.streamingApiUrl = streaming || streamingApi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/utils/usePageVisibility.js
Normal file
14
src/utils/usePageVisibility.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function usePageVisibility(fn = () => {}, deps = []) {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
const hidden = document.hidden || document.visibilityState === 'hidden';
|
||||||
|
fn(!hidden);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}, [fn, ...deps]);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue