2022-12-24 17:26:43 +03:00
|
|
|
import './status.css';
|
|
|
|
|
2023-03-24 10:05:57 +03:00
|
|
|
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
2022-12-21 13:02:13 +03:00
|
|
|
import debounce from 'just-debounce-it';
|
2023-01-25 11:41:28 +03:00
|
|
|
import pRetry from 'p-retry';
|
2023-01-09 12:05:42 +03:00
|
|
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
2022-12-31 04:52:31 +03:00
|
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
2022-12-24 17:26:43 +03:00
|
|
|
import { InView } from 'react-intersection-observer';
|
2023-02-09 18:01:33 +03:00
|
|
|
import { matchPath, useNavigate, useParams } from 'react-router-dom';
|
2023-01-30 15:51:06 +03:00
|
|
|
import { useDebouncedCallback } from 'use-debounce';
|
2022-12-10 12:14:48 +03:00
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
2023-01-28 20:02:25 +03:00
|
|
|
import Avatar from '../components/avatar';
|
2022-12-10 12:14:48 +03:00
|
|
|
import Icon from '../components/icon';
|
2023-01-20 19:23:59 +03:00
|
|
|
import Link from '../components/link';
|
2022-12-10 12:14:48 +03:00
|
|
|
import Loader from '../components/loader';
|
2022-12-24 17:26:43 +03:00
|
|
|
import NameText from '../components/name-text';
|
2023-01-05 05:50:27 +03:00
|
|
|
import RelativeTime from '../components/relative-time';
|
2022-12-10 12:14:48 +03:00
|
|
|
import Status from '../components/status';
|
2023-02-05 19:17:19 +03:00
|
|
|
import { api } from '../utils/api';
|
2022-12-22 19:30:55 +03:00
|
|
|
import htmlContentLength from '../utils/html-content-length';
|
2022-12-19 12:38:20 +03:00
|
|
|
import shortenNumber from '../utils/shorten-number';
|
2023-02-06 11:35:03 +03:00
|
|
|
import states, {
|
|
|
|
saveStatus,
|
|
|
|
statusKey,
|
|
|
|
threadifyStatus,
|
|
|
|
} from '../utils/states';
|
2023-03-21 19:09:36 +03:00
|
|
|
import statusPeek from '../utils/status-peek';
|
2023-01-11 08:28:42 +03:00
|
|
|
import { getCurrentAccount } from '../utils/store-utils';
|
2023-01-10 05:44:16 +03:00
|
|
|
import useScroll from '../utils/useScroll';
|
2022-12-10 12:14:48 +03:00
|
|
|
import useTitle from '../utils/useTitle';
|
|
|
|
|
2022-12-22 19:30:55 +03:00
|
|
|
const LIMIT = 40;
|
2023-01-28 20:02:25 +03:00
|
|
|
const THREAD_LIMIT = 20;
|
2022-12-22 19:30:55 +03:00
|
|
|
|
2023-03-28 15:33:00 +03:00
|
|
|
let cachedRepliesToggle = {};
|
2023-01-23 12:58:33 +03:00
|
|
|
let cachedStatusesMap = {};
|
2023-01-21 14:52:51 +03:00
|
|
|
function resetScrollPosition(id) {
|
2023-01-23 12:58:33 +03:00
|
|
|
delete cachedStatusesMap[id];
|
2023-01-21 14:52:51 +03:00
|
|
|
delete states.scrollPositions[id];
|
|
|
|
}
|
|
|
|
|
2023-01-20 19:23:59 +03:00
|
|
|
function StatusPage() {
|
2023-02-06 11:35:03 +03:00
|
|
|
const { id, ...params } = useParams();
|
2023-02-21 19:42:43 +03:00
|
|
|
const { masto, instance } = api({ instance: params.instance });
|
|
|
|
const {
|
|
|
|
masto: currentMasto,
|
|
|
|
instance: currentInstance,
|
|
|
|
authenticated,
|
|
|
|
} = api();
|
2023-02-17 12:37:31 +03:00
|
|
|
const sameInstance = instance === currentInstance;
|
2023-01-27 15:54:18 +03:00
|
|
|
const navigate = useNavigate();
|
2022-12-10 12:14:48 +03:00
|
|
|
const snapStates = useSnapshot(states);
|
2022-12-21 13:02:13 +03:00
|
|
|
const [statuses, setStatuses] = useState([]);
|
2022-12-10 12:14:48 +03:00
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
const heroStatusRef = useRef();
|
2023-02-06 11:35:03 +03:00
|
|
|
const sKey = statusKey(id, instance);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
const scrollableRef = useRef();
|
2022-12-30 15:37:57 +03:00
|
|
|
useEffect(() => {
|
|
|
|
scrollableRef.current?.focus();
|
|
|
|
}, []);
|
2022-12-20 20:02:48 +03:00
|
|
|
useEffect(() => {
|
2022-12-21 13:02:13 +03:00
|
|
|
const onScroll = debounce(() => {
|
|
|
|
// console.log('onScroll');
|
2022-12-27 04:05:45 +03:00
|
|
|
if (!scrollableRef.current) return;
|
2022-12-21 13:02:13 +03:00
|
|
|
const { scrollTop } = scrollableRef.current;
|
2023-01-16 15:32:51 +03:00
|
|
|
if (uiState !== 'loading') {
|
|
|
|
states.scrollPositions[id] = scrollTop;
|
|
|
|
}
|
2023-02-20 15:59:46 +03:00
|
|
|
}, 50);
|
2022-12-21 13:02:13 +03:00
|
|
|
scrollableRef.current.addEventListener('scroll', onScroll, {
|
|
|
|
passive: true,
|
|
|
|
});
|
|
|
|
onScroll();
|
|
|
|
return () => {
|
2023-02-20 15:59:46 +03:00
|
|
|
onScroll.cancel();
|
2022-12-21 13:02:13 +03:00
|
|
|
scrollableRef.current?.removeEventListener('scroll', onScroll);
|
|
|
|
};
|
2023-01-16 15:32:51 +03:00
|
|
|
}, [id, uiState !== 'loading']);
|
2022-12-21 13:02:13 +03:00
|
|
|
|
2023-01-09 11:56:16 +03:00
|
|
|
const scrollOffsets = useRef();
|
2023-03-13 19:36:40 +03:00
|
|
|
const initContext = ({ reloadHero } = {}) => {
|
2023-01-09 11:56:16 +03:00
|
|
|
console.debug('initContext', id);
|
2022-12-21 13:02:13 +03:00
|
|
|
setUIState('loading');
|
2022-12-28 13:06:05 +03:00
|
|
|
let heroTimer;
|
2022-12-21 13:02:13 +03:00
|
|
|
|
2023-01-23 12:58:33 +03:00
|
|
|
const cachedStatuses = cachedStatusesMap[id];
|
2022-12-23 06:28:25 +03:00
|
|
|
if (cachedStatuses) {
|
|
|
|
// Case 1: It's cached, let's restore them to make it snappy
|
|
|
|
const reallyCachedStatuses = cachedStatuses.filter(
|
2023-02-06 11:35:03 +03:00
|
|
|
(s) => states.statuses[sKey],
|
2022-12-23 06:28:25 +03:00
|
|
|
// Some are not cached in the global state, so we need to filter them out
|
|
|
|
);
|
|
|
|
setStatuses(reallyCachedStatuses);
|
2022-12-20 20:02:48 +03:00
|
|
|
} else {
|
2023-01-09 11:56:16 +03:00
|
|
|
// const heroIndex = statuses.findIndex((s) => s.id === id);
|
|
|
|
// if (heroIndex !== -1) {
|
|
|
|
// // Case 2: It's in current statuses. Slice off all descendant statuses after the hero status to be safe
|
|
|
|
// const slicedStatuses = statuses.slice(0, heroIndex + 1);
|
|
|
|
// setStatuses(slicedStatuses);
|
|
|
|
// } else {
|
|
|
|
// Case 3: Not cached and not in statuses, let's start from scratch
|
|
|
|
setStatuses([{ id }]);
|
|
|
|
// }
|
2022-12-10 12:14:48 +03:00
|
|
|
}
|
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
(async () => {
|
2023-01-25 11:41:28 +03:00
|
|
|
const heroFetch = () =>
|
|
|
|
pRetry(() => masto.v1.statuses.fetch(id), {
|
2023-01-26 16:02:39 +03:00
|
|
|
retries: 4,
|
2023-01-25 11:41:28 +03:00
|
|
|
});
|
|
|
|
const contextFetch = pRetry(() => masto.v1.statuses.fetchContext(id), {
|
2023-01-26 16:02:39 +03:00
|
|
|
retries: 8,
|
2023-01-25 11:41:28 +03:00
|
|
|
});
|
2022-12-23 07:30:49 +03:00
|
|
|
|
2023-02-06 11:35:03 +03:00
|
|
|
const hasStatus = !!snapStates.statuses[sKey];
|
|
|
|
let heroStatus = snapStates.statuses[sKey];
|
2023-03-13 19:36:40 +03:00
|
|
|
if (hasStatus && !reloadHero) {
|
2023-01-09 11:56:16 +03:00
|
|
|
console.debug('Hero status is cached');
|
2022-12-28 13:06:05 +03:00
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
heroStatus = await heroFetch();
|
2023-02-06 11:35:03 +03:00
|
|
|
saveStatus(heroStatus, instance);
|
2023-01-09 20:31:38 +03:00
|
|
|
// Give time for context to appear
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
setTimeout(resolve, 100);
|
|
|
|
});
|
2022-12-28 13:06:05 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
2022-12-21 13:02:13 +03:00
|
|
|
setUIState('error');
|
2022-12-27 16:30:18 +03:00
|
|
|
return;
|
2022-12-21 13:02:13 +03:00
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
}
|
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
try {
|
2022-12-23 07:30:49 +03:00
|
|
|
const context = await contextFetch;
|
2022-12-21 13:02:13 +03:00
|
|
|
const { ancestors, descendants } = context;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
ancestors.forEach((status) => {
|
2023-02-06 11:35:03 +03:00
|
|
|
saveStatus(status, instance, {
|
|
|
|
skipThreading: true,
|
|
|
|
});
|
2022-12-21 13:02:13 +03:00
|
|
|
});
|
|
|
|
const nestedDescendants = [];
|
|
|
|
descendants.forEach((status) => {
|
2023-02-06 11:35:03 +03:00
|
|
|
saveStatus(status, instance, {
|
|
|
|
skipThreading: true,
|
|
|
|
});
|
2022-12-21 13:02:13 +03:00
|
|
|
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);
|
2022-12-18 15:46:13 +03:00
|
|
|
} else {
|
2022-12-21 13:02:13 +03:00
|
|
|
// 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 {
|
2023-01-28 20:29:26 +03:00
|
|
|
// If no parent, something is wrong
|
|
|
|
console.warn('No parent found for', status);
|
2022-12-21 13:02:13 +03:00
|
|
|
}
|
2022-12-18 15:46:13 +03:00
|
|
|
}
|
2022-12-21 13:02:13 +03:00
|
|
|
});
|
2022-12-18 15:46:13 +03:00
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
console.log({ ancestors, descendants, nestedDescendants });
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-03-30 15:07:07 +03:00
|
|
|
function expandReplies(_replies) {
|
|
|
|
return _replies?.map((_r) => ({
|
|
|
|
id: _r.id,
|
|
|
|
account: _r.account,
|
|
|
|
repliesCount: _r.repliesCount,
|
|
|
|
content: _r.content,
|
|
|
|
replies: expandReplies(_r.__replies),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
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,
|
2023-03-30 15:07:07 +03:00
|
|
|
replies: expandReplies(s.__replies),
|
2022-12-21 13:02:13 +03:00
|
|
|
})),
|
|
|
|
];
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2022-12-21 13:02:13 +03:00
|
|
|
setUIState('default');
|
2023-01-09 11:56:16 +03:00
|
|
|
scrollOffsets.current = {
|
|
|
|
offsetTop: heroStatusRef.current?.offsetTop,
|
|
|
|
scrollTop: scrollableRef.current?.scrollTop,
|
|
|
|
};
|
2022-12-21 13:02:13 +03:00
|
|
|
console.log({ allStatuses });
|
|
|
|
setStatuses(allStatuses);
|
2023-01-23 12:58:33 +03:00
|
|
|
cachedStatusesMap[id] = allStatuses;
|
2023-01-11 12:45:37 +03:00
|
|
|
|
|
|
|
// Let's threadify this one
|
|
|
|
// Note that all non-hero statuses will trigger saveStatus which will threadify them too
|
|
|
|
// By right, at this point, all descendant statuses should be cached
|
2023-02-06 11:35:03 +03:00
|
|
|
threadifyStatus(heroStatus, instance);
|
2022-12-21 13:02:13 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setUIState('error');
|
|
|
|
}
|
|
|
|
})();
|
2022-12-28 13:06:05 +03:00
|
|
|
|
|
|
|
return () => {
|
|
|
|
clearTimeout(heroTimer);
|
|
|
|
};
|
2023-01-06 16:29:16 +03:00
|
|
|
};
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
useEffect(initContext, [id, masto]);
|
2023-01-09 11:56:16 +03:00
|
|
|
useEffect(() => {
|
|
|
|
if (!statuses.length) return;
|
2023-01-09 20:31:38 +03:00
|
|
|
console.debug('STATUSES', statuses);
|
2023-01-09 11:56:16 +03:00
|
|
|
const scrollPosition = states.scrollPositions[id];
|
|
|
|
console.debug('scrollPosition', scrollPosition);
|
2023-01-09 20:31:38 +03:00
|
|
|
if (!!scrollPosition) {
|
2023-01-09 11:56:16 +03:00
|
|
|
console.debug('Case 1', {
|
2023-01-21 14:52:51 +03:00
|
|
|
id,
|
2023-01-09 11:56:16 +03:00
|
|
|
scrollPosition,
|
|
|
|
});
|
|
|
|
scrollableRef.current.scrollTop = scrollPosition;
|
|
|
|
} else if (scrollOffsets.current) {
|
|
|
|
const newScrollOffsets = {
|
|
|
|
offsetTop: heroStatusRef.current?.offsetTop,
|
|
|
|
scrollTop: scrollableRef.current?.scrollTop,
|
|
|
|
};
|
|
|
|
const newScrollTop =
|
2023-01-27 06:48:13 +03:00
|
|
|
newScrollOffsets.offsetTop -
|
|
|
|
scrollOffsets.current.offsetTop +
|
|
|
|
newScrollOffsets.scrollTop;
|
2023-01-09 11:56:16 +03:00
|
|
|
console.debug('Case 2', {
|
|
|
|
scrollOffsets: scrollOffsets.current,
|
|
|
|
newScrollOffsets,
|
|
|
|
newScrollTop,
|
|
|
|
statuses: [...statuses],
|
|
|
|
});
|
|
|
|
scrollableRef.current.scrollTop = newScrollTop;
|
2023-01-27 06:48:13 +03:00
|
|
|
} else if (statuses.length === 1) {
|
|
|
|
console.debug('Case 3', {
|
|
|
|
id,
|
|
|
|
});
|
|
|
|
scrollableRef.current.scrollTop = 0;
|
2023-01-09 11:56:16 +03:00
|
|
|
}
|
|
|
|
|
2023-01-09 20:31:38 +03:00
|
|
|
// RESET
|
2023-01-09 11:56:16 +03:00
|
|
|
scrollOffsets.current = null;
|
|
|
|
}, [statuses]);
|
2023-01-06 16:29:16 +03:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-01-09 11:56:16 +03:00
|
|
|
if (snapStates.reloadStatusPage <= 0) return;
|
2023-01-06 16:29:16 +03:00
|
|
|
// Delete the cache for the context
|
|
|
|
(async () => {
|
|
|
|
try {
|
2023-01-11 08:28:42 +03:00
|
|
|
const { instanceURL } = getCurrentAccount();
|
2023-01-06 16:29:16 +03:00
|
|
|
const contextURL = `https://${instanceURL}/api/v1/statuses/${id}/context`;
|
|
|
|
console.log('Clear cache', contextURL);
|
|
|
|
const apiCache = await caches.open('api');
|
|
|
|
await apiCache.delete(contextURL, { ignoreVary: true });
|
|
|
|
|
2023-03-13 19:36:40 +03:00
|
|
|
return initContext({
|
|
|
|
reloadHero: true,
|
|
|
|
});
|
2023-01-06 16:29:16 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}, [snapStates.reloadStatusPage]);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-01-09 11:56:16 +03:00
|
|
|
useEffect(() => {
|
|
|
|
return () => {
|
2023-01-10 15:05:47 +03:00
|
|
|
// RESET
|
2023-01-09 11:56:16 +03:00
|
|
|
states.scrollPositions = {};
|
|
|
|
states.reloadStatusPage = 0;
|
2023-01-23 12:58:33 +03:00
|
|
|
cachedStatusesMap = {};
|
2023-03-28 15:33:00 +03:00
|
|
|
cachedRepliesToggle = {};
|
2023-01-09 11:56:16 +03:00
|
|
|
};
|
|
|
|
}, []);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-02-11 13:55:21 +03:00
|
|
|
const heroStatus = snapStates.statuses[sKey] || snapStates.statuses[id];
|
2022-12-10 12:14:48 +03:00
|
|
|
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 '';
|
2023-03-21 19:09:36 +03:00
|
|
|
let text = statusPeek(heroStatus);
|
2022-12-10 12:14:48 +03:00
|
|
|
if (text.length > 64) {
|
2022-12-15 12:14:33 +03:00
|
|
|
// "The title should ideally be less than 64 characters in length"
|
|
|
|
// https://www.w3.org/Provider/Style/TITLE.html
|
2022-12-10 12:14:48 +03:00
|
|
|
text = text.slice(0, 64) + '…';
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
}, [heroStatus]);
|
|
|
|
useTitle(
|
|
|
|
heroDisplayName && heroContentText
|
2022-12-15 12:14:33 +03:00
|
|
|
? `${heroDisplayName}: "${heroContentText}"`
|
2022-12-10 12:14:48 +03:00
|
|
|
: 'Status',
|
2023-02-09 18:01:33 +03:00
|
|
|
'/:instance?/s/:id',
|
2022-12-10 12:14:48 +03:00
|
|
|
);
|
|
|
|
|
2023-03-24 10:05:57 +03:00
|
|
|
const postInstance = useMemo(() => {
|
|
|
|
if (!heroStatus) return;
|
|
|
|
const { url } = heroStatus;
|
|
|
|
if (!url) return;
|
|
|
|
return new URL(url).hostname;
|
|
|
|
}, [heroStatus]);
|
|
|
|
const postSameInstance = useMemo(() => {
|
|
|
|
if (!postInstance) return;
|
|
|
|
return postInstance === instance;
|
|
|
|
}, [postInstance, instance]);
|
|
|
|
|
2023-01-20 19:23:59 +03:00
|
|
|
const closeLink = useMemo(() => {
|
2023-02-11 12:58:49 +03:00
|
|
|
const { prevLocation } = snapStates;
|
2023-02-17 06:28:25 +03:00
|
|
|
const pathname =
|
|
|
|
(prevLocation?.pathname || '') + (prevLocation?.search || '');
|
2023-02-09 18:01:33 +03:00
|
|
|
if (
|
|
|
|
!pathname ||
|
|
|
|
matchPath('/:instance/s/:id', pathname) ||
|
|
|
|
matchPath('/s/:id', pathname)
|
|
|
|
) {
|
|
|
|
return '/';
|
|
|
|
}
|
2023-01-20 19:23:59 +03:00
|
|
|
return pathname;
|
|
|
|
}, []);
|
2023-01-29 10:23:53 +03:00
|
|
|
const onClose = () => {
|
|
|
|
states.showMediaModal = false;
|
|
|
|
};
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2022-12-22 19:30:55 +03:00
|
|
|
const [limit, setLimit] = useState(LIMIT);
|
2022-12-18 15:46:13 +03:00
|
|
|
const showMore = useMemo(() => {
|
|
|
|
// return number of statuses to show
|
|
|
|
return statuses.length - limit;
|
|
|
|
}, [statuses.length, limit]);
|
|
|
|
|
2023-01-28 20:02:25 +03:00
|
|
|
const hasManyStatuses = statuses.length > THREAD_LIMIT;
|
2022-12-21 13:02:13 +03:00
|
|
|
const hasDescendants = statuses.some((s) => s.descendant);
|
2023-01-10 05:44:16 +03:00
|
|
|
const ancestors = statuses.filter((s) => s.ancestor);
|
2022-12-19 05:05:27 +03:00
|
|
|
|
2022-12-24 17:26:43 +03:00
|
|
|
const [heroInView, setHeroInView] = useState(true);
|
|
|
|
const onView = useDebouncedCallback(setHeroInView, 100);
|
2022-12-29 06:47:10 +03:00
|
|
|
const heroPointer = useMemo(() => {
|
|
|
|
// get top offset of heroStatus
|
2022-12-29 11:15:58 +03:00
|
|
|
if (!heroStatusRef.current || heroInView) return null;
|
2022-12-29 06:47:10 +03:00
|
|
|
const { top } = heroStatusRef.current.getBoundingClientRect();
|
|
|
|
return top > 0 ? 'down' : 'up';
|
|
|
|
}, [heroInView]);
|
2022-12-24 17:26:43 +03:00
|
|
|
|
2022-12-31 04:52:31 +03:00
|
|
|
useHotkeys(['esc', 'backspace'], () => {
|
2023-01-27 15:54:18 +03:00
|
|
|
// location.hash = closeLink;
|
2023-01-29 10:23:53 +03:00
|
|
|
onClose();
|
2023-01-27 15:54:18 +03:00
|
|
|
navigate(closeLink);
|
|
|
|
});
|
|
|
|
|
|
|
|
useHotkeys('j', () => {
|
|
|
|
const activeStatus = document.activeElement.closest(
|
|
|
|
'.status-link, .status-focus',
|
|
|
|
);
|
|
|
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
|
|
const allStatusLinks = Array.from(
|
2023-01-31 20:19:14 +03:00
|
|
|
// Select all statuses except those inside collapsed details/summary
|
|
|
|
// Hat-tip to @AmeliaBR@front-end.social
|
|
|
|
// https://front-end.social/@AmeliaBR/109784776146144471
|
|
|
|
scrollableRef.current.querySelectorAll(
|
|
|
|
'.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
|
|
|
|
),
|
|
|
|
);
|
2023-01-27 15:54:18 +03:00
|
|
|
console.log({ allStatusLinks });
|
|
|
|
if (
|
|
|
|
activeStatus &&
|
|
|
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
|
|
|
activeStatusRect.bottom > 0
|
|
|
|
) {
|
|
|
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
|
|
|
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
|
|
|
if (nextStatus) {
|
|
|
|
nextStatus.focus();
|
|
|
|
nextStatus.scrollIntoViewIfNeeded?.();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If active status is not in viewport, get the topmost status-link in viewport
|
|
|
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
|
|
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
|
|
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
|
|
|
});
|
|
|
|
if (topmostStatusLink) {
|
|
|
|
topmostStatusLink.focus();
|
|
|
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
useHotkeys('k', () => {
|
|
|
|
const activeStatus = document.activeElement.closest(
|
|
|
|
'.status-link, .status-focus',
|
|
|
|
);
|
|
|
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
|
|
const allStatusLinks = Array.from(
|
2023-01-31 20:19:14 +03:00
|
|
|
scrollableRef.current.querySelectorAll(
|
|
|
|
'.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
|
|
|
|
),
|
|
|
|
);
|
2023-01-27 15:54:18 +03:00
|
|
|
if (
|
|
|
|
activeStatus &&
|
|
|
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
|
|
|
activeStatusRect.bottom > 0
|
|
|
|
) {
|
|
|
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
|
|
|
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
|
|
|
if (prevStatus) {
|
|
|
|
prevStatus.focus();
|
|
|
|
prevStatus.scrollIntoViewIfNeeded?.();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If active status is not in viewport, get the topmost status-link in viewport
|
|
|
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
|
|
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
|
|
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
|
|
|
});
|
|
|
|
if (topmostStatusLink) {
|
|
|
|
topmostStatusLink.focus();
|
|
|
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
|
|
|
}
|
|
|
|
}
|
2022-12-31 04:52:31 +03:00
|
|
|
});
|
|
|
|
|
2023-01-31 19:10:38 +03:00
|
|
|
// NOTE: I'm not sure if 'x' is the best shortcut for this, might change it later
|
|
|
|
// IDEA: x is for expand
|
|
|
|
useHotkeys('x', () => {
|
|
|
|
const activeStatus = document.activeElement.closest(
|
|
|
|
'.status-link, .status-focus',
|
|
|
|
);
|
|
|
|
if (activeStatus) {
|
|
|
|
const details = activeStatus.nextElementSibling;
|
|
|
|
if (details && details.tagName.toLowerCase() === 'details') {
|
|
|
|
details.open = !details.open;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-01-14 14:42:04 +03:00
|
|
|
const { nearReachStart } = useScroll({
|
2023-02-28 16:56:41 +03:00
|
|
|
scrollableRef,
|
2023-02-25 05:50:02 +03:00
|
|
|
distanceFromStartPx: 16,
|
2023-01-10 05:44:16 +03:00
|
|
|
});
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
return (
|
|
|
|
<div class="deck-backdrop">
|
2023-01-29 10:23:53 +03:00
|
|
|
<Link to={closeLink} onClick={onClose}></Link>
|
2022-12-10 12:14:48 +03:00
|
|
|
<div
|
2022-12-30 15:37:57 +03:00
|
|
|
tabIndex="-1"
|
2022-12-21 13:02:13 +03:00
|
|
|
ref={scrollableRef}
|
2022-12-10 12:14:48 +03:00
|
|
|
class={`status-deck deck contained ${
|
|
|
|
statuses.length > 1 ? 'padded-bottom' : ''
|
|
|
|
}`}
|
|
|
|
>
|
2022-12-24 17:26:43 +03:00
|
|
|
<header
|
|
|
|
class={`${heroInView ? 'inview' : ''}`}
|
2023-01-09 16:51:30 +03:00
|
|
|
onDblClick={(e) => {
|
|
|
|
// reload statuses
|
|
|
|
states.reloadStatusPage++;
|
|
|
|
}}
|
2022-12-24 17:26:43 +03:00
|
|
|
>
|
2022-12-19 11:25:57 +03:00
|
|
|
{/* <div>
|
2022-12-18 15:53:32 +03:00
|
|
|
<Link class="button plain deck-close" href={closeLink}>
|
|
|
|
<Icon icon="chevron-left" size="xl" />
|
|
|
|
</Link>
|
2022-12-19 11:25:57 +03:00
|
|
|
</div> */}
|
2023-02-11 18:35:03 +03:00
|
|
|
<div class="header-grid header-grid-2">
|
2023-02-08 14:11:33 +03:00
|
|
|
<h1>
|
|
|
|
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
|
|
|
<>
|
|
|
|
<span class="hero-heading">
|
|
|
|
<NameText
|
|
|
|
account={heroStatus.account}
|
|
|
|
instance={instance}
|
|
|
|
showAvatar
|
|
|
|
short
|
|
|
|
/>{' '}
|
|
|
|
<span class="insignificant">
|
|
|
|
•{' '}
|
|
|
|
<RelativeTime
|
|
|
|
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'}
|
2023-01-28 20:02:25 +03:00
|
|
|
/>
|
2023-02-08 14:11:33 +03:00
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
Status{' '}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="ancestors-indicator light small"
|
|
|
|
onClick={(e) => {
|
|
|
|
// Scroll to top
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
scrollableRef.current.scrollTo({
|
|
|
|
top: 0,
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
hidden={!ancestors.length || nearReachStart}
|
|
|
|
>
|
|
|
|
<Icon icon="arrow-up" />
|
|
|
|
<Icon icon="comment" />{' '}
|
|
|
|
<span class="insignificant">
|
|
|
|
{shortenNumber(ancestors.length)}
|
|
|
|
</span>
|
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</h1>
|
|
|
|
<div class="header-side">
|
2023-02-11 15:56:33 +03:00
|
|
|
{uiState === 'loading' ? (
|
|
|
|
<Loader abrupt />
|
|
|
|
) : (
|
|
|
|
<Menu
|
|
|
|
align="end"
|
|
|
|
portal={{
|
|
|
|
// Need this, else the menu click will cause scroll jump
|
|
|
|
target: scrollableRef.current,
|
2023-02-06 14:09:25 +03:00
|
|
|
}}
|
2023-02-11 15:56:33 +03:00
|
|
|
menuButton={
|
|
|
|
<button type="button" class="button plain4">
|
|
|
|
<Icon icon="more" alt="Actions" size="xl" />
|
|
|
|
</button>
|
|
|
|
}
|
2023-02-06 14:09:25 +03:00
|
|
|
>
|
2023-02-08 14:11:33 +03:00
|
|
|
<MenuItem
|
2023-03-19 15:12:45 +03:00
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
onClick={() => {
|
|
|
|
states.reloadStatusPage++;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="refresh" />
|
|
|
|
<span>Refresh</span>
|
|
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
2023-02-08 14:11:33 +03:00
|
|
|
onClick={() => {
|
2023-02-11 15:56:33 +03:00
|
|
|
// 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();
|
|
|
|
});
|
2023-02-08 14:11:33 +03:00
|
|
|
}}
|
|
|
|
>
|
2023-02-11 15:56:33 +03:00
|
|
|
<Icon icon="eye-open" />{' '}
|
|
|
|
<span>Show all sensitive content</span>
|
2023-02-08 14:11:33 +03:00
|
|
|
</MenuItem>
|
2023-03-24 10:05:57 +03:00
|
|
|
<MenuDivider />
|
|
|
|
<MenuHeader className="plain">Experimental</MenuHeader>
|
|
|
|
<MenuItem
|
|
|
|
disabled={postSameInstance}
|
|
|
|
onClick={() => {
|
|
|
|
const statusURL = getInstanceStatusURL(heroStatus.url);
|
|
|
|
if (statusURL) {
|
|
|
|
navigate(statusURL);
|
|
|
|
} else {
|
|
|
|
alert('Unable to switch');
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="transfer" />
|
|
|
|
<small class="menu-double-lines">
|
|
|
|
Switch to post's instance (<b>{postInstance}</b>)
|
|
|
|
</small>
|
|
|
|
</MenuItem>
|
2023-02-11 15:56:33 +03:00
|
|
|
</Menu>
|
|
|
|
)}
|
2023-02-08 14:11:33 +03:00
|
|
|
<Link
|
|
|
|
class="button plain deck-close"
|
|
|
|
to={closeLink}
|
|
|
|
onClick={onClose}
|
|
|
|
>
|
|
|
|
<Icon icon="x" size="xl" />
|
|
|
|
</Link>
|
|
|
|
</div>
|
2022-12-10 12:14:48 +03:00
|
|
|
</div>
|
|
|
|
</header>
|
2023-01-01 10:28:07 +03:00
|
|
|
{!!statuses.length && heroStatus ? (
|
|
|
|
<ul
|
|
|
|
class={`timeline flat contextual grow ${
|
|
|
|
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 ? (
|
2023-02-19 09:49:53 +03:00
|
|
|
<>
|
|
|
|
<InView
|
|
|
|
threshold={0.1}
|
|
|
|
onChange={onView}
|
|
|
|
class="status-focus"
|
|
|
|
tabIndex={0}
|
|
|
|
>
|
|
|
|
<Status
|
|
|
|
statusID={statusID}
|
|
|
|
instance={instance}
|
|
|
|
withinContext
|
|
|
|
size="l"
|
2023-03-07 17:38:06 +03:00
|
|
|
enableTranslate
|
2023-02-19 09:49:53 +03:00
|
|
|
/>
|
|
|
|
</InView>
|
2023-03-31 14:46:54 +03:00
|
|
|
{uiState !== 'loading' && !authenticated ? (
|
|
|
|
<div class="post-status-banner">
|
|
|
|
<p>
|
|
|
|
You're not logged in. Interactions (reply, boost,
|
|
|
|
etc) are not possible.
|
|
|
|
</p>
|
|
|
|
<Link to="/login" class="button">
|
|
|
|
Log in
|
|
|
|
</Link>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
!sameInstance && (
|
2023-02-19 09:49:53 +03:00
|
|
|
<div class="post-status-banner">
|
|
|
|
<p>
|
2023-03-31 14:46:54 +03:00
|
|
|
This post is from another instance (
|
|
|
|
<b>{instance}</b>). Interactions (reply, boost,
|
2023-02-19 09:49:53 +03:00
|
|
|
etc) are not possible.
|
|
|
|
</p>
|
2023-03-31 14:46:54 +03:00
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
onClick={() => {
|
|
|
|
setUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const results =
|
|
|
|
await currentMasto.v2.search({
|
|
|
|
q: heroStatus.url,
|
|
|
|
type: 'statuses',
|
|
|
|
resolve: true,
|
|
|
|
limit: 1,
|
|
|
|
});
|
|
|
|
if (results.statuses.length) {
|
|
|
|
const status = results.statuses[0];
|
|
|
|
navigate(
|
|
|
|
currentInstance
|
|
|
|
? `/${currentInstance}/s/${status.id}`
|
|
|
|
: `/s/${status.id}`,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
throw new Error('No results');
|
2023-03-31 14:21:27 +03:00
|
|
|
}
|
2023-03-31 14:46:54 +03:00
|
|
|
} catch (e) {
|
|
|
|
setUIState('default');
|
|
|
|
alert('Error: ' + e);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="transfer" /> Switch to my instance to
|
|
|
|
enable interactions
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
)}
|
2023-02-19 09:49:53 +03:00
|
|
|
</>
|
2023-01-01 10:28:07 +03:00
|
|
|
) : (
|
2023-01-21 14:52:51 +03:00
|
|
|
<Link
|
|
|
|
class="status-link"
|
2023-02-05 19:17:19 +03:00
|
|
|
to={
|
|
|
|
instance
|
2023-02-06 14:54:18 +03:00
|
|
|
? `/${instance}/s/${statusID}`
|
2023-02-05 19:17:19 +03:00
|
|
|
: `/s/${statusID}`
|
|
|
|
}
|
2023-01-21 14:52:51 +03:00
|
|
|
onClick={() => {
|
|
|
|
resetScrollPosition(statusID);
|
|
|
|
}}
|
|
|
|
>
|
2023-01-01 10:28:07 +03:00
|
|
|
<Status
|
|
|
|
statusID={statusID}
|
2023-02-05 19:17:19 +03:00
|
|
|
instance={instance}
|
2023-01-01 10:28:07 +03:00
|
|
|
withinContext
|
|
|
|
size={thread || ancestor ? 'm' : 's'}
|
2023-03-07 17:38:06 +03:00
|
|
|
enableTranslate
|
2023-01-01 10:28:07 +03:00
|
|
|
/>
|
2023-01-28 20:02:25 +03:00
|
|
|
{/* {replies?.length > LIMIT && (
|
2023-01-01 10:28:07 +03:00
|
|
|
<div class="replies-link">
|
|
|
|
<Icon icon="comment" />{' '}
|
|
|
|
<span title={replies.length}>
|
|
|
|
{shortenNumber(replies.length)}
|
|
|
|
</span>
|
|
|
|
</div>
|
2023-01-28 20:02:25 +03:00
|
|
|
)} */}
|
2023-01-01 10:28:07 +03:00
|
|
|
</Link>
|
2022-12-22 19:30:55 +03:00
|
|
|
)}
|
2023-01-28 20:02:25 +03:00
|
|
|
{descendant && replies?.length > 0 && (
|
|
|
|
<SubComments
|
2023-02-05 19:17:19 +03:00
|
|
|
instance={instance}
|
2023-01-28 20:02:25 +03:00
|
|
|
hasManyStatuses={hasManyStatuses}
|
|
|
|
replies={replies}
|
2023-02-07 07:01:36 +03:00
|
|
|
hasParentThread={thread}
|
2023-03-30 15:07:07 +03:00
|
|
|
level={1}
|
2023-01-28 20:02:25 +03:00
|
|
|
/>
|
|
|
|
)}
|
2023-01-01 10:28:07 +03:00
|
|
|
{uiState === 'loading' &&
|
|
|
|
isHero &&
|
|
|
|
!!heroStatus?.repliesCount &&
|
|
|
|
!hasDescendants && (
|
|
|
|
<div class="status-loading">
|
|
|
|
<Loader />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{uiState === 'error' &&
|
|
|
|
isHero &&
|
|
|
|
!!heroStatus?.repliesCount &&
|
|
|
|
!hasDescendants && (
|
|
|
|
<div class="status-error">
|
|
|
|
Unable to load replies.
|
|
|
|
<br />
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="plain"
|
|
|
|
onClick={() => {
|
|
|
|
states.reloadStatusPage++;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Try again
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
{showMore > 0 && (
|
|
|
|
<li>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="plain block"
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
onClick={() => setLimit((l) => l + LIMIT)}
|
|
|
|
style={{ marginBlockEnd: '6em' }}
|
|
|
|
>
|
|
|
|
Show more…{' '}
|
|
|
|
<span class="tag">
|
|
|
|
{showMore > LIMIT ? `${LIMIT}+` : showMore}
|
|
|
|
</span>
|
|
|
|
</button>
|
2022-12-10 12:14:48 +03:00
|
|
|
</li>
|
2023-01-01 10:28:07 +03:00
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
{uiState === 'loading' && (
|
|
|
|
<ul class="timeline flat contextual grow loading">
|
|
|
|
<li>
|
|
|
|
<Status skeleton size="l" />
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
)}
|
|
|
|
{uiState === 'error' && (
|
|
|
|
<p class="ui-state">
|
|
|
|
Unable to load status
|
|
|
|
<br />
|
|
|
|
<br />
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
onClick={() => {
|
|
|
|
states.reloadStatusPage++;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Try again
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
2022-12-10 12:14:48 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2022-12-16 08:27:04 +03:00
|
|
|
}
|
|
|
|
|
2023-03-30 15:07:07 +03:00
|
|
|
function SubComments({
|
|
|
|
hasManyStatuses,
|
|
|
|
replies,
|
|
|
|
instance,
|
|
|
|
hasParentThread,
|
|
|
|
level,
|
|
|
|
}) {
|
2023-01-31 11:12:57 +03:00
|
|
|
// Set isBrief = true:
|
|
|
|
// - if less than or 2 replies
|
|
|
|
// - if replies have no sub-replies
|
|
|
|
// - if total number of characters of content from replies is less than 500
|
2022-12-22 19:30:55 +03:00
|
|
|
let isBrief = false;
|
|
|
|
if (replies.length <= 2) {
|
2023-01-31 11:12:57 +03:00
|
|
|
const containsSubReplies = replies.some(
|
|
|
|
(r) => r.repliesCount > 0 || r.replies?.length > 0,
|
|
|
|
);
|
|
|
|
if (!containsSubReplies) {
|
|
|
|
let totalLength = replies.reduce((acc, reply) => {
|
|
|
|
const { content } = reply;
|
|
|
|
const length = htmlContentLength(content);
|
|
|
|
return acc + length;
|
|
|
|
}, 0);
|
|
|
|
isBrief = totalLength < 500;
|
|
|
|
}
|
2022-12-22 19:30:55 +03:00
|
|
|
}
|
|
|
|
|
2023-01-31 11:12:57 +03:00
|
|
|
// Total comments count, including sub-replies
|
|
|
|
const diveDeep = (replies) => {
|
|
|
|
return replies.reduce((acc, reply) => {
|
|
|
|
const { repliesCount, replies } = reply;
|
|
|
|
const count = replies?.length || repliesCount;
|
|
|
|
return acc + count + diveDeep(replies || []);
|
|
|
|
}, 0);
|
|
|
|
};
|
|
|
|
const totalComments = replies.length + diveDeep(replies);
|
|
|
|
const sameCount = replies.length === totalComments;
|
|
|
|
|
2023-01-28 20:02:25 +03:00
|
|
|
// Get the first 3 accounts, unique by id
|
|
|
|
const accounts = replies
|
|
|
|
.map((r) => r.account)
|
|
|
|
.filter((a, i, arr) => arr.findIndex((b) => b.id === a.id) === i)
|
2023-01-31 11:12:57 +03:00
|
|
|
.slice(0, 3);
|
2023-01-28 20:02:25 +03:00
|
|
|
|
2023-02-10 12:35:38 +03:00
|
|
|
const open =
|
|
|
|
(!hasParentThread || replies.length === 1) && (isBrief || !hasManyStatuses);
|
2023-03-28 15:33:00 +03:00
|
|
|
const openBefore = cachedRepliesToggle[replies[0].id];
|
2022-12-22 19:30:55 +03:00
|
|
|
|
|
|
|
return (
|
2023-03-28 15:33:00 +03:00
|
|
|
<details
|
|
|
|
class="replies"
|
|
|
|
open={openBefore || open}
|
|
|
|
onToggle={(e) => {
|
|
|
|
const { open } = e.target;
|
|
|
|
// use first reply as ID
|
|
|
|
cachedRepliesToggle[replies[0].id] = open;
|
|
|
|
}}
|
2023-03-30 15:07:07 +03:00
|
|
|
style={{
|
|
|
|
'--comments-level': level,
|
|
|
|
}}
|
|
|
|
data-comments-level={level}
|
|
|
|
data-comments-level-overflow={level > 4}
|
2023-03-28 15:33:00 +03:00
|
|
|
>
|
2023-03-30 15:07:07 +03:00
|
|
|
<summary class="replies-summary" hidden={open}>
|
2023-01-28 20:02:25 +03:00
|
|
|
<span class="avatars">
|
|
|
|
{accounts.map((a) => (
|
|
|
|
<Avatar
|
|
|
|
key={a.id}
|
|
|
|
url={a.avatarStatic}
|
|
|
|
title={`${a.displayName} @${a.username}`}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</span>
|
|
|
|
<span>
|
|
|
|
<span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
|
|
|
|
repl
|
|
|
|
{replies.length === 1 ? 'y' : 'ies'}
|
|
|
|
</span>
|
2023-01-31 11:12:57 +03:00
|
|
|
{!sameCount && totalComments > 1 && (
|
|
|
|
<>
|
|
|
|
{' '}
|
|
|
|
·{' '}
|
|
|
|
<span>
|
|
|
|
<span title={totalComments}>{shortenNumber(totalComments)}</span>{' '}
|
|
|
|
comment
|
|
|
|
{totalComments === 1 ? '' : 's'}
|
|
|
|
</span>
|
|
|
|
</>
|
|
|
|
)}
|
2022-12-22 19:30:55 +03:00
|
|
|
</summary>
|
|
|
|
<ul>
|
2022-12-23 20:11:11 +03:00
|
|
|
{replies.map((r) => (
|
|
|
|
<li key={r.id}>
|
2022-12-22 19:30:55 +03:00
|
|
|
<Link
|
|
|
|
class="status-link"
|
2023-02-06 14:54:18 +03:00
|
|
|
to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`}
|
2023-01-21 14:52:51 +03:00
|
|
|
onClick={() => {
|
|
|
|
resetScrollPosition(r.id);
|
|
|
|
}}
|
2022-12-22 19:30:55 +03:00
|
|
|
>
|
2023-02-05 19:17:19 +03:00
|
|
|
<Status
|
|
|
|
statusID={r.id}
|
|
|
|
instance={instance}
|
|
|
|
withinContext
|
|
|
|
size="s"
|
2023-03-07 17:38:06 +03:00
|
|
|
enableTranslate
|
2023-02-05 19:17:19 +03:00
|
|
|
/>
|
2023-01-28 20:29:26 +03:00
|
|
|
{!r.replies?.length && r.repliesCount > 0 && (
|
2022-12-23 20:11:11 +03:00
|
|
|
<div class="replies-link">
|
|
|
|
<Icon icon="comment" />{' '}
|
|
|
|
<span title={r.repliesCount}>
|
|
|
|
{shortenNumber(r.repliesCount)}
|
|
|
|
</span>
|
|
|
|
</div>
|
2023-01-28 20:29:26 +03:00
|
|
|
)}
|
2022-12-22 19:30:55 +03:00
|
|
|
</Link>
|
2023-01-28 20:02:25 +03:00
|
|
|
{r.replies?.length && (
|
|
|
|
<SubComments
|
2023-02-05 19:17:19 +03:00
|
|
|
instance={instance}
|
2023-01-28 20:02:25 +03:00
|
|
|
hasManyStatuses={hasManyStatuses}
|
|
|
|
replies={r.replies}
|
2023-03-30 15:07:07 +03:00
|
|
|
level={level + 1}
|
2023-01-28 20:02:25 +03:00
|
|
|
/>
|
|
|
|
)}
|
2022-12-22 19:30:55 +03:00
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
</details>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-03 05:05:57 +03:00
|
|
|
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/([^\/]+)\/?$/i;
|
|
|
|
const statusNoteRegex = /\/notes\/([^\/]+)\/?$/i;
|
2023-03-24 10:05:57 +03:00
|
|
|
function getInstanceStatusURL(url) {
|
|
|
|
// Regex /:username/:id, where username = @username or @username@domain, id = anything
|
|
|
|
const { hostname, pathname } = new URL(url);
|
|
|
|
const [, username, domain, id] = pathname.match(statusRegex) || [];
|
|
|
|
if (id) {
|
|
|
|
return `/${hostname}/s/${id}`;
|
|
|
|
}
|
2023-04-03 05:05:57 +03:00
|
|
|
const [, noteId] = pathname.match(statusNoteRegex) || [];
|
|
|
|
if (noteId) {
|
|
|
|
return `/${hostname}/s/${noteId}`;
|
|
|
|
}
|
2023-03-24 10:05:57 +03:00
|
|
|
}
|
|
|
|
|
2022-12-16 08:27:04 +03:00
|
|
|
export default StatusPage;
|