phanpy/src/pages/status.jsx
Lim Chee Aun 8babc97877 Fix status jumping bug
This part is too complex.
The thread can get very fancy and it's impossible to handle all use-cases without a lot of code.
Solution is just fading all of them out while loading.
2022-12-20 15:32:31 +08:00

280 lines
8.5 KiB
JavaScript

import { Link } from 'preact-router/match';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useSnapshot } from 'valtio';
import Icon from '../components/icon';
import Loader from '../components/loader';
import Status from '../components/status';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import useTitle from '../utils/useTitle';
function StatusPage({ id }) {
const snapStates = useSnapshot(states);
const [statuses, setStatuses] = useState([{ id }]);
const [uiState, setUIState] = useState('default');
const heroStatusRef = useRef();
useEffect(async () => {
const containsStatus = statuses.find((s) => s.id === id);
if (!containsStatus) {
setStatuses([{ id }]);
}
setUIState('loading');
const hasStatus = snapStates.statuses.has(id);
let heroStatus = snapStates.statuses.get(id);
try {
heroStatus = await masto.statuses.fetch(id);
states.statuses.set(id, heroStatus);
} catch (e) {
// Silent fail if status is cached
if (!hasStatus) {
setUIState('error');
alert('Error fetching status');
}
return;
}
try {
const context = await masto.statuses.fetchContext(id);
const { ancestors, descendants } = context;
ancestors.forEach((status) => {
states.statuses.set(status.id, status);
});
const nestedDescendants = [];
descendants.forEach((status) => {
states.statuses.set(status.id, status);
if (status.inReplyToAccountId === status.account.id) {
// If replying to self, it's part of the thread, level 1
nestedDescendants.push(status);
} else if (status.inReplyToId === heroStatus.id) {
// If replying to the hero status, it's a reply, level 1
nestedDescendants.push(status);
} else {
// If replying to someone else, it's a reply to a reply, level 2
const parent = descendants.find((s) => s.id === status.inReplyToId);
if (parent) {
if (!parent.__replies) {
parent.__replies = [];
}
parent.__replies.push(status);
} else {
// If no parent, it's probably a reply to a reply to a reply, level 3
console.warn('[LEVEL 3] No parent found for', status);
}
}
});
console.log({ ancestors, descendants, nestedDescendants });
const allStatuses = [
...ancestors.map((s) => ({
id: s.id,
ancestor: true,
accountID: s.account.id,
})),
{ id, accountID: heroStatus.account.id },
...nestedDescendants.map((s) => ({
id: s.id,
accountID: s.account.id,
descendant: true,
thread: s.account.id === heroStatus.account.id,
replies: s.__replies?.map((r) => r.id),
})),
];
console.log({ allStatuses });
setStatuses(allStatuses);
} catch (e) {
console.error(e);
setUIState('error');
}
setUIState('default');
}, [id, snapStates.reloadStatusPage]);
useLayoutEffect(() => {
if (heroStatusRef.current && statuses.length > 1) {
heroStatusRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [id]);
useLayoutEffect(() => {
const hasAncestor = statuses.some((s) => s.ancestor);
if (hasAncestor) {
heroStatusRef.current?.scrollIntoView({
// behavior: 'smooth',
block: 'start',
});
}
}, [statuses]);
const heroStatus = snapStates.statuses.get(id);
const heroDisplayName = useMemo(() => {
// Remove shortcodes from display name
if (!heroStatus) return '';
const { account } = heroStatus;
const div = document.createElement('div');
div.innerHTML = account.displayName;
return div.innerText.trim();
}, [heroStatus]);
const heroContentText = useMemo(() => {
if (!heroStatus) return '';
const { spoilerText, content } = heroStatus;
let text;
if (spoilerText) {
text = spoilerText;
} else {
const div = document.createElement('div');
div.innerHTML = content;
text = div.innerText.trim();
}
if (text.length > 64) {
// "The title should ideally be less than 64 characters in length"
// https://www.w3.org/Provider/Style/TITLE.html
text = text.slice(0, 64) + '…';
}
return text;
}, [heroStatus]);
useTitle(
heroDisplayName && heroContentText
? `${heroDisplayName}: "${heroContentText}"`
: 'Status',
);
const prevRoute = states.history.findLast((h) => {
return h === '/' || /notifications/i.test(h);
});
const closeLink = `#${prevRoute || '/'}`;
const [limit, setLimit] = useState(40);
const showMore = useMemo(() => {
// return number of statuses to show
return statuses.length - limit;
}, [statuses.length, limit]);
const hasManyStatuses = statuses.length > 40;
return (
<div class="deck-backdrop">
<Link href={closeLink}></Link>
<div
class={`status-deck deck contained ${
statuses.length > 1 ? 'padded-bottom' : ''
}`}
>
<header>
{/* <div>
<Link class="button plain deck-close" href={closeLink}>
<Icon icon="chevron-left" size="xl" />
</Link>
</div> */}
<h1>Status</h1>
<div class="header-side">
<Loader hidden={uiState !== 'loading'} />
<Link class="button plain deck-close" href={closeLink}>
<Icon icon="x" size="xl" />
</Link>
</div>
</header>
<ul
class={`timeline flat contextual ${
uiState === 'loading' ? 'loading' : ''
}`}
>
{statuses.slice(0, limit).map((status) => {
const {
id: statusID,
ancestor,
descendant,
thread,
replies,
} = status;
const isHero = statusID === id;
return (
<li
key={statusID}
ref={isHero ? heroStatusRef : null}
class={`${ancestor ? 'ancestor' : ''} ${
descendant ? 'descendant' : ''
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
>
{isHero ? (
<Status statusID={statusID} withinContext size="l" />
) : (
<Link
class="
status-link
"
href={`#/s/${statusID}`}
>
<Status
statusID={statusID}
withinContext
size={thread || ancestor ? 'm' : 's'}
/>
</Link>
)}
{descendant && replies?.length > 0 && (
<details class="replies" open={!hasManyStatuses}>
<summary hidden={!hasManyStatuses}>
<span title={replies.length}>
{shortenNumber(replies.length)}
</span>{' '}
repl{replies.length === 1 ? 'y' : 'ies'}
</summary>
<ul>
{replies.map((replyID) => (
<li key={replyID}>
<Link class="status-link" href={`#/s/${replyID}`}>
<Status statusID={replyID} withinContext size="s" />
</Link>
</li>
))}
</ul>
</details>
)}
{uiState === 'loading' &&
isHero &&
!!heroStatus?.repliesCount &&
statuses.length === 1 && (
<div class="status-loading">
<Loader />
</div>
)}
</li>
);
})}
{showMore > 0 && (
<li>
<button
type="button"
class="plain block"
disabled={uiState === 'loading'}
onClick={() => setLimit((l) => l + 40)}
style={{ marginBlockEnd: '6em' }}
>
Show more&hellip;{' '}
<span class="tag">{showMore > 40 ? '40+' : showMore}</span>
</button>
</li>
)}
</ul>
</div>
</div>
);
}
export default StatusPage;