From 2f24713d71f7eb695a24b7907d59fde597863f06 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sun, 18 Dec 2022 20:46:13 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Real=20nested=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Collapsed/expandable replies - Pagination for many many comments --- src/app.css | 108 ++++++++++++++++++++++++++++++- src/components/status.css | 8 --- src/pages/status.jsx | 130 ++++++++++++++++++++++++-------------- 3 files changed, 189 insertions(+), 57 deletions(-) diff --git a/src/app.css b/src/app.css index fac7fcfb..63e05686 100644 --- a/src/app.css +++ b/src/app.css @@ -146,7 +146,41 @@ a.mention span { .timeline.contextual > li.descendant { position: relative; } -.timeline.contextual > li.descendant.indirect:before { +.timeline.contextual > li.descendant:not(.thread) { + padding-bottom: 1em; +} +.timeline.contextual > li.descendant:not(.thread) > .status-link { + padding-left: 40px; +} +.timeline.contextual + > li.descendant.thread + > .status-link + + .replies + > summary { + margin-left: calc(50px + 16px + 16px); +} +.timeline.contextual + > li.descendant.thread + > .status-link + + .replies + .status-link { + padding-left: calc(50px + 16px + 16px); +} +.timeline.contextual + > li.descendant:not(.thread) + > .status-link + + .replies + > summary { + margin-left: calc(40px + 16px); +} +.timeline.contextual + > li.descendant:not(.thread) + > .status-link + + .replies + .status-link { + padding-left: calc(40px + 16px); +} +.timeline.contextual > li.descendant:not(.thread):before { --radius: 10px; --diameter: calc(var(--radius) * 2); content: ''; @@ -161,9 +195,79 @@ a.mention span { border-color: transparent transparent var(--comment-line-color) transparent; transform: rotate(45deg); } -.timeline.contextual > li.descendant.indirect .status-link { +.timeline.contextual > li .replies { + margin-top: -16px; + font-size: 90%; +} +.timeline.contextual > li .replies :is(ul, li) { + margin: 0; + padding: 0; + list-style: none; +} +.timeline.contextual > li .replies summary { + padding: 8px 16px; + background-color: var(--bg-faded-color); + display: inline-block; + border-radius: 8px; + cursor: pointer; + text-transform: uppercase; + font-weight: bold; + font-size: 12px; + color: var(--text-insignificant-color); + user-select: none; +} +.timeline.contextual > li .replies summary:active, +.timeline.contextual > li .replies[open] summary { + color: var(--text-color); + background-color: var(--comment-line-color); + background-image: linear-gradient( + to top right, + var(--comment-line-color), + var(--bg-faded-color) + ); +} +.timeline.contextual > li .replies[open] summary { + border-bottom-left-radius: 0; +} +.timeline.contextual > li .replies li { position: relative; } +.timeline.contextual > li .replies li .status { + --width: 3px; + --left: 0px; + --right: calc(var(--left) + var(--width)); + background-image: linear-gradient( + to right, + transparent, + transparent var(--left), + var(--comment-line-color) var(--left), + var(--comment-line-color) var(--right), + transparent var(--right), + transparent + ); + background-repeat: no-repeat; +} +.timeline.contextual > li .replies li:last-child .status { + background-size: 100% 20px; +} +.timeline.contextual > li .replies li:before { + --radius: 10px; + --diameter: calc(var(--radius) * 2); + content: ''; + position: absolute; + top: 10px; + left: calc(40px + 16px); + width: var(--diameter); + height: var(--diameter); + border-radius: var(--radius); + border-style: solid; + border-width: var(--width); + border-color: transparent transparent var(--comment-line-color) transparent; + transform: rotate(45deg); +} +.timeline.contextual > li.thread .replies li:before { + left: calc(50px + 16px + 16px); +} .timeline-deck.compact .status { max-height: max(25vh, 160px); diff --git a/src/components/status.css b/src/components/status.css index 72b7419b..589b9862 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -90,14 +90,6 @@ .status.skeleton > .avatar { background-color: var(--outline-color); } -.indirect .status { - padding-left: 57px; -} -.indirect .status .avatar { - width: 25px !important; - height: 25px !important; - transform: translateX(5px); -} .status .container { flex-grow: 1; diff --git a/src/pages/status.jsx b/src/pages/status.jsx index ddc9d932..cee847ff 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -28,15 +28,18 @@ function StatusPage({ id }) { setUIState('loading'); - if (!states.statuses.has(id)) { - try { - const status = await masto.statuses.fetch(id); - states.statuses.set(id, status); - } catch (e) { + 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; } + return; } try { @@ -46,38 +49,46 @@ function StatusPage({ id }) { ancestors.forEach((status) => { states.statuses.set(status.id, status); }); - const directReplies = []; + const nestedDescendants = []; descendants.forEach((status) => { states.statuses.set(status.id, status); - if (status.inReplyToId === id) { - directReplies.push(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, directReplies }); - if (directReplies.length) { - const heroStatus = states.statuses.get(id); - const heroStatusRepliesCount = heroStatus.repliesCount; - if (heroStatusRepliesCount != directReplies.length) { - // If replies count doesn't match, refetch the status - const status = await masto.statuses.fetch(id); - states.statuses.set(id, status); - } - } + console.log({ ancestors, descendants, nestedDescendants }); const allStatuses = [ ...ancestors.map((s) => ({ id: s.id, ancestor: true })), { id }, - ...descendants.map((s) => ({ + ...nestedDescendants.map((s) => ({ id: s.id, descendant: true, - directReply: - s.inReplyToId === id || s.inReplyToAccountId === s.account.id, - // I can assume if the reply is to the same account, it's a direct reply. In other words, it's a thread?!? + 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'); } @@ -103,7 +114,7 @@ function StatusPage({ id }) { } }, [statuses]); - const heroStatus = states.statuses.get(id); + const heroStatus = snapStates.statuses.get(id); const heroDisplayName = useMemo(() => { // Remove shortcodes from display name if (!heroStatus) return ''; @@ -136,14 +147,17 @@ function StatusPage({ id }) { : 'Status', ); - const comments = statuses.filter((s) => s.descendant); - const replies = comments.filter((s) => s.directReply); - 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]); + return (
@@ -162,8 +176,14 @@ function StatusPage({ id }) {
+ {showMore > 0 && ( + + )} );