diff --git a/src/App.tsx b/src/App.tsx index 028a234..9721c84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,9 @@ 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"; @@ -39,7 +41,6 @@ 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 ( @@ -52,8 +53,7 @@ const Routing: Component = () => { - - + diff --git a/src/masto/clients.ts b/src/masto/clients.ts index d9f255d..4ed109b 100644 --- a/src/masto/clients.ts +++ b/src/masto/clients.ts @@ -1,9 +1,9 @@ import { Accessor, createContext, - createMemo, createRenderEffect, createResource, + Signal, useContext, } from "solid-js"; import { Account } from "../accounts/stores"; @@ -76,60 +76,3 @@ 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. - * - * 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 [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/material/BottomSheet.module.css b/src/material/BottomSheet.module.css index fad6682..410858b 100644 --- a/src/material/BottomSheet.module.css +++ b/src/material/BottomSheet.module.css @@ -23,20 +23,9 @@ box-shadow: var(--tutu-shadow-e16); - :global(.MuiToolbar-root) { - > :global(.MuiButtonBase-root) { - - &:first-child { - margin-left: -0.5em; - margin-right: 24px; - } - - &:last-child { - margin-right: -0.5em; - margin-left: 24px; - } - - } + :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { + margin-left: -0.5em; + margin-right: 24px; } @media (max-width: 560px) { @@ -54,6 +43,7 @@ &.animated { position: absolute; + transform: translateY(-50%); overflow: hidden; will-change: width, height, top, left; @@ -64,6 +54,12 @@ & * { overflow: hidden; } + + @media (max-width: 560px) { + & { + transform: none; + } + } } &.bottom { @@ -75,6 +71,7 @@ & { transform: none; height: unset; + } } } diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 1fab238..747126c 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -118,9 +118,12 @@ 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 }, ); @@ -148,9 +151,12 @@ 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 }, ); diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx deleted file mode 100644 index fff795d..0000000 --- a/src/profiles/Profile.tsx +++ /dev/null @@ -1,252 +0,0 @@ -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 d229da8..91ba685 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/${toot.id}`, { + navigate(`/${encodeURIComponent(acct)}/${toot.id}`, { state: reply ? { tootReply: true, @@ -213,7 +213,7 @@ const Home: ParentComponent = (props) => { Public - + $settings.setKey( diff --git a/src/timelines/ProfileMenuButton.tsx b/src/timelines/ProfileMenuButton.tsx index 6d407f8..4fe8f04 100644 --- a/src/timelines/ProfileMenuButton.tsx +++ b/src/timelines/ProfileMenuButton.tsx @@ -24,10 +24,7 @@ import { import { A } from "@solidjs/router"; const ProfileMenuButton: ParentComponent<{ - profile?: { - account: { site: string }; - inf?: { displayName: string; avatar: string; username: string; id: string }; - }; + profile?: { displayName: string; avatar: string; username: string }; onClick?: () => void; onClose?: () => void; }> = (props) => { @@ -51,83 +48,79 @@ const ProfileMenuButton: ParentComponent<{ return ( <> - - - - - - - - - - + + + + + + + + + - - - - - Bookmarks - - - - - - Likes - - - - - - Lists - - - - {props.children} + + + + + Bookmarks + + + + + + Likes + + + + + + Lists + - - - - - - Settings - - + + {props.children} + + + + + + + Settings + + ); }; diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index b917c10..caa0054 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 { useSessionForAcctStr } from "../masto/clients"; +import { createUnauthorizedClient, useSessions } from "../masto/clients"; import { resolveCustomEmoji } from "../masto/toot"; import RegularToot from "./RegularToot"; import type { mastodon } from "masto"; @@ -45,10 +45,24 @@ 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 = useSessionForAcctStr(acctText) + 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 pushedCount = () => { return location.state?.tootBottomSheetPushedCount || 0; @@ -161,7 +175,7 @@ const TootBottomSheet: Component = (props) => { return; } setCache(params.acct, status); - navigate(`/${params.acct}/toot/${status.id}`, { + navigate(`/${params.acct}/${status.id}`, { state: { tootBottomSheetPushedCount: pushedCount() + 1, }, diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx deleted file mode 100644 index a72fb65..0000000 --- a/src/timelines/TootList.tsx +++ /dev/null @@ -1,104 +0,0 @@ -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;