diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 8321872..49119da 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -1,26 +1,16 @@ import { - Component, - For, - onCleanup, createSignal, Show, - untrack, onMount, type ParentComponent, children, Suspense, - Match, - Switch as JsSwitch, - ErrorBoundary, } from "solid-js"; import { useDocumentTitle } from "../utils"; import { type mastodon } from "masto"; import Scaffold from "../material/Scaffold"; import { AppBar, - Button, - Fab, - LinearProgress, ListItemSecondaryAction, ListItemText, MenuItem, @@ -29,205 +19,21 @@ import { } from "@suid/material"; import { css } from "solid-styled"; import { TimeSourceProvider, createTimeSource } from "../platform/timesrc"; -import TootThread from "./TootThread.js"; import ProfileMenuButton from "./ProfileMenuButton"; import Tabs from "../material/Tabs"; import Tab from "../material/Tab"; -import { Create as CreateTootIcon } from "@suid/icons-material"; -import { useTimeline } from "../masto/timelines"; import { makeEventListener } from "@solid-primitives/event-listener"; import BottomSheet, { HERO as BOTTOM_SHEET_HERO, } from "../material/BottomSheet"; import { $settings } from "../settings/stores"; import { useStore } from "@nanostores/solid"; -import { vibrate } from "../platform/hardware"; -import PullDownToRefresh from "./PullDownToRefresh"; import { HeroSourceProvider, type HeroSource } from "../platform/anim"; import { useNavigate } from "@solidjs/router"; import { useSignedInProfiles } from "../masto/acct"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; -import TootComposer from "./TootComposer"; - -const TimelinePanel: Component<{ - client: mastodon.rest.Client; - name: "home" | "public" | "trends"; - prefetch?: boolean; - fullRefetch?: number; - - openFullScreenToot: ( - toot: mastodon.v1.Status, - srcElement?: HTMLElement, - reply?: boolean, - ) => void; -}> = (props) => { - const [scrollLinked, setScrollLinked] = createSignal(); - const [ - timeline, - snapshot, - { refetch: refetchTimeline, mutate: mutateTimeline }, - ] = useTimeline( - () => - props.name !== "trends" - ? props.client.v1.timelines[props.name] - : props.client.v1.trends.statuses, - { fullRefresh: props.fullRefetch }, - ); - const [expandedThreadId, setExpandedThreadId] = createSignal(); - const [typing, setTyping] = createSignal(false); - - const tlEndObserver = new IntersectionObserver(() => { - if (untrack(() => props.prefetch) && !snapshot.loading) - refetchTimeline({ direction: "old" }); - }); - - onCleanup(() => tlEndObserver.disconnect()); - - const onBookmark = async ( - index: number, - client: mastodon.rest.Client, - status: mastodon.v1.Status, - ) => { - const result = await (status.bookmarked - ? client.v1.statuses.$select(status.id).unbookmark() - : client.v1.statuses.$select(status.id).bookmark()); - mutateTimeline((o) => { - o[index] = result; - return o; - }); - }; - - const onBoost = async ( - index: number, - client: mastodon.rest.Client, - status: mastodon.v1.Status, - ) => { - const reblogged = status.reblog - ? status.reblog.reblogged - : status.reblogged; - vibrate(50); - mutateTimeline(index, (x) => { - if (x.reblog) { - x.reblog = { ...x.reblog, reblogged: !reblogged }; - return Object.assign({}, x); - } else { - return Object.assign({}, x, { - reblogged: !reblogged, - }); - } - }); - const result = reblogged - ? await client.v1.statuses.$select(status.id).unreblog() - : (await client.v1.statuses.$select(status.id).reblog()).reblog!; - mutateTimeline((o) => { - Object.assign(o[index].reblog ?? o[index], { - reblogged: result.reblogged, - reblogsCount: result.reblogsCount, - }); - return o; - }); - }; - - return ( - { - return

Oops: {String(err)}

; - }} - > - refetchTimeline({ direction: "new" })} - /> -
- setTimeout(() => { - setScrollLinked(e.parentElement!); - }, 0) - } - > - - refetchTimeline({ direction: "new" })} - /> - - - {(item, index) => { - let element: HTMLElement | undefined; - return ( - onBoost(index(), ...args)} - onBookmark={(...args) => onBookmark(index(), ...args)} - onReply={(client, status) => - props.openFullScreenToot(status, element, true) - } - client={props.client} - expanded={item.id === expandedThreadId() ? 1 : 0} - onExpandChange={(x) => { - setTyping(false) - if (item.id !== expandedThreadId()) { - setExpandedThreadId((x) => (x ? undefined : item.id)); - } else if (x === 2) { - props.openFullScreenToot(item, element); - } - }} - /> - ); - }} - -
- -
tlEndObserver.observe(e)}>
- -
- -
-
-
- - - - - - - - -
-
- ); -}; +import TrendTimelinePanel from "./TrendTimelinePanel"; +import TimelinePanel from "./TimelinePanel"; const Home: ParentComponent = (props) => { let panelList: HTMLDivElement; @@ -340,7 +146,9 @@ const Home: ParentComponent = (props) => { console.warn("no account info?"); return; } - setHeroSrc((x) => Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement })); + setHeroSrc((x) => + Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }), + ); const acct = `${inf.username}@${p.account.site}`; setTootBottomSheetCache(acct, toot); navigate(`/${encodeURIComponent(acct)}/${toot.id}`, { @@ -439,12 +247,10 @@ const Home: ParentComponent = (props) => {
-
diff --git a/src/timelines/Thread.tsx b/src/timelines/Thread.tsx new file mode 100644 index 0000000..d7f4832 --- /dev/null +++ b/src/timelines/Thread.tsx @@ -0,0 +1,94 @@ +import type { mastodon } from "masto"; +import { + For, + Show, + createResource, + createSignal, + type Component, + type Ref, +} from "solid-js"; +import CompactToot from "./CompactToot"; +import { useTimeSource } from "../platform/timesrc"; +import RegularToot from "./RegularToot"; +import cardStyle from "../material/cards.module.css"; +import { css } from "solid-styled"; + +type TootActions = { + onBoost(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; +}; + +type ThreadProps = { + ref?: Ref; + client: mastodon.rest.Client; + toots: readonly mastodon.v1.Status[]; + isExpended: (status: mastodon.v1.Status) => boolean; + + onItemClick(status: mastodon.v1.Status, event: MouseEvent): void; +} & TootActions; + +const Thread: Component = (props) => { + const boost = (status: mastodon.v1.Status) => { + props.onBoost(props.client, status); + }; + + const bookmark = (status: mastodon.v1.Status) => { + props.onBookmark(props.client, status); + }; + + const reply = (status: mastodon.v1.Status) => { + props.onReply(props.client, status); + }; + + css` + article { + transition: + margin 90ms var(--tutu-anim-curve-sharp), + 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 ( +
1, + }} + > + + {(status, index) => ( + bookmark(s)} + onRetoot={(s) => boost(s)} + onReply={(s) => reply(s)} + onClick={[props.onItemClick, status]} + /> + )} + +
+ ); +}; + +export default Thread; diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx new file mode 100644 index 0000000..04b8f46 --- /dev/null +++ b/src/timelines/TimelinePanel.tsx @@ -0,0 +1,203 @@ +import { + Component, + For, + onCleanup, + createSignal, + Show, + untrack, + Match, + Switch as JsSwitch, + ErrorBoundary, +} from "solid-js"; +import { type mastodon } from "masto"; +import { + Button, + LinearProgress, +} from "@suid/material"; +import TootThread from "./TootThread.js"; +import { useTimeline } from "../masto/timelines"; +import { vibrate } from "../platform/hardware"; +import PullDownToRefresh from "./PullDownToRefresh"; +import TootComposer from "./TootComposer"; + +const TimelinePanel: Component<{ + client: mastodon.rest.Client; + name: "home" | "public" | "trends"; + prefetch?: boolean; + fullRefetch?: number; + + openFullScreenToot: ( + toot: mastodon.v1.Status, + srcElement?: HTMLElement, + reply?: boolean, + ) => void; +}> = (props) => { + const [scrollLinked, setScrollLinked] = createSignal(); + const [ + timeline, + snapshot, + { refetch: refetchTimeline, mutate: mutateTimeline }, + ] = useTimeline( + () => + props.name !== "trends" + ? props.client.v1.timelines[props.name] + : props.client.v1.trends.statuses, + { fullRefresh: props.fullRefetch }, + ); + const [expandedThreadId, setExpandedThreadId] = createSignal(); + const [typing, setTyping] = createSignal(false); + + const tlEndObserver = new IntersectionObserver(() => { + if (untrack(() => props.prefetch) && !snapshot.loading) + refetchTimeline({ direction: "old" }); + }); + + onCleanup(() => tlEndObserver.disconnect()); + + const onBookmark = async ( + index: number, + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + const result = await (status.bookmarked + ? client.v1.statuses.$select(status.id).unbookmark() + : client.v1.statuses.$select(status.id).bookmark()); + mutateTimeline((o) => { + o[index] = result; + return o; + }); + }; + + const onBoost = async ( + index: number, + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + const reblogged = status.reblog + ? status.reblog.reblogged + : status.reblogged; + vibrate(50); + mutateTimeline(index, (x) => { + if (x.reblog) { + x.reblog = { ...x.reblog, reblogged: !reblogged }; + return Object.assign({}, x); + } else { + return Object.assign({}, x, { + reblogged: !reblogged, + }); + } + }); + const result = reblogged + ? await client.v1.statuses.$select(status.id).unreblog() + : (await client.v1.statuses.$select(status.id).reblog()).reblog!; + mutateTimeline((o) => { + Object.assign(o[index].reblog ?? o[index], { + reblogged: result.reblogged, + reblogsCount: result.reblogsCount, + }); + return o; + }); + }; + + return ( + { + return

Oops: {String(err)}

; + }} + > + refetchTimeline({ direction: "new" })} + /> +
+ setTimeout(() => { + setScrollLinked(e.parentElement!); + }, 0) + } + > + + refetchTimeline({ direction: "new" })} + /> + + + {(item, index) => { + let element: HTMLElement | undefined; + return ( + onBoost(index(), ...args)} + onBookmark={(...args) => onBookmark(index(), ...args)} + onReply={(client, status) => + props.openFullScreenToot(status, element, true) + } + client={props.client} + expanded={item.id === expandedThreadId() ? 1 : 0} + onExpandChange={(x) => { + setTyping(false) + if (item.id !== expandedThreadId()) { + setExpandedThreadId((x) => (x ? undefined : item.id)); + } else if (x === 2) { + props.openFullScreenToot(item, element); + } + }} + /> + ); + }} + +
+ +
tlEndObserver.observe(e)}>
+ +
+ +
+
+
+ + + + + + + + +
+
+ ); +}; + +export default TimelinePanel \ No newline at end of file diff --git a/src/timelines/TrendTimelinePanel.tsx b/src/timelines/TrendTimelinePanel.tsx new file mode 100644 index 0000000..ddb97bc --- /dev/null +++ b/src/timelines/TrendTimelinePanel.tsx @@ -0,0 +1,189 @@ +import { + Component, + For, + onCleanup, + createSignal, + Show, + untrack, + Match, + Switch as JsSwitch, + ErrorBoundary, + createSelector, +} from "solid-js"; +import { type mastodon } from "masto"; +import { Button, LinearProgress } from "@suid/material"; +import { createTimelineSnapshot } from "../masto/timelines.js"; +import { vibrate } from "../platform/hardware.js"; +import PullDownToRefresh from "./PullDownToRefresh.jsx"; +import Thread from "./Thread.jsx"; + +const TrendTimelinePanel: Component<{ + client: mastodon.rest.Client; + prefetch?: boolean; + + openFullScreenToot: ( + toot: mastodon.v1.Status, + srcElement?: HTMLElement, + reply?: boolean, + ) => void; +}> = (props) => { + const [scrollLinked, setScrollLinked] = createSignal(); + const [ + timeline, + snapshot, + { refetch: refetchTimeline, mutate: mutateTimeline }, + ] = createTimelineSnapshot( + () => props.client.v1.trends.statuses, + () => 120, + ); + const [expandedId, setExpandedId] = createSignal(); + + const tlEndObserver = new IntersectionObserver(() => { + if (untrack(() => props.prefetch) && !snapshot.loading) + refetchTimeline({ direction: "old" }); + }); + + onCleanup(() => tlEndObserver.disconnect()); + + const isExpandedId = createSelector(expandedId); + + const isExpanded = (st: mastodon.v1.Status) => isExpandedId(st.id); + + const onBookmark = async ( + index: number, + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + const result = await (status.bookmarked + ? client.v1.statuses.$select(status.id).unbookmark() + : client.v1.statuses.$select(status.id).bookmark()); + mutateTimeline((o) => { + o![index] = [result]; + return o; + }); + }; + + const onBoost = async ( + index: number, + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + const reblogged = status.reblog + ? status.reblog.reblogged + : status.reblogged; + vibrate(50); + mutateTimeline(index, (th) => { + const x = th[0]; + if (x.reblog) { + x.reblog = { ...x.reblog, reblogged: !reblogged }; + return [Object.assign({}, x)]; + } else { + return [ + Object.assign({}, x, { + reblogged: !reblogged, + }), + ]; + } + }); + const result = reblogged + ? await client.v1.statuses.$select(status.id).unreblog() + : (await client.v1.statuses.$select(status.id).reblog()).reblog!; + mutateTimeline(index, (th) => { + Object.assign(th[0].reblog ?? th[0], { + reblogged: result.reblogged, + reblogsCount: result.reblogsCount, + }); + return th; + }); + }; + + return ( + { + return

Oops: {String(err)}

; + }} + > + refetchTimeline({ direction: "new" })} + /> +
+ setTimeout(() => { + setScrollLinked(e.parentElement!); + }, 0) + } + > + + {(item, index) => { + let element: HTMLElement | undefined; + return ( + onBoost(index(), ...args)} + onBookmark={(...args) => onBookmark(index(), ...args)} + onReply={(client, status) => + props.openFullScreenToot(status, element, true) + } + client={props.client} + isExpended={isExpanded} + onItemClick={(x) => { + if (x.id !== expandedId()) { + setExpandedId((o) => (o ? undefined : x.id)); + } else { + props.openFullScreenToot(x, element); + } + }} + /> + ); + }} + +
+ +
tlEndObserver.observe(e)}>
+ +
+ +
+
+
+ + + + + + + + +
+
+ ); +}; + +export default TrendTimelinePanel;