TootList: mentions in toot links to profile page
All checks were successful
/ depoly (push) Successful in 1m17s
All checks were successful
/ depoly (push) Successful in 1m17s
- bug: main toot in TootBottomSheet does not link profile
This commit is contained in:
parent
92cc451811
commit
31b27237cd
5 changed files with 165 additions and 91 deletions
|
@ -17,22 +17,15 @@ type Timeline<T extends mastodon.DefaultPaginationParams> = {
|
|||
|
||||
type TimelineParamsOf<T> = T extends Timeline<infer P> ? P : never;
|
||||
|
||||
export function createTimelineSnapshot<
|
||||
T extends Timeline<mastodon.DefaultPaginationParams>,
|
||||
>(timeline: Accessor<T>, limit: Accessor<number>) {
|
||||
export function createTimelineControlsForArray(
|
||||
status: () => mastodon.v1.Status[] | undefined,
|
||||
) {
|
||||
const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>();
|
||||
const [shot, { refetch }] = createResource(
|
||||
() => [timeline(), limit()] as const,
|
||||
async ([tl, limit]) => {
|
||||
const ls = await tl.list({ limit }).next();
|
||||
return ls.value;
|
||||
},
|
||||
);
|
||||
|
||||
const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]);
|
||||
|
||||
createEffect(() => {
|
||||
const nls = catchError(shot, (e) => console.error(e));
|
||||
const nls = status();
|
||||
if (!nls) return;
|
||||
|
||||
setThreads([]);
|
||||
|
@ -70,24 +63,40 @@ export function createTimelineSnapshot<
|
|||
setThreads(newThreads);
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
list: threads,
|
||||
get(id: string) {
|
||||
return lookup.get(id);
|
||||
},
|
||||
getPath(id: string) {
|
||||
const node = lookup.get(id);
|
||||
if (!node) return;
|
||||
return collectPath(node);
|
||||
},
|
||||
set(id: string, value: mastodon.v1.Status) {
|
||||
const node = untrack(() => lookup.get(id));
|
||||
if (!node) return;
|
||||
node.value = value;
|
||||
lookup.set(id, node);
|
||||
},
|
||||
return {
|
||||
list: threads,
|
||||
get(id: string) {
|
||||
return lookup.get(id);
|
||||
},
|
||||
getPath(id: string) {
|
||||
const node = lookup.get(id);
|
||||
if (!node) return;
|
||||
return collectPath(node);
|
||||
},
|
||||
set(id: string, value: mastodon.v1.Status) {
|
||||
const node = untrack(() => lookup.get(id));
|
||||
if (!node) return;
|
||||
node.value = value;
|
||||
lookup.set(id, node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createTimelineSnapshot<
|
||||
T extends Timeline<mastodon.DefaultPaginationParams>,
|
||||
>(timeline: Accessor<T>, limit: Accessor<number>) {
|
||||
const [shot, { refetch }] = createResource(
|
||||
() => [timeline(), limit()] as const,
|
||||
async ([tl, limit]) => {
|
||||
const ls = await tl.list({ limit }).next();
|
||||
return ls.value;
|
||||
},
|
||||
);
|
||||
|
||||
const controls = createTimelineControlsForArray(shot);
|
||||
|
||||
return [
|
||||
controls,
|
||||
shot,
|
||||
{
|
||||
refetch,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
catchError,
|
||||
createRenderEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
|
@ -77,13 +78,18 @@ const Profile: Component = () => {
|
|||
);
|
||||
onCleanup(() => obx.disconnect());
|
||||
|
||||
const [profile] = createResource(
|
||||
const [profileErrorUncaught] = createResource(
|
||||
() => [session().client, params.id] as const,
|
||||
async ([client, id]) => {
|
||||
return await client.v1.accounts.$select(id).fetch();
|
||||
},
|
||||
);
|
||||
|
||||
const profile = () =>
|
||||
catchError(profileErrorUncaught, (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
const [recentTootFilter, setRecentTootFilter] = createSignal({
|
||||
boost: false,
|
||||
reply: true,
|
||||
|
@ -271,7 +277,7 @@ const Profile: Component = () => {
|
|||
ref={(e) => obx.observe(e)}
|
||||
src={bannerImg()}
|
||||
style={{
|
||||
"object-fit": "contain",
|
||||
"object-fit": "cover",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
|
|
|
@ -5,20 +5,13 @@ import {
|
|||
type JSX,
|
||||
Show,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
createEffect,
|
||||
type Accessor,
|
||||
createMemo,
|
||||
} from "solid-js";
|
||||
import tootStyle from "./toot.module.css";
|
||||
import { formatRelative } from "date-fns";
|
||||
import Img from "../material/Img.js";
|
||||
import {
|
||||
Body1,
|
||||
Body2,
|
||||
Caption,
|
||||
Subheading,
|
||||
Title,
|
||||
} from "../material/typography.js";
|
||||
import { Body1, Body2, Title } from "../material/typography.js";
|
||||
import { css } from "solid-styled";
|
||||
import {
|
||||
BookmarkAddOutlined,
|
||||
|
@ -32,7 +25,7 @@ import {
|
|||
} from "@suid/icons-material";
|
||||
import { useTimeSource } from "../platform/timesrc.js";
|
||||
import { resolveCustomEmoji } from "../masto/toot.js";
|
||||
import { Divider, IconButton } from "@suid/material";
|
||||
import { Divider } from "@suid/material";
|
||||
import cardStyle from "../material/cards.module.css";
|
||||
import Button from "../material/Button.js";
|
||||
import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
|
||||
|
@ -43,13 +36,24 @@ import { canShare, share } from "../platform/share";
|
|||
import { makeAcctText, useDefaultSession } from "../masto/clients";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
type TootContentViewProps = {
|
||||
source?: string;
|
||||
emojis?: mastodon.v1.CustomEmoji[];
|
||||
mentions: mastodon.v1.StatusMention[];
|
||||
} & JSX.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const TootContentView: Component<TootContentViewProps> = (props) => {
|
||||
const [managed, rest] = splitProps(props, ["source", "emojis"]);
|
||||
const session = useDefaultSession();
|
||||
const [managed, rest] = splitProps(props, ["source", "emojis", "mentions"]);
|
||||
|
||||
const clientFinder = createMemo(() =>
|
||||
session() ? makeAcctText(session()!) : undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(ref) => {
|
||||
|
@ -60,6 +64,21 @@ const TootContentView: Component<TootContentViewProps> = (props) => {
|
|||
: managed.source
|
||||
: "";
|
||||
});
|
||||
|
||||
createRenderEffect(() => {
|
||||
const finder = clientFinder();
|
||||
for (const mention of props.mentions) {
|
||||
const elements = ref.querySelectorAll<HTMLAnchorElement>(
|
||||
`a[href='${mention.url}']`,
|
||||
);
|
||||
for (const e of elements) {
|
||||
e.onclick = preventDefault;
|
||||
e.dataset.rel = "acct";
|
||||
e.dataset.client = finder;
|
||||
e.dataset.acctId = mention.id.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
></div>
|
||||
|
@ -212,16 +231,18 @@ function TootActionGroup<T extends mastodon.v1.Status>(
|
|||
);
|
||||
}
|
||||
|
||||
function TootAuthorGroup(props: {
|
||||
status: mastodon.v1.Status;
|
||||
now: Date;
|
||||
onClick?: JSX.EventHandlerUnion<HTMLDivElement, MouseEvent>;
|
||||
}) {
|
||||
const toot = () => props.status;
|
||||
function TootAuthorGroup(
|
||||
props: {
|
||||
status: mastodon.v1.Status;
|
||||
now: Date;
|
||||
} & JSX.HTMLElementTags["div"],
|
||||
) {
|
||||
const [managed, rest] = splitProps(props, ["status", "now"]);
|
||||
const toot = () => managed.status;
|
||||
const dateFnLocale = useDateFnLocale();
|
||||
|
||||
return (
|
||||
<div class={tootStyle.tootAuthorGrp} onClick={props.onClick}>
|
||||
<div class={tootStyle.tootAuthorGrp} {...rest}>
|
||||
<Img src={toot().account.avatar} class={tootStyle.tootAvatar} />
|
||||
<div class={tootStyle.tootAuthorNameGrp}>
|
||||
<Body2
|
||||
|
@ -236,7 +257,7 @@ function TootAuthorGroup(props: {
|
|||
}}
|
||||
/>
|
||||
<time datetime={toot().createdAt}>
|
||||
{formatRelative(toot().createdAt, props.now, {
|
||||
{formatRelative(toot().createdAt, managed.now, {
|
||||
locale: dateFnLocale(),
|
||||
})}
|
||||
</time>
|
||||
|
@ -431,10 +452,17 @@ const RegularToot: Component<TootCardProps> = (props) => {
|
|||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<TootAuthorGroup status={toot()} now={now()} onClick={openProfile} />
|
||||
<TootAuthorGroup
|
||||
status={toot()}
|
||||
now={now()}
|
||||
data-rel="acct"
|
||||
data-client={session() ? makeAcctText(session()!) : undefined}
|
||||
data-acct-id={toot().account.id}
|
||||
/>
|
||||
<TootContentView
|
||||
source={toot().content}
|
||||
emojis={toot().emojis}
|
||||
mentions={toot().mentions}
|
||||
class={tootStyle.tootContent}
|
||||
/>
|
||||
<Show when={toot().card}>
|
||||
|
|
|
@ -25,6 +25,8 @@ import { vibrate } from "../platform/hardware";
|
|||
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
|
||||
import TootComposer from "./TootComposer";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import { createTimelineControlsForArray } from "../masto/timelines";
|
||||
import TootList from "./TootList";
|
||||
|
||||
let cachedEntry: [string, mastodon.v1.Status] | undefined;
|
||||
|
||||
|
@ -48,7 +50,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
const time = createTimeSource();
|
||||
const [isInTyping, setInTyping] = createSignal(false);
|
||||
const acctText = () => decodeURIComponent(params.acct);
|
||||
const session = useSessionForAcctStr(acctText)
|
||||
const session = useSessionForAcctStr(acctText);
|
||||
|
||||
const pushedCount = () => {
|
||||
return location.state?.tootBottomSheetPushedCount || 0;
|
||||
|
@ -84,20 +86,24 @@ const TootBottomSheet: Component = (props) => {
|
|||
},
|
||||
);
|
||||
|
||||
const ancestors = () => tootContext()?.ancestors ?? [];
|
||||
const descendants = () => tootContext()?.descendants ?? [];
|
||||
const ancestors = createTimelineControlsForArray(
|
||||
() => tootContext()?.ancestors,
|
||||
);
|
||||
const descendants = createTimelineControlsForArray(
|
||||
() => tootContext()?.descendants,
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
if (ancestors().length > 0) {
|
||||
if (ancestors.list.length > 0) {
|
||||
document.querySelector(`#toot-${toot()!.id}`)?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
useDocumentTitle(() => {
|
||||
const t = toot()?.reblog ?? toot()
|
||||
const name = t?.account.displayName ?? "Someone"
|
||||
return `${name}'s toot`
|
||||
})
|
||||
const t = toot()?.reblog ?? toot();
|
||||
const name = t?.account.displayName ?? "Someone";
|
||||
return `${name}'s toot`;
|
||||
});
|
||||
|
||||
const tootDisplayName = () => {
|
||||
const t = toot()?.reblog ?? toot();
|
||||
|
@ -174,7 +180,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const others = ancestors().map((x) => x.account);
|
||||
const others = ancestors.list.map((x) => ancestors.get(x)!.value.account);
|
||||
|
||||
const values = [tootAcct, ...others].map((x) => `@${x.acct}`);
|
||||
return Array.from(new Set(values).keys());
|
||||
|
@ -216,11 +222,7 @@ const TootBottomSheet: Component = (props) => {
|
|||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
{pushedCount() > 0 ? <BackIcon /> : <CloseIcon />}
|
||||
</IconButton>
|
||||
<Title
|
||||
component="div"
|
||||
class="name"
|
||||
use:solid-styled
|
||||
>
|
||||
<Title component="div" class="name" use:solid-styled>
|
||||
<span
|
||||
ref={(e: HTMLElement) =>
|
||||
createRenderEffect(
|
||||
|
@ -235,17 +237,11 @@ const TootBottomSheet: Component = (props) => {
|
|||
}
|
||||
>
|
||||
<TimeSourceProvider value={time}>
|
||||
<For each={ancestors()}>
|
||||
{(item) => (
|
||||
<RegularToot
|
||||
id={`toot-${item.id}`}
|
||||
class={cards.card}
|
||||
status={item}
|
||||
actionable={false}
|
||||
onClick={[switchContext, item]}
|
||||
></RegularToot>
|
||||
)}
|
||||
</For>
|
||||
<TootList
|
||||
threads={ancestors.list}
|
||||
onUnknownThread={ancestors.getPath}
|
||||
onChangeToot={ancestors.set}
|
||||
/>
|
||||
|
||||
<article>
|
||||
<Show when={toot()}>
|
||||
|
@ -291,17 +287,11 @@ const TootBottomSheet: Component = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<For each={descendants()}>
|
||||
{(item) => (
|
||||
<RegularToot
|
||||
id={`toot-${item.id}`}
|
||||
class={cards.card}
|
||||
status={item}
|
||||
actionable={false}
|
||||
onClick={[switchContext, item]}
|
||||
></RegularToot>
|
||||
)}
|
||||
</For>
|
||||
<TootList
|
||||
threads={descendants.list}
|
||||
onUnknownThread={descendants.getPath}
|
||||
onChangeToot={descendants.set}
|
||||
/>
|
||||
</TimeSourceProvider>
|
||||
<div style={{ height: "var(--safe-area-inset-bottom, 0)" }}></div>
|
||||
</Scaffold>
|
||||
|
|
|
@ -9,12 +9,29 @@ import {
|
|||
import { type mastodon } from "masto";
|
||||
import { vibrate } from "../platform/hardware";
|
||||
import Thread from "./Thread.jsx";
|
||||
import { useDefaultSession } from "../masto/clients";
|
||||
import { makeAcctText, useDefaultSession } from "../masto/clients";
|
||||
import { useHeroSource } from "../platform/anim";
|
||||
import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet";
|
||||
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
/**
|
||||
* find bottom-to-top the element with `data-rel`.
|
||||
*/
|
||||
function findElementActionable(
|
||||
element: HTMLElement,
|
||||
top: HTMLElement,
|
||||
): HTMLElement | undefined {
|
||||
let current = element;
|
||||
while (!current.dataset.rel) {
|
||||
if (!current.parentElement || current.parentElement === top) {
|
||||
return undefined;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
const TootList: Component<{
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
threads: string[];
|
||||
|
@ -90,11 +107,35 @@ const TootList: Component<{
|
|||
});
|
||||
};
|
||||
|
||||
const onItemClick = (status: mastodon.v1.Status, event: MouseEvent) => {
|
||||
if (status.id !== expandedThreadId()) {
|
||||
setExpandedThreadId((x) => (x ? undefined : status.id));
|
||||
const onItemClick = (
|
||||
status: mastodon.v1.Status,
|
||||
event: MouseEvent & { target: HTMLElement; currentTarget: HTMLElement },
|
||||
) => {
|
||||
const actionableElement = findElementActionable(
|
||||
event.target,
|
||||
event.currentTarget,
|
||||
);
|
||||
|
||||
if (actionableElement) {
|
||||
if (actionableElement.dataset.rel === "acct") {
|
||||
event.stopPropagation();
|
||||
|
||||
const target = actionableElement as HTMLAnchorElement;
|
||||
|
||||
const acct =
|
||||
encodeURIComponent(target.dataset.client ||
|
||||
`@${new URL(target.href).origin}`);
|
||||
|
||||
navigate(`/${acct}/profile/${target.dataset.acctId}`);
|
||||
} else {
|
||||
console.warn("unknown action", actionableElement.dataset.rel);
|
||||
}
|
||||
} else {
|
||||
openFullScreenToot(status, event.currentTarget as HTMLElement);
|
||||
if (status.id !== expandedThreadId()) {
|
||||
setExpandedThreadId((x) => (x ? undefined : status.id));
|
||||
} else {
|
||||
openFullScreenToot(status, event.currentTarget as HTMLElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue