Status thread page improvements

- Show level 3 comments
- Change header-tap to scroll top to a button instead (prevent accidental scroll top)
- Show avatars in <summary>
- Clean up CSS a bit
This commit is contained in:
Lim Chee Aun 2023-01-29 01:02:25 +08:00
parent ae90b41aae
commit a088b48eb7
3 changed files with 196 additions and 82 deletions

View file

@ -148,17 +148,26 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
border-bottom: none; border-bottom: none;
} }
.timeline.contextual {
--thread-start: 40px;
--line-start: 40px;
--line-width: 3px;
--line-end: calc(var(--line-start) + var(--line-width));
--line-margin-end: 16px;
--line-radius: 10px;
--line-diameter: calc(var(--line-radius) * 2);
--avatar-size: 50px;
--avatar-margin-start: 16px;
--avatar-margin-end: 12px;
}
.timeline.contextual > li { .timeline.contextual > li {
--width: 3px;
--left: 40px;
--right: calc(var(--left) + var(--width));
background-image: linear-gradient( background-image: linear-gradient(
to right, to right,
transparent, transparent,
transparent var(--left), transparent var(--line-start),
var(--comment-line-color) var(--left), var(--comment-line-color) var(--line-start),
var(--comment-line-color) var(--right), var(--comment-line-color) var(--line-end),
transparent var(--right), transparent var(--line-end),
transparent transparent
); );
background-repeat: no-repeat; background-repeat: no-repeat;
@ -184,41 +193,83 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
> .status-link > .status-link
+ .replies + .replies
> summary { > summary {
margin-left: calc(50px + 16px + 12px); margin-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
.timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
> summary {
margin-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
} }
.timeline.contextual .timeline.contextual
> li.descendant.thread > li.descendant.thread
> .status-link > .status-link
+ .replies + .replies
.status-link { .status-link {
padding-left: calc(50px + 16px + 12px); padding-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
.timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
.status-link {
padding-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
} }
.timeline.contextual .timeline.contextual
> li.descendant:not(.thread) > li.descendant:not(.thread)
> .status-link > .status-link
+ .replies + .replies
> summary { > summary {
margin-left: calc(40px + 16px); margin-left: calc(var(--thread-start) + var(--line-margin-end));
}
.timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
> summary {
margin-left: calc(
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
);
} }
.timeline.contextual .timeline.contextual
> li.descendant:not(.thread) > li.descendant:not(.thread)
> .status-link > .status-link
+ .replies + .replies
.status-link { .status-link {
padding-left: calc(40px + 16px); padding-left: calc(var(--thread-start) + var(--line-margin-end));
}
.timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
.status-link {
--line-margin-end: 32px;
} }
.timeline.contextual > li.descendant:not(.thread):before { .timeline.contextual > li.descendant:not(.thread):before {
--radius: 10px;
--diameter: calc(var(--radius) * 2);
content: ''; content: '';
position: absolute; position: absolute;
top: 10px; top: 10px;
left: 40px; left: var(--line-start);
width: var(--diameter); width: var(--line-diameter);
height: var(--diameter); height: var(--line-diameter);
border-radius: var(--radius); border-radius: var(--line-radius);
border-style: solid; border-style: solid;
border-width: var(--width); border-width: var(--line-width);
border-color: transparent transparent var(--comment-line-color) transparent; border-color: transparent transparent var(--comment-line-color) transparent;
transform: rotate(45deg); transform: rotate(45deg);
} }
@ -230,7 +281,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
font-size: 90%; font-size: 90%;
} }
.timeline.contextual > li.thread > .status-link .replies-link { .timeline.contextual > li.thread > .status-link .replies-link {
margin-left: calc(50px + 16px + 12px); margin-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
} }
.timeline.contextual > li .replies-link * { .timeline.contextual > li .replies-link * {
vertical-align: middle; vertical-align: middle;
@ -243,7 +296,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
.timeline.contextual > li .replies summary { .timeline.contextual > li .replies > summary {
padding: 8px 16px; padding: 8px 16px;
background-color: var(--bg-faded-color); background-color: var(--bg-faded-color);
display: inline-block; display: inline-block;
@ -256,9 +309,16 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
user-select: none; user-select: none;
box-shadow: 0 0 0 2px var(--bg-color); box-shadow: 0 0 0 2px var(--bg-color);
position: relative; position: relative;
list-style: none;
} }
.timeline.contextual > li .replies summary:active, .timeline.contextual > li .replies > summary > * {
.timeline.contextual > li .replies[open] summary { vertical-align: middle;
}
.timeline.contextual > li .replies > summary .avatars {
margin-right: 8px;
}
.timeline.contextual > li .replies > summary:active,
.timeline.contextual > li .replies[open] > summary {
color: var(--text-color); color: var(--text-color);
background-color: var(--comment-line-color); background-color: var(--comment-line-color);
background-image: linear-gradient( background-image: linear-gradient(
@ -267,7 +327,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
var(--bg-faded-color) var(--bg-faded-color)
); );
} }
.timeline.contextual > li .replies[open] summary { .timeline.contextual > li .replies[open] > summary {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.timeline.contextual > li .replies summary[hidden] { .timeline.contextual > li .replies summary[hidden] {
@ -277,43 +337,66 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
position: relative; position: relative;
} }
.timeline.contextual > li .replies li { .timeline.contextual > li .replies li {
--width: 3px; --line-start: calc(var(--thread-start) + var(--line-margin-end));
--left: calc(40px + 16px); --line-end: calc(var(--line-start) + var(--line-width));
--right: calc(var(--left) + var(--width));
background-image: linear-gradient( background-image: linear-gradient(
to right, to right,
transparent, transparent,
transparent var(--left), transparent var(--line-start),
var(--comment-line-color) var(--left), var(--comment-line-color) var(--line-start),
var(--comment-line-color) var(--right), var(--comment-line-color) var(--line-end),
transparent var(--right), transparent var(--line-end),
transparent transparent
); );
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.timeline.contextual > li .replies .replies li {
--line-start: calc(
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
);
}
.timeline.contextual > li.thread .replies li { .timeline.contextual > li.thread .replies li {
--left: calc(50px + 16px + 12px); --line-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
.timeline.contextual > li.thread .replies .replies li {
--line-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
} }
.timeline.contextual > li .replies li:last-child { .timeline.contextual > li .replies li:last-child {
background-size: 100% 20px; background-size: 100% 20px;
} }
.timeline.contextual > li .replies li:before { .timeline.contextual > li .replies li:before {
--radius: 10px;
--diameter: calc(var(--radius) * 2);
content: ''; content: '';
position: absolute; position: absolute;
top: 10px; top: 10px;
left: calc(40px + 16px); left: var(--line-start);
width: var(--diameter); width: var(--line-diameter);
height: var(--diameter); height: var(--line-diameter);
border-radius: var(--radius); border-radius: var(--line-radius);
border-style: solid; border-style: solid;
border-width: var(--width); border-width: var(--line-width);
border-color: transparent transparent var(--comment-line-color) transparent; border-color: transparent transparent var(--comment-line-color) transparent;
transform: rotate(45deg); transform: rotate(45deg);
} }
.timeline.contextual > li .replies .replies li:before {
--line-start: calc(
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
);
}
.timeline.contextual > li.thread .replies li:before { .timeline.contextual > li.thread .replies li:before {
left: calc(50px + 16px + 12px); --line-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
.timeline.contextual > li.thread .replies .replies li:before {
--line-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
} }
.timeline.contextual.loading > li:not(.hero) { .timeline.contextual.loading > li:not(.hero) {
/* opacity: 0.5; */ /* opacity: 0.5; */

View file

@ -20,7 +20,6 @@
.hero-heading { .hero-heading {
font-size: 16px; font-size: 16px;
pointer-events: none;
display: inline-block; display: inline-block;
margin-bottom: 0.25em; margin-bottom: 0.25em;
} }

View file

@ -9,6 +9,7 @@ import { InView } from 'react-intersection-observer';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Avatar from '../components/avatar';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import Loader from '../components/loader'; import Loader from '../components/loader';
@ -24,6 +25,7 @@ import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 40; const LIMIT = 40;
const THREAD_LIMIT = 20;
let cachedStatusesMap = {}; let cachedStatusesMap = {};
function resetScrollPosition(id) { function resetScrollPosition(id) {
@ -164,8 +166,16 @@ function StatusPage() {
thread: s.account.id === heroStatus.account.id, thread: s.account.id === heroStatus.account.id,
replies: s.__replies?.map((r) => ({ replies: s.__replies?.map((r) => ({
id: r.id, id: r.id,
account: r.account,
repliesCount: r.repliesCount, repliesCount: r.repliesCount,
content: r.content, content: r.content,
replies: r.__replies?.map((r2) => ({
// Level 3
id: r2.id,
account: r2.account,
repliesCount: r2.repliesCount,
content: r2.content,
})),
})), })),
})), })),
]; ];
@ -305,7 +315,7 @@ function StatusPage() {
return statuses.length - limit; return statuses.length - limit;
}, [statuses.length, limit]); }, [statuses.length, limit]);
const hasManyStatuses = statuses.length > LIMIT; const hasManyStatuses = statuses.length > THREAD_LIMIT;
const hasDescendants = statuses.some((s) => s.descendant); const hasDescendants = statuses.some((s) => s.descendant);
const ancestors = statuses.filter((s) => s.ancestor); const ancestors = statuses.filter((s) => s.ancestor);
@ -405,17 +415,6 @@ function StatusPage() {
> >
<header <header
class={`${heroInView ? 'inview' : ''}`} class={`${heroInView ? 'inview' : ''}`}
onClick={(e) => {
if (
!/^(a|button)$/i.test(e.target.tagName) &&
heroStatusRef.current
) {
heroStatusRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}}
onDblClick={(e) => { onDblClick={(e) => {
// reload statuses // reload statuses
states.reloadStatusPage++; states.reloadStatusPage++;
@ -428,23 +427,34 @@ function StatusPage() {
</div> */} </div> */}
<h1> <h1>
{!heroInView && heroStatus && uiState !== 'loading' ? ( {!heroInView && heroStatus && uiState !== 'loading' ? (
<span class="hero-heading"> <>
{!!heroPointer && ( <span class="hero-heading">
<> <NameText showAvatar account={heroStatus.account} short />{' '}
<Icon <span class="insignificant">
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'} &bull;{' '}
/>{' '} <RelativeTime
</> datetime={heroStatus.createdAt}
)} format="micro"
<NameText showAvatar account={heroStatus.account} short />{' '} />
<span class="insignificant"> </span>
&bull;{' '} </span>{' '}
<RelativeTime <button
datetime={heroStatus.createdAt} type="button"
format="micro" 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'}
/> />
</span> </button>
</span> </>
) : ( ) : (
<> <>
Status{' '} Status{' '}
@ -551,24 +561,22 @@ function StatusPage() {
withinContext withinContext
size={thread || ancestor ? 'm' : 's'} size={thread || ancestor ? 'm' : 's'}
/> />
{replies?.length > LIMIT && ( {/* {replies?.length > LIMIT && (
<div class="replies-link"> <div class="replies-link">
<Icon icon="comment" />{' '} <Icon icon="comment" />{' '}
<span title={replies.length}> <span title={replies.length}>
{shortenNumber(replies.length)} {shortenNumber(replies.length)}
</span> </span>
</div> </div>
)} )} */}
</Link> </Link>
)} )}
{descendant && {descendant && replies?.length > 0 && (
replies?.length > 0 && <SubComments
replies?.length <= LIMIT && ( hasManyStatuses={hasManyStatuses}
<SubComments replies={replies}
hasManyStatuses={hasManyStatuses} />
replies={replies} )}
/>
)}
{uiState === 'loading' && {uiState === 'loading' &&
isHero && isHero &&
!!heroStatus?.repliesCount && !!heroStatus?.repliesCount &&
@ -658,13 +666,31 @@ function SubComments({ hasManyStatuses, replies }) {
isBrief = totalLength < 500; isBrief = totalLength < 500;
} }
// 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)
.slice(0, 5);
const open = isBrief || !hasManyStatuses; const open = isBrief || !hasManyStatuses;
return ( return (
<details class="replies" open={open}> <details class="replies" open={open}>
<summary hidden={open}> <summary hidden={open}>
<span title={replies.length}>{shortenNumber(replies.length)}</span> repl <span class="avatars">
{replies.length === 1 ? 'y' : 'ies'} {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>
</summary> </summary>
<ul> <ul>
{replies.map((r) => ( {replies.map((r) => (
@ -677,15 +703,21 @@ function SubComments({ hasManyStatuses, replies }) {
}} }}
> >
<Status statusID={r.id} withinContext size="s" /> <Status statusID={r.id} withinContext size="s" />
{r.repliesCount > 0 && ( {/* {r.repliesCount > 0 && (
<div class="replies-link"> <div class="replies-link">
<Icon icon="comment" />{' '} <Icon icon="comment" />{' '}
<span title={r.repliesCount}> <span title={r.repliesCount}>
{shortenNumber(r.repliesCount)} {shortenNumber(r.repliesCount)}
</span> </span>
</div> </div>
)} )} */}
</Link> </Link>
{r.replies?.length && (
<SubComments
hasManyStatuses={hasManyStatuses}
replies={r.replies}
/>
)}
</li> </li>
))} ))}
</ul> </ul>