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, + )}`; } } 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/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< 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()} 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} > 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}
+
); diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index f3a5823..4e8a930 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -10,12 +10,14 @@ import { onCleanup, Show, type Component, + createMemo, } from "solid-js"; import Scaffold from "../material/Scaffold"; import { AppBar, Avatar, Button, + Checkbox, CircularProgress, Divider, IconButton, @@ -66,6 +68,8 @@ const Profile: Component = () => { const time = createTimeSource(); const menuButId = createUniqueId(); + const recentTootListId = createUniqueId(); + const optMenuId = createUniqueId(); const [menuOpen, setMenuOpen] = createSignal(false); @@ -87,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; @@ -126,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 = () => @@ -137,10 +160,38 @@ 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())); + }; + + 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 ( { @@ -185,12 +238,25 @@ const Profile: Component = () => { class="Profile" > document.getElementById(menuButId)!.getBoundingClientRect() } + aria-label="Options for the Profile" > + + + + + + + + + {/* // for future */} + + { "margin-top": "calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))", }} + role="presentation" > obx.observe(e)} @@ -283,7 +350,8 @@ const Profile: Component = () => { height: "100%", }} crossOrigin="anonymous" - onLoad={async (event) => { + alt={`Banner image for ${profile()?.displayName || "the user"}`} + onLoad={(event) => { const ins = new FastAverageColor(); const colors = ins.getColor(event.currentTarget); setBannerSampledColors({ @@ -296,23 +364,21 @@ const Profile: Component = () => {
- + - - - createRenderEffect(() => { - e.innerHTML = resolveCustomEmoji( - session().account?.inf?.displayName || "", - session().account?.inf?.emojis ?? [], - ); - }) - } - > + + 's Home + + @@ -323,9 +389,10 @@ const Profile: Component = () => { color: bannerSampledColors()?.text, }} > -
+
{ ref={(e) => createRenderEffect(() => (e.innerHTML = displayName())) } + aria-label="Display name" > - {fullUsername()} + {fullUsername()}
- + {<>} @@ -360,20 +434,24 @@ const Profile: Component = () => { ); }} > - Subscribe + {relationship()?.following ? "Subscribed" : "Subscribe"}
- -
+
createRenderEffect(() => (e.innerHTML = description() || "")) } - >
+ > - +
{(item, index) => { @@ -422,6 +500,7 @@ const Profile: Component = () => { { > ; @@ -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 = () => { 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)!;