diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index b8b38b6..1aaefb1 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -7,6 +7,7 @@ import { createEffect, createResource, untrack, + type Resource, type ResourceFetcherInfo, } from "solid-js"; import { createStore } from "solid-js/store"; @@ -19,7 +20,7 @@ type TimelineParamsOf = T extends Timeline ? P : never; export function createTimelineControlsForArray( status: () => mastodon.v1.Status[] | undefined, -) { +): TimelineControls { const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); @@ -84,11 +85,14 @@ export function createTimelineControlsForArray( export function createTimelineSnapshot< T extends Timeline, ->(timeline: Accessor, limit: Accessor) { +>( + timeline: Accessor, + params: Accessor>, +): TimelineResource { const [shot, { refetch }] = createResource( - () => [timeline(), limit()] as const, + () => [timeline(), params()] as const, async ([tl, limit]) => { - const ls = await tl.list({ limit }).next(); + const ls = await tl.list(limit).next(); 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 | undefined; + /** + * Collect the path from the node to the most-top node. + */ + getPath(id: string): TreeNode[] | undefined; + /** + * Set the node value. + */ + set(id: string, value: mastodon.v1.Status): void; +}; + +export type TimelineResource< + R, +> = [ + TimelineControls, + Resource, + { 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< T extends Timeline, ->(timeline: Accessor, params: Accessor>) { +>( + timeline: Accessor, + params: Accessor>, +): TimelineResource> | undefined> { const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); diff --git a/src/material/theme.css b/src/material/theme.css index 1228ffd..f1ed6ed 100644 --- a/src/material/theme.css +++ b/src/material/theme.css @@ -82,25 +82,27 @@ --tutu-color-error-on-surface: #d32f2f; --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 */ - --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 */ - --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) */ - --tutu-shadow-e4: 0px 4px 8px 0px #9e9e9e; + --tutu-shadow-e4: 0px 4px 8px 0px var(--tutu-color-shadow); /* 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) */ - --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 */ - --tutu-shadow-e9: 0px 9px 18px 0px #9e9e9e; + --tutu-shadow-e9: 0px 9px 18px 0px var(--tutu-color-shadow); /* 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 */ - --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 */ - --tutu-shadow-e24: 0px 24px 48px 0px #9e9e9e; + --tutu-shadow-e24: 0px 24px 48px 0px var(--tutu-color-shadow); /* Dialog, picker */ --tutu-anim-curve-std: cubic-bezier(0.4, 0, 0.2, 1); diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index 55b37c6..9015e8b 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -39,7 +39,7 @@ import { resolveCustomEmoji } from "../masto/toot"; import { FastAverageColor } from "fast-average-color"; import { useWindowSize } from "@solid-primitives/resize-observer"; import { css } from "solid-styled"; -import { createTimeline } from "../masto/timelines"; +import { createTimeline, createTimelineSnapshot } from "../masto/timelines"; import TootList from "../timelines/TootList"; import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; import TootFilterButton from "./TootFilterButton"; @@ -91,6 +91,7 @@ const Profile: Component = () => { }); const [recentTootFilter, setRecentTootFilter] = createSignal({ + pinned: true, boost: false, reply: 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 avatarImg = () => profile()?.avatar; const displayName = () => @@ -112,6 +120,10 @@ const Profile: Component = () => { const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name const description = () => profile()?.note; + const isTootListLoading = () => + recentTootChunk.loading || + (recentTootFilter().pinned && pinnedTootChunk.loading); + css` .intro { background-color: var(--tutu-color-surface-d); @@ -177,6 +189,22 @@ const Profile: Component = () => { overflow: hidden; 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 ( @@ -356,9 +384,10 @@ const Profile: Component = () => { -
+
{
+ + + + { size="large" color="primary" onClick={[refetchRecentToots, "prev"]} - disabled={recentTootChunk.loading} + disabled={isTootListLoading()} > - }> + }> diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index 8739366..4cfb16f 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -1,5 +1,6 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"; import { + catchError, createEffect, createRenderEffect, createResource, @@ -42,7 +43,6 @@ function getCache(acct: string, id: string) { const TootBottomSheet: Component = (props) => { const params = useParams<{ acct: string; id: string }>(); const location = useLocation<{ - tootBottomSheetPushedCount?: number; tootReply?: boolean; }>(); const navigate = useNavigate(); @@ -51,10 +51,6 @@ const TootBottomSheet: Component = (props) => { const acctText = () => decodeURIComponent(params.acct); const session = useSessionForAcctStr(acctText); - const pushedCount = () => { - return location.state?.tootBottomSheetPushedCount || 0; - }; - const [remoteToot, { mutate: setRemoteToot }] = createResource( () => [session().client, params.id] as const, 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) => { const tootId = toot()?.id; @@ -78,12 +76,18 @@ const TootBottomSheet: Component = (props) => { } }); - const [tootContext, { refetch: refetchContext }] = createResource( - () => [session().client, params.id] as const, - async ([client, id]) => { - return await client.v1.statuses.$select(id).context.fetch(); - }, - ); + const [tootContextErrorUncaught, { refetch: refetchContext }] = + createResource( + () => [session().client, params.id] as const, + async ([client, id]) => { + return await client.v1.statuses.$select(id).context.fetch(); + }, + ); + + const tootContext = () => + catchError(tootContextErrorUncaught, (error) => { + console.error(error); + }); const ancestors = createTimelineControlsForArray( () => tootContext()?.ancestors, @@ -160,19 +164,6 @@ const TootBottomSheet: Component = (props) => { 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 tootAcct = remoteToot()?.reblog?.account ?? remoteToot()?.account; if (!tootAcct) { @@ -246,7 +237,7 @@ const TootBottomSheet: Component = (props) => { sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} > - {pushedCount() > 0 ? : } + <span @@ -302,7 +293,7 @@ const TootBottomSheet: Component = (props) => { /> </Show> - <Show when={tootContext.loading}> + <Show when={tootContextErrorUncaught.loading}> <div style={{ display: "flex", diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx index 3842d13..7c712b6 100644 --- a/src/timelines/TootList.tsx +++ b/src/timelines/TootList.tsx @@ -18,7 +18,7 @@ import { findElementActionable } from "./RegularToot"; const TootList: Component<{ ref?: Ref<HTMLDivElement>; - threads: string[]; + threads: readonly string[]; onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onChangeToot: (id: string, value: mastodon.v1.Status) => void; }> = (props) => { diff --git a/src/timelines/TrendTimelinePanel.tsx b/src/timelines/TrendTimelinePanel.tsx index 8391e2d..e34edcf 100644 --- a/src/timelines/TrendTimelinePanel.tsx +++ b/src/timelines/TrendTimelinePanel.tsx @@ -28,7 +28,7 @@ const TrendTimelinePanel: Component<{ const [timeline, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot( () => props.client.v1.trends.statuses, - () => 120, + () => ({ limit: 120 }), ); return ( @@ -40,7 +40,7 @@ const TrendTimelinePanel: Component<{ <PullDownToRefresh linkedElement={scrollLinked()} loading={snapshot.loading} - onRefresh={() => refetchTimeline({ direction: "new" })} + onRefresh={() => refetchTimeline("next")} /> <div ref={(e) => diff --git a/src/timelines/toot-components/BoostIcon.css b/src/timelines/toot-components/BoostIcon.css index 188011b..23a4702 100644 --- a/src/timelines/toot-components/BoostIcon.css +++ b/src/timelines/toot-components/BoostIcon.css @@ -1,11 +1,13 @@ -.icon__boost { - padding: 0; - display: inline-block; +.BoostIcon { + display: inline-flex; border-radius: 2px; + background-color: green; + padding: 0.125em; + align-items: center; - > :global(svg) { - color: green; - font-size: 1rem; + > svg { + color: white; + font-size: 1em; vertical-align: middle; } } \ No newline at end of file diff --git a/src/timelines/toot-components/BoostIcon.tsx b/src/timelines/toot-components/BoostIcon.tsx index 1087778..c41861e 100644 --- a/src/timelines/toot-components/BoostIcon.tsx +++ b/src/timelines/toot-components/BoostIcon.tsx @@ -13,7 +13,7 @@ import "./BoostIcon.css"; const BoostIcon: Component<JSX.HTMLElementTags["i"]> = (props) => { const [managed, rest] = splitProps(props, ["class"]); return ( - <i class={["icon__boost", managed.class].join(" ")} {...rest}> + <i class={["BoostIcon", managed.class].join(" ")} {...rest}> <Repeat /> </i> ); diff --git a/src/timelines/toot.module.css b/src/timelines/toot.module.css index 0985c4b..85d9b04 100644 --- a/src/timelines/toot.module.css +++ b/src/timelines/toot.module.css @@ -222,6 +222,7 @@ grid-template-columns: auto 1fr auto; gap: 8px; margin-bottom: 8px; + align-items: center; } .tootAttachmentGrp {