From a0811ed6c4d1ed4b76531b8f13429c2024ae17b7 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 15:57:53 +0800 Subject: [PATCH 1/9] Profile: add current default account into menu --- src/profiles/Profile.tsx | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index f3a5823..6976565 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -10,6 +10,7 @@ import { onCleanup, Show, type Component, + createMemo, } from "solid-js"; import Scaffold from "../material/Scaffold"; import { @@ -137,6 +138,17 @@ const Profile: Component = () => { recentTootChunk.loading || (recentTootFilter().pinned && pinnedTootChunk.loading); + const sessionDisplayName = createMemo(() => + resolveCustomEmoji( + session().account?.inf?.displayName || "", + session().account?.inf?.emojis ?? [], + ), + ); + + const useSessionDisplayName = (e: HTMLElement) => { + createRenderEffect(() => (e.innerHTML = sessionDisplayName())); + }; + return ( { document.getElementById(menuButId)!.getBoundingClientRect() } > + + + + + + + + + {/* // for future */} + + { height: "100%", }} crossOrigin="anonymous" - onLoad={async (event) => { + onLoad={(event) => { const ins = new FastAverageColor(); const colors = ins.getColor(event.currentTarget); setBannerSampledColors({ @@ -301,16 +324,7 @@ const Profile: Component = () => { - - createRenderEffect(() => { - e.innerHTML = resolveCustomEmoji( - session().account?.inf?.displayName || "", - session().account?.inf?.emojis ?? [], - ); - }) - } - > + 's Home From 832a59031aba65e124d2fed5f027177ba7f1345d Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 17:08:30 +0800 Subject: [PATCH 2/9] UnexpectedError: add error repl if failed to parse --- src/UnexpectedError.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/UnexpectedError.tsx b/src/UnexpectedError.tsx index aa7f468..906d99d 100644 --- a/src/UnexpectedError.tsx +++ b/src/UnexpectedError.tsx @@ -18,7 +18,16 @@ const UnexpectedError: Component<{ error?: any }> = (props) => { .join("\n"); return `${err.name}: ${err.message}\n${strackMsg}`; } catch (reason) { - return `\n${reason}`; + return `\n${reason}\n${JSON.stringify( + { + name: err.name, + stack: err.stack, + cause: err.cause, + message: err.message, + }, + undefined, + 2, + )}`; } } From dde0c249f44effcb7da914c544308bda3f743576 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 17:09:20 +0800 Subject: [PATCH 3/9] useSignedInProfiles: removed - use useSessions() instead --- src/masto/acct.ts | 28 ---------------------------- src/settings/Settings.tsx | 11 +++++------ src/timelines/Home.tsx | 6 +++--- 3 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 src/masto/acct.ts diff --git a/src/masto/acct.ts b/src/masto/acct.ts deleted file mode 100644 index ff9d488..0000000 --- a/src/masto/acct.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Accessor, createResource } from "solid-js"; -import type { mastodon } from "masto"; -import { useSessions } from "./clients"; -import { updateAcctInf } from "../accounts/stores"; - -export function useSignedInProfiles() { - const sessions = useSessions(); - const [accessor, tools] = createResource(sessions, async (all) => { - return Promise.all( - all.map(async (x, i) => ({ ...x, inf: await updateAcctInf(i) })), - ); - }); - return [ - () => { - try { - const value = accessor(); - if (value) { - return value; - } - } catch (reason) { - console.error("useSignedInProfiles: update acct info failed", reason); - } - - return sessions().map((x) => ({ ...x, inf: x.account.inf })); - }, - tools, - ] as const; -} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0bb04d7..dd82f25 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -31,12 +31,10 @@ import { import { A, useNavigate } from "@solidjs/router"; import { Title } from "../material/typography.jsx"; import { css } from "solid-styled"; -import { useSignedInProfiles } from "../masto/acct.js"; import { signOut, type Account } from "../accounts/stores.js"; import { format } from "date-fns"; import { useStore } from "@nanostores/solid"; import { $settings } from "./stores.js"; -import { useRegisterSW } from "virtual:pwa-register/solid"; import { autoMatchLangTag, autoMatchRegion, @@ -46,6 +44,7 @@ import { import { type Template } from "@solid-primitives/i18n"; import BottomSheet from "../material/BottomSheet.jsx"; import { useServiceWorker } from "../platform/host.js"; +import { useSessions } from "../masto/clients.js"; type Strings = { ["lang.auto"]: Template<{ detected: string }>; @@ -64,7 +63,7 @@ const Settings: ParentComponent = (props) => { const { needRefresh, offlineReady } = useServiceWorker(); const dateFnLocale = useDateFnLocale(); - const [profiles] = useSignedInProfiles(); + const profiles = useSessions(); const doSignOut = (acct: Account) => { signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken); @@ -118,9 +117,9 @@ const Settings: ParentComponent = (props) => { - {({ account: acct, inf }) => ( -
    - {`@${inf?.username ?? "..."}@${new URL(acct.site).host}`} + {({ account: acct }) => ( +
      + {`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`} {t("Notifications")} diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index f60eb35..99045bf 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -30,10 +30,10 @@ import { $settings } from "../settings/stores"; import { useStore } from "@nanostores/solid"; import { HeroSourceProvider, type HeroSource } from "../platform/anim"; import { useNavigate } from "@solidjs/router"; -import { useSignedInProfiles } from "../masto/acct"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; import TrendTimelinePanel from "./TrendTimelinePanel"; import TimelinePanel from "./TimelinePanel"; +import { useSessions } from "../masto/clients"; const Home: ParentComponent = (props) => { let panelList: HTMLDivElement; @@ -42,11 +42,11 @@ const Home: ParentComponent = (props) => { const settings$ = useStore($settings); - const [profiles] = useSignedInProfiles(); + const profiles = useSessions(); const profile = () => { const all = profiles(); if (all.length > 0) { - return all[0].inf; + return all[0].account.inf; } }; const client = () => { From bdaaeadba62ef803db0eb6658ab1d8ff57533b40 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 17:10:12 +0800 Subject: [PATCH 4/9] TootList: accept id in props --- src/timelines/TootList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx index 6145b75..da3f065 100644 --- a/src/timelines/TootList.tsx +++ b/src/timelines/TootList.tsx @@ -18,6 +18,7 @@ import { findElementActionable } from "./RegularToot"; const TootList: Component<{ ref?: Ref; + id?: string; threads: readonly string[]; onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onChangeToot: (id: string, value: mastodon.v1.Status) => void; @@ -149,7 +150,7 @@ const TootList: Component<{ return

      Oops: {String(err)}

      ; }} > -
      +
      {(itemId, index) => { const path = props.onUnknownThread(itemId)!; From 9da4a3a4b31a9fcf2f206e7fdcf090a11ca4ff2f Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 18:02:30 +0800 Subject: [PATCH 5/9] Scaffold: add roles to position containers --- src/material/Scaffold.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index fa9631f..693793f 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -36,12 +36,12 @@ const Scaffold: Component = (props) => { return ( <> -
      + -
      {props.fab}
      +
      { @@ -61,7 +61,7 @@ const Scaffold: Component = (props) => { {managed.children}
      -
      {props.bottom}
      +
      ); From 8205ff661ba81e1026d9656598f7bc41ae494855 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 18:03:05 +0800 Subject: [PATCH 6/9] BottomSheet: remove focus --- src/material/BottomSheet.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 2ebed75..8a1dd62 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -228,6 +228,8 @@ const BottomSheet: ParentComponent = (props) => { }} onClick={onDialogClick} ref={element!} + tabIndex={-1} + role="presentation" > {ochildren() ?? cache()} From c22860a7e39a35b38605b88aed637c84ae053d13 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 18:03:27 +0800 Subject: [PATCH 7/9] Menu: support ARIA attributes --- src/material/Menu.css | 5 +++++ src/material/Menu.tsx | 44 +++++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/material/Menu.css b/src/material/Menu.css index 5c88653..1837db3 100644 --- a/src/material/Menu.css +++ b/src/material/Menu.css @@ -23,6 +23,11 @@ &.e4 { box-shadow: var(--tutu-shadow-e12); } + + &>.container { + background: var(--tutu-color-surface); + display: contents; + } } dialog.Menu::backdrop { diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx index 916fc76..44628f9 100644 --- a/src/material/Menu.tsx +++ b/src/material/Menu.tsx @@ -3,6 +3,7 @@ import { MenuList } from "@suid/material"; import { createEffect, createSignal, + splitProps, type Component, type JSX, type ParentProps, @@ -16,11 +17,15 @@ import { export type Anchor = Pick & { e?: number }; -export type MenuProps = ParentProps<{ - open?: boolean; - onClose?: JSX.EventHandlerUnion; - anchor: () => Anchor; -}>; +export type MenuProps = ParentProps< + { + open?: boolean; + onClose?: JSX.EventHandlerUnion; + anchor: () => Anchor; + + id?: string; + } & JSX.AriaAttributes +>; function px(n?: number) { if (n) { @@ -74,6 +79,7 @@ export function createManagedMenuState() { const Menu: Component = (props) => { let root: HTMLDialogElement; const windowSize = useWindowSize(); + const [, rest] = splitProps(props, ["open", "onClose", "anchor"]); const [anchorPos, setAnchorPos] = createSignal<{ left?: number; @@ -83,15 +89,16 @@ const Menu: Component = (props) => { if (import.meta.env.DEV) { createEffect(() => { - switch (anchorPos().e) { - case 1: - case 2: - case 3: - case 3: - return; - default: - console.warn('value %s is invalid for param "e"', anchorPos().e); - } + if (anchorPos().e) + switch (anchorPos().e) { + case 1: + case 2: + case 3: + case 4: + return; + default: + console.warn('value %s is invalid for param "e"', anchorPos().e); + } }); } @@ -202,12 +209,13 @@ const Menu: Component = (props) => { top: px(anchorPos().top), /* FIXME: the content may be overflow */ }} + role="presentation" + tabIndex={-1} + {...rest} > From 97a1fb9cf14cc5c741112c89fc151b06c2072db1 Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 18:03:53 +0800 Subject: [PATCH 8/9] createTimelineControlsForArray: catch error --- src/masto/timelines.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 1aaefb1..6e5eb9b 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -26,7 +26,9 @@ export function createTimelineControlsForArray( const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); createEffect(() => { - const nls = status(); + const nls = catchError(status, (e) => { + console.error(e); + }); if (!nls) return; setThreads([]); @@ -226,9 +228,7 @@ export type TimelineControls = { set(id: string, value: mastodon.v1.Status): void; }; -export type TimelineResource< - R, -> = [ +export type TimelineResource = [ TimelineControls, Resource, { refetch(info?: TimelineFetchDirection): void }, @@ -238,7 +238,7 @@ export type TimelineResource< * Create auto managed timeline controls. * * The error from the resource is not thrown in the - * {@link TimelineControls.list} and {@link TimelineControls}.get*. + * {@link TimelineControls["list"]} and {@link TimelineControls}.get*. * Use the second value from {@link TimelineResource} to catch the error. */ export function createTimeline< From 6705b754d1cd415584183cdaf3c0958c0350160c Mon Sep 17 00:00:00 2001 From: thislight Date: Mon, 4 Nov 2024 18:16:23 +0800 Subject: [PATCH 9/9] Profile: support subscribe to home timeline --- src/profiles/Profile.tsx | 96 +++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index 6976565..4e8a930 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -17,6 +17,7 @@ import { AppBar, Avatar, Button, + Checkbox, CircularProgress, Divider, IconButton, @@ -67,6 +68,8 @@ const Profile: Component = () => { const time = createTimeSource(); const menuButId = createUniqueId(); + const recentTootListId = createUniqueId(); + const optMenuId = createUniqueId(); const [menuOpen, setMenuOpen] = createSignal(false); @@ -88,17 +91,20 @@ const Profile: Component = () => { ); onCleanup(() => obx.disconnect()); - const [profileErrorUncaught] = createResource( + const [profileUncaught] = createResource( () => [session().client, params.id] as const, async ([client, id]) => { return await client.v1.accounts.$select(id).fetch(); }, ); - const profile = () => - catchError(profileErrorUncaught, (err) => { - console.error(err); - }); + const profile = () => { + try { + return profileUncaught(); + } catch (reason) { + console.error(reason); + } + }; const isCurrentSessionProfile = () => { return session().account?.inf?.url === profile()?.url; @@ -127,6 +133,22 @@ const Profile: Component = () => { }, ); + const [relationshipUncaught, { mutate: mutateRelationship }] = createResource( + () => [session(), params.id] as const, + async ([sess, id]) => { + if (!sess.account) return; // No account, no relation + const relations = await session().client.v1.accounts.relationships.fetch({ + id: [id], + }); + return relations.length > 0 ? relations[0] : undefined; + }, + ); + + const relationship = () => + catchError(relationshipUncaught, (reason) => { + console.error(reason); + }); + const bannerImg = () => profile()?.header; const avatarImg = () => profile()?.avatar; const displayName = () => @@ -149,10 +171,27 @@ const Profile: Component = () => { createRenderEffect(() => (e.innerHTML = sessionDisplayName())); }; + const toggleSubscribeHome = async () => { + const client = session().client; + if (!session().account) return; + const isSubscribed = relationship()?.following ?? false; + mutateRelationship((x) => Object.assign({ following: !isSubscribed }, x)); + subscribeMenuState.onClose(); + + if (isSubscribed) { + const nrel = await client.v1.accounts.$select(params.id).unfollow(); + mutateRelationship(nrel); + } else { + const nrel = await client.v1.accounts.$select(params.id).follow(); + mutateRelationship(nrel); + } + }; + return ( { @@ -197,11 +238,13 @@ const Profile: Component = () => { class="Profile" > document.getElementById(menuButId)!.getBoundingClientRect() } + aria-label="Options for the Profile" > @@ -296,6 +339,7 @@ const Profile: Component = () => { "margin-top": "calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))", }} + role="presentation" > obx.observe(e)} @@ -306,6 +350,7 @@ const Profile: Component = () => { height: "100%", }} crossOrigin="anonymous" + alt={`Banner image for ${profile()?.displayName || "the user"}`} onLoad={(event) => { const ins = new FastAverageColor(); const colors = ins.getColor(event.currentTarget); @@ -319,14 +364,21 @@ const Profile: Component = () => {
      - + - + 's Home + + @@ -337,9 +389,10 @@ const Profile: Component = () => { color: bannerSampledColors()?.text, }} > -
      +
      { ref={(e) => createRenderEffect(() => (e.innerHTML = displayName())) } + aria-label="Display name" > - {fullUsername()} + {fullUsername()}
      - + {<>} @@ -374,20 +434,24 @@ const Profile: Component = () => { ); }} > - Subscribe + {relationship()?.following ? "Subscribed" : "Subscribe"}
      -
      -
      +
      createRenderEffect(() => (e.innerHTML = description() || "")) } - >
      + > - +
      {(item, index) => { @@ -436,6 +500,7 @@ const Profile: Component = () => { { >