Compare commits

..

No commits in common. "6705b754d1cd415584183cdaf3c0958c0350160c" and "02d883e53b19d7c1da396dfe601a80a8e4581524" have entirely different histories.

11 changed files with 91 additions and 167 deletions

View file

@ -18,16 +18,7 @@ const UnexpectedError: Component<{ error?: any }> = (props) => {
.join("\n"); .join("\n");
return `${err.name}: ${err.message}\n${strackMsg}`; return `${err.name}: ${err.message}\n${strackMsg}`;
} catch (reason) { } catch (reason) {
return `<failed to build the stacktrace of "${err}"...>\n${reason}\n${JSON.stringify( return `<failed to build the stacktrace of "${err}"...>\n${reason}`;
{
name: err.name,
stack: err.stack,
cause: err.cause,
message: err.message,
},
undefined,
2,
)}`;
} }
} }

28
src/masto/acct.ts Normal file
View file

@ -0,0 +1,28 @@
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;
}

View file

@ -26,9 +26,7 @@ export function createTimelineControlsForArray(
const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]);
createEffect(() => { createEffect(() => {
const nls = catchError(status, (e) => { const nls = status();
console.error(e);
});
if (!nls) return; if (!nls) return;
setThreads([]); setThreads([]);
@ -228,7 +226,9 @@ export type TimelineControls = {
set(id: string, value: mastodon.v1.Status): void; set(id: string, value: mastodon.v1.Status): void;
}; };
export type TimelineResource<R> = [ export type TimelineResource<
R,
> = [
TimelineControls, TimelineControls,
Resource<R>, Resource<R>,
{ refetch(info?: TimelineFetchDirection): void }, { refetch(info?: TimelineFetchDirection): void },
@ -238,7 +238,7 @@ export type TimelineResource<R> = [
* Create auto managed timeline controls. * Create auto managed timeline controls.
* *
* The error from the resource is not thrown in the * 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. * Use the second value from {@link TimelineResource} to catch the error.
*/ */
export function createTimeline< export function createTimeline<

View file

@ -228,8 +228,6 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
}} }}
onClick={onDialogClick} onClick={onDialogClick}
ref={element!} ref={element!}
tabIndex={-1}
role="presentation"
> >
{ochildren() ?? cache()} {ochildren() ?? cache()}
</dialog> </dialog>

View file

@ -23,11 +23,6 @@
&.e4 { &.e4 {
box-shadow: var(--tutu-shadow-e12); box-shadow: var(--tutu-shadow-e12);
} }
&>.container {
background: var(--tutu-color-surface);
display: contents;
}
} }
dialog.Menu::backdrop { dialog.Menu::backdrop {

View file

@ -3,7 +3,6 @@ import { MenuList } from "@suid/material";
import { import {
createEffect, createEffect,
createSignal, createSignal,
splitProps,
type Component, type Component,
type JSX, type JSX,
type ParentProps, type ParentProps,
@ -17,15 +16,11 @@ import {
export type Anchor = Pick<DOMRect, "top" | "left" | "right"> & { e?: number }; export type Anchor = Pick<DOMRect, "top" | "left" | "right"> & { e?: number };
export type MenuProps = ParentProps< export type MenuProps = ParentProps<{
{
open?: boolean; open?: boolean;
onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>; onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>;
anchor: () => Anchor; anchor: () => Anchor;
}>;
id?: string;
} & JSX.AriaAttributes
>;
function px(n?: number) { function px(n?: number) {
if (n) { if (n) {
@ -79,7 +74,6 @@ export function createManagedMenuState() {
const Menu: Component<MenuProps> = (props) => { const Menu: Component<MenuProps> = (props) => {
let root: HTMLDialogElement; let root: HTMLDialogElement;
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
const [anchorPos, setAnchorPos] = createSignal<{ const [anchorPos, setAnchorPos] = createSignal<{
left?: number; left?: number;
@ -89,12 +83,11 @@ const Menu: Component<MenuProps> = (props) => {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
createEffect(() => { createEffect(() => {
if (anchorPos().e)
switch (anchorPos().e) { switch (anchorPos().e) {
case 1: case 1:
case 2: case 2:
case 3: case 3:
case 4: case 3:
return; return;
default: default:
console.warn('value %s is invalid for param "e"', anchorPos().e); console.warn('value %s is invalid for param "e"', anchorPos().e);
@ -209,13 +202,12 @@ const Menu: Component<MenuProps> = (props) => {
top: px(anchorPos().top), top: px(anchorPos().top),
/* FIXME: the content may be overflow */ /* FIXME: the content may be overflow */
}} }}
role="presentation"
tabIndex={-1}
{...rest}
> >
<div <div
class="container" style={{
role="presentation" background: "var(--tutu-color-surface)",
display: "contents"
}}
> >
<MenuList>{props.children}</MenuList> <MenuList>{props.children}</MenuList>
</div> </div>

View file

@ -36,12 +36,12 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
return ( return (
<> <>
<Show when={props.topbar}> <Show when={props.topbar}>
<div class="Scaffold__topbar" ref={setTopbarElement} role="presentation"> <div class="Scaffold__topbar" ref={setTopbarElement}>
{props.topbar} {props.topbar}
</div> </div>
</Show> </Show>
<Show when={props.fab}> <Show when={props.fab}>
<div class="Scaffold__fab-dock" role="presentation">{props.fab}</div> <div class="Scaffold__fab-dock">{props.fab}</div>
</Show> </Show>
<div <div
ref={(e) => { ref={(e) => {
@ -61,7 +61,7 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
{managed.children} {managed.children}
</div> </div>
<Show when={props.bottom}> <Show when={props.bottom}>
<div class="Scaffold__bottom-dock" role="presentation">{props.bottom}</div> <div class="Scaffold__bottom-dock">{props.bottom}</div>
</Show> </Show>
</> </>
); );

View file

@ -10,14 +10,12 @@ import {
onCleanup, onCleanup,
Show, Show,
type Component, type Component,
createMemo,
} from "solid-js"; } from "solid-js";
import Scaffold from "../material/Scaffold"; import Scaffold from "../material/Scaffold";
import { import {
AppBar, AppBar,
Avatar, Avatar,
Button, Button,
Checkbox,
CircularProgress, CircularProgress,
Divider, Divider,
IconButton, IconButton,
@ -68,8 +66,6 @@ const Profile: Component = () => {
const time = createTimeSource(); const time = createTimeSource();
const menuButId = createUniqueId(); const menuButId = createUniqueId();
const recentTootListId = createUniqueId();
const optMenuId = createUniqueId();
const [menuOpen, setMenuOpen] = createSignal(false); const [menuOpen, setMenuOpen] = createSignal(false);
@ -91,20 +87,17 @@ const Profile: Component = () => {
); );
onCleanup(() => obx.disconnect()); onCleanup(() => obx.disconnect());
const [profileUncaught] = createResource( const [profileErrorUncaught] = createResource(
() => [session().client, params.id] as const, () => [session().client, params.id] as const,
async ([client, id]) => { async ([client, id]) => {
return await client.v1.accounts.$select(id).fetch(); return await client.v1.accounts.$select(id).fetch();
}, },
); );
const profile = () => { const profile = () =>
try { catchError(profileErrorUncaught, (err) => {
return profileUncaught(); console.error(err);
} catch (reason) { });
console.error(reason);
}
};
const isCurrentSessionProfile = () => { const isCurrentSessionProfile = () => {
return session().account?.inf?.url === profile()?.url; return session().account?.inf?.url === profile()?.url;
@ -133,22 +126,6 @@ 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 bannerImg = () => profile()?.header;
const avatarImg = () => profile()?.avatar; const avatarImg = () => profile()?.avatar;
const displayName = () => const displayName = () =>
@ -160,38 +137,10 @@ const Profile: Component = () => {
recentTootChunk.loading || recentTootChunk.loading ||
(recentTootFilter().pinned && pinnedTootChunk.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 ( return (
<Scaffold <Scaffold
topbar={ topbar={
<AppBar <AppBar
role="navigation"
position="static" position="static"
color={scrolledPastBanner() ? "primary" : "transparent"} color={scrolledPastBanner() ? "primary" : "transparent"}
elevation={scrolledPastBanner() ? undefined : 0} elevation={scrolledPastBanner() ? undefined : 0}
@ -225,10 +174,8 @@ const Profile: Component = () => {
<IconButton <IconButton
id={menuButId} id={menuButId}
aria-controls={optMenuId}
color="inherit" color="inherit"
onClick={[setMenuOpen, true]} onClick={[setMenuOpen, true]}
aria-label="Open Options for the Profile"
> >
<MoreVert /> <MoreVert />
</IconButton> </IconButton>
@ -238,25 +185,12 @@ const Profile: Component = () => {
class="Profile" class="Profile"
> >
<Menu <Menu
id={optMenuId}
open={menuOpen()} open={menuOpen()}
onClose={[setMenuOpen, false]} onClose={[setMenuOpen, false]}
anchor={() => anchor={() =>
document.getElementById(menuButId)!.getBoundingClientRect() document.getElementById(menuButId)!.getBoundingClientRect()
} }
aria-label="Options for the Profile"
> >
<Show when={session().account}>
<MenuItem>
<ListItemAvatar>
<Avatar src={session().account?.inf?.avatar} />
</ListItemAvatar>
<ListItemText secondary={"Default account"}>
<span ref={useSessionDisplayName}></span>
</ListItemText>
{/* <ArrowRight /> // for future */}
</MenuItem>
</Show>
<Show when={session().account && profile()}> <Show when={session().account && profile()}>
<Show <Show
when={isCurrentSessionProfile()} when={isCurrentSessionProfile()}
@ -339,7 +273,6 @@ const Profile: Component = () => {
"margin-top": "margin-top":
"calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))", "calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))",
}} }}
role="presentation"
> >
<img <img
ref={(e) => obx.observe(e)} ref={(e) => obx.observe(e)}
@ -350,8 +283,7 @@ const Profile: Component = () => {
height: "100%", height: "100%",
}} }}
crossOrigin="anonymous" crossOrigin="anonymous"
alt={`Banner image for ${profile()?.displayName || "the user"}`} onLoad={async (event) => {
onLoad={(event) => {
const ins = new FastAverageColor(); const ins = new FastAverageColor();
const colors = ins.getColor(event.currentTarget); const colors = ins.getColor(event.currentTarget);
setBannerSampledColors({ setBannerSampledColors({
@ -364,21 +296,23 @@ const Profile: Component = () => {
</div> </div>
<Menu {...subscribeMenuState}> <Menu {...subscribeMenuState}>
<MenuItem <MenuItem disabled>
onClick={toggleSubscribeHome}
aria-details="Subscribe or Unsubscribe this account on your home timeline"
>
<ListItemAvatar> <ListItemAvatar>
<Avatar src={session().account?.inf?.avatar}></Avatar> <Avatar src={session().account?.inf?.avatar}></Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText>
secondary={relationship()?.following ? "Subscribed" : undefined} <span
> ref={(e) =>
<span ref={useSessionDisplayName}></span> createRenderEffect(() => {
e.innerHTML = resolveCustomEmoji(
session().account?.inf?.displayName || "",
session().account?.inf?.emojis ?? [],
);
})
}
></span>
<span>'s Home</span> <span>'s Home</span>
</ListItemText> </ListItemText>
<Checkbox checked={relationship()?.following ?? false} />
</MenuItem> </MenuItem>
</Menu> </Menu>
@ -389,10 +323,9 @@ const Profile: Component = () => {
color: bannerSampledColors()?.text, color: bannerSampledColors()?.text,
}} }}
> >
<section class="acct-grp"> <div class="acct-grp">
<Avatar <Avatar
src={avatarImg()} src={avatarImg()}
alt={`${profile()?.displayName || "the user"}'s avatar`}
sx={{ sx={{
marginTop: "calc(-16px - 72px / 2)", marginTop: "calc(-16px - 72px / 2)",
width: "72px", width: "72px",
@ -404,19 +337,12 @@ const Profile: Component = () => {
ref={(e) => ref={(e) =>
createRenderEffect(() => (e.innerHTML = displayName())) createRenderEffect(() => (e.innerHTML = displayName()))
} }
aria-label="Display name"
></span> ></span>
<span aria-label="Complete username">{fullUsername()}</span> <span>{fullUsername()}</span>
</div> </div>
<div> <div>
<Switch> <Switch>
<Match <Match when={!session().account || profileErrorUncaught.loading}>
when={
!session().account ||
profileUncaught.loading ||
profileUncaught.error
}
>
{<></>} {<></>}
</Match> </Match>
<Match when={isCurrentSessionProfile()}> <Match when={isCurrentSessionProfile()}>
@ -434,24 +360,20 @@ const Profile: Component = () => {
); );
}} }}
> >
{relationship()?.following ? "Subscribed" : "Subscribe"} Subscribe
</Button> </Button>
</Match> </Match>
</Switch> </Switch>
</div> </div>
</section> </div>
<section <div
class="description" class="description"
aria-label={`${profile()?.displayName || "the user"}'s description`}
ref={(e) => ref={(e) =>
createRenderEffect(() => (e.innerHTML = description() || "")) createRenderEffect(() => (e.innerHTML = description() || ""))
} }
></section> ></div>
<table <table class="acct-fields">
class="acct-fields"
aria-label={`${profile()?.displayName || "the user"}'s fields`}
>
<tbody> <tbody>
<For each={profile()?.fields ?? []}> <For each={profile()?.fields ?? []}>
{(item, index) => { {(item, index) => {
@ -500,7 +422,6 @@ const Profile: Component = () => {
<Divider /> <Divider />
</Show> </Show>
<TootList <TootList
id={recentTootListId}
threads={recentToots.list} threads={recentToots.list}
onUnknownThread={recentToots.getPath} onUnknownThread={recentToots.getPath}
onChangeToot={recentToots.set} onChangeToot={recentToots.set}
@ -516,7 +437,6 @@ const Profile: Component = () => {
> >
<IconButton <IconButton
aria-label="Load More" aria-label="Load More"
aria-controls={recentTootListId}
size="large" size="large"
color="primary" color="primary"
onClick={[refetchRecentToots, "prev"]} onClick={[refetchRecentToots, "prev"]}

View file

@ -31,10 +31,12 @@ import {
import { A, useNavigate } from "@solidjs/router"; import { A, useNavigate } from "@solidjs/router";
import { Title } from "../material/typography.jsx"; import { Title } from "../material/typography.jsx";
import { css } from "solid-styled"; import { css } from "solid-styled";
import { useSignedInProfiles } from "../masto/acct.js";
import { signOut, type Account } from "../accounts/stores.js"; import { signOut, type Account } from "../accounts/stores.js";
import { format } from "date-fns"; import { format } from "date-fns";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { $settings } from "./stores.js"; import { $settings } from "./stores.js";
import { useRegisterSW } from "virtual:pwa-register/solid";
import { import {
autoMatchLangTag, autoMatchLangTag,
autoMatchRegion, autoMatchRegion,
@ -44,7 +46,6 @@ import {
import { type Template } from "@solid-primitives/i18n"; import { type Template } from "@solid-primitives/i18n";
import BottomSheet from "../material/BottomSheet.jsx"; import BottomSheet from "../material/BottomSheet.jsx";
import { useServiceWorker } from "../platform/host.js"; import { useServiceWorker } from "../platform/host.js";
import { useSessions } from "../masto/clients.js";
type Strings = { type Strings = {
["lang.auto"]: Template<{ detected: string }>; ["lang.auto"]: Template<{ detected: string }>;
@ -63,7 +64,7 @@ const Settings: ParentComponent = (props) => {
const { needRefresh, offlineReady } = useServiceWorker(); const { needRefresh, offlineReady } = useServiceWorker();
const dateFnLocale = useDateFnLocale(); const dateFnLocale = useDateFnLocale();
const profiles = useSessions(); const [profiles] = useSignedInProfiles();
const doSignOut = (acct: Account) => { const doSignOut = (acct: Account) => {
signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken); signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken);
@ -117,9 +118,9 @@ const Settings: ParentComponent = (props) => {
<Divider /> <Divider />
</ul> </ul>
<For each={profiles()}> <For each={profiles()}>
{({ account: acct }) => ( {({ account: acct, inf }) => (
<ul data-site={acct.site} data-username={acct.inf?.username}> <ul data-site={acct.site} data-username={inf?.username}>
<ListSubheader>{`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader> <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItemButton disabled> <ListItemButton disabled>
<ListItemText>{t("Notifications")}</ListItemText> <ListItemText>{t("Notifications")}</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>

View file

@ -30,10 +30,10 @@ import { $settings } from "../settings/stores";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { HeroSourceProvider, type HeroSource } from "../platform/anim"; import { HeroSourceProvider, type HeroSource } from "../platform/anim";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { useSignedInProfiles } from "../masto/acct";
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
import TrendTimelinePanel from "./TrendTimelinePanel"; import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { useSessions } from "../masto/clients";
const Home: ParentComponent = (props) => { const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement; let panelList: HTMLDivElement;
@ -42,11 +42,11 @@ const Home: ParentComponent = (props) => {
const settings$ = useStore($settings); const settings$ = useStore($settings);
const profiles = useSessions(); const [profiles] = useSignedInProfiles();
const profile = () => { const profile = () => {
const all = profiles(); const all = profiles();
if (all.length > 0) { if (all.length > 0) {
return all[0].account.inf; return all[0].inf;
} }
}; };
const client = () => { const client = () => {

View file

@ -18,7 +18,6 @@ import { findElementActionable } from "./RegularToot";
const TootList: Component<{ const TootList: Component<{
ref?: Ref<HTMLDivElement>; ref?: Ref<HTMLDivElement>;
id?: string;
threads: readonly string[]; threads: readonly string[];
onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined;
onChangeToot: (id: string, value: mastodon.v1.Status) => void; onChangeToot: (id: string, value: mastodon.v1.Status) => void;
@ -150,7 +149,7 @@ const TootList: Component<{
return <p>Oops: {String(err)}</p>; return <p>Oops: {String(err)}</p>;
}} }}
> >
<div ref={props.ref} id={props.id} class="toot-list"> <div ref={props.ref} class="toot-list">
<For each={props.threads}> <For each={props.threads}>
{(itemId, index) => { {(itemId, index) => {
const path = props.onUnknownThread(itemId)!; const path = props.onUnknownThread(itemId)!;