Compare commits

...

6 commits

Author SHA1 Message Date
thislight
e7410c7296
Profile: pinned toots
All checks were successful
/ depoly (push) Successful in 1m26s
2024-10-31 00:18:47 +08:00
thislight
dbddaae05c
Profile: sticky filter bar 2024-10-31 00:00:07 +08:00
thislight
3e3910bfe1
material/theme: use color with alpha for shadow 2024-10-30 23:59:15 +08:00
thislight
404ad1e678
BoostIcon: fix color 2024-10-30 23:40:03 +08:00
thislight
b55618664c
masto/timelines: better documentation 2024-10-30 23:27:30 +08:00
thislight
d88921f245
TootBottomSheet: catch fetch error 2024-10-30 23:00:54 +08:00
9 changed files with 135 additions and 56 deletions

View file

@ -7,6 +7,7 @@ import {
createEffect, createEffect,
createResource, createResource,
untrack, untrack,
type Resource,
type ResourceFetcherInfo, type ResourceFetcherInfo,
} from "solid-js"; } from "solid-js";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
@ -19,7 +20,7 @@ type TimelineParamsOf<T> = T extends Timeline<infer P> ? P : never;
export function createTimelineControlsForArray( export function createTimelineControlsForArray(
status: () => mastodon.v1.Status[] | undefined, status: () => mastodon.v1.Status[] | undefined,
) { ): TimelineControls {
const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>(); const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>();
const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]);
@ -84,11 +85,14 @@ export function createTimelineControlsForArray(
export function createTimelineSnapshot< export function createTimelineSnapshot<
T extends Timeline<mastodon.DefaultPaginationParams>, T extends Timeline<mastodon.DefaultPaginationParams>,
>(timeline: Accessor<T>, limit: Accessor<number>) { >(
timeline: Accessor<T>,
params: Accessor<TimelineParamsOf<T>>,
): TimelineResource<mastodon.v1.Status[] | undefined> {
const [shot, { refetch }] = createResource( const [shot, { refetch }] = createResource(
() => [timeline(), limit()] as const, () => [timeline(), params()] as const,
async ([tl, limit]) => { async ([tl, limit]) => {
const ls = await tl.list({ limit }).next(); const ls = await tl.list(limit).next();
return ls.value; return ls.value;
}, },
); );
@ -198,9 +202,51 @@ function createTimelineChunk<
); );
} }
export type TimelineControls = {
/**
* The threads.
*
* The identifiers here is the most-bottom toot id in the thread.
*
* @see You can use {@link TimelineControls.get} and {@link TimelineControls.getPath} to resolve them if
* the context is needed.
*/
list: readonly mastodon.v1.Status["id"][];
/**
* Get the single node.
*/
get(id: string): TreeNode<mastodon.v1.Status> | undefined;
/**
* Collect the path from the node to the most-top node.
*/
getPath(id: string): TreeNode<mastodon.v1.Status>[] | undefined;
/**
* Set the node value.
*/
set(id: string, value: mastodon.v1.Status): void;
};
export type TimelineResource<
R,
> = [
TimelineControls,
Resource<R>,
{ refetch(info?: TimelineFetchDirection): void },
];
/**
* Create auto managed timeline controls.
*
* The error from the resource is not thrown in the
* {@link TimelineControls.list} and {@link TimelineControls}.get*.
* Use the second value from {@link TimelineResource} to catch the error.
*/
export function createTimeline< export function createTimeline<
T extends Timeline<mastodon.DefaultPaginationParams>, T extends Timeline<mastodon.DefaultPaginationParams>,
>(timeline: Accessor<T>, params: Accessor<TimelineParamsOf<T>>) { >(
timeline: Accessor<T>,
params: Accessor<TimelineParamsOf<T>>,
): TimelineResource<TimelineChunk<TimelineParamsOf<T>> | undefined> {
const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>(); const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>();
const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]);

View file

@ -82,25 +82,27 @@
--tutu-color-error-on-surface: #d32f2f; --tutu-color-error-on-surface: #d32f2f;
--tutu-color-inactive-on-surface: #757575; --tutu-color-inactive-on-surface: #757575;
--tutu-shadow-e1: 0px 1px 2px 0px #9e9e9e; --tutu-color-shadow: rgba(0, 0, 0, 0.15);
--tutu-shadow-e1: 0px 1px 2px 0px var(--tutu-color-shadow);
/* Switch */ /* Switch */
--tutu-shadow-e2: 0px 2px 4px 0px #9e9e9e; --tutu-shadow-e2: 0px 2px 4px 0px var(--tutu-color-shadow);
/* (Resting) cards, raised button, quick entry / search bar */ /* (Resting) cards, raised button, quick entry / search bar */
--tutu-shadow-e3: 0px 3px 6px 0px #9e9e9e; --tutu-shadow-e3: 0px 3px 6px 0px var(--tutu-color-shadow);
/* Refresh indicator, quick entry / search bar (scrolled) */ /* Refresh indicator, quick entry / search bar (scrolled) */
--tutu-shadow-e4: 0px 4px 8px 0px #9e9e9e; --tutu-shadow-e4: 0px 4px 8px 0px var(--tutu-color-shadow);
/* App bar */ /* App bar */
--tutu-shadow-e6: 0px 6px 12px 0px #9e9e9e; --tutu-shadow-e6: 0px 6px 12px 0px var(--tutu-color-shadow);
/* Snack bar, FAB (resting) */ /* Snack bar, FAB (resting) */
--tutu-shadow-e8: 0px 8px 16px 0px #9e9e9e; --tutu-shadow-e8: 0px 8px 16px 0px var(--tutu-color-shadow);
/* Menu, (picked-up) cards, (pressed) raise button */ /* Menu, (picked-up) cards, (pressed) raise button */
--tutu-shadow-e9: 0px 9px 18px 0px #9e9e9e; --tutu-shadow-e9: 0px 9px 18px 0px var(--tutu-color-shadow);
/* Submenu (+1dp for each submenu) */ /* Submenu (+1dp for each submenu) */
--tutu-shadow-e12: 0px 12px 24px 0px #9e9e9e; --tutu-shadow-e12: 0px 12px 24px 0px var(--tutu-color-shadow);
/* (pressed) FAB */ /* (pressed) FAB */
--tutu-shadow-e16: 0px 16px 32px 0px #9e9e9e; --tutu-shadow-e16: 0px 16px 32px 0px var(--tutu-color-shadow);
/* Nav drawer, right drawer, modal bottom sheet */ /* Nav drawer, right drawer, modal bottom sheet */
--tutu-shadow-e24: 0px 24px 48px 0px #9e9e9e; --tutu-shadow-e24: 0px 24px 48px 0px var(--tutu-color-shadow);
/* Dialog, picker */ /* Dialog, picker */
--tutu-anim-curve-std: cubic-bezier(0.4, 0, 0.2, 1); --tutu-anim-curve-std: cubic-bezier(0.4, 0, 0.2, 1);

View file

@ -39,7 +39,7 @@ import { resolveCustomEmoji } from "../masto/toot";
import { FastAverageColor } from "fast-average-color"; import { FastAverageColor } from "fast-average-color";
import { useWindowSize } from "@solid-primitives/resize-observer"; import { useWindowSize } from "@solid-primitives/resize-observer";
import { css } from "solid-styled"; import { css } from "solid-styled";
import { createTimeline } from "../masto/timelines"; import { createTimeline, createTimelineSnapshot } from "../masto/timelines";
import TootList from "../timelines/TootList"; import TootList from "../timelines/TootList";
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
import TootFilterButton from "./TootFilterButton"; import TootFilterButton from "./TootFilterButton";
@ -91,6 +91,7 @@ const Profile: Component = () => {
}); });
const [recentTootFilter, setRecentTootFilter] = createSignal({ const [recentTootFilter, setRecentTootFilter] = createSignal({
pinned: true,
boost: false, boost: false,
reply: true, reply: true,
original: true, original: true,
@ -105,6 +106,13 @@ const Profile: Component = () => {
}, },
); );
const [pinnedToots, pinnedTootChunk] = createTimelineSnapshot(
() => session().client.v1.accounts.$select(params.id).statuses,
() => {
return { limit: 20, pinned: true };
},
);
const bannerImg = () => profile()?.header; const bannerImg = () => profile()?.header;
const avatarImg = () => profile()?.avatar; const avatarImg = () => profile()?.avatar;
const displayName = () => const displayName = () =>
@ -112,6 +120,10 @@ const Profile: Component = () => {
const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name
const description = () => profile()?.note; const description = () => profile()?.note;
const isTootListLoading = () =>
recentTootChunk.loading ||
(recentTootFilter().pinned && pinnedTootChunk.loading);
css` css`
.intro { .intro {
background-color: var(--tutu-color-surface-d); background-color: var(--tutu-color-surface-d);
@ -177,6 +189,22 @@ const Profile: Component = () => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.toot-list-toolbar {
position: sticky;
top: var(--scaffold-topbar-height);
z-index: calc(var(--tutu-zidx-nav, 1) - 1);
background: var(--tutu-color-surface);
border-bottom: 1px solid var(--tutu-color-surface-d);
contain: content;
/* TODO: box-shadow is needed here (same as app bar, e6).
There is no good way to detect if the sticky is "sticked" -
so let's leave it for future.
For now we use a trick to make it looks better.
*/
box-shadow: 0px -2px 4px 0px var(--tutu-color-shadow);
}
`; `;
return ( return (
@ -356,9 +384,10 @@ const Profile: Component = () => {
</table> </table>
</div> </div>
<div> <div class="toot-list-toolbar">
<TootFilterButton <TootFilterButton
options={{ options={{
pinned: "Pinneds",
boost: "Boosts", boost: "Boosts",
reply: "Replies", reply: "Replies",
original: "Originals", original: "Originals",
@ -370,6 +399,14 @@ const Profile: Component = () => {
</div> </div>
<TimeSourceProvider value={time}> <TimeSourceProvider value={time}>
<Show when={recentTootFilter().pinned}>
<TootList
threads={pinnedToots.list}
onUnknownThread={pinnedToots.getPath}
onChangeToot={pinnedToots.set}
/>
<Divider />
</Show>
<TootList <TootList
threads={recentToots.list} threads={recentToots.list}
onUnknownThread={recentToots.getPath} onUnknownThread={recentToots.getPath}
@ -389,9 +426,9 @@ const Profile: Component = () => {
size="large" size="large"
color="primary" color="primary"
onClick={[refetchRecentToots, "prev"]} onClick={[refetchRecentToots, "prev"]}
disabled={recentTootChunk.loading} disabled={isTootListLoading()}
> >
<Show when={recentTootChunk.loading} fallback={<ExpandMore />}> <Show when={isTootListLoading()} fallback={<ExpandMore />}>
<CircularProgress sx={{ width: "24px", height: "24px" }} /> <CircularProgress sx={{ width: "24px", height: "24px" }} />
</Show> </Show>
</IconButton> </IconButton>

View file

@ -1,5 +1,6 @@
import { useLocation, useNavigate, useParams } from "@solidjs/router"; import { useLocation, useNavigate, useParams } from "@solidjs/router";
import { import {
catchError,
createEffect, createEffect,
createRenderEffect, createRenderEffect,
createResource, createResource,
@ -42,7 +43,6 @@ function getCache(acct: string, id: string) {
const TootBottomSheet: Component = (props) => { const TootBottomSheet: Component = (props) => {
const params = useParams<{ acct: string; id: string }>(); const params = useParams<{ acct: string; id: string }>();
const location = useLocation<{ const location = useLocation<{
tootBottomSheetPushedCount?: number;
tootReply?: boolean; tootReply?: boolean;
}>(); }>();
const navigate = useNavigate(); const navigate = useNavigate();
@ -51,10 +51,6 @@ const TootBottomSheet: Component = (props) => {
const acctText = () => decodeURIComponent(params.acct); const acctText = () => decodeURIComponent(params.acct);
const session = useSessionForAcctStr(acctText); const session = useSessionForAcctStr(acctText);
const pushedCount = () => {
return location.state?.tootBottomSheetPushedCount || 0;
};
const [remoteToot, { mutate: setRemoteToot }] = createResource( const [remoteToot, { mutate: setRemoteToot }] = createResource(
() => [session().client, params.id] as const, () => [session().client, params.id] as const,
async ([client, id]) => { async ([client, id]) => {
@ -62,7 +58,9 @@ const TootBottomSheet: Component = (props) => {
}, },
); );
const toot = () => remoteToot() ?? getCache(acctText(), params.id); const toot = () => catchError(remoteToot, (error) => {
console.error(error)
}) ?? getCache(acctText(), params.id);
createEffect((lastTootId?: string) => { createEffect((lastTootId?: string) => {
const tootId = toot()?.id; const tootId = toot()?.id;
@ -78,13 +76,19 @@ const TootBottomSheet: Component = (props) => {
} }
}); });
const [tootContext, { refetch: refetchContext }] = createResource( const [tootContextErrorUncaught, { refetch: refetchContext }] =
createResource(
() => [session().client, params.id] as const, () => [session().client, params.id] as const,
async ([client, id]) => { async ([client, id]) => {
return await client.v1.statuses.$select(id).context.fetch(); return await client.v1.statuses.$select(id).context.fetch();
}, },
); );
const tootContext = () =>
catchError(tootContextErrorUncaught, (error) => {
console.error(error);
});
const ancestors = createTimelineControlsForArray( const ancestors = createTimelineControlsForArray(
() => tootContext()?.ancestors, () => tootContext()?.ancestors,
); );
@ -160,19 +164,6 @@ const TootBottomSheet: Component = (props) => {
setRemoteToot(result); setRemoteToot(result);
}; };
const switchContext = (status: mastodon.v1.Status) => {
if (isInTyping()) {
setInTyping(false);
return;
}
setCache(params.acct, status);
navigate(`/${params.acct}/toot/${status.id}`, {
state: {
tootBottomSheetPushedCount: pushedCount() + 1,
},
});
};
const defaultMentions = () => { const defaultMentions = () => {
const tootAcct = remoteToot()?.reblog?.account ?? remoteToot()?.account; const tootAcct = remoteToot()?.reblog?.account ?? remoteToot()?.account;
if (!tootAcct) { if (!tootAcct) {
@ -246,7 +237,7 @@ const TootBottomSheet: Component = (props) => {
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
> >
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple> <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
{pushedCount() > 0 ? <BackIcon /> : <CloseIcon />} <CloseIcon />
</IconButton> </IconButton>
<Title component="div" class="name" use:solid-styled> <Title component="div" class="name" use:solid-styled>
<span <span
@ -302,7 +293,7 @@ const TootBottomSheet: Component = (props) => {
/> />
</Show> </Show>
<Show when={tootContext.loading}> <Show when={tootContextErrorUncaught.loading}>
<div <div
style={{ style={{
display: "flex", display: "flex",

View file

@ -18,7 +18,7 @@ import { findElementActionable } from "./RegularToot";
const TootList: Component<{ const TootList: Component<{
ref?: Ref<HTMLDivElement>; ref?: Ref<HTMLDivElement>;
threads: string[]; threads: readonly string[];
onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined;
onChangeToot: (id: string, value: mastodon.v1.Status) => void; onChangeToot: (id: string, value: mastodon.v1.Status) => void;
}> = (props) => { }> = (props) => {

View file

@ -28,7 +28,7 @@ const TrendTimelinePanel: Component<{
const [timeline, snapshot, { refetch: refetchTimeline }] = const [timeline, snapshot, { refetch: refetchTimeline }] =
createTimelineSnapshot( createTimelineSnapshot(
() => props.client.v1.trends.statuses, () => props.client.v1.trends.statuses,
() => 120, () => ({ limit: 120 }),
); );
return ( return (
@ -40,7 +40,7 @@ const TrendTimelinePanel: Component<{
<PullDownToRefresh <PullDownToRefresh
linkedElement={scrollLinked()} linkedElement={scrollLinked()}
loading={snapshot.loading} loading={snapshot.loading}
onRefresh={() => refetchTimeline({ direction: "new" })} onRefresh={() => refetchTimeline("next")}
/> />
<div <div
ref={(e) => ref={(e) =>

View file

@ -1,11 +1,13 @@
.icon__boost { .BoostIcon {
padding: 0; display: inline-flex;
display: inline-block;
border-radius: 2px; border-radius: 2px;
background-color: green;
padding: 0.125em;
align-items: center;
> :global(svg) { > svg {
color: green; color: white;
font-size: 1rem; font-size: 1em;
vertical-align: middle; vertical-align: middle;
} }
} }

View file

@ -13,7 +13,7 @@ import "./BoostIcon.css";
const BoostIcon: Component<JSX.HTMLElementTags["i"]> = (props) => { const BoostIcon: Component<JSX.HTMLElementTags["i"]> = (props) => {
const [managed, rest] = splitProps(props, ["class"]); const [managed, rest] = splitProps(props, ["class"]);
return ( return (
<i class={["icon__boost", managed.class].join(" ")} {...rest}> <i class={["BoostIcon", managed.class].join(" ")} {...rest}>
<Repeat /> <Repeat />
</i> </i>
); );

View file

@ -222,6 +222,7 @@
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
align-items: center;
} }
.tootAttachmentGrp { .tootAttachmentGrp {