Compare commits

...

5 commits

Author SHA1 Message Date
thislight
5d6eb7282a
use innerHTML property
All checks were successful
/ depoly (push) Successful in 1m18s
2024-11-24 17:16:06 +08:00
thislight
f56b92fff0
TootAuthorGroup: remove css module 2024-11-24 17:07:14 +08:00
thislight
edfbf5505b
TootLangPicker: fix scrolling 2024-11-24 16:55:40 +08:00
thislight
2c35950d27
Masonry: fix do not reflow in resizing 2024-11-24 16:46:00 +08:00
thislight
5199f2bb6a
Masonry: fix typo 2024-11-24 16:00:56 +08:00
15 changed files with 144 additions and 179 deletions

2
.env
View file

@ -2,4 +2,4 @@ DEV_SERVER_HTTPS_CERT_BASE=
DEV_SERVER_HTTPS_CERT_PASS= DEV_SERVER_HTTPS_CERT_PASS=
DEV_LOCATOR_EDITOR=vscode DEV_LOCATOR_EDITOR=vscode
VITE_DEVTOOLS_OVERLAY=true VITE_DEVTOOLS_OVERLAY=true
VITE_PLATFROM_MASONRY_ALWAYS_COMPAT= VITE_PLATFORM_MASONRY_ALWAYS_COMPAT=

View file

@ -20,17 +20,6 @@ export function resolveCustomEmoji(
}); });
} }
export function appliedCustomEmoji(
target: { innerHTML: string },
content: string,
emojis?: mastodon.v1.CustomEmoji[],
) {
createRenderEffect(() => {
const result = emojis ? resolveCustomEmoji(content, emojis) : content;
target.innerHTML = result;
});
}
export function hasCustomEmoji(s: string) { export function hasCustomEmoji(s: string) {
return CUSTOM_EMOJI_REGEX.test(s); return CUSTOM_EMOJI_REGEX.test(s);
} }

2
src/overrides.d.ts vendored
View file

@ -14,7 +14,7 @@ interface ImportMetaEnv {
/** /**
* Always use compatible version of Masonry. * Always use compatible version of Masonry.
*/ */
readonly VITE_PLATFROM_MASONRY_ALWAYS_COMPAT?: string readonly VITE_PLATFORM_MASONRY_ALWAYS_COMPAT?: string
} }
interface ImportMeta { interface ImportMeta {

View file

@ -46,30 +46,49 @@ function createCompatMasonry(
const size = createElementSize(element); const size = createElementSize(element);
const treeMutObx = new MutationObserver(() => { let layoutNotRequested = true;
layout.reloadItems?.();
});
onCleanup(() => treeMutObx.disconnect()); const reflow = () => {
layout.layout?.();
layoutNotRequested = true;
};
createRenderEffect(() => { createRenderEffect(() => {
const opts = options(); const opts = options();
layout.option?.(opts); layout.option?.(opts);
}); });
const treeMutObx = new MutationObserver(() => {
layout.reloadItems?.();
if (layoutNotRequested) {
layoutNotRequested = false;
requestAnimationFrame(reflow);
}
});
onCleanup(() => treeMutObx.disconnect());
createRenderEffect(() => { createRenderEffect(() => {
treeMutObx.observe(element, { childList: true }); treeMutObx.observe(element, { childList: true });
}); });
createRenderEffect(() => { createRenderEffect(() => {
const width = size.width; // only tracking width const width = size.width; // only tracking width
layout.layout?.(); if (layoutNotRequested) {
layoutNotRequested = false;
requestAnimationFrame(reflow);
}
}); });
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.on("vite:afterUpdate", () => { const onHotReloadUpdate = () => {
layout.layout?.(); if (layoutNotRequested) {
}); layoutNotRequested = false;
requestAnimationFrame(reflow);
}
};
import.meta.hot.on("vite:afterUpdate", onHotReloadUpdate);
import.meta.hot.off("vite:afterUpdate", onHotReloadUpdate);
} }
} }
@ -80,11 +99,11 @@ const supportsCSSMasonryLayout = /* @__PURE__ */ CSS.supports(
console.debug("supports css masonry layout", supportsCSSMasonryLayout); console.debug("supports css masonry layout", supportsCSSMasonryLayout);
const useNativeImpl = import.meta.env.VITE_PLATFROM_MASONRY_ALWAYS_COMPAT const useNativeImpl = import.meta.env.VITE_PLATFORM_MASONRY_ALWAYS_COMPAT
? false ? false
: supportsCSSMasonryLayout; : supportsCSSMasonryLayout;
if (import.meta.env.VITE_PLATFROM_MASONRY_ALWAYS_COMPAT) { if (import.meta.env.VITE_PLATFORM_MASONRY_ALWAYS_COMPAT) {
console.warn( console.warn(
"Masonry is in compat mode because VITE_PLATFORM_MASONRY_ALWAYS_COMPAT is enabled", "Masonry is in compat mode because VITE_PLATFORM_MASONRY_ALWAYS_COMPAT is enabled",
); );

View file

@ -171,10 +171,6 @@ const Profile: Component = () => {
), ),
); );
const useSessionDisplayName = (e: HTMLElement) => {
createRenderEffect(() => (e.innerHTML = sessionDisplayName()));
};
const toggleSubscribeHome = async (event: Event) => { const toggleSubscribeHome = async (event: Event) => {
const client = session().client; const client = session().client;
if (!session().account) return; if (!session().account) return;
@ -218,9 +214,7 @@ const Profile: Component = () => {
style={{ style={{
visibility: scrolledPastBanner() ? undefined : "hidden", visibility: scrolledPastBanner() ? undefined : "hidden",
}} }}
ref={(e: HTMLElement) => innerHTML={displayName()}
createRenderEffect(() => (e.innerHTML = displayName()))
}
></Title> ></Title>
<IconButton <IconButton
@ -253,7 +247,7 @@ const Profile: Component = () => {
<Avatar src={session().account?.inf?.avatar} /> <Avatar src={session().account?.inf?.avatar} />
</ListItemAvatar> </ListItemAvatar>
<ListItemText secondary={"Default account"}> <ListItemText secondary={"Default account"}>
<span ref={useSessionDisplayName}></span> <span innerHTML={sessionDisplayName()}></span>
</ListItemText> </ListItemText>
{/* <ArrowRight /> // for future */} {/* <ArrowRight /> // for future */}
</MenuItem> </MenuItem>
@ -384,7 +378,7 @@ const Profile: Component = () => {
: undefined : undefined
} }
> >
<span ref={useSessionDisplayName}></span> <span innerHTML={sessionDisplayName()}></span>
<span>'s Home</span> <span>'s Home</span>
</ListItemText> </ListItemText>
@ -419,9 +413,7 @@ const Profile: Component = () => {
</Show> </Show>
<Body2 <Body2
component="span" component="span"
ref={(e: HTMLElement) => innerHTML={displayName()}
createRenderEffect(() => (e.innerHTML = displayName()))
}
aria-label="Display name" aria-label="Display name"
></Body2> ></Body2>
</div> </div>
@ -464,9 +456,7 @@ const Profile: Component = () => {
<section <section
class="description" class="description"
aria-label={`${profile()?.displayName || "the user"}'s description`} aria-label={`${profile()?.displayName || "the user"}'s description`}
ref={(e) => innerHTML={description() || ""}
createRenderEffect(() => (e.innerHTML = description() || ""))
}
></section> ></section>
<table <table
@ -484,11 +474,7 @@ const Profile: Component = () => {
<Verified /> <Verified />
</Show> </Show>
</td> </td>
<td <td innerHTML={item.value}></td>
ref={(e) => {
createRenderEffect(() => (e.innerHTML = item.value));
}}
></td>
</tr> </tr>
); );
}} }}

View file

@ -10,23 +10,19 @@ import {
createContext, createContext,
useContext, useContext,
} from "solid-js"; } from "solid-js";
import tootStyle from "./toot.module.css";
import { formatRelative } from "date-fns";
import Img from "~material/Img.js";
import { Body2 } from "~material/typography.js"; import { Body2 } from "~material/typography.js";
import { SmartToySharp, Lock } from "@suid/icons-material";
import { useTimeSource } from "~platform/timesrc.js"; import { useTimeSource } from "~platform/timesrc.js";
import { resolveCustomEmoji } from "../masto/toot.js"; import { resolveCustomEmoji } from "../masto/toot.js";
import { Divider } from "@suid/material"; import { Divider } from "@suid/material";
import cardStyle from "~material/cards.module.css"; import cardStyle from "~material/cards.module.css";
import MediaAttachmentGrid from "./toots/MediaAttachmentGrid.jsx"; import MediaAttachmentGrid from "./toots/MediaAttachmentGrid.jsx";
import { useDateFnLocale } from "~platform/i18n";
import { makeAcctText, useDefaultSession } from "../masto/clients"; import { makeAcctText, useDefaultSession } from "../masto/clients";
import TootContent from "./toots/TootContent"; import TootContent from "./toots/TootContent";
import BoostIcon from "./toots/BoostIcon"; import BoostIcon from "./toots/BoostIcon";
import PreviewCard from "./toots/PreviewCard"; import PreviewCard from "./toots/PreviewCard";
import TootPoll from "./toots/TootPoll"; import TootPoll from "./toots/TootPoll";
import TootActionGroup from "./toots/TootActionGroup.js"; import TootActionGroup from "./toots/TootActionGroup.js";
import TootAuthorGroup from "./toots/TootAuthorGroup.js";
import "./RegularToot.css"; import "./RegularToot.css";
export type TootEnv = { export type TootEnv = {
@ -66,63 +62,15 @@ type RegularTootProps = {
export function findRootToot(element: HTMLElement) { export function findRootToot(element: HTMLElement) {
let current: HTMLElement | null = element; let current: HTMLElement | null = element;
while (current && !current.classList.contains(tootStyle.toot)) { while (current && !current.classList.contains("RegularToot")) {
current = current.parentElement; current = current.parentElement;
} }
if (!current) { if (!current) {
throw Error( throw Error(`the element must be placed under a element with .RegularToot`);
`the element must be placed under a element with ${tootStyle.toot}`,
);
} }
return current; return current;
} }
function TootAuthorGroup(
props: {
status: mastodon.v1.Status;
now: Date;
} & JSX.HTMLElementTags["div"],
) {
const [managed, rest] = splitProps(props, ["status", "now"]);
const toot = () => managed.status;
const dateFnLocale = useDateFnLocale();
return (
<div class={tootStyle.tootAuthorGrp} {...rest}>
<Img src={toot().account.avatar} class={tootStyle.tootAvatar} />
<div class={tootStyle.tootAuthorNameGrp}>
<div class={tootStyle.tootAuthorNamePrimary}>
<Show when={toot().account.bot}>
<SmartToySharp class="acct-mark" aria-label="Bot" />
</Show>
<Show when={toot().account.locked}>
<Lock class="acct-mark" aria-label="Locked" />
</Show>
<Body2
component="span"
ref={(e: { innerHTML: string }) => {
createRenderEffect(() => {
e.innerHTML = resolveCustomEmoji(
toot().account.displayName,
toot().account.emojis,
);
});
}}
/>
</div>
<time datetime={toot().createdAt}>
{formatRelative(toot().createdAt, managed.now, {
locale: dateFnLocale(),
})}
</time>
<span>
@{toot().account.username}@{new URL(toot().account.url).hostname}
</span>
</div>
</div>
);
}
/** /**
* find bottom-to-top the element with `data-action`. * find bottom-to-top the element with `data-action`.
*/ */
@ -195,8 +143,8 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
<> <>
<article <article
classList={{ classList={{
"RegularToot": true, RegularToot: true,
"expanded": props.evaluated, expanded: props.evaluated,
"thread-top": props.thread === "top", "thread-top": props.thread === "top",
"thread-mid": props.thread === "middle", "thread-mid": props.thread === "middle",
"thread-btm": props.thread === "bottom", "thread-btm": props.thread === "bottom",
@ -210,14 +158,10 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
<div class="retoot-grp"> <div class="retoot-grp">
<BoostIcon /> <BoostIcon />
<Body2 <Body2
ref={(e: { innerHTML: string }) => { innerHTML={resolveCustomEmoji(
createRenderEffect(() => { status().account.displayName,
e.innerHTML = resolveCustomEmoji( toot().emojis,
status().account.displayName, )}
toot().emojis,
);
});
}}
></Body2> ></Body2>
<span>boosts</span> <span>boosts</span>
</div> </div>

View file

@ -261,13 +261,7 @@ const TootBottomSheet: Component = (props) => {
> >
<BackButton color="inherit" /> <BackButton color="inherit" />
<Title component="div" class="name" use:solid-styled> <Title component="div" class="name" use:solid-styled>
<span <span innerHTML={tootDisplayName() ?? "Someone"}></span>
ref={(e: HTMLElement) =>
createRenderEffect(
() => (e.innerHTML = tootDisplayName() ?? "Someone"),
)
}
></span>
<span>'s toot</span> <span>'s toot</span>
</Title> </Title>
</Toolbar> </Toolbar>

View file

@ -44,7 +44,7 @@ import "./TootComposer.css";
import BottomSheet from "~material/BottomSheet"; import BottomSheet from "~material/BottomSheet";
import { useAppLocale } from "~platform/i18n"; import { useAppLocale } from "~platform/i18n";
import iso639_1 from "iso-639-1"; import iso639_1 from "iso-639-1";
import ChooseTootLang from "./ChooseTootLang"; import ChooseTootLang from "./TootLangPicker";
import type { mastodon } from "masto"; import type { mastodon } from "masto";
import cardStyles from "~material/cards.module.css"; import cardStyles from "~material/cards.module.css";
import Menu, { createManagedMenuState } from "~material/Menu"; import Menu, { createManagedMenuState } from "~material/Menu";
@ -349,15 +349,10 @@ const TootComposer: Component<{
</ListItemAvatar> </ListItemAvatar>
<ListItemText secondary={"Default account"}> <ListItemText secondary={"Default account"}>
<span <span
ref={(e) => { innerHTML={resolveCustomEmoji(
createRenderEffect(() => { session()?.account.inf?.displayName || "",
const inf = session()?.account.inf; session()?.account.inf?.emojis ?? [],
return (e.innerHTML = resolveCustomEmoji( )}
inf?.displayName || "",
inf?.emojis ?? [],
));
});
}}
></span> ></span>
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>

View file

@ -0,0 +1,4 @@
.TootLangPicker {
overflow: auto;
}

View file

@ -1,9 +1,4 @@
import { import { For, onMount, type Component, type JSX } from "solid-js";
For,
onMount,
type Component,
type JSX,
} from "solid-js";
import Scaffold from "~material/Scaffold"; import Scaffold from "~material/Scaffold";
import { import {
AppBar, AppBar,
@ -19,6 +14,7 @@ import { Close as CloseIcon } from "@suid/icons-material";
import iso639_1 from "iso-639-1"; import iso639_1 from "iso-639-1";
import { createTranslator } from "~platform/i18n"; import { createTranslator } from "~platform/i18n";
import { Title } from "~material/typography"; import { Title } from "~material/typography";
import "./TootLangPicker.css";
type ChooseTootLangProps = { type ChooseTootLangProps = {
code: string; code: string;
@ -58,6 +54,7 @@ const ChooseTootLang: Component<ChooseTootLangProps> = (props) => {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
} }
class="TootLangPicker"
> >
<List <List
ref={listRef!} ref={listRef!}

View file

@ -1,4 +1,4 @@
.tootAuthorGrp { .TootAuthorGroup {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
@ -10,7 +10,7 @@
} }
} }
.tootAuthorNameGrp { .TootAuthorGroup > .name-grp {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
color: var(--tutu-color-secondary-text-on-surface); color: var(--tutu-color-secondary-text-on-surface);
@ -30,10 +30,10 @@
} }
} }
.tootAuthorNamePrimary { .TootAuthorGroup > .name-grp > .name-primary {
color: var(--tutu-color-on-surface); color: var(--tutu-color-on-surface);
> :global(.acct-mark) { > .acct-mark {
font-size: 1.2em; font-size: 1.2em;
color: var(--tutu-color-secondary-text-on-surface); color: var(--tutu-color-secondary-text-on-surface);
vertical-align: sub; vertical-align: sub;
@ -41,7 +41,7 @@
} }
} }
.tootAvatar { .TootAuthorGroup > .avatar {
width: calc(var(--toot-avatar-size, 40px) - 1px); width: calc(var(--toot-avatar-size, 40px) - 1px);
aspect-ratio: 1/1; aspect-ratio: 1/1;
object-fit: contain; object-fit: contain;
@ -49,4 +49,4 @@
overflow: hidden; overflow: hidden;
border: 1px solid var(--tutu-color-surface); border: 1px solid var(--tutu-color-surface);
background-color: var(--tutu-color-surface-d); background-color: var(--tutu-color-surface-d);
} }

View file

@ -0,0 +1,53 @@
import { SmartToySharp, Lock } from "@suid/icons-material";
import { formatRelative } from "date-fns";
import type { mastodon } from "masto";
import { createRenderEffect, Show, splitProps, type JSX } from "solid-js";
import Img from "~material/Img";
import { Body2 } from "~material/typography";
import { useAppLocale } from "~platform/i18n";
import { resolveCustomEmoji } from "../../masto/toot";
import "./TootAuthorGroup.css";
function TootAuthorGroup(
props: {
status: mastodon.v1.Status;
now: Date;
} & JSX.HTMLElementTags["div"],
) {
const [managed, rest] = splitProps(props, ["status", "now"]);
const toot = () => managed.status;
const { dateFn: dateFnLocale } = useAppLocale();
return (
<div class="TootAuthorGroup" {...rest}>
<Img src={toot().account.avatar} class="avatar" />
<div class={"name-grp"}>
<div class="name-primary">
<Show when={toot().account.bot}>
<SmartToySharp class="acct-mark" aria-label="Bot" />
</Show>
<Show when={toot().account.locked}>
<Lock class="acct-mark" aria-label="Locked" />
</Show>
<Body2
component="span"
innerHTML={resolveCustomEmoji(
toot().account.displayName,
toot().account.emojis,
)}
/>
</div>
<time datetime={toot().createdAt}>
{formatRelative(toot().createdAt, managed.now, {
locale: dateFnLocale(),
})}
</time>
<span>
@{toot().account.username}@{new URL(toot().account.url).hostname}
</span>
</div>
</div>
);
}
export default TootAuthorGroup;

View file

@ -81,15 +81,13 @@ const TootContent: Component<TootContentProps> = (oprops) => {
<Show when={props.sensitive}> <Show when={props.sensitive}>
<div> <div>
<span <span
ref={(ref) => { innerHTML={
createRenderEffect(() => { props.spoilerText
ref.innerHTML = props.spoilerText ? props.emojis
? props.emojis ? resolveCustomEmoji(props.spoilerText, props.emojis)
? resolveCustomEmoji(props.spoilerText, props.emojis) : props.spoilerText
: props.spoilerText : ""
: ""; }
});
}}
></span> ></span>
<Button onClick={props.onToggleReveal}>{t("cw")}</Button> <Button onClick={props.onToggleReveal}>{t("cw")}</Button>
</div> </div>
@ -97,14 +95,12 @@ const TootContent: Component<TootContentProps> = (oprops) => {
<Show when={shouldRevealContent()}> <Show when={shouldRevealContent()}>
<div <div
class="content" class="content"
ref={(ref) => innerHTML={
createRenderEffect(() => { props.source
ref.innerHTML = props.source ? props.emojis
? props.emojis ? resolveCustomEmoji(props.source, props.emojis)
? resolveCustomEmoji(props.source, props.emojis) : props.source
: props.source : ""
: "";
})
} }
></div> ></div>
</Show> </Show>

View file

@ -20,10 +20,7 @@ import {
ListItemText, ListItemText,
Radio, Radio,
} from "@suid/material"; } from "@suid/material";
import { import { formatDistance, isBefore } from "date-fns";
formatDistance,
isBefore,
} from "date-fns";
import { useTimeSource } from "~platform/timesrc"; import { useTimeSource } from "~platform/timesrc";
import { useDateFnLocale } from "~platform/i18n"; import { useDateFnLocale } from "~platform/i18n";
import TootPollDialog from "./TootPollDialog"; import TootPollDialog from "./TootPollDialog";
@ -31,13 +28,13 @@ import { ANIM_CURVE_STD } from "~material/theme";
import { useTootEnv } from "../RegularToot"; import { useTootEnv } from "../RegularToot";
type TootPollProps = { type TootPollProps = {
value: mastodon.v1.Poll value: mastodon.v1.Poll;
status: mastodon.v1.Status status: mastodon.v1.Status;
}; };
const TootPoll: Component<TootPollProps> = (props) => { const TootPoll: Component<TootPollProps> = (props) => {
let list: HTMLUListElement; let list: HTMLUListElement;
const {vote}= useTootEnv() const { vote } = useTootEnv();
const now = useTimeSource(); const now = useTimeSource();
const dateFnLocale = useDateFnLocale(); const dateFnLocale = useDateFnLocale();
@ -46,7 +43,7 @@ const TootPoll: Component<TootPollProps> = (props) => {
const [initialVote, setInitialVote] = createSignal(0); const [initialVote, setInitialVote] = createSignal(0);
const poll = () => props.value const poll = () => props.value;
const isShowResult = () => { const isShowResult = () => {
const n = mustShowResult(); const n = mustShowResult();
@ -118,14 +115,10 @@ const TootPoll: Component<TootPollProps> = (props) => {
> >
<ListItemText> <ListItemText>
<span <span
ref={(e) => innerHTML={resolveCustomEmoji(
createRenderEffect(() => { option().title,
e.innerHTML = resolveCustomEmoji( option().emojis,
option().title, )}
option().emojis,
);
})
}
></span> ></span>
</ListItemText> </ListItemText>

View file

@ -96,15 +96,10 @@ const TootPollDialog: Component<TootPollDialogPoll> = (props) => {
> >
<ListItemText> <ListItemText>
<span <span
ref={(e) => innerHTML={resolveCustomEmoji(
createRenderEffect( option().title,
() => option().emojis,
(e.innerHTML = resolveCustomEmoji( )}
option().title,
option().emojis,
)),
)
}
></span> ></span>
</ListItemText> </ListItemText>