Compare commits
No commits in common. "c7f26053ca00e72aaf5269fcad063654b8fe5c7b" and "1c0a83dbab78758961b2081ea57a5b31723d6e66" have entirely different histories.
c7f26053ca
...
1c0a83dbab
19 changed files with 98 additions and 351 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -7,7 +7,6 @@
|
|||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>Tutu</title>
|
||||
<link rel="stylesheet" href="/src/App.css" />
|
||||
<script src="/src/platform/polyfills.ts" type="module"></script>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
19
package.json
19
package.json
|
@ -16,22 +16,22 @@
|
|||
"devDependencies": {
|
||||
"@suid/vite-plugin": "^0.2.0",
|
||||
"@types/hammerjs": "^2.0.45",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.2",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"vite-plugin-solid-styled": "^0.11.1",
|
||||
"wrangler": "^3.70.0"
|
||||
"wrangler": "^3.64.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanostores/persistent": "^0.9.1",
|
||||
"@nanostores/solid": "^0.4.2",
|
||||
"@solid-primitives/event-listener": "^2.3.3",
|
||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"@solid-primitives/resize-observer": "^2.0.25",
|
||||
"@solidjs/router": "^0.11.5",
|
||||
"@suid/icons-material": "^0.7.0",
|
||||
"@suid/material": "^0.16.0",
|
||||
|
@ -40,10 +40,9 @@
|
|||
"hammerjs": "^2.0.8",
|
||||
"masto": "^6.8.0",
|
||||
"nanostores": "^0.9.5",
|
||||
"solid-js": "^1.8.20",
|
||||
"solid-js": "^1.8.18",
|
||||
"solid-styled": "^0.11.1",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"web-animations-js": "^2.3.2"
|
||||
"stacktrace-js": "^2.0.2"
|
||||
},
|
||||
"packageManager": "bun@1.1.21"
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ const AccountMastodonOAuth2Callback = lazy(
|
|||
);
|
||||
const TimelineHome = lazy(() => import("./timelines/Home.js"));
|
||||
const Settings = lazy(() => import("./settings/Settings.js"));
|
||||
const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js"));
|
||||
|
||||
const Routing: Component = () => {
|
||||
return (
|
||||
|
@ -30,7 +29,6 @@ const Routing: Component = () => {
|
|||
<Route path="/" component={TimelineHome}>
|
||||
<Route path=""></Route>
|
||||
<Route path="/settings" component={Settings}></Route>
|
||||
<Route path="/:acct/:id" component={TootBottomSheet}></Route>
|
||||
</Route>
|
||||
<Route path={"/accounts"}>
|
||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
||||
|
@ -55,7 +53,7 @@ const App: Component = () => {
|
|||
);
|
||||
});
|
||||
|
||||
const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
|
||||
const UnexpectedError = lazy(() => import("./UnexpectedError.js"))
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
|
|
|
@ -3,6 +3,18 @@ import type { mastodon } from "masto";
|
|||
import { useSessions } from "./clients";
|
||||
import { updateAcctInf } from "../accounts/stores";
|
||||
|
||||
export function useAcctProfile(client: Accessor<mastodon.rest.Client>) {
|
||||
return createResource(
|
||||
client,
|
||||
(client) => {
|
||||
return client.v1.accounts.verifyCredentials();
|
||||
},
|
||||
{
|
||||
name: "MastodonAccountProfile",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSignedInProfiles() {
|
||||
const sessions = useSessions();
|
||||
const [accessor, tools] = createResource(sessions, async (all) => {
|
||||
|
@ -12,11 +24,11 @@ export function useSignedInProfiles() {
|
|||
});
|
||||
return [
|
||||
() => {
|
||||
const value = accessor();
|
||||
if (!value) {
|
||||
if (accessor.loading) {
|
||||
accessor();
|
||||
return sessions().map((x) => ({ ...x, inf: x.account.inf }));
|
||||
}
|
||||
return value;
|
||||
return accessor();
|
||||
},
|
||||
tools,
|
||||
] as const;
|
||||
|
|
|
@ -14,49 +14,55 @@ type Timeline = {
|
|||
};
|
||||
|
||||
export function useTimeline(timeline: Accessor<Timeline>) {
|
||||
let minId: string | undefined;
|
||||
let maxId: string | undefined;
|
||||
let otl: Timeline | undefined;
|
||||
let npager: mastodon.Paginator<mastodon.v1.Status[], unknown> | undefined;
|
||||
let opager: mastodon.Paginator<mastodon.v1.Status[], unknown> | undefined;
|
||||
const idSet = new Set<string>();
|
||||
const [snapshot, { refetch }] = createResource<
|
||||
{
|
||||
records: mastodon.v1.Status[];
|
||||
direction: "new" | "old";
|
||||
tlChanged: boolean;
|
||||
},
|
||||
{ records: mastodon.v1.Status[]; direction: "old" | "new" },
|
||||
[Timeline],
|
||||
TimelineFetchTips | undefined
|
||||
>(
|
||||
() => [timeline()] as const,
|
||||
async ([tl], info) => {
|
||||
let tlChanged = false;
|
||||
if (otl !== tl) {
|
||||
console.debug("timeline reset");
|
||||
npager = opager = undefined;
|
||||
minId = undefined;
|
||||
maxId = undefined;
|
||||
idSet.clear();
|
||||
otl = tl;
|
||||
tlChanged = true;
|
||||
}
|
||||
const direction =
|
||||
typeof info.refetching !== "boolean"
|
||||
? (info.refetching?.direction ?? "old")
|
||||
? info.refetching?.direction
|
||||
: "old";
|
||||
const pager = await tl.list(
|
||||
direction === "old"
|
||||
? {
|
||||
maxId: minId,
|
||||
}
|
||||
: {
|
||||
minId: maxId,
|
||||
},
|
||||
);
|
||||
const diff = pager.filter((x) => !idSet.has(x.id));
|
||||
for (const v of diff.map((x) => x.id)) {
|
||||
idSet.add(v);
|
||||
}
|
||||
if (direction === "old") {
|
||||
if (!opager) {
|
||||
opager = tl.list({}).setDirection("next");
|
||||
minId = pager[pager.length - 1]?.id;
|
||||
if (!maxId && pager.length > 0) {
|
||||
maxId = pager[0].id;
|
||||
}
|
||||
const next = await opager.next();
|
||||
return {
|
||||
direction,
|
||||
records: next.value ?? [],
|
||||
end: next.done,
|
||||
tlChanged,
|
||||
direction: "old" as const,
|
||||
records: diff,
|
||||
};
|
||||
} else {
|
||||
if (!npager) {
|
||||
npager = tl.list({}).setDirection("prev");
|
||||
maxId = pager.length > 0 ? pager[0].id : undefined;
|
||||
if (!minId && pager.length > 0) {
|
||||
minId = pager[pager.length - 1]?.id;
|
||||
}
|
||||
const next = await npager.next();
|
||||
const page = next.value ?? [];
|
||||
return { direction, records: page, end: next.done, tlChanged };
|
||||
return { direction: "new" as const, records: diff };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -66,10 +72,7 @@ export function useTimeline(timeline: Accessor<Timeline>) {
|
|||
createEffect(() => {
|
||||
const shot = snapshot();
|
||||
if (!shot) return;
|
||||
const { direction, records, tlChanged } = shot;
|
||||
if (tlChanged) {
|
||||
setStore(() => []);
|
||||
}
|
||||
const { direction, records } = shot;
|
||||
if (direction == "new") {
|
||||
setStore((x) => [...records, ...x]);
|
||||
} else if (direction == "old") {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { cache } from "@solidjs/router";
|
||||
import type { mastodon } from "masto";
|
||||
import { createRenderEffect, createResource, type Accessor } from "solid-js";
|
||||
|
||||
|
|
|
@ -11,14 +11,10 @@
|
|||
border-radius: 2px;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
&::backdrop {
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
box-shadow: var(--tutu-shadow-e16);
|
||||
|
||||
:global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child {
|
||||
color: white;
|
||||
margin-left: -0.5em;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
@ -34,9 +30,4 @@
|
|||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.animated {
|
||||
position: absolute;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,115 +1,22 @@
|
|||
import {
|
||||
createEffect,
|
||||
createRenderEffect,
|
||||
onCleanup,
|
||||
onMount,
|
||||
startTransition,
|
||||
useTransition,
|
||||
type ParentComponent,
|
||||
} from "solid-js";
|
||||
import { createEffect, type ParentComponent } from "solid-js";
|
||||
import styles from "./BottomSheet.module.css";
|
||||
import { useHeroSignal } from "../platform/anim";
|
||||
|
||||
export type BottomSheetProps = {
|
||||
open?: boolean;
|
||||
};
|
||||
|
||||
export const HERO = Symbol("BottomSheet Hero Symbol");
|
||||
|
||||
function composeAnimationFrame(
|
||||
{
|
||||
top,
|
||||
left,
|
||||
height,
|
||||
width,
|
||||
}: Record<"top" | "left" | "height" | "width", number>,
|
||||
x: Record<string, unknown>,
|
||||
) {
|
||||
return {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
...x,
|
||||
};
|
||||
}
|
||||
|
||||
const MOVE_SPEED = 1400; // 1400px/s, bottom sheet is big and a bit heavier than small papers
|
||||
|
||||
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||
let element: HTMLDialogElement;
|
||||
let animation: Animation | undefined;
|
||||
const hero = useHeroSignal(HERO);
|
||||
|
||||
const [pending] = useTransition()
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open) {
|
||||
if (!element.open && !pending()) {
|
||||
animatedOpen();
|
||||
if (!element.open) {
|
||||
element.showModal();
|
||||
}
|
||||
} else {
|
||||
if (element.open) {
|
||||
animatedClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const animatedClose = () => {
|
||||
const endRect = hero();
|
||||
if (endRect) {
|
||||
const startRect = element.getBoundingClientRect();
|
||||
const animation = animateHero(startRect, endRect, element, true);
|
||||
const onClose = () => {
|
||||
element.close();
|
||||
};
|
||||
animation.addEventListener("finish", onClose);
|
||||
animation.addEventListener("cancel", onClose);
|
||||
} else {
|
||||
element.close();
|
||||
}
|
||||
};
|
||||
|
||||
const animatedOpen = () => {
|
||||
element.showModal();
|
||||
const startRect = hero();
|
||||
if (!startRect) return;
|
||||
const endRect = element.getBoundingClientRect();
|
||||
animateHero(startRect, endRect, element);
|
||||
};
|
||||
|
||||
const animateHero = (
|
||||
startRect: DOMRect,
|
||||
endRect: DOMRect,
|
||||
element: HTMLElement,
|
||||
reserve?: boolean,
|
||||
) => {
|
||||
const easing = "cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
element.classList.add(styles.animated);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(Math.abs(startRect.top - endRect.top), 2) +
|
||||
Math.pow(Math.abs(startRect.left - startRect.top), 2),
|
||||
);
|
||||
const duration = (distance / MOVE_SPEED) * 1000;
|
||||
animation = element.animate(
|
||||
[
|
||||
composeAnimationFrame(startRect, { opacity: reserve ? 1 : 0.5 }),
|
||||
composeAnimationFrame(endRect, { opacity: reserve ? 0.5 : 1 }),
|
||||
],
|
||||
{ easing, duration },
|
||||
);
|
||||
const onAnimationEnd = () => {
|
||||
element.classList.remove(styles.animated);
|
||||
animation = undefined;
|
||||
};
|
||||
animation.addEventListener("finish", onAnimationEnd);
|
||||
animation.addEventListener("cancel", onAnimationEnd);
|
||||
return animation;
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
if (animation) {
|
||||
animation.cancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,49 +1,13 @@
|
|||
import {
|
||||
createContext,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
untrack,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type Signal,
|
||||
} from "solid-js";
|
||||
import { createContext, useContext, type Accessor } from "solid-js";
|
||||
|
||||
export type HeroSource = {
|
||||
[key: string | symbol | number]: DOMRect | undefined;
|
||||
[key: string | symbol | number]: HTMLElement | undefined;
|
||||
};
|
||||
|
||||
const HeroSourceContext = createContext<Signal<HeroSource>>(/* __@PURE__ */undefined);
|
||||
const HeroSourceContext = createContext<Accessor<HeroSource>>(() => ({}));
|
||||
|
||||
export const HeroSourceProvider = HeroSourceContext.Provider;
|
||||
|
||||
function useHeroSource() {
|
||||
export function useHeroSource() {
|
||||
return useContext(HeroSourceContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use hero value for the {@link key}.
|
||||
*/
|
||||
export function useHeroSignal(
|
||||
key: string | symbol | number,
|
||||
): Accessor<DOMRect | undefined> {
|
||||
const source = useHeroSource();
|
||||
if (source) {
|
||||
const [get, set] = createSignal<DOMRect>();
|
||||
|
||||
createRenderEffect(() => {
|
||||
const value = source[0]();
|
||||
if (value[key]) {
|
||||
set(value[key]);
|
||||
source[1]((x) => {
|
||||
const cpy = Object.assign({}, x);
|
||||
delete cpy[key];
|
||||
return cpy;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return get;
|
||||
} else {
|
||||
return () => undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
export function isiOS() {
|
||||
return [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod'
|
||||
].includes(navigator.platform)
|
||||
// iPad on iOS 13 detection
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
//! This module has side effect.
|
||||
//! It recommended to include the module by <script> tag.
|
||||
if (!document.body.animate) {
|
||||
// @ts-ignore: this file is polyfill, no exposed decls
|
||||
import("web-animations-js").then(() => {
|
||||
console.warn("web animation polyfill is included");
|
||||
});
|
||||
}
|
|
@ -50,7 +50,7 @@ const Settings: ParentComponent = () => {
|
|||
topbar={
|
||||
<AppBar position="static">
|
||||
<Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]}>
|
||||
<IconButton onClick={[navigate, -1]}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Title>Settings</Title>
|
||||
|
|
|
@ -8,9 +8,9 @@ import {
|
|||
onMount,
|
||||
type ParentComponent,
|
||||
children,
|
||||
Suspense,
|
||||
} from "solid-js";
|
||||
import { useDocumentTitle } from "../utils";
|
||||
import { useSessions } from "../masto/clients";
|
||||
import { type mastodon } from "masto";
|
||||
import Scaffold from "../material/Scaffold";
|
||||
import {
|
||||
|
@ -27,32 +27,23 @@ import {
|
|||
import { css } from "solid-styled";
|
||||
import { TimeSourceProvider, createTimeSource } from "../platform/timesrc";
|
||||
import TootThread from "./TootThread.js";
|
||||
import { useAcctProfile } from "../masto/acct";
|
||||
import ProfileMenuButton from "./ProfileMenuButton";
|
||||
import Tabs from "../material/Tabs";
|
||||
import Tab from "../material/Tab";
|
||||
import { Create as CreateTootIcon } from "@suid/icons-material";
|
||||
import { useTimeline } from "../masto/timelines";
|
||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||
import BottomSheet, {
|
||||
HERO as BOTTOM_SHEET_HERO,
|
||||
} from "../material/BottomSheet";
|
||||
import BottomSheet from "../material/BottomSheet";
|
||||
import { $settings } from "../settings/stores";
|
||||
import { useStore } from "@nanostores/solid";
|
||||
import { vibrate } from "../platform/hardware";
|
||||
import PullDownToRefresh from "./PullDownToRefresh";
|
||||
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useSignedInProfiles } from "../masto/acct";
|
||||
|
||||
const TimelinePanel: Component<{
|
||||
client: mastodon.rest.Client;
|
||||
name: "home" | "public" | "trends";
|
||||
prefetch?: boolean;
|
||||
|
||||
openFullScreenToot: (
|
||||
toot: mastodon.v1.Status,
|
||||
srcElement?: HTMLElement,
|
||||
) => void;
|
||||
}> = (props) => {
|
||||
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
||||
const [
|
||||
|
@ -134,22 +125,18 @@ const TimelinePanel: Component<{
|
|||
>
|
||||
<For each={timeline}>
|
||||
{(item, index) => {
|
||||
let element: HTMLElement | undefined;
|
||||
return (
|
||||
<TootThread
|
||||
ref={element}
|
||||
status={item}
|
||||
onBoost={(...args) => onBoost(index(), ...args)}
|
||||
onBookmark={(...args) => onBookmark(index(), ...args)}
|
||||
client={props.client}
|
||||
expanded={item.id === expandedThreadId() ? 1 : 0}
|
||||
onExpandChange={(x) => {
|
||||
if (item.id !== expandedThreadId()) {
|
||||
setExpandedThreadId(item.id);
|
||||
} else if (x === 2) {
|
||||
props.openFullScreenToot(item, element);
|
||||
}
|
||||
}}
|
||||
onExpandChange={() =>
|
||||
setExpandedThreadId(
|
||||
item.id !== expandedThreadId() ? item.id : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -195,13 +182,10 @@ const Home: ParentComponent = (props) => {
|
|||
const now = createTimeSource();
|
||||
|
||||
const settings$ = useStore($settings);
|
||||
const sessions = useSessions();
|
||||
const client = () => sessions()[0].client;
|
||||
const [profile] = useAcctProfile(client);
|
||||
|
||||
const [profiles] = useSignedInProfiles();
|
||||
const profile = () => profiles()[0].inf;
|
||||
const client = () => profiles()[0].client;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
|
||||
const [panelOffset, setPanelOffset] = createSignal(0);
|
||||
const prefetching = () => !settings$().prefetchTootsDisabled;
|
||||
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
|
||||
|
@ -275,22 +259,6 @@ const Home: ParentComponent = (props) => {
|
|||
}
|
||||
};
|
||||
|
||||
const openFullScreenToot = (
|
||||
toot: mastodon.v1.Status,
|
||||
srcElement?: HTMLElement,
|
||||
) => {
|
||||
const p = profiles()[0];
|
||||
const inf = p.account.inf ?? profile();
|
||||
if (!inf) {
|
||||
console.warn("no account info?");
|
||||
return;
|
||||
}
|
||||
const rect = srcElement?.getBoundingClientRect();
|
||||
setHeroSrc((x) => Object.assign({}, x, { [BOTTOM_SHEET_HERO]: rect }));
|
||||
const acct = `${inf.username}@${p.account.site}`;
|
||||
navigate(`/${encodeURIComponent(acct)}/${toot.id}`);
|
||||
};
|
||||
|
||||
css`
|
||||
.tab-panel {
|
||||
overflow: visible auto;
|
||||
|
@ -376,7 +344,6 @@ const Home: ParentComponent = (props) => {
|
|||
client={client()}
|
||||
name="home"
|
||||
prefetch={prefetching()}
|
||||
openFullScreenToot={openFullScreenToot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -386,7 +353,6 @@ const Home: ParentComponent = (props) => {
|
|||
client={client()}
|
||||
name="trends"
|
||||
prefetch={prefetching()}
|
||||
openFullScreenToot={openFullScreenToot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -396,18 +362,13 @@ const Home: ParentComponent = (props) => {
|
|||
client={client()}
|
||||
name="public"
|
||||
prefetch={prefetching()}
|
||||
openFullScreenToot={openFullScreenToot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</TimeSourceProvider>
|
||||
<Suspense>
|
||||
<HeroSourceProvider value={[heroSrc, setHeroSrc]}>
|
||||
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
||||
</HeroSourceProvider>
|
||||
</Suspense>
|
||||
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
||||
</Scaffold>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,9 @@ import type { mastodon } from "masto";
|
|||
import { type Component, For, createSignal } from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
import tootStyle from "./toot.module.css";
|
||||
import MediaViewer from "./MediaViewer";
|
||||
import { Portal } from "solid-js/web";
|
||||
import MediaViewer, { MEDIA_VIEWER_HEROSRC } from "./MediaViewer";
|
||||
import { HeroSourceProvider } from "../platform/anim";
|
||||
|
||||
const MediaAttachmentGrid: Component<{
|
||||
attachments: mastodon.v1.MediaAttachment[];
|
||||
|
@ -56,6 +58,13 @@ const MediaAttachmentGrid: Component<{
|
|||
}
|
||||
}}
|
||||
</For>
|
||||
<HeroSourceProvider
|
||||
value={() => ({
|
||||
[MEDIA_VIEWER_HEROSRC]: rootRef.children.item(
|
||||
viewerIndex() || 0,
|
||||
) as HTMLElement,
|
||||
})}
|
||||
>
|
||||
<MediaViewer
|
||||
show={viewerOpened()}
|
||||
index={viewerIndex() || 0}
|
||||
|
@ -63,6 +72,7 @@ const MediaAttachmentGrid: Component<{
|
|||
media={props.attachments}
|
||||
onClose={() => setViewerIndex(undefined)}
|
||||
/>
|
||||
</HeroSourceProvider>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
untrack,
|
||||
} from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
import { useHeroSource } from "../platform/anim";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { IconButton, Toolbar } from "@suid/material";
|
||||
import { ArrowLeft, ArrowRight, Close } from "@suid/icons-material";
|
||||
|
@ -40,6 +42,8 @@ function clamp(input: number, min: number, max: number) {
|
|||
const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
|
||||
let rootRef: HTMLDialogElement;
|
||||
|
||||
const heroSource = useHeroSource();
|
||||
const heroSourceEl = () => heroSource()[MEDIA_VIEWER_HEROSRC];
|
||||
type State = {
|
||||
ref?: HTMLElement;
|
||||
media: mastodon.v1.MediaAttachment;
|
||||
|
|
|
@ -103,7 +103,7 @@ const PullDownToRefresh: Component<{
|
|||
const handleTouch = (event: TouchEvent) => {
|
||||
if (event.targetTouches.length > 1) {
|
||||
lastTouchId = 0;
|
||||
lastTouchScreenY = 0;
|
||||
lastTouchScreenY;
|
||||
return;
|
||||
}
|
||||
const item = event.targetTouches.item(0)!;
|
||||
|
@ -128,12 +128,7 @@ const PullDownToRefresh: Component<{
|
|||
lastTouchScreenY = 0;
|
||||
holding = false;
|
||||
if (untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) {
|
||||
setTimeout(props.onRefresh, 0);
|
||||
} else {
|
||||
if (released) {
|
||||
released = false;
|
||||
requestAnimationFrame(updatePullDown);
|
||||
}
|
||||
setTimeout(props.onRefresh, 0)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,80 +1,7 @@
|
|||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { createResource, Show, type Component } from "solid-js";
|
||||
import Scaffold from "../material/Scaffold";
|
||||
import TootThread from "./TootThread";
|
||||
import { AppBar, IconButton, Toolbar } from "@suid/material";
|
||||
import { Title } from "../material/typography";
|
||||
import { Close as CloseIcon } from "@suid/icons-material";
|
||||
import { isiOS } from "../platform/host";
|
||||
import { createUnauthorizedClient, useSessions } from "../masto/clients";
|
||||
import { resolveCustomEmoji } from "../masto/toot";
|
||||
import RegularToot from "./RegularToot";
|
||||
import type { Component } from "solid-js";
|
||||
|
||||
const TootBottomSheet: Component = (props) => {
|
||||
const params = useParams<{ acct: string; id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const allSession = useSessions();
|
||||
const session = () => {
|
||||
const [inputUsername, inputSite] = decodeURIComponent(params.acct).split(
|
||||
"@",
|
||||
2,
|
||||
);
|
||||
const authedSession = allSession().find(
|
||||
(x) =>
|
||||
x.account.site === inputSite &&
|
||||
x.account.inf?.username === inputUsername,
|
||||
);
|
||||
return authedSession ?? { client: createUnauthorizedClient(inputSite) };
|
||||
};
|
||||
|
||||
const [remoteToot] = createResource(
|
||||
() => [session().client, params.id] as const,
|
||||
async ([client, id]) => {
|
||||
return await client.v1.statuses.$select(id).fetch();
|
||||
},
|
||||
);
|
||||
|
||||
const toot = remoteToot;
|
||||
|
||||
const tootTitle = () => {
|
||||
const t = toot();
|
||||
if (t) {
|
||||
const name = resolveCustomEmoji(t.account.displayName, t.account.emojis);
|
||||
return `${name}'s toot`;
|
||||
}
|
||||
return "A toot";
|
||||
};
|
||||
|
||||
return (
|
||||
<Scaffold
|
||||
topbar={
|
||||
<AppBar
|
||||
sx={{
|
||||
backgroundColor: "var(--tutu-color-surface)",
|
||||
color: "var(--tutu-color-on-surface)",
|
||||
}}
|
||||
elevation={1}
|
||||
position="static"
|
||||
>
|
||||
<Toolbar
|
||||
variant="dense"
|
||||
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
|
||||
>
|
||||
<IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Title>{tootTitle}</Title>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Show when={toot()}>
|
||||
<RegularToot status={toot()!}></RegularToot>
|
||||
</Show>
|
||||
</div>
|
||||
</Scaffold>
|
||||
);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default TootBottomSheet;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { mastodon } from "masto";
|
||||
import { Show, createResource, createSignal, type Component, type Ref } from "solid-js";
|
||||
import { Show, createResource, createSignal, type Component } from "solid-js";
|
||||
import CompactToot from "./CompactToot";
|
||||
import { useTimeSource } from "../platform/timesrc";
|
||||
import RegularToot from "./RegularToot";
|
||||
|
@ -7,7 +7,6 @@ import cardStyle from "../material/cards.module.css";
|
|||
import { css } from "solid-styled";
|
||||
|
||||
type TootThreadProps = {
|
||||
ref?: Ref<HTMLElement>,
|
||||
status: mastodon.v1.Status;
|
||||
client: mastodon.rest.Client;
|
||||
expanded?: 0 | 1 | 2;
|
||||
|
@ -71,7 +70,6 @@ const TootThread: Component<TootThreadProps> = (props) => {
|
|||
|
||||
return (
|
||||
<article
|
||||
ref={props.ref}
|
||||
classList={{ "thread-line": !!inReplyTo(), expanded: expanded() > 0 }}
|
||||
onClick={() => props.onExpandChange?.(nextExpandLevel[expanded()])}
|
||||
>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue