mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
Refactor out a Timeline component
Also replace login() with createClient() for faster log in
This commit is contained in:
parent
1a5816f886
commit
aaeca7dd03
12 changed files with 452 additions and 182 deletions
19
src/app.css
19
src/app.css
|
@ -115,6 +115,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
padding: 0;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.deck > header h1:first-child {
|
||||
text-align: left;
|
||||
|
@ -972,6 +973,24 @@ meter.donut:is(.danger, .explode):after {
|
|||
display: block;
|
||||
}
|
||||
|
||||
/* 404 */
|
||||
|
||||
#not-found-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
color: var(--text-insignificant-color);
|
||||
background-image: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
var(--bg-color) 25%,
|
||||
var(--bg-faded-color)
|
||||
);
|
||||
text-shadow: 0 1px var(--bg-color);
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
html,
|
||||
body {
|
||||
|
|
121
src/app.jsx
121
src/app.jsx
|
@ -2,7 +2,7 @@ import './app.css';
|
|||
import 'toastify-js/src/toastify.css';
|
||||
|
||||
import debounce from 'just-debounce-it';
|
||||
import { login } from 'masto';
|
||||
import { createClient } from 'masto';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
|
@ -21,10 +21,15 @@ import Icon from './components/icon';
|
|||
import Link from './components/link';
|
||||
import Loader from './components/loader';
|
||||
import Modal from './components/modal';
|
||||
import NotFound from './pages/404';
|
||||
import Bookmarks from './pages/bookmarks';
|
||||
import Favourites from './pages/favourites';
|
||||
import Hashtags from './pages/hashtags';
|
||||
import Home from './pages/home';
|
||||
import Lists from './pages/lists';
|
||||
import Login from './pages/login';
|
||||
import Notifications from './pages/notifications';
|
||||
import Public from './pages/public';
|
||||
import Settings from './pages/settings';
|
||||
import Status from './pages/status';
|
||||
import Welcome from './pages/welcome';
|
||||
|
@ -74,11 +79,9 @@ function App() {
|
|||
const { access_token: accessToken } = tokenJSON;
|
||||
store.session.set('accessToken', accessToken);
|
||||
|
||||
window.masto = await login({
|
||||
initMasto({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
disableVersionCheck: true,
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||
|
@ -112,22 +115,12 @@ function App() {
|
|||
const instanceURL = account.instanceURL;
|
||||
const accessToken = account.accessToken;
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
if (accessToken) setIsLoggedIn(true);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setUIState('loading');
|
||||
window.masto = await login({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
disableVersionCheck: true,
|
||||
timeout: 30_000,
|
||||
});
|
||||
setIsLoggedIn(true);
|
||||
} catch (e) {
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
setUIState('default');
|
||||
})();
|
||||
initMasto({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
});
|
||||
} else {
|
||||
setUIState('default');
|
||||
}
|
||||
|
@ -164,34 +157,8 @@ function App() {
|
|||
useEffect(() => {
|
||||
// HACK: prevent this from running again due to HMR
|
||||
if (states.init) return;
|
||||
|
||||
if (isLoggedIn) {
|
||||
requestAnimationFrame(() => {
|
||||
// startStream();
|
||||
startVisibility();
|
||||
|
||||
// Collect instance info
|
||||
(async () => {
|
||||
// Request v2, fallback to v1 if fail
|
||||
let info;
|
||||
try {
|
||||
info = await masto.v2.instance.fetch();
|
||||
} catch (e) {}
|
||||
if (!info) {
|
||||
try {
|
||||
info = await masto.v1.instances.fetch();
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!info) return;
|
||||
console.log(info);
|
||||
const { uri, domain } = info;
|
||||
if (uri || domain) {
|
||||
const instances = store.local.getJSON('instances') || {};
|
||||
instances[(domain || uri).toLowerCase()] = info;
|
||||
store.local.setJSON('instances', instances);
|
||||
}
|
||||
})();
|
||||
});
|
||||
requestAnimationFrame(startVisibility);
|
||||
states.init = true;
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
@ -211,7 +178,7 @@ function App() {
|
|||
|
||||
const nonRootLocation = useMemo(() => {
|
||||
const { pathname } = location;
|
||||
return !/\/(login|welcome)$/.test(pathname);
|
||||
return !/^\/(login|welcome|p)/.test(pathname);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
|
@ -236,7 +203,12 @@ function App() {
|
|||
{isLoggedIn && (
|
||||
<Route path="/notifications" element={<Notifications />} />
|
||||
)}
|
||||
{isLoggedIn && <Route path="/bookmarks" element={<Bookmarks />} />}
|
||||
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
|
||||
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
|
||||
{isLoggedIn && <Route path="/l/:id" element={<Lists />} />}
|
||||
{isLoggedIn && <Route path="/t/:hashtag" element={<Hashtags />} />}
|
||||
<Route path="/p/l?/:instance" element={<Public />} />
|
||||
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
||||
</Routes>
|
||||
<Routes>
|
||||
{isLoggedIn && <Route path="/s/:id" element={<Status />} />}
|
||||
|
@ -344,6 +316,50 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
function initMasto(params) {
|
||||
const clientParams = {
|
||||
url: params.url || 'https://mastodon.social',
|
||||
accessToken: params.accessToken || null,
|
||||
disableVersionCheck: true,
|
||||
timeout: 30_000,
|
||||
};
|
||||
window.masto = createClient(clientParams);
|
||||
|
||||
(async () => {
|
||||
// Request v2, fallback to v1 if fail
|
||||
let info;
|
||||
try {
|
||||
info = await masto.v2.instance.fetch();
|
||||
} catch (e) {}
|
||||
if (!info) {
|
||||
try {
|
||||
info = await masto.v1.instances.fetch();
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!info) return;
|
||||
console.log(info);
|
||||
const {
|
||||
// v1
|
||||
uri,
|
||||
urls: { streamingApi } = {},
|
||||
// v2
|
||||
domain,
|
||||
configuration: { urls: { streaming } = {} } = {},
|
||||
} = info;
|
||||
if (uri || domain) {
|
||||
const instances = store.local.getJSON('instances') || {};
|
||||
instances[(domain || uri).toLowerCase()] = info;
|
||||
store.local.setJSON('instances', instances);
|
||||
}
|
||||
if (streamingApi || streaming) {
|
||||
window.masto = createClient({
|
||||
...clientParams,
|
||||
streamingApiUrl: streaming || streamingApi,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
let ws;
|
||||
async function startStream() {
|
||||
if (
|
||||
|
@ -417,18 +433,18 @@ async function startStream() {
|
|||
};
|
||||
}
|
||||
|
||||
let lastHidden;
|
||||
function startVisibility() {
|
||||
const handleVisible = (visible) => {
|
||||
if (!visible) {
|
||||
const timestamp = Date.now();
|
||||
store.session.set('lastHidden', timestamp);
|
||||
lastHidden = timestamp;
|
||||
} else {
|
||||
const timestamp = Date.now();
|
||||
const lastHidden = store.session.get('lastHidden');
|
||||
const diff = timestamp - lastHidden;
|
||||
const diffMins = Math.round(diff / 1000 / 60);
|
||||
if (diffMins > 1) {
|
||||
console.log('visible', { lastHidden, diffMins });
|
||||
console.log(`visible: ${visible}`, { lastHidden, diffMins });
|
||||
if (!lastHidden || diffMins > 1) {
|
||||
(async () => {
|
||||
try {
|
||||
const firstStatusID = states.homeLast?.id;
|
||||
|
@ -492,6 +508,7 @@ function startVisibility() {
|
|||
console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible'));
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
requestAnimationFrame(handleVisibilityChange);
|
||||
return {
|
||||
stop: () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
|
|
@ -33,7 +33,11 @@ import Link from './link';
|
|||
import RelativeTime from './relative-time';
|
||||
|
||||
function fetchAccount(id) {
|
||||
return masto.v1.accounts.fetch(id);
|
||||
try {
|
||||
return masto.v1.accounts.fetch(id);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
const memFetchAccount = mem(fetchAccount);
|
||||
|
||||
|
|
151
src/components/timeline.jsx
Normal file
151
src/components/timeline.jsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
|
||||
import useScroll from '../utils/useScroll';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import Loader from './loader';
|
||||
import Status from './status';
|
||||
|
||||
function Timeline({ title, id, emptyText, errorText, fetchItems = () => {} }) {
|
||||
if (title) {
|
||||
useTitle(title);
|
||||
}
|
||||
const [items, setItems] = useState([]);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const scrollableRef = useRef(null);
|
||||
const { nearReachEnd, reachStart } = useScroll({
|
||||
scrollableElement: scrollableRef.current,
|
||||
});
|
||||
|
||||
const loadItems = (firstLoad) => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const { done, value } = await fetchItems(firstLoad);
|
||||
if (value?.length) {
|
||||
if (firstLoad) {
|
||||
setItems(value);
|
||||
} else {
|
||||
setItems([...items, ...value]);
|
||||
}
|
||||
setShowMore(!done);
|
||||
} else {
|
||||
setShowMore(false);
|
||||
}
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollableRef.current?.scrollTo({ top: 0 });
|
||||
loadItems(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (reachStart) {
|
||||
loadItems(true);
|
||||
}
|
||||
}, [reachStart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nearReachEnd && showMore) {
|
||||
loadItems();
|
||||
}
|
||||
}, [nearReachEnd, showMore]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${id}-page`}
|
||||
class="deck-container"
|
||||
ref={scrollableRef}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div class="timeline-deck deck">
|
||||
<header
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="header-side">
|
||||
<Link to="/" class="button plain">
|
||||
<Icon icon="home" size="l" />
|
||||
</Link>
|
||||
</div>
|
||||
<h1>{title}</h1>
|
||||
<div class="header-side">
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</div>
|
||||
</header>
|
||||
{!!items.length ? (
|
||||
<>
|
||||
<ul class="timeline">
|
||||
{items.map((status) => (
|
||||
<li key={`timeline-${status.id}`}>
|
||||
<Link class="status-link" to={`/s/${status.id}`}>
|
||||
<Status status={status} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{showMore && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => loadItems()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
{uiState === 'loading' ? (
|
||||
<Loader abrupt />
|
||||
) : (
|
||||
<>Show more…</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : uiState === 'loading' ? (
|
||||
<ul class="timeline">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<li key={i}>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
uiState !== 'loading' && <p class="ui-state">{emptyText}</p>
|
||||
)}
|
||||
{uiState === 'error' ? (
|
||||
<p class="ui-state">
|
||||
{errorText}
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
class="button plain"
|
||||
onClick={() => loadItems(!items.length)}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
uiState !== 'loading' &&
|
||||
!!items.length &&
|
||||
!showMore && <p class="ui-state insignificant">The end.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Timeline;
|
|
@ -2,7 +2,7 @@ import './index.css';
|
|||
|
||||
import './app.css';
|
||||
|
||||
import { login } from 'masto';
|
||||
import { createClient } from 'masto';
|
||||
import { render } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
|
@ -14,12 +14,12 @@ if (window.opener) {
|
|||
console = window.opener.console;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
(() => {
|
||||
if (window.masto) return;
|
||||
console.warn('window.masto not found. Trying to log in...');
|
||||
try {
|
||||
const { instanceURL, accessToken } = getCurrentAccount();
|
||||
window.masto = await login({
|
||||
window.masto = createClient({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
disableVersionCheck: true,
|
||||
|
|
15
src/pages/404.jsx
Normal file
15
src/pages/404.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Link from '../components/link';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div id="not-found-page" className="deck-container" tabIndex="-1">
|
||||
<div>
|
||||
<h1>404</h1>
|
||||
<p>Page not found.</p>
|
||||
<p>
|
||||
<Link to="/">Go home</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,141 +1,26 @@
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import Status from '../components/status';
|
||||
import useTitle from '../utils/useTitle';
|
||||
import Timeline from '../components/timeline';
|
||||
|
||||
const LIMIT = 40;
|
||||
const LIMIT = 20;
|
||||
|
||||
function Bookmarks() {
|
||||
useTitle('Bookmarks');
|
||||
const [bookmarks, setBookmarks] = useState([]);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
const bookmarksIterator = useRef();
|
||||
async function fetchBookmarks(firstLoad) {
|
||||
if (firstLoad || !bookmarksIterator.current) {
|
||||
bookmarksIterator.current = masto.v1.bookmarks.list({ limit: LIMIT });
|
||||
}
|
||||
const allBookmarks = await bookmarksIterator.current.next();
|
||||
const bookmarksValue = allBookmarks.value;
|
||||
if (bookmarksValue?.length) {
|
||||
if (firstLoad) {
|
||||
setBookmarks(bookmarksValue);
|
||||
} else {
|
||||
setBookmarks([...bookmarks, ...bookmarksValue]);
|
||||
}
|
||||
}
|
||||
return allBookmarks;
|
||||
return await bookmarksIterator.current.next();
|
||||
}
|
||||
|
||||
const loadBookmarks = (firstLoad) => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const { done } = await fetchBookmarks(firstLoad);
|
||||
setShowMore(!done);
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBookmarks(true);
|
||||
}, []);
|
||||
|
||||
const scrollableRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="bookmarks-page"
|
||||
class="deck-container"
|
||||
ref={scrollableRef}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div class="timeline-deck deck">
|
||||
<header
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDblClick={(e) => {
|
||||
loadBookmarks(true);
|
||||
}}
|
||||
>
|
||||
<div class="header-side">
|
||||
<Link to="/" class="button plain">
|
||||
<Icon icon="home" size="l" />
|
||||
</Link>
|
||||
</div>
|
||||
<h1>Bookmarks</h1>
|
||||
<div class="header-side">
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</div>
|
||||
</header>
|
||||
{!!bookmarks.length ? (
|
||||
<>
|
||||
<ul class="timeline">
|
||||
{bookmarks.map((status) => (
|
||||
<li key={`bookmark-${status.id}`}>
|
||||
<Link class="status-link" to={`/s/${status.id}`}>
|
||||
<Status status={status} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{showMore && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => loadBookmarks()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
{uiState === 'loading' ? <Loader /> : <>Show more…</>}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
uiState !== 'loading' && (
|
||||
<p class="ui-state">No bookmarks yet. Go bookmark something!</p>
|
||||
)
|
||||
)}
|
||||
{uiState === 'loading' ? (
|
||||
<ul class="timeline">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<li key={i}>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : uiState === 'error' ? (
|
||||
<p class="ui-state">
|
||||
Unable to load bookmarks.
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
class="button plain"
|
||||
onClick={() => loadBookmarks(!bookmarks.length)}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
bookmarks.length &&
|
||||
!showMore && <p class="ui-state insignificant">The end.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Timeline
|
||||
title="Bookmarks"
|
||||
id="bookmarks"
|
||||
emptyText="No bookmarks yet. Go bookmark something!"
|
||||
errorText="Unable to load bookmarks"
|
||||
fetchItems={fetchBookmarks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
27
src/pages/favourites.jsx
Normal file
27
src/pages/favourites.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useRef } from 'preact/hooks';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Favourites() {
|
||||
const favouritesIterator = useRef();
|
||||
async function fetchFavourites(firstLoad) {
|
||||
if (firstLoad || !favouritesIterator.current) {
|
||||
favouritesIterator.current = masto.v1.favourites.list({ limit: LIMIT });
|
||||
}
|
||||
return await favouritesIterator.current.next();
|
||||
}
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
title="Favourites"
|
||||
id="favourites"
|
||||
emptyText="No favourites yet. Go favourite something!"
|
||||
errorText="Unable to load favourites"
|
||||
fetchItems={fetchFavourites}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Favourites;
|
32
src/pages/hashtags.jsx
Normal file
32
src/pages/hashtags.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useRef } from 'preact/hooks';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Hashtags() {
|
||||
const { hashtag } = useParams();
|
||||
const hashtagsIterator = useRef();
|
||||
async function fetchHashtags(firstLoad) {
|
||||
if (firstLoad || !hashtagsIterator.current) {
|
||||
hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, {
|
||||
limit: LIMIT,
|
||||
});
|
||||
}
|
||||
return await hashtagsIterator.current.next();
|
||||
}
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
key={hashtag}
|
||||
title={`#${hashtag}`}
|
||||
id="hashtags"
|
||||
emptyText="No one has posted anything with this tag yet."
|
||||
errorText="Unable to load posts with this tag"
|
||||
fetchItems={fetchHashtags}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Hashtags;
|
43
src/pages/lists.jsx
Normal file
43
src/pages/lists.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Lists() {
|
||||
const { id } = useParams();
|
||||
const listsIterator = useRef();
|
||||
async function fetchLists(firstLoad) {
|
||||
if (firstLoad || !listsIterator.current) {
|
||||
listsIterator.current = masto.v1.timelines.listList(id, {
|
||||
limit: LIMIT,
|
||||
});
|
||||
}
|
||||
return await listsIterator.current.next();
|
||||
}
|
||||
|
||||
const [title, setTitle] = useState(`List ${id}`);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const list = await masto.v1.lists.fetch(id);
|
||||
setTitle(list.title);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
title={title}
|
||||
id="lists"
|
||||
emptyText="Nothing yet."
|
||||
errorText="Unable to load posts."
|
||||
fetchItems={fetchLists}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Lists;
|
76
src/pages/public.jsx
Normal file
76
src/pages/public.jsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
// EXPERIMENTAL: This is a work in progress and may not work as expected.
|
||||
import { useMatch, useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
let nextUrl = null;
|
||||
|
||||
function Public() {
|
||||
const isLocal = !!useMatch('/p/l/:instance');
|
||||
const params = useParams();
|
||||
const { instance = '' } = params;
|
||||
async function fetchPublic(firstLoad) {
|
||||
const url = firstLoad
|
||||
? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}`
|
||||
: nextUrl;
|
||||
if (!url) return { values: [], done: true };
|
||||
const response = await fetch(url);
|
||||
let value = await response.json();
|
||||
if (value) {
|
||||
value = camelCaseKeys(value);
|
||||
}
|
||||
const done = !response.headers.has('link');
|
||||
nextUrl = done
|
||||
? null
|
||||
: response.headers.get('link').match(/<(.+?)>; rel="next"/)?.[1];
|
||||
console.debug({
|
||||
url,
|
||||
value,
|
||||
done,
|
||||
nextUrl,
|
||||
});
|
||||
return { value, done };
|
||||
}
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
key={instance + isLocal}
|
||||
title={`${instance} (${isLocal ? 'local' : 'federated'})`}
|
||||
id="public"
|
||||
emptyText="No one has posted anything yet."
|
||||
errorText="Unable to load posts"
|
||||
fetchItems={fetchPublic}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function camelCaseKeys(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => camelCaseKeys(item));
|
||||
}
|
||||
return new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
let value = undefined;
|
||||
if (prop in target) {
|
||||
value = target[prop];
|
||||
}
|
||||
if (!value) {
|
||||
const snakeCaseProp = prop.replace(
|
||||
/([A-Z])/g,
|
||||
(g) => `_${g.toLowerCase()}`,
|
||||
);
|
||||
if (snakeCaseProp in target) {
|
||||
value = target[snakeCaseProp];
|
||||
}
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return camelCaseKeys(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default Public;
|
|
@ -1,4 +1,5 @@
|
|||
function emojifyText(text, emojis = []) {
|
||||
if (!text) return '';
|
||||
if (!emojis.length) return text;
|
||||
// Replace shortcodes in text with emoji
|
||||
// emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }]
|
||||
|
|
Loading…
Reference in a new issue