mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-23 09:45:46 +03:00
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:
parent
ae90b41aae
commit
a088b48eb7
3 changed files with 196 additions and 82 deletions
161
src/app.css
161
src/app.css
|
@ -148,17 +148,26 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
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 {
|
||||
--width: 3px;
|
||||
--left: 40px;
|
||||
--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 var(--line-start),
|
||||
var(--comment-line-color) var(--line-start),
|
||||
var(--comment-line-color) var(--line-end),
|
||||
transparent var(--line-end),
|
||||
transparent
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
|
@ -184,41 +193,83 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
> .status-link
|
||||
+ .replies
|
||||
> 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
|
||||
> li.descendant.thread
|
||||
> .status-link
|
||||
+ .replies
|
||||
.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
|
||||
> li.descendant:not(.thread)
|
||||
> .status-link
|
||||
+ .replies
|
||||
> 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
|
||||
> li.descendant:not(.thread)
|
||||
> .status-link
|
||||
+ .replies
|
||||
.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 {
|
||||
--radius: 10px;
|
||||
--diameter: calc(var(--radius) * 2);
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 40px;
|
||||
width: var(--diameter);
|
||||
height: var(--diameter);
|
||||
border-radius: var(--radius);
|
||||
left: var(--line-start);
|
||||
width: var(--line-diameter);
|
||||
height: var(--line-diameter);
|
||||
border-radius: var(--line-radius);
|
||||
border-style: solid;
|
||||
border-width: var(--width);
|
||||
border-width: var(--line-width);
|
||||
border-color: transparent transparent var(--comment-line-color) transparent;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
@ -230,7 +281,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
font-size: 90%;
|
||||
}
|
||||
.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 * {
|
||||
vertical-align: middle;
|
||||
|
@ -243,7 +296,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.timeline.contextual > li .replies summary {
|
||||
.timeline.contextual > li .replies > summary {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--bg-faded-color);
|
||||
display: inline-block;
|
||||
|
@ -256,9 +309,16 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
user-select: none;
|
||||
box-shadow: 0 0 0 2px var(--bg-color);
|
||||
position: relative;
|
||||
list-style: none;
|
||||
}
|
||||
.timeline.contextual > li .replies summary:active,
|
||||
.timeline.contextual > li .replies[open] summary {
|
||||
.timeline.contextual > li .replies > 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);
|
||||
background-color: var(--comment-line-color);
|
||||
background-image: linear-gradient(
|
||||
|
@ -267,7 +327,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
var(--bg-faded-color)
|
||||
);
|
||||
}
|
||||
.timeline.contextual > li .replies[open] summary {
|
||||
.timeline.contextual > li .replies[open] > summary {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.timeline.contextual > li .replies summary[hidden] {
|
||||
|
@ -277,43 +337,66 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
position: relative;
|
||||
}
|
||||
.timeline.contextual > li .replies li {
|
||||
--width: 3px;
|
||||
--left: calc(40px + 16px);
|
||||
--right: calc(var(--left) + var(--width));
|
||||
--line-start: calc(var(--thread-start) + var(--line-margin-end));
|
||||
--line-end: calc(var(--line-start) + var(--line-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 var(--line-start),
|
||||
var(--comment-line-color) var(--line-start),
|
||||
var(--comment-line-color) var(--line-end),
|
||||
transparent var(--line-end),
|
||||
transparent
|
||||
);
|
||||
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 {
|
||||
--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 {
|
||||
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);
|
||||
left: var(--line-start);
|
||||
width: var(--line-diameter);
|
||||
height: var(--line-diameter);
|
||||
border-radius: var(--line-radius);
|
||||
border-style: solid;
|
||||
border-width: var(--width);
|
||||
border-width: var(--line-width);
|
||||
border-color: transparent transparent var(--comment-line-color) transparent;
|
||||
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 {
|
||||
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) {
|
||||
/* opacity: 0.5; */
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
|
||||
.hero-heading {
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { InView } from 'react-intersection-observer';
|
|||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Avatar from '../components/avatar';
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
|
@ -24,6 +25,7 @@ import useScroll from '../utils/useScroll';
|
|||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 40;
|
||||
const THREAD_LIMIT = 20;
|
||||
|
||||
let cachedStatusesMap = {};
|
||||
function resetScrollPosition(id) {
|
||||
|
@ -164,8 +166,16 @@ function StatusPage() {
|
|||
thread: s.account.id === heroStatus.account.id,
|
||||
replies: s.__replies?.map((r) => ({
|
||||
id: r.id,
|
||||
account: r.account,
|
||||
repliesCount: r.repliesCount,
|
||||
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;
|
||||
}, [statuses.length, limit]);
|
||||
|
||||
const hasManyStatuses = statuses.length > LIMIT;
|
||||
const hasManyStatuses = statuses.length > THREAD_LIMIT;
|
||||
const hasDescendants = statuses.some((s) => s.descendant);
|
||||
const ancestors = statuses.filter((s) => s.ancestor);
|
||||
|
||||
|
@ -405,17 +415,6 @@ function StatusPage() {
|
|||
>
|
||||
<header
|
||||
class={`${heroInView ? 'inview' : ''}`}
|
||||
onClick={(e) => {
|
||||
if (
|
||||
!/^(a|button)$/i.test(e.target.tagName) &&
|
||||
heroStatusRef.current
|
||||
) {
|
||||
heroStatusRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDblClick={(e) => {
|
||||
// reload statuses
|
||||
states.reloadStatusPage++;
|
||||
|
@ -428,23 +427,34 @@ function StatusPage() {
|
|||
</div> */}
|
||||
<h1>
|
||||
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
||||
<span class="hero-heading">
|
||||
{!!heroPointer && (
|
||||
<>
|
||||
<Icon
|
||||
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
<NameText showAvatar account={heroStatus.account} short />{' '}
|
||||
<span class="insignificant">
|
||||
•{' '}
|
||||
<RelativeTime
|
||||
datetime={heroStatus.createdAt}
|
||||
format="micro"
|
||||
<>
|
||||
<span class="hero-heading">
|
||||
<NameText showAvatar account={heroStatus.account} 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'}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Status{' '}
|
||||
|
@ -551,24 +561,22 @@ function StatusPage() {
|
|||
withinContext
|
||||
size={thread || ancestor ? 'm' : 's'}
|
||||
/>
|
||||
{replies?.length > LIMIT && (
|
||||
{/* {replies?.length > LIMIT && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
<span title={replies.length}>
|
||||
{shortenNumber(replies.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</Link>
|
||||
)}
|
||||
{descendant &&
|
||||
replies?.length > 0 &&
|
||||
replies?.length <= LIMIT && (
|
||||
<SubComments
|
||||
hasManyStatuses={hasManyStatuses}
|
||||
replies={replies}
|
||||
/>
|
||||
)}
|
||||
{descendant && replies?.length > 0 && (
|
||||
<SubComments
|
||||
hasManyStatuses={hasManyStatuses}
|
||||
replies={replies}
|
||||
/>
|
||||
)}
|
||||
{uiState === 'loading' &&
|
||||
isHero &&
|
||||
!!heroStatus?.repliesCount &&
|
||||
|
@ -658,13 +666,31 @@ function SubComments({ hasManyStatuses, replies }) {
|
|||
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;
|
||||
|
||||
return (
|
||||
<details class="replies" open={open}>
|
||||
<summary hidden={open}>
|
||||
<span title={replies.length}>{shortenNumber(replies.length)}</span> repl
|
||||
{replies.length === 1 ? 'y' : 'ies'}
|
||||
<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>
|
||||
</summary>
|
||||
<ul>
|
||||
{replies.map((r) => (
|
||||
|
@ -677,15 +703,21 @@ function SubComments({ hasManyStatuses, replies }) {
|
|||
}}
|
||||
>
|
||||
<Status statusID={r.id} withinContext size="s" />
|
||||
{r.repliesCount > 0 && (
|
||||
{/* {r.repliesCount > 0 && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
<span title={r.repliesCount}>
|
||||
{shortenNumber(r.repliesCount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</Link>
|
||||
{r.replies?.length && (
|
||||
<SubComments
|
||||
hasManyStatuses={hasManyStatuses}
|
||||
replies={r.replies}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
Loading…
Reference in a new issue