remove utils
All checks were successful
/ depoly (push) Successful in 1m22s

* add ~platform/DocumentTitle
* add titles for some pages
This commit is contained in:
thislight 2025-01-02 22:43:37 +08:00
parent 1115135380
commit 1c8a3f0bbb
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
12 changed files with 843 additions and 823 deletions

View file

@ -8,13 +8,13 @@ import {
} from "solid-js";
import { acceptAccountViaAuthCode } from "./stores";
import { $settings } from "../settings/stores";
import { useDocumentTitle } from "../utils";
import cards from "~material/cards.module.css";
import { LinearProgress } from "@suid/material";
import Img from "~material/Img";
import { createRestAPIClient } from "masto";
import { Title } from "~material/typography";
import { useNavigator } from "~platform/StackedRouter";
import DocumentTitle from "~platform/DocumentTitle";
type OAuth2CallbackParams = {
code?: string;
@ -27,13 +27,12 @@ const MastodonOAuth2Callback: Component = () => {
const titleId = createUniqueId();
const [params] = useSearchParams<OAuth2CallbackParams>();
const { push: navigate } = useNavigator();
const setDocumentTitle = useDocumentTitle("Back from Mastodon...");
const [siteImg, setSiteImg] = createSignal<{
src: string;
srcset?: string;
blurhash: string;
}>();
const [siteTitle, setSiteTitle] = createSignal("the Mastodon server");
const [siteTitle, setSiteTitle] = createSignal("Mastodon");
onMount(async () => {
const onGoingOAuth2Process = $settings.get().onGoingOAuth2Process;
@ -42,7 +41,6 @@ const MastodonOAuth2Callback: Component = () => {
url: onGoingOAuth2Process,
});
const ins = await client.v2.instance.fetch();
setDocumentTitle(`Back from ${ins.title}...`);
setSiteTitle(ins.title);
const srcset = [];
@ -93,42 +91,45 @@ const MastodonOAuth2Callback: Component = () => {
});
});
return (
<div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
aria-labelledby={titleId}
/>
<Show
when={siteImg()}
fallback={
<i
aria-busy="true"
aria-label="Preparing image..."
style={{ height: "235px", display: "block" }}
></i>
}
>
<Img
src={siteImg()?.src}
srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash}
<>
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
<div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
alt={`Banner image for ${siteTitle()}`}
style={{ height: "235px", display: "block" }}
id={progressId}
aria-labelledby={titleId}
/>
</Show>
<Show
when={siteImg()}
fallback={
<i
aria-busy="true"
aria-label="Preparing image..."
style={{ height: "235px", display: "block" }}
></i>
}
>
<Img
src={siteImg()?.src}
srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash}
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
alt={`Banner image for ${siteTitle()}`}
style={{ height: "235px", display: "block" }}
/>
</Show>
<Title component="h6" id={titleId}>
Contracting {siteTitle}...
</Title>
<p>
If this page stays too long, you can close this page and sign in
again.
</p>
<Title component="h6" id={titleId}>
Contracting {siteTitle}...
</Title>
<p>
If this page stays too long, you can close this page and sign in
again.
</p>
</div>
</div>
</div>
</>
);
};

View file

@ -2,7 +2,6 @@ import {
Component,
Show,
createEffect,
createSelector,
createSignal,
createUniqueId,
onMount,
@ -10,15 +9,14 @@ import {
import cards from "~material/cards.module.css";
import TextField from "~material/TextField.js";
import Button from "~material/Button.js";
import { useDocumentTitle } from "../utils";
import { Title } from "~material/typography";
import { css } from "solid-styled";
import { LinearProgress } from "@suid/material";
import { createRestAPIClient } from "masto";
import { getOrRegisterApp } from "./stores";
import { useSearchParams } from "@solidjs/router";
import { $settings } from "../settings/stores";
import "./SignIn.css";
import DocumentTitle from "~platform/DocumentTitle";
type ErrorParams = {
error: string;
@ -36,8 +34,6 @@ const SignIn: Component = () => {
const [serverUrlError, setServerUrlError] = createSignal(false);
const [targetSiteTitle, setTargetSiteTitle] = createSignal("");
useDocumentTitle("Sign In");
const serverUrl = () => {
const url = rawServerUrl();
if (url.length === 0 || /^%w:/.test(url)) {
@ -115,55 +111,58 @@ const SignIn: Component = () => {
};
return (
<main class="SignIn">
<Show when={params.error || params.errorDescription}>
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
<p>Authorization is failed.</p>
<p>{params.errorDescription}</p>
<p>
Please try again later. If the problem persists, you can ask for
help from the server administrator.
</p>
</div>
</Show>
<div
class={`${cards.card} key-content`}
aria-busy={currentState() !== "inactive" ? "true" : "false"}
aria-describedby={
currentState() !== "inactive" ? progressId : undefined
}
style={{
padding: `var(--safe-area-inset-top) var(--safe-area-inset-right) var(--safe-area-inset-bottom) var(--safe-area-inset-left)`,
}}
>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
sx={currentState() === "inactive" ? { display: "none" } : undefined}
/>
<form onSubmit={onStartOAuth2}>
<Title component="h6">Sign in with Your Mastodon Account</Title>
<TextField
label="Mastodon Server"
name="serverUrl"
onInput={setRawServerUrl}
required
helperText={serverUrlHelperText()}
error={!!serverUrlError()}
/>
<div style={{ display: "flex", "justify-content": "end" }}>
<Button type="submit" disabled={currentState() !== "inactive"}>
{currentState() == "inactive"
? "Continue"
: currentState() == "contracting"
? `Contracting ${new URL(serverUrl()).host}...`
: `Moving to ${targetSiteTitle}`}
</Button>
<>
<DocumentTitle>Sign In</DocumentTitle>
<main class="SignIn">
<Show when={params.error || params.errorDescription}>
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
<p>Authorization is failed.</p>
<p>{params.errorDescription}</p>
<p>
Please try again later. If the problem persists, you can ask for
help from the server administrator.
</p>
</div>
</form>
</div>
</main>
</Show>
<div
class={`${cards.card} key-content`}
aria-busy={currentState() !== "inactive" ? "true" : "false"}
aria-describedby={
currentState() !== "inactive" ? progressId : undefined
}
style={{
padding: `var(--safe-area-inset-top) var(--safe-area-inset-right) var(--safe-area-inset-bottom) var(--safe-area-inset-left)`,
}}
>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
sx={currentState() === "inactive" ? { display: "none" } : undefined}
/>
<form onSubmit={onStartOAuth2}>
<Title component="h6">Sign in with Your Mastodon Account</Title>
<TextField
label="Mastodon Server"
name="serverUrl"
onInput={setRawServerUrl}
required
helperText={serverUrlHelperText()}
error={!!serverUrlError()}
/>
<div style={{ display: "flex", "justify-content": "end" }}>
<Button type="submit" disabled={currentState() !== "inactive"}>
{currentState() == "inactive"
? "Continue"
: currentState() == "contracting"
? `Contracting ${new URL(serverUrl()).host}...`
: `Moving to ${targetSiteTitle}`}
</Button>
</div>
</form>
</div>
</main>
</>
);
};

View file

@ -3,14 +3,12 @@ import {
splitProps,
Component,
createSignal,
createEffect,
onMount,
createRenderEffect,
Show,
} from "solid-js";
import { css } from "solid-styled";
import { decode } from "blurhash";
import { mergeClass } from "../utils";
type ImgProps = {
blurhash?: string;
@ -24,6 +22,7 @@ const Img: Component<ImgProps> = (props) => {
"blurhash",
"keepBlur",
"class",
"classList",
"style",
]);
const [isImgLoaded, setIsImgLoaded] = createSignal(false);
@ -61,21 +60,21 @@ const Img: Component<ImgProps> = (props) => {
const onImgLoaded = () => {
setIsImgLoaded(true);
setImgSize({
width: imgE.width,
height: imgE.height,
width: imgE!.width,
height: imgE!.height,
});
};
const onMetadataLoaded = () => {
setImgSize({
width: imgE.width,
height: imgE.height,
width: imgE!.width,
height: imgE!.height,
});
};
onMount(() => {
setImgSize((x) => {
const parent = imgE.parentElement;
const parent = imgE!.parentElement;
if (!parent) return x;
return x
? x
@ -87,7 +86,14 @@ const Img: Component<ImgProps> = (props) => {
});
return (
<div class={mergeClass(managed.class, "img-root")} style={managed.style}>
<div
classList={{
...managed.classList,
[managed.class ?? ""]: true,
"img-root": true,
}}
style={managed.style}
>
<Show when={managed.blurhash}>
<canvas
ref={(canvas) => {

View file

@ -0,0 +1,22 @@
import { children, createRenderEffect, onCleanup, type JSX } from "solid-js";
/**
* Document title.
*
* The `children` must be plain text.
*/
export default function (props: { children?: JSX.Element }) {
let otitle: string | undefined;
createRenderEffect(() => (otitle = document.title));
const title = children(() => props.children);
createRenderEffect(
() => (document.title = (title.toArray() as string[]).join("")),
);
onCleanup(() => (document.title = otitle!));
return <></>;
}

View file

@ -65,6 +65,7 @@ import {
} from "../timelines/toots/ItemSelectionProvider";
import AppTopBar from "~material/AppTopBar";
import type { Account } from "../accounts/stores";
import DocumentTitle from "~platform/DocumentTitle";
const Profile: Component = () => {
const { pop } = useNavigator();
@ -216,355 +217,360 @@ const Profile: Component = () => {
};
return (
<Scaffold
topbar={
<AppTopBar
role="navigation"
position="static"
color={scrolledPastBanner() ? "primary" : "transparent"}
elevation={scrolledPastBanner() ? undefined : 0}
style={{
color: scrolledPastBanner()
? undefined
: bannerSampledColors()?.text,
}}
>
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
<Close />
</IconButton>
<Title
class="Profile__page-title"
<>
<DocumentTitle>{profile()?.displayName ?? "Someone"}</DocumentTitle>
<Scaffold
topbar={
<AppTopBar
role="navigation"
position="static"
color={scrolledPastBanner() ? "primary" : "transparent"}
elevation={scrolledPastBanner() ? undefined : 0}
style={{
visibility: scrolledPastBanner() ? undefined : "hidden",
color: scrolledPastBanner()
? undefined
: bannerSampledColors()?.text,
}}
innerHTML={displayName()}
></Title>
<IconButton
id={menuButId}
aria-controls={optMenuId}
color="inherit"
onClick={[setMenuOpen, true]}
aria-label="Open Options for the Profile"
>
<MoreVert />
</IconButton>
</AppTopBar>
}
class="Profile"
>
<div class="details" role="presentation">
<Menu
id={optMenuId}
open={menuOpen()}
onClose={[setMenuOpen, false]}
anchor={() =>
document.getElementById(menuButId)!.getBoundingClientRect()
}
aria-label="Options for the Profile"
>
<Show when={session().account}>
<MenuItem>
<ListItemAvatar>
<Avatar src={(session().account as Account).inf?.avatar} />
</ListItemAvatar>
<ListItemText secondary={"Default account"}>
<span innerHTML={sessionDisplayName()}></span>
</ListItemText>
{/* <ArrowRight /> // for future */}
</MenuItem>
</Show>
<Show when={session().account && profile()}>
<Show
when={isCurrentSessionProfile()}
fallback={
<MenuItem
onClick={(event) => {
const { left, right, top } =
event.currentTarget.getBoundingClientRect();
openSubscribeMenu({
left,
right,
top,
e: 1,
});
}}
>
<ListItemIcon>
<PlaylistAdd />
</ListItemIcon>
<ListItemText>Subscribe...</ListItemText>
</MenuItem>
}
<IconButton color="inherit" onClick={[pop, 1]} aria-label="Close">
<Close />
</IconButton>
<Title
class="Profile__page-title"
style={{
visibility: scrolledPastBanner() ? undefined : "hidden",
}}
innerHTML={displayName()}
></Title>
<IconButton
id={menuButId}
aria-controls={optMenuId}
color="inherit"
onClick={[setMenuOpen, true]}
aria-label="Open Options for the Profile"
>
<MenuItem disabled>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText>Edit...</ListItemText>
<MoreVert />
</IconButton>
</AppTopBar>
}
class="Profile"
>
<div class="details" role="presentation">
<Menu
id={optMenuId}
open={menuOpen()}
onClose={[setMenuOpen, false]}
anchor={() =>
document.getElementById(menuButId)!.getBoundingClientRect()
}
aria-label="Options for the Profile"
>
<Show when={session().account}>
<MenuItem>
<ListItemAvatar>
<Avatar src={(session().account as Account).inf?.avatar} />
</ListItemAvatar>
<ListItemText secondary={"Default account"}>
<span innerHTML={sessionDisplayName()}></span>
</ListItemText>
{/* <ArrowRight /> // for future */}
</MenuItem>
</Show>
<Divider />
</Show>
<MenuItem disabled>
<ListItemIcon>
<Group />
</ListItemIcon>
<ListItemText>Followers</ListItemText>
<ListItemSecondaryAction>
<span aria-label="The number of the account follower">
{profile()?.followersCount ?? ""}
</span>
</ListItemSecondaryAction>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<Subject />
</ListItemIcon>
<ListItemText>Following</ListItemText>
<ListItemSecondaryAction>
<span aria-label="The number the account following">
{profile()?.followingCount ?? ""}
</span>
</ListItemSecondaryAction>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<PersonOff />
</ListItemIcon>
<ListItemText>Blocklist</ListItemText>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<Send />
</ListItemIcon>
<ListItemText>Mention in...</ListItemText>
</MenuItem>
<Divider />
<MenuItem
component={"a"}
href={profile()?.url}
target="_blank"
rel="noopener noreferrer"
>
<ListItemIcon>
<OpenInBrowser />
</ListItemIcon>
<ListItemText>Open in browser...</ListItemText>
</MenuItem>
<MenuItem onClick={() => share({ url: profile()?.url })}>
<ListItemIcon>
<Share />
</ListItemIcon>
<ListItemText>Share...</ListItemText>
</MenuItem>
</Menu>
<div
style={{
height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
}}
class="banner"
role="presentation"
>
<img
ref={(e) => obx.observe(e)}
src={bannerImg()}
crossOrigin="anonymous"
alt={`Banner image for ${profile()?.displayName || "the user"}`}
onLoad={(event) => {
const ins = new FastAverageColor();
const colors = ins.getColor(event.currentTarget);
setBannerSampledColors({
average: colors.hex,
text: colors.isDark ? "white" : "black",
});
ins.destroy();
}}
></img>
</div>
<Menu {...subscribeMenuState}>
<MenuItem
onClick={toggleSubscribeHome}
aria-label={`${relationship()?.following ? "Unfollow" : "Follow"} on your home timeline`}
>
<ListItemAvatar>
<Avatar src={(session().account as Account).inf?.avatar}></Avatar>
</ListItemAvatar>
<ListItemText
secondary={
relationship()?.following
? undefined
: profile()?.locked
? "A request will be sent"
: undefined
}
>
<span innerHTML={sessionDisplayName()}></span>
<span>'s Home</span>
</ListItemText>
<Checkbox checked={relationship()?.following ?? false} />
</MenuItem>
</Menu>
<div
class="intro"
style={{
"background-color": bannerSampledColors()?.average,
color: bannerSampledColors()?.text,
}}
>
<section class="acct-grp">
<Avatar
src={avatarImg()}
alt={`${profile()?.displayName || "the user"}'s avatar`}
sx={{
marginTop: "calc(-16px - 72px / 2)",
width: "72px",
height: "72px",
}}
></Avatar>
<div class="name-grp">
<div class="display-name">
<Show when={profile()?.bot}>
<SmartToySharp class="acct-mark" aria-label="Bot" />
</Show>
<Show when={profile()?.locked}>
<Lock class="acct-mark" aria-label="Locked" />
</Show>
<Body2
component="span"
innerHTML={displayName()}
aria-label="Display name"
></Body2>
</div>
<span aria-label="Complete username" class="username">
{fullUsername()}
</span>
</div>
<div role="presentation">
<Switch>
<Match
when={
!session().account ||
profileUncaught.loading ||
profileUncaught.error
}
>
{<></>}
</Match>
<Match when={isCurrentSessionProfile()}>
<IconButton color="inherit">
<Edit />
</IconButton>
</Match>
<Match when={true}>
<Button
variant="contained"
color="secondary"
<Show when={session().account && profile()}>
<Show
when={isCurrentSessionProfile()}
fallback={
<MenuItem
onClick={(event) => {
openSubscribeMenu(
event.currentTarget.getBoundingClientRect(),
);
const { left, right, top } =
event.currentTarget.getBoundingClientRect();
openSubscribeMenu({
left,
right,
top,
e: 1,
});
}}
>
{relationship()?.following ? "Subscribed" : "Subscribe"}
</Button>
</Match>
</Switch>
</div>
</section>
<section
class="description"
aria-label={`${profile()?.displayName || "the user"}'s description`}
innerHTML={description() || ""}
></section>
<table
class="acct-fields"
aria-label={`${profile()?.displayName || "the user"}'s fields`}
>
<tbody>
<For each={profile()?.fields ?? []}>
{(item, index) => {
return (
<tr data-field-index={index()}>
<td>{item.name}</td>
<td>
<Show when={item.verifiedAt}>
<Verified />
</Show>
</td>
<td innerHTML={item.value}></td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</div>
<div class="recent-toots" role="presentation">
<div class="toot-list-toolbar">
<TootFilterButton
options={{
pinned: "Pinneds",
boost: "Boosts",
reply: "Replies",
original: "Originals",
}}
applied={recentTootFilter()}
onApply={setRecentTootFilter}
disabledKeys={["original"]}
></TootFilterButton>
</div>
<ItemSelectionProvider value={selectionState}>
<TimeSourceProvider value={time}>
<Show
when={recentTootFilter().pinned && pinnedToots.list.length > 0}
>
<TootList
threads={pinnedToots.list}
onUnknownThread={pinnedToots.getPath}
onChangeToot={pinnedToots.set}
/>
<ListItemIcon>
<PlaylistAdd />
</ListItemIcon>
<ListItemText>Subscribe...</ListItemText>
</MenuItem>
}
>
<MenuItem disabled>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText>Edit...</ListItemText>
</MenuItem>
</Show>
<Divider />
</Show>
<TootList
id={recentTootListId}
threads={recentToots.list}
onUnknownThread={recentToots.getPath}
onChangeToot={recentToots.set}
/>
</TimeSourceProvider>
</ItemSelectionProvider>
<Show when={!recentTootChunk()?.done}>
<MenuItem disabled>
<ListItemIcon>
<Group />
</ListItemIcon>
<ListItemText>Followers</ListItemText>
<ListItemSecondaryAction>
<span aria-label="The number of the account follower">
{profile()?.followersCount ?? ""}
</span>
</ListItemSecondaryAction>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<Subject />
</ListItemIcon>
<ListItemText>Following</ListItemText>
<ListItemSecondaryAction>
<span aria-label="The number the account following">
{profile()?.followingCount ?? ""}
</span>
</ListItemSecondaryAction>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<PersonOff />
</ListItemIcon>
<ListItemText>Blocklist</ListItemText>
</MenuItem>
<MenuItem disabled>
<ListItemIcon>
<Send />
</ListItemIcon>
<ListItemText>Mention in...</ListItemText>
</MenuItem>
<Divider />
<MenuItem
component={"a"}
href={profile()?.url}
target="_blank"
rel="noopener noreferrer"
>
<ListItemIcon>
<OpenInBrowser />
</ListItemIcon>
<ListItemText>Open in browser...</ListItemText>
</MenuItem>
<MenuItem onClick={() => share({ url: profile()?.url })}>
<ListItemIcon>
<Share />
</ListItemIcon>
<ListItemText>Share...</ListItemText>
</MenuItem>
</Menu>
<div
style={{
"text-align": "center",
"padding-bottom": "var(--safe-area-inset-bottom)",
height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
}}
class="banner"
role="presentation"
>
<img
ref={(e) => obx.observe(e)}
src={bannerImg()}
crossOrigin="anonymous"
alt={`Banner image for ${profile()?.displayName || "the user"}`}
onLoad={(event) => {
const ins = new FastAverageColor();
const colors = ins.getColor(event.currentTarget);
setBannerSampledColors({
average: colors.hex,
text: colors.isDark ? "white" : "black",
});
ins.destroy();
}}
></img>
</div>
<Menu {...subscribeMenuState}>
<MenuItem
onClick={toggleSubscribeHome}
aria-label={`${relationship()?.following ? "Unfollow" : "Follow"} on your home timeline`}
>
<ListItemAvatar>
<Avatar
src={(session().account as Account).inf?.avatar}
></Avatar>
</ListItemAvatar>
<ListItemText
secondary={
relationship()?.following
? undefined
: profile()?.locked
? "A request will be sent"
: undefined
}
>
<span innerHTML={sessionDisplayName()}></span>
<span>'s Home</span>
</ListItemText>
<Checkbox checked={relationship()?.following ?? false} />
</MenuItem>
</Menu>
<div
class="intro"
style={{
"background-color": bannerSampledColors()?.average,
color: bannerSampledColors()?.text,
}}
>
<IconButton
aria-label="Load More"
aria-controls={recentTootListId}
size="large"
color="primary"
onClick={[refetchRecentToots, "prev"]}
disabled={isTootListLoading()}
<section class="acct-grp">
<Avatar
src={avatarImg()}
alt={`${profile()?.displayName || "the user"}'s avatar`}
sx={{
marginTop: "calc(-16px - 72px / 2)",
width: "72px",
height: "72px",
}}
></Avatar>
<div class="name-grp">
<div class="display-name">
<Show when={profile()?.bot}>
<SmartToySharp class="acct-mark" aria-label="Bot" />
</Show>
<Show when={profile()?.locked}>
<Lock class="acct-mark" aria-label="Locked" />
</Show>
<Body2
component="span"
innerHTML={displayName()}
aria-label="Display name"
></Body2>
</div>
<span aria-label="Complete username" class="username">
{fullUsername()}
</span>
</div>
<div role="presentation">
<Switch>
<Match
when={
!session().account ||
profileUncaught.loading ||
profileUncaught.error
}
>
{<></>}
</Match>
<Match when={isCurrentSessionProfile()}>
<IconButton color="inherit">
<Edit />
</IconButton>
</Match>
<Match when={true}>
<Button
variant="contained"
color="secondary"
onClick={(event) => {
openSubscribeMenu(
event.currentTarget.getBoundingClientRect(),
);
}}
>
{relationship()?.following ? "Subscribed" : "Subscribe"}
</Button>
</Match>
</Switch>
</div>
</section>
<section
class="description"
aria-label={`${profile()?.displayName || "the user"}'s description`}
innerHTML={description() || ""}
></section>
<table
class="acct-fields"
aria-label={`${profile()?.displayName || "the user"}'s fields`}
>
<Show when={isTootListLoading()} fallback={<ExpandMore />}>
<CircularProgress sx={{ width: "24px", height: "24px" }} />
</Show>
</IconButton>
<tbody>
<For each={profile()?.fields ?? []}>
{(item, index) => {
return (
<tr data-field-index={index()}>
<td>{item.name}</td>
<td>
<Show when={item.verifiedAt}>
<Verified />
</Show>
</td>
<td innerHTML={item.value}></td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</Show>
</div>
</Scaffold>
</div>
<div class="recent-toots" role="presentation">
<div class="toot-list-toolbar">
<TootFilterButton
options={{
pinned: "Pinneds",
boost: "Boosts",
reply: "Replies",
original: "Originals",
}}
applied={recentTootFilter()}
onApply={setRecentTootFilter}
disabledKeys={["original"]}
></TootFilterButton>
</div>
<ItemSelectionProvider value={selectionState}>
<TimeSourceProvider value={time}>
<Show
when={recentTootFilter().pinned && pinnedToots.list.length > 0}
>
<TootList
threads={pinnedToots.list}
onUnknownThread={pinnedToots.getPath}
onChangeToot={pinnedToots.set}
/>
<Divider />
</Show>
<TootList
id={recentTootListId}
threads={recentToots.list}
onUnknownThread={recentToots.getPath}
onChangeToot={recentToots.set}
/>
</TimeSourceProvider>
</ItemSelectionProvider>
<Show when={!recentTootChunk()?.done}>
<div
style={{
"text-align": "center",
"padding-bottom": "var(--safe-area-inset-bottom)",
}}
>
<IconButton
aria-label="Load More"
aria-controls={recentTootListId}
size="large"
color="primary"
onClick={[refetchRecentToots, "prev"]}
disabled={isTootListLoading()}
>
<Show when={isTootListLoading()} fallback={<ExpandMore />}>
<CircularProgress sx={{ width: "24px", height: "24px" }} />
</Show>
</IconButton>
</div>
</Show>
</div>
</Scaffold>
</>
);
};

View file

@ -26,6 +26,7 @@ import { useStore } from "@nanostores/solid";
import { $settings } from "./stores";
import { useNavigator } from "~platform/StackedRouter";
import AppTopBar from "~material/AppTopBar";
import DocumentTitle from "~platform/DocumentTitle";
const ChooseLang: Component = () => {
const { pop } = useNavigator();
@ -53,67 +54,70 @@ const ChooseLang: Component = () => {
};
return (
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<ArrowBack />
</IconButton>
<Title>{t("Choose Language")}</Title>
</AppTopBar>
}
>
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
<>
<DocumentTitle>{t("Choose Language")}</DocumentTitle>
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<ArrowBack />
</IconButton>
<Title>{t("Choose Language")}</Title>
</AppTopBar>
}
>
<ListItemButton
onClick={() => {
onCodeChange(code() ? undefined : matchedLangCode());
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
>
<ListItemText>
{t("lang.auto", {
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof code() === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_LANGS}>
{(c) => (
<ListItemButton
disabled={typeof code() === "undefined"}
onClick={[onCodeChange, c]}
>
<ListItemText>{t(`lang.${c}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={
code() === c ||
(code() === undefined && matchedLangCode() == c)
}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
</List>
<ListItemButton
onClick={() => {
onCodeChange(code() ? undefined : matchedLangCode());
}}
>
<ListItemText>
{t("lang.auto", {
detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof code() === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_LANGS}>
{(c) => (
<ListItemButton
disabled={typeof code() === "undefined"}
onClick={[onCodeChange, c]}
>
<ListItemText>{t(`lang.${c}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={
code() === c ||
(code() === undefined && matchedLangCode() == c)
}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
</List>
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
<For each={unsupportedLangCodes()}>
{(code) => (
<ListItem>
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
</ListItem>
)}
</For>
<List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
<For each={unsupportedLangCodes()}>
{(code) => (
<ListItem>
<ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
</ListItem>
)}
</For>
</List>
</List>
</List>
</Scaffold>
</Scaffold>
</>
);
};

View file

@ -19,9 +19,10 @@ import { useStore } from "@nanostores/solid";
import { $settings } from "./stores";
import { useNavigator } from "~platform/StackedRouter";
import AppTopBar from "~material/AppTopBar";
import DocumentTitle from "~platform/DocumentTitle";
const Motions: Component = () => {
const {pop} = useNavigator();
const { pop } = useNavigator();
const [t] = createTranslator(
(code) =>
import(`./i18n/${code}.json`) as Promise<{
@ -30,55 +31,58 @@ const Motions: Component = () => {
);
const settings = useStore($settings);
return (
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<>
<DocumentTitle>{t("motions")}</DocumentTitle>
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<ArrowBack />
</IconButton>
<Title>{t("motions")}</Title>
</AppTopBar>
}
>
<List
sx={{
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
}}
</AppTopBar>
}
>
<li>
<ul style={{ "padding-left": 0 }}>
<ListSubheader>{t("motions.gifs")}</ListSubheader>
<ListItemButton
onClick={() =>
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
}
>
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
<ListItemSecondaryAction>
<Switch checked={settings().autoPlayGIFs}></Switch>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
</ul>
</li>
<li>
<ul style={{ "padding-left": 0 }}>
<ListSubheader>{t("motions.vids")}</ListSubheader>
<ListItemButton
onClick={() =>
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
}
>
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
<ListItemSecondaryAction>
<Switch checked={settings().autoPlayVideos}></Switch>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
</ul>
</li>
</List>
</Scaffold>
<List
sx={{
paddingBottom: "calc(var(--safe-area-inset-bottom, 0px) + 16px)",
}}
>
<li>
<ul style={{ "padding-left": 0 }}>
<ListSubheader>{t("motions.gifs")}</ListSubheader>
<ListItemButton
onClick={() =>
$settings.setKey("autoPlayGIFs", !settings().autoPlayGIFs)
}
>
<ListItemText>{t("motions.gifs.autoplay")}</ListItemText>
<ListItemSecondaryAction>
<Switch checked={settings().autoPlayGIFs}></Switch>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
</ul>
</li>
<li>
<ul style={{ "padding-left": 0 }}>
<ListSubheader>{t("motions.vids")}</ListSubheader>
<ListItemButton
onClick={() =>
$settings.setKey("autoPlayVideos", !settings().autoPlayVideos)
}
>
<ListItemText>{t("motions.vids.autoplay")}</ListItemText>
<ListItemSecondaryAction>
<Switch checked={settings().autoPlayVideos}></Switch>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
</ul>
</li>
</List>
</Scaffold>
</>
);
};

View file

@ -24,6 +24,7 @@ import { $settings } from "./stores";
import { useStore } from "@nanostores/solid";
import { useNavigator } from "~platform/StackedRouter";
import AppTopBar from "~material/AppTopBar";
import DocumentTitle from "~platform/DocumentTitle";
const ChooseRegion: Component = () => {
const { pop } = useNavigator();
@ -48,59 +49,62 @@ const ChooseRegion: Component = () => {
};
return (
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<ArrowBack />
</IconButton>
<Title>{t("Choose Region")}</Title>
</AppTopBar>
}
>
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
<>
<DocumentTitle>{t("Choose Region")}</DocumentTitle>
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<ArrowBack />
</IconButton>
<Title>{t("Choose Region")}</Title>
</AppTopBar>
}
>
<ListItemButton
onClick={() => {
onCodeChange(region() ? undefined : matchedRegionCode());
<List
sx={{
paddingBottom: "var(--safe-area-inset-bottom, 0)",
}}
>
<ListItemText>
{t("region.auto", {
detected:
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof region() === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<ListItemButton
onClick={() => {
onCodeChange(region() ? undefined : matchedRegionCode());
}}
>
<ListItemText>
{t("region.auto", {
detected:
t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
})}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={typeof region() === "undefined"} />
</ListItemSecondaryAction>
</ListItemButton>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_REGIONS}>
{(code) => (
<ListItemButton
disabled={typeof region() === "undefined"}
onClick={[onCodeChange, code]}
>
<ListItemText>{t(`region.${code}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={
region() === code ||
(region() === undefined && matchedRegionCode() == code)
}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
<List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
<For each={SUPPORTED_REGIONS}>
{(code) => (
<ListItemButton
disabled={typeof region() === "undefined"}
onClick={[onCodeChange, code]}
>
<ListItemText>{t(`region.${code}`)}</ListItemText>
<ListItemSecondaryAction>
<Radio
checked={
region() === code ||
(region() === undefined && matchedRegionCode() == code)
}
/>
</ListItemSecondaryAction>
</ListItemButton>
)}
</For>
</List>
</List>
</List>
</Scaffold>
</Scaffold>
</>
);
};

View file

@ -41,6 +41,7 @@ import { makeAcctText, useSessions } from "../masto/clients.js";
import { useNavigator } from "~platform/StackedRouter.jsx";
import AppTopBar from "~material/AppTopBar.jsx";
import MastodonLogo from "./MastodonLogo.jsx";
import DocumentTitle from "~platform/DocumentTitle.jsx";
type Inset = {
top?: number;
@ -197,225 +198,228 @@ const Settings: Component = () => {
}
`;
return (
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<CloseIcon />
</IconButton>
<Title>{t("Settings")}</Title>
</AppTopBar>
}
>
<List class="setting-list" use:solid-styled>
<li>
<ul>
<ListSubheader>{t("Accounts")}</ListSubheader>
<ListItemButton disabled>
<ListItemText>{t("All Notifications")}</ListItemText>
<ListItemSecondaryAction>
<Switch value={false} disabled />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton disabled>
<ListItemText>{t("Sign in...")}</ListItemText>
</ListItemButton>
<Divider />
</ul>
<For each={profiles()}>
{({ account: acct }) => (
<ul data-site={acct.site} data-username={acct.inf?.username}>
<ListSubheader>{`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItemButton disabled>
<ListItemText>{t("Notifications")}</ListItemText>
<ListItemSecondaryAction>
<Switch value={false} disabled />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton onClick={[doSignOut, acct]}>
<ListItemIcon>
<Logout />
</ListItemIcon>
<ListItemText>{t("Sign out")}</ListItemText>
</ListItemButton>
<Divider />
</ul>
)}
</For>
</li>
<li>
<ListSubheader>{t("timelines")}</ListSubheader>
<ListItemButton
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!settings$().prefetchTootsDisabled,
)
}
>
<ListItemText secondary={t("Prefetch Toots.2nd")}>
{t("Prefetch Toots")}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={!settings$().prefetchTootsDisabled} />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton component={A} href="./motions">
<ListItemIcon>
<AnimationIcon></AnimationIcon>
</ListItemIcon>
<ListItemText>{t("motions")}</ListItemText>
</ListItemButton>
<Divider />
</li>
<li>
<ListSubheader>{t("storage")}</ListSubheader>
<ListItemButton disabled>
<ListItemIcon>
<DeleteForever />
</ListItemIcon>
<ListItemText secondary={t("storage.cache.UA-managed")}>
{t("storage.cache.clear")}
</ListItemText>
</ListItemButton>
</li>
<li>
<ListSubheader>{t("This Application")}</ListSubheader>
<ListItemButton component={A} href="./language">
<ListItemIcon>
<TranslateIcon />
</ListItemIcon>
<ListItemText
secondary={
settings$().language === undefined
? t("lang.auto", {
detected:
t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
})
: t("lang." + settings$().language)
}
>
{t("Language")}
</ListItemText>
</ListItemButton>
<Divider />
<ListItemButton component={A} href="./region">
<ListItemIcon>
<PublicIcon />
</ListItemIcon>
<ListItemText
secondary={
settings$().region === undefined
? t("region.auto", {
detected:
t("region." + autoMatchRegion()) ?? autoMatchRegion(),
})
: t("region." + settings$().region)
}
>
{t("Region")}
</ListItemText>
</ListItemButton>
<Divider />
<ListItem
secondaryAction={
<IconButton
component={A}
aria-label={t("mastodonlink.open")}
href={`/${encodeURIComponent(profiles().length > 0 ? makeAcctText(profiles()[0]) : "@")}/profile/@tutu@indieweb.social`}
>
<MastodonLogo />
</IconButton>
}
>
<ListItemText secondary={t("About Tutu.2nd")}>
{t("About Tutu")}
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemText
secondary={t("version", {
packageVersion: import.meta.env.PACKAGE_VERSION,
builtAt: format(
import.meta.env.BUILT_AT,
t("datefmt") || "yyyy/MM/dd",
{ locale: dateFnLocale() },
),
buildMode: import.meta.env.MODE,
})}
>
{needRefresh() ? t("updates.ready") : t("updates.no")}
</ListItemText>
<Show when={needRefresh()}>
<ListItemSecondaryAction>
<IconButton
aria-label="Restart Now"
onClick={() => window.location.reload()}
>
<RefreshIcon />
</IconButton>
</ListItemSecondaryAction>
</Show>
</ListItem>
<Divider />
{import.meta.env.VITE_CODE_VERSION ? (
<>
<ListItem>
<ListItemText secondary={import.meta.env.VITE_CODE_VERSION}>
{t("version.code")}
</ListItemText>
</ListItem>
<Divider />
</>
) : (
<></>
)}
</li>
{import.meta.env.DEV ? (
<>
<DocumentTitle>{t("Settings")}</DocumentTitle>
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={[pop, 1]} disableRipple>
<CloseIcon />
</IconButton>
<Title>{t("Settings")}</Title>
</AppTopBar>
}
>
<List class="setting-list" use:solid-styled>
<li>
<ListSubheader>Developer Tools</ListSubheader>
<ListItem
secondaryAction={
window.screen?.orientation ? (
<NativeSelect
sx={{ maxWidth: "40vw" }}
onChange={(event) => {
const k = event.currentTarget.value;
setupSafeAreaEmulation(k);
}}
>
<option>Don't change</option>
<option value={"ua"}>User agent</option>
<option value={"iphone15"}>
iPhone 15 and Plus, Pro, Pro Max
</option>
<option value={"iphone12"}>iPhone 12, 13 and 14</option>
<option value={"iphone13mini"}>iPhone 13 mini</option>
</NativeSelect>
) : undefined
<ul>
<ListSubheader>{t("Accounts")}</ListSubheader>
<ListItemButton disabled>
<ListItemText>{t("All Notifications")}</ListItemText>
<ListItemSecondaryAction>
<Switch value={false} disabled />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton disabled>
<ListItemText>{t("Sign in...")}</ListItemText>
</ListItemButton>
<Divider />
</ul>
<For each={profiles()}>
{({ account: acct }) => (
<ul data-site={acct.site} data-username={acct.inf?.username}>
<ListSubheader>{`@${acct.inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItemButton disabled>
<ListItemText>{t("Notifications")}</ListItemText>
<ListItemSecondaryAction>
<Switch value={false} disabled />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton onClick={[doSignOut, acct]}>
<ListItemIcon>
<Logout />
</ListItemIcon>
<ListItemText>{t("Sign out")}</ListItemText>
</ListItemButton>
<Divider />
</ul>
)}
</For>
</li>
<li>
<ListSubheader>{t("timelines")}</ListSubheader>
<ListItemButton
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!settings$().prefetchTootsDisabled,
)
}
>
<ListItemText secondary={t("Prefetch Toots.2nd")}>
{t("Prefetch Toots")}
</ListItemText>
<ListItemSecondaryAction>
<Switch checked={!settings$().prefetchTootsDisabled} />
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton component={A} href="./motions">
<ListItemIcon>
<AnimationIcon></AnimationIcon>
</ListItemIcon>
<ListItemText>{t("motions")}</ListItemText>
</ListItemButton>
<Divider />
</li>
<li>
<ListSubheader>{t("storage")}</ListSubheader>
<ListItemButton disabled>
<ListItemIcon>
<DeleteForever />
</ListItemIcon>
<ListItemText secondary={t("storage.cache.UA-managed")}>
{t("storage.cache.clear")}
</ListItemText>
</ListItemButton>
</li>
<li>
<ListSubheader>{t("This Application")}</ListSubheader>
<ListItemButton component={A} href="./language">
<ListItemIcon>
<TranslateIcon />
</ListItemIcon>
<ListItemText
secondary={
window.screen?.orientation
? undefined
: "Unsupported on This Platform"
settings$().language === undefined
? t("lang.auto", {
detected:
t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
})
: t("lang." + settings$().language)
}
>
Safe Area Insets
{t("Language")}
</ListItemText>
</ListItemButton>
<Divider />
<ListItemButton component={A} href="./region">
<ListItemIcon>
<PublicIcon />
</ListItemIcon>
<ListItemText
secondary={
settings$().region === undefined
? t("region.auto", {
detected:
t("region." + autoMatchRegion()) ?? autoMatchRegion(),
})
: t("region." + settings$().region)
}
>
{t("Region")}
</ListItemText>
</ListItemButton>
<Divider />
<ListItem
secondaryAction={
<IconButton
component={A}
aria-label={t("mastodonlink.open")}
href={`/${encodeURIComponent(profiles().length > 0 ? makeAcctText(profiles()[0]) : "@")}/profile/@tutu@indieweb.social`}
>
<MastodonLogo />
</IconButton>
}
>
<ListItemText secondary={t("About Tutu.2nd")}>
{t("About Tutu")}
</ListItemText>
</ListItem>
<Divider />
<ListItem>
<ListItemText
secondary={t("version", {
packageVersion: import.meta.env.PACKAGE_VERSION,
builtAt: format(
import.meta.env.BUILT_AT,
t("datefmt") || "yyyy/MM/dd",
{ locale: dateFnLocale() },
),
buildMode: import.meta.env.MODE,
})}
>
{needRefresh() ? t("updates.ready") : t("updates.no")}
</ListItemText>
<Show when={needRefresh()}>
<ListItemSecondaryAction>
<IconButton
aria-label="Restart Now"
onClick={() => window.location.reload()}
>
<RefreshIcon />
</IconButton>
</ListItemSecondaryAction>
</Show>
</ListItem>
<Divider />
{import.meta.env.VITE_CODE_VERSION ? (
<>
<ListItem>
<ListItemText secondary={import.meta.env.VITE_CODE_VERSION}>
{t("version.code")}
</ListItemText>
</ListItem>
<Divider />
</>
) : (
<></>
)}
</li>
) : (
<></>
)}
</List>
</Scaffold>
{import.meta.env.DEV ? (
<li>
<ListSubheader>Developer Tools</ListSubheader>
<ListItem
secondaryAction={
window.screen?.orientation ? (
<NativeSelect
sx={{ maxWidth: "40vw" }}
onChange={(event) => {
const k = event.currentTarget.value;
setupSafeAreaEmulation(k);
}}
>
<option>Don't change</option>
<option value={"ua"}>User agent</option>
<option value={"iphone15"}>
iPhone 15 and Plus, Pro, Pro Max
</option>
<option value={"iphone12"}>iPhone 12, 13 and 14</option>
<option value={"iphone13mini"}>iPhone 13 mini</option>
</NativeSelect>
) : undefined
}
>
<ListItemText
secondary={
window.screen?.orientation
? undefined
: "Unsupported on This Platform"
}
>
Safe Area Insets
</ListItemText>
</ListItem>
<Divider />
</li>
) : (
<></>
)}
</List>
</Scaffold>
</>
);
};

View file

@ -5,7 +5,6 @@ import {
createEffect,
useTransition,
} from "solid-js";
import { useDocumentTitle } from "../utils";
import Scaffold from "~material/Scaffold";
import {
ListItemSecondaryAction,
@ -30,6 +29,7 @@ import {
import AppTopBar from "~material/AppTopBar";
import { createTranslator } from "~platform/i18n";
import { useWindowSize } from "@solid-primitives/resize-observer";
import DocumentTitle from "~platform/DocumentTitle";
type StringRes = Record<
"tabs.home" | "tabs.trending" | "tabs.public" | "set.prefetch-toots",
@ -38,7 +38,6 @@ type StringRes = Record<
const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement;
useDocumentTitle("Timelines");
const [t] = createTranslator(
(code) => import(`./i18n/${code}.json`) as Promise<{ default: StringRes }>,
);
@ -179,6 +178,7 @@ const Home: ParentComponent = (props) => {
return (
<>
<DocumentTitle>Timelines</DocumentTitle>
<Scaffold
topbar={
<AppTopBar>

View file

@ -14,7 +14,6 @@ import cards from "~material/cards.module.css";
import { css } from "solid-styled";
import { createTimeSource, TimeSourceProvider } from "~platform/timesrc";
import TootComposer from "./TootComposer";
import { useDocumentTitle } from "../utils";
import { createTimelineControlsForArray } from "../masto/timelines";
import TootList from "./TootList";
import "./TootBottomSheet.css";
@ -26,6 +25,7 @@ import ItemSelectionProvider, {
import AppTopBar from "~material/AppTopBar";
import { fetchStatus } from "../masto/statuses";
import { type Account } from "../accounts/stores";
import DocumentTitle from "~platform/DocumentTitle";
const TootBottomSheet: Component = (props) => {
const params = useParams<{ acct: string; id: string }>();
@ -65,11 +65,11 @@ const TootBottomSheet: Component = (props) => {
() => tootContext()?.descendants,
);
useDocumentTitle(() => {
const documentTitle = () => {
const t = toot()?.reblog ?? toot();
const name = t?.account.displayName ?? "Someone";
return `${name}'s toot`;
});
};
const tootDisplayName = () => {
const t = toot()?.reblog ?? toot();
@ -163,6 +163,7 @@ const TootBottomSheet: Component = (props) => {
}
class="TootBottomSheet"
>
<DocumentTitle>{documentTitle()}</DocumentTitle>
<div class="Scrollable">
<TimeSourceProvider value={time}>
<ItemSelectionProvider value={selectionState}>

View file

@ -1,31 +0,0 @@
import {
createRenderEffect,
onCleanup,
type Accessor,
} from "solid-js";
export function useDocumentTitle(newTitle?: string | Accessor<string>) {
const capturedTitle = document.title;
createRenderEffect(() => {
if (newTitle)
document.title = typeof newTitle === "string" ? newTitle : newTitle();
});
onCleanup(() => {
document.title = capturedTitle;
});
return (x: ((x: string) => string) | string) =>
(document.title = typeof x === "string" ? x : x(document.title));
}
export function mergeClass(c1: string | undefined, c2: string | undefined) {
if (!c1) {
return c2;
}
if (!c2) {
return c1;
}
return [c1, c2].join(" ");
}