From b5da86fa5c617d6d9136139d45e7b7576ba30fad Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 17 Oct 2024 20:39:04 +0800 Subject: [PATCH 1/5] TootBottomSheet: moved to /:acct/toot/:id --- src/App.tsx | 2 +- src/timelines/Home.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9721c84..399d257 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,7 +53,7 @@ const Routing: Component = () => { - + diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 91ba685..88ba5bf 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -151,7 +151,7 @@ const Home: ParentComponent = (props) => { ); const acct = `${inf.username}@${p.account.site}`; setTootBottomSheetCache(acct, toot); - navigate(`/${encodeURIComponent(acct)}/${toot.id}`, { + navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { state: reply ? { tootReply: true, From 040016ddce1fb8932ee972ff924d41659451fca6 Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 17 Oct 2024 20:57:20 +0800 Subject: [PATCH 2/5] BottomSheet: fix the animation problem --- src/material/BottomSheet.module.css | 8 -------- src/material/BottomSheet.tsx | 6 ------ 2 files changed, 14 deletions(-) diff --git a/src/material/BottomSheet.module.css b/src/material/BottomSheet.module.css index 410858b..154de26 100644 --- a/src/material/BottomSheet.module.css +++ b/src/material/BottomSheet.module.css @@ -43,7 +43,6 @@ &.animated { position: absolute; - transform: translateY(-50%); overflow: hidden; will-change: width, height, top, left; @@ -54,12 +53,6 @@ & * { overflow: hidden; } - - @media (max-width: 560px) { - & { - transform: none; - } - } } &.bottom { @@ -71,7 +64,6 @@ & { transform: none; height: unset; - } } } diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 747126c..1fab238 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -118,12 +118,9 @@ const BottomSheet: ParentComponent = (props) => { animation = element.animate( { - top: [`${rect.top}px`, `${rect.top}px`], left: reserve ? [`${rect.left}px`, `${window.innerWidth}px`] : [`${window.innerWidth}px`, `${rect.left}px`], - width: [`${rect.width}px`, `${rect.width}px`], - height: [`${rect.height}px`, `${rect.height}px`], }, { easing, duration }, ); @@ -151,12 +148,9 @@ const BottomSheet: ParentComponent = (props) => { animation = element.animate( { - left: [`${rect.left}px`, `${rect.left}px`], top: reserve ? [`${rect.top}px`, `${window.innerHeight}px`] : [`${window.innerHeight}px`, `${rect.top}px`], - width: [`${rect.width}px`, `${rect.width}px`], - height: [`${rect.height}px`, `${rect.height}px`], }, { easing, duration }, ); From 657c886fab156e8d042334deab8b276535be1837 Mon Sep 17 00:00:00 2001 From: thislight Date: Fri, 18 Oct 2024 11:58:17 +0800 Subject: [PATCH 3/5] add useSessionForAcctStr --- src/masto/clients.ts | 37 +++++++++++++++++++++++++++++++ src/timelines/TootBottomSheet.tsx | 18 ++------------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/masto/clients.ts b/src/masto/clients.ts index 4ed109b..ebe7deb 100644 --- a/src/masto/clients.ts +++ b/src/masto/clients.ts @@ -1,6 +1,7 @@ import { Accessor, createContext, + createMemo, createRenderEffect, createResource, Signal, @@ -76,3 +77,39 @@ function useSessionsRaw() { } return store; } + +/** + * Get a session for the specific acct string. + * + * Acct string is a string in the pattern of `{username}@{site_with_protocol}`, + * like `@thislight@https://mastodon.social`, can be used to identify (tempoarily) + * an session on the tutu instance. + * + * The `site_with_protocol` is required. + * + * - If the username is present, the session matches the username and the site is returned; or, + * - If the username is not present, any session on the site is returned; or, + * - If no available session available for the pattern, an unauthorised session is returned. + * + * In an unauthorised session, the `.account` is `undefined` and the `client` is an + * unauthorised client for the site. This client may not available for some operations. + */ +export function useSessionForAcctStr(acct: Accessor) { + const allSessions = useSessions() + + return createMemo(() => { + const parts = acct().split("@", 2) + const [inputUsername, inputSite] = acct().split("@", 2); + const authedSession = allSessions().find( + (x) => + x.account.site === inputSite && + x.account.inf?.username === inputUsername, + ); + return ( + authedSession ?? { + client: createUnauthorizedClient(inputSite), + account: undefined, + } + ); + }); +} diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index caa0054..06add1c 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -15,7 +15,7 @@ import { ArrowBack as BackIcon, Close as CloseIcon, } from "@suid/icons-material"; -import { createUnauthorizedClient, useSessions } from "../masto/clients"; +import { useSessionForAcctStr } from "../masto/clients"; import { resolveCustomEmoji } from "../masto/toot"; import RegularToot from "./RegularToot"; import type { mastodon } from "masto"; @@ -45,24 +45,10 @@ const TootBottomSheet: Component = (props) => { tootReply?: boolean; }>(); const navigate = useNavigate(); - const allSession = useSessions(); const time = createTimeSource(); const [isInTyping, setInTyping] = createSignal(false); const acctText = () => decodeURIComponent(params.acct); - const session = () => { - const [inputUsername, inputSite] = acctText().split("@", 2); - const authedSession = allSession().find( - (x) => - x.account.site === inputSite && - x.account.inf?.username === inputUsername, - ); - return ( - authedSession ?? { - client: createUnauthorizedClient(inputSite), - account: undefined, - } - ); - }; + const session = useSessionForAcctStr(acctText) const pushedCount = () => { return location.state?.tootBottomSheetPushedCount || 0; From e9c39492ec096fb62fb982c30b711bb50a6285ea Mon Sep 17 00:00:00 2001 From: thislight Date: Fri, 18 Oct 2024 18:13:07 +0800 Subject: [PATCH 4/5] TootBottomSheet: fix context switch wrong target --- src/timelines/TootBottomSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index 06add1c..b917c10 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -161,7 +161,7 @@ const TootBottomSheet: Component = (props) => { return; } setCache(params.acct, status); - navigate(`/${params.acct}/${status.id}`, { + navigate(`/${params.acct}/toot/${status.id}`, { state: { tootBottomSheetPushedCount: pushedCount() + 1, }, From bea1d6abfa47b1cbfbca3b8a02a9d18c9a470d70 Mon Sep 17 00:00:00 2001 From: thislight Date: Fri, 18 Oct 2024 19:15:35 +0800 Subject: [PATCH 5/5] Profile: first prototype --- src/App.tsx | 6 +- src/masto/clients.ts | 24 ++- src/material/BottomSheet.module.css | 17 +- src/profiles/Profile.tsx | 252 ++++++++++++++++++++++++++++ src/timelines/Home.tsx | 2 +- src/timelines/ProfileMenuButton.tsx | 149 ++++++++-------- src/timelines/TootList.tsx | 104 ++++++++++++ 7 files changed, 474 insertions(+), 80 deletions(-) create mode 100644 src/profiles/Profile.tsx create mode 100644 src/timelines/TootList.tsx diff --git a/src/App.tsx b/src/App.tsx index 399d257..028a234 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,9 +24,7 @@ import { ResultDispatcher, type JSONRPC, } from "./serviceworker/workerrpc.js"; -import { - Service -} from "./serviceworker/services.js" +import { Service } from "./serviceworker/services.js"; import { makeEventListener } from "@solid-primitives/event-listener"; import { ServiceWorkerProvider } from "./platform/host.js"; @@ -41,6 +39,7 @@ const MotionSettings = lazy(() => import("./settings/Motions.js")); const LanguageSettings = lazy(() => import("./settings/Language.js")); const RegionSettings = lazy(() => import("./settings/Region.jsx")); const UnexpectedError = lazy(() => import("./UnexpectedError.js")); +const Profile = lazy(() => import("./profiles/Profile.js")); const Routing: Component = () => { return ( @@ -54,6 +53,7 @@ const Routing: Component = () => { + diff --git a/src/masto/clients.ts b/src/masto/clients.ts index ebe7deb..d9f255d 100644 --- a/src/masto/clients.ts +++ b/src/masto/clients.ts @@ -4,7 +4,6 @@ import { createMemo, createRenderEffect, createResource, - Signal, useContext, } from "solid-js"; import { Account } from "../accounts/stores"; @@ -78,6 +77,26 @@ function useSessionsRaw() { return store; } +const DefaultSessionContext = /* @__PURE__ */ createContext>(() => 0) + +export const DefaultSessionProvider = DefaultSessionContext.Provider; + +/** + * Return the default session (the first session). + * + * This function may return `undefined`, but it will try to redirect the user to the sign in. + */ +export function useDefaultSession() { + const sessions = useSessions() + const sessionIndex = useContext(DefaultSessionContext) + + return () => { + if (sessions().length > 0) { + return sessions()[sessionIndex()] + } + } +} + /** * Get a session for the specific acct string. * @@ -98,7 +117,6 @@ export function useSessionForAcctStr(acct: Accessor) { const allSessions = useSessions() return createMemo(() => { - const parts = acct().split("@", 2) const [inputUsername, inputSite] = acct().split("@", 2); const authedSession = allSessions().find( (x) => @@ -113,3 +131,5 @@ export function useSessionForAcctStr(acct: Accessor) { ); }); } + + diff --git a/src/material/BottomSheet.module.css b/src/material/BottomSheet.module.css index 154de26..fad6682 100644 --- a/src/material/BottomSheet.module.css +++ b/src/material/BottomSheet.module.css @@ -23,9 +23,20 @@ box-shadow: var(--tutu-shadow-e16); - :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { - margin-left: -0.5em; - margin-right: 24px; + :global(.MuiToolbar-root) { + > :global(.MuiButtonBase-root) { + + &:first-child { + margin-left: -0.5em; + margin-right: 24px; + } + + &:last-child { + margin-right: -0.5em; + margin-left: 24px; + } + + } } @media (max-width: 560px) { diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx new file mode 100644 index 0000000..fff795d --- /dev/null +++ b/src/profiles/Profile.tsx @@ -0,0 +1,252 @@ +import { + createRenderEffect, + createResource, + createSignal, + For, + onCleanup, + Show, + type Component, +} from "solid-js"; +import Scaffold from "../material/Scaffold"; +import { AppBar, Avatar, Button, IconButton, Toolbar } from "@suid/material"; +import { Close, MoreVert, Verified } from "@suid/icons-material"; +import { Title } from "../material/typography"; +import { useNavigate, useParams } from "@solidjs/router"; +import { useSessionForAcctStr } from "../masto/clients"; +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 TootList from "../timelines/TootList"; +import { createIntersectionObserver } from "@solid-primitives/intersection-observer"; +import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; + +const Profile: Component = () => { + const navigate = useNavigate(); + const params = useParams<{ acct: string; id: string }>(); + const acctText = () => decodeURIComponent(params.acct); + const session = useSessionForAcctStr(acctText); + const [bannerSampledColors, setBannerSampledColors] = createSignal<{ + average: string; + text: string; + }>(); + const windowSize = useWindowSize(); + const time = createTimeSource(); + + const [scrolledPastBanner, setScrolledPastBanner] = createSignal(false); + const obx = new IntersectionObserver( + (entries) => { + const ent = entries[0]; + if (ent.intersectionRatio < 0.1) { + setScrolledPastBanner(true); + } else { + setScrolledPastBanner(false); + } + }, + { + threshold: 0.1, + }, + ); + + onCleanup(() => obx.disconnect()); + + const [profile] = createResource( + () => [session().client, params.id] as const, + async ([client, id]) => { + return await client.v1.accounts.$select(id).fetch(); + }, + ); + + const [recentToots] = createTimeline( + () => session().client.v1.accounts.$select(params.id).statuses, + () => 20, + ); + + const bannerImg = () => profile()?.header; + const avatarImg = () => profile()?.avatar; + const displayName = () => + resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []); + const fullUsername = () => `@${profile()?.acct ?? "..."}`; // TODO: full user name + const description = () => profile()?.note; + + css` + .intro { + background-color: var(--tutu-color-surface-d); + color: var(--tutu-color-on-surface); + padding: 16px 12px; + display: flex; + flex-flow: column nowrap; + gap: 16px; + } + + .acct-grp { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 16px; + align-items: center; + } + + .name-grp { + display: flex; + flex-flow: column nowrap; + } + + table.acct-fields { + & td > :global(a) { + display: inline-flex; + min-height: 44px; + align-items: center; + color: inherit; + } + + & :global(a > .invisible) { + display: none; + } + + & :global(svg) { + vertical-align: middle; + } + } + + .page-title { + flex-grow: 1; + } + `; + + return ( + + + + + + + createRenderEffect(() => (e.innerHTML = displayName())) + } + > + + + + + + } + > +
+ obx.observe(e)} + src={bannerImg()} + style={{ + "object-fit": "contain", + width: "100%", + height: "100%", + }} + crossOrigin="anonymous" + onLoad={async (event) => { + const ins = new FastAverageColor(); + const colors = ins.getColor(event.currentTarget); + setBannerSampledColors({ + average: colors.hex, + text: colors.isDark ? "white" : "black", + }); + }} + > +
+ +
+
+ +
+ + createRenderEffect(() => (e.innerHTML = displayName())) + } + > + {fullUsername()} +
+
+ +
+
+
+ createRenderEffect(() => (e.innerHTML = description() || "")) + } + >
+ + + + {(item, index) => { + return ( + + + + + + ); + }} + + +
{item.name} + + + + { + createRenderEffect(() => (e.innerHTML = item.value)); + }} + >
+
+ + + + +
+ ); +}; + +export default Profile; diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 88ba5bf..d229da8 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -213,7 +213,7 @@ const Home: ParentComponent = (props) => { Public - + $settings.setKey( diff --git a/src/timelines/ProfileMenuButton.tsx b/src/timelines/ProfileMenuButton.tsx index 4fe8f04..6d407f8 100644 --- a/src/timelines/ProfileMenuButton.tsx +++ b/src/timelines/ProfileMenuButton.tsx @@ -24,7 +24,10 @@ import { import { A } from "@solidjs/router"; const ProfileMenuButton: ParentComponent<{ - profile?: { displayName: string; avatar: string; username: string }; + profile?: { + account: { site: string }; + inf?: { displayName: string; avatar: string; username: string; id: string }; + }; onClick?: () => void; onClose?: () => void; }> = (props) => { @@ -48,79 +51,83 @@ const ProfileMenuButton: ParentComponent<{ return ( <> - + + + + - - - - - - - - - + + + + + - - - - - Bookmarks - - - - - - Likes - - - - - - Lists - + + + + + Bookmarks + + + + + + Likes + + + + + + Lists + + + + {props.children} - - {props.children} - - - - - - - Settings - - + + + + + + Settings + + ); }; diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx new file mode 100644 index 0000000..a72fb65 --- /dev/null +++ b/src/timelines/TootList.tsx @@ -0,0 +1,104 @@ +import { + Component, + For, + onCleanup, + createSignal, + Show, + untrack, + Match, + Switch as JsSwitch, + ErrorBoundary, + type Ref, +} from "solid-js"; +import { type mastodon } from "masto"; +import { Button, LinearProgress } from "@suid/material"; +import { createTimeline } from "../masto/timelines"; +import { vibrate } from "../platform/hardware"; +import PullDownToRefresh from "./PullDownToRefresh"; +import TootComposer from "./TootComposer"; +import Thread from "./Thread.jsx"; +import { useDefaultSession } from "../masto/clients"; + +const TootList: Component<{ + ref?: Ref; + threads: string[]; + onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; + onChangeToot: (id: string, value: mastodon.v1.Status) => void; +}> = (props) => { + const session = useDefaultSession(); + const [expandedThreadId, setExpandedThreadId] = createSignal(); + + const onBookmark = async ( + 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()); + props.onChangeToot(result.id, result); + }; + + const onBoost = async ( + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + vibrate(50); + const rootStatus = status.reblog ? status.reblog : status; + const reblogged = rootStatus.reblogged; + if (status.reblog) { + status.reblog = { ...status.reblog, reblogged: !reblogged }; + props.onChangeToot(status.id, status); + } else { + props.onChangeToot( + status.id, + Object.assign(status, { + reblogged: !reblogged, + }), + ); + } + const result = reblogged + ? await client.v1.statuses.$select(status.id).unreblog() + : (await client.v1.statuses.$select(status.id).reblog()).reblog!; + props.onChangeToot( + status.id, + Object.assign(status.reblog ?? status, result.reblog), + ); + }; + + return ( + { + return

Oops: {String(err)}

; + }} + > +
+ + {(itemId, index) => { + const path = props.onUnknownThread(itemId)!; + const toots = path.reverse().map((x) => x.value); + + return ( + {}} + client={session()?.client!} + isExpended={(status) => status.id === expandedThreadId()} + onItemClick={(status, event) => { + if (status.id !== expandedThreadId()) { + setExpandedThreadId((x) => (x ? undefined : status.id)); + } else { + // TODO: open full-screen toot + } + }} + /> + ); + }} + +
+
+ ); +}; + +export default TootList;