adjust thread visual

This commit is contained in:
thislight 2024-10-12 19:48:34 +08:00
parent 2b2b5ea1ae
commit 295a905b8b
5 changed files with 131 additions and 64 deletions

View file

@ -190,7 +190,7 @@ function checkOrCreatePager(
newDirection: TimelineFetchDirection, newDirection: TimelineFetchDirection,
) { ) {
if (!lastPager) { if (!lastPager) {
return timeline.list({ }).setDirection(newDirection); return timeline.list({}).setDirection(newDirection);
} else { } else {
let pager = lastPager; let pager = lastPager;
if (pager.getDirection() !== newDirection) { if (pager.getDirection() !== newDirection) {
@ -261,6 +261,7 @@ export function createTimeline(
if (!chk) { if (!chk) {
return; return;
} }
console.debug("fetched chunk", chk); console.debug("fetched chunk", chk);
batch(() => { batch(() => {

View file

@ -270,7 +270,9 @@ const Home: ParentComponent = (props) => {
</TimeSourceProvider> </TimeSourceProvider>
<Suspense> <Suspense>
<HeroSourceProvider value={[heroSrc, setHeroSrc]}> <HeroSourceProvider value={[heroSrc, setHeroSrc]}>
<BottomSheet open={!!child()}>{child()}</BottomSheet> <BottomSheet open={!!child()} onClose={() => navigate(-1)}>
{child()}
</BottomSheet>
</HeroSourceProvider> </HeroSourceProvider>
</Suspense> </Suspense>
</Scaffold> </Scaffold>

View file

@ -111,13 +111,17 @@ type TootActionGroupProps<T extends mastodon.v1.Status> = {
onRetoot?: (value: T) => void; onRetoot?: (value: T) => void;
onFavourite?: (value: T) => void; onFavourite?: (value: T) => void;
onBookmark?: (value: T) => void; onBookmark?: (value: T) => void;
onReply?: (value: T) => void; onReply?: (
value: T,
event: MouseEvent & { currentTarget: HTMLButtonElement },
) => void;
}; };
type TootCardProps = { type TootCardProps = {
status: mastodon.v1.Status; status: mastodon.v1.Status;
actionable?: boolean; actionable?: boolean;
evaluated?: boolean; evaluated?: boolean;
thread?: "top" | "bottom" | "middle";
} & TootActionGroupProps<mastodon.v1.Status> & } & TootActionGroupProps<mastodon.v1.Status> &
JSX.HTMLElementTags["article"]; JSX.HTMLElementTags["article"];
@ -125,19 +129,40 @@ function isolatedCallback(e: MouseEvent) {
e.stopPropagation(); e.stopPropagation();
} }
export function findRootToot(element: HTMLElement) {
let current: HTMLElement | null = element;
while (current && !current.classList.contains(tootStyle.toot)) {
current = current.parentElement;
}
if (!current) {
throw Error(
`the element must be placed under a element with ${tootStyle.toot}`,
);
}
return current;
}
function TootActionGroup<T extends mastodon.v1.Status>( function TootActionGroup<T extends mastodon.v1.Status>(
props: TootActionGroupProps<T> & { value: T }, props: TootActionGroupProps<T> & { value: T },
) { ) {
let actGrpElement: HTMLDivElement;
const toot = () => props.value; const toot = () => props.value;
return ( return (
<div class={tootStyle.tootBottomActionGrp} onClick={isolatedCallback}> <div
<Button ref={actGrpElement!}
class={tootStyle.tootActionWithCount} class={tootStyle.tootBottomActionGrp}
onClick={() => props.onReply?.(toot())} onClick={isolatedCallback}
> >
<ReplyAll /> <Show when={props.onReply}>
<span>{toot().repliesCount}</span> <Button
</Button> class={tootStyle.tootActionWithCount}
onClick={[props.onReply!, props.value]}
>
<ReplyAll />
<span>{toot().repliesCount}</span>
</Button>
</Show>
<Button <Button
class={tootStyle.tootActionWithCount} class={tootStyle.tootActionWithCount}
style={{ style={{
@ -288,7 +313,7 @@ const RegularToot: Component<TootCardProps> = (props) => {
let rootRef: HTMLElement; let rootRef: HTMLElement;
const [managed, managedActionGroup, rest] = splitProps( const [managed, managedActionGroup, rest] = splitProps(
props, props,
["status", "lang", "class", "actionable", "evaluated"], ["status", "lang", "class", "actionable", "evaluated", "thread"],
["onRetoot", "onFavourite", "onBookmark", "onReply"], ["onRetoot", "onFavourite", "onBookmark", "onReply"],
); );
const now = useTimeSource(); const now = useTimeSource();
@ -300,6 +325,42 @@ const RegularToot: Component<TootCardProps> = (props) => {
margin-left: calc(var(--toot-avatar-size) + var(--card-pad) + 8px); margin-left: calc(var(--toot-avatar-size) + var(--card-pad) + 8px);
margin-block: 8px; margin-block: 8px;
} }
.thread-top,
.thread-mid,
.thread-btm {
position: relative;
&::before {
content: "";
position: absolute;
left: 36px;
background-color: var(--tutu-color-secondary);
width: 2px;
display: block;
}
}
.thread-mid {
&::before {
top: 0;
bottom: 0;
}
}
.thread-top {
&::before {
top: 16px;
bottom: 0;
}
}
.thread-btm {
&::before {
top: 0;
height: 16px;
}
}
`; `;
return ( return (
@ -308,6 +369,9 @@ const RegularToot: Component<TootCardProps> = (props) => {
classList={{ classList={{
[tootStyle.toot]: true, [tootStyle.toot]: true,
[tootStyle.expanded]: managed.evaluated, [tootStyle.expanded]: managed.evaluated,
"thread-top": managed.thread === "top",
"thread-mid": managed.thread === "middle",
"thread-btm": managed.thread === "bottom",
[managed.class || ""]: true, [managed.class || ""]: true,
}} }}
ref={rootRef!} ref={rootRef!}

View file

@ -9,14 +9,19 @@ import {
} from "solid-js"; } from "solid-js";
import CompactToot from "./CompactToot"; import CompactToot from "./CompactToot";
import { useTimeSource } from "../platform/timesrc"; import { useTimeSource } from "../platform/timesrc";
import RegularToot from "./RegularToot"; import RegularToot, { findRootToot } from "./RegularToot";
import cardStyle from "../material/cards.module.css"; import cardStyle from "../material/cards.module.css";
import { css } from "solid-styled"; import { css } from "solid-styled";
type TootActionTarget = {
client: mastodon.rest.Client;
status: mastodon.v1.Status;
};
type TootActions = { type TootActions = {
onBoost(client: mastodon.rest.Client, status: mastodon.v1.Status): void; onBoost(client: mastodon.rest.Client, status: mastodon.v1.Status): void;
onBookmark(client: mastodon.rest.Client, status: mastodon.v1.Status): void; onBookmark(client: mastodon.rest.Client, status: mastodon.v1.Status): void;
onReply(client: mastodon.rest.Client, status: mastodon.v1.Status): void; onReply(target: TootActionTarget, element: HTMLElement): void;
}; };
type ThreadProps = { type ThreadProps = {
@ -37,55 +42,48 @@ const Thread: Component<ThreadProps> = (props) => {
props.onBookmark(props.client, status); props.onBookmark(props.client, status);
}; };
const reply = (status: mastodon.v1.Status) => { const reply = (
props.onReply(props.client, status); status: mastodon.v1.Status,
event: MouseEvent & { currentTarget: HTMLElement },
) => {
const element = findRootToot(event.currentTarget);
props.onReply({ client: props.client, status }, element);
}; };
css` css`
article { .thread {
transition: user-select: none;
margin 90ms var(--tutu-anim-curve-sharp), cursor: pointer;
var(--tutu-transition-shadow); }
user-select: none; `
cursor: pointer;
}
.thread-line {
position: relative;
&::before {
content: "";
position: absolute;
left: 36px;
top: 16px;
bottom: 0;
background-color: var(--tutu-color-secondary);
width: 2px;
display: block;
}
}
`;
return ( return (
<article <article ref={props.ref} class="thread">
ref={props.ref}
classList={{
"thread-line": props.toots.length > 1,
}}
>
<For each={props.toots}> <For each={props.toots}>
{(status, index) => ( {(status, index) => {
<RegularToot const useThread = props.toots.length > 1;
data-status-id={status.id} const threadPosition = useThread
data-thread-sort={index()} ? index() === 0
status={status} ? "top"
class={`${cardStyle.card}`} : index() === props.toots.length - 1
evaluated={props.isExpended(status)} ? "bottom"
actionable={props.isExpended(status)} : "middle"
onBookmark={(s) => bookmark(s)} : undefined;
onRetoot={(s) => boost(s)} return (
onReply={(s) => reply(s)} <RegularToot
onClick={[props.onItemClick, status]} data-status-id={status.id}
/> data-thread-sort={index()}
)} status={status}
thread={threadPosition}
class={cardStyle.card}
evaluated={props.isExpended(status)}
actionable={props.isExpended(status)}
onBookmark={(s) => bookmark(s)}
onRetoot={(s) => boost(s)}
onReply={reply}
onClick={[props.onItemClick, status]}
/>
);
}}
</For> </For>
</article> </article>
); );

View file

@ -113,27 +113,29 @@ const TimelinePanel: Component<{
</Show> </Show>
<For each={timeline.list}> <For each={timeline.list}>
{(itemId, index) => { {(itemId, index) => {
let element: HTMLElement | undefined;
const path = timeline.getPath(itemId)!; const path = timeline.getPath(itemId)!;
const toots = path.reverse().map((x) => x.value); const toots = path.reverse().map((x) => x.value);
return ( return (
<Thread <Thread
ref={element}
toots={toots} toots={toots}
onBoost={onBoost} onBoost={onBoost}
onBookmark={onBookmark} onBookmark={onBookmark}
onReply={(client, status) => onReply={
props.openFullScreenToot(status, element, true) ({ status }, element) =>
props.openFullScreenToot(status, element, true)
} }
client={props.client} client={props.client}
isExpended={(status) => status.id === expandedThreadId()} isExpended={(status) => status.id === expandedThreadId()}
onItemClick={(status) => { onItemClick={(status, event) => {
setTyping(false); setTyping(false);
if (status.id !== expandedThreadId()) { if (status.id !== expandedThreadId()) {
setExpandedThreadId((x) => (x ? undefined : status.id)); setExpandedThreadId((x) => (x ? undefined : status.id));
} else { } else {
props.openFullScreenToot(status, element); props.openFullScreenToot(
status,
event.currentTarget as HTMLElement,
);
} }
}} }}
/> />