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">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>Tutu</title>
|
<title>Tutu</title>
|
||||||
<link rel="stylesheet" href="/src/App.css" />
|
<link rel="stylesheet" href="/src/App.css" />
|
||||||
<script src="/src/platform/polyfills.ts" type="module"></script>
|
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
19
package.json
19
package.json
|
@ -16,22 +16,22 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@suid/vite-plugin": "^0.2.0",
|
"@suid/vite-plugin": "^0.2.0",
|
||||||
"@types/hammerjs": "^2.0.45",
|
"@types/hammerjs": "^2.0.45",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.2",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.2",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.3.2",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"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": "^2.10.2",
|
||||||
"vite-plugin-solid-styled": "^0.11.1",
|
"vite-plugin-solid-styled": "^0.11.1",
|
||||||
"wrangler": "^3.70.0"
|
"wrangler": "^3.64.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nanostores/persistent": "^0.9.1",
|
"@nanostores/persistent": "^0.9.1",
|
||||||
"@nanostores/solid": "^0.4.2",
|
"@nanostores/solid": "^0.4.2",
|
||||||
"@solid-primitives/event-listener": "^2.3.3",
|
"@solid-primitives/event-listener": "^2.3.3",
|
||||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
"@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",
|
"@solidjs/router": "^0.11.5",
|
||||||
"@suid/icons-material": "^0.7.0",
|
"@suid/icons-material": "^0.7.0",
|
||||||
"@suid/material": "^0.16.0",
|
"@suid/material": "^0.16.0",
|
||||||
|
@ -40,10 +40,9 @@
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"masto": "^6.8.0",
|
"masto": "^6.8.0",
|
||||||
"nanostores": "^0.9.5",
|
"nanostores": "^0.9.5",
|
||||||
"solid-js": "^1.8.20",
|
"solid-js": "^1.8.18",
|
||||||
"solid-styled": "^0.11.1",
|
"solid-styled": "^0.11.1",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2"
|
||||||
"web-animations-js": "^2.3.2"
|
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.21"
|
"packageManager": "bun@1.1.21"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ const AccountMastodonOAuth2Callback = lazy(
|
||||||
);
|
);
|
||||||
const TimelineHome = lazy(() => import("./timelines/Home.js"));
|
const TimelineHome = lazy(() => import("./timelines/Home.js"));
|
||||||
const Settings = lazy(() => import("./settings/Settings.js"));
|
const Settings = lazy(() => import("./settings/Settings.js"));
|
||||||
const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js"));
|
|
||||||
|
|
||||||
const Routing: Component = () => {
|
const Routing: Component = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -30,7 +29,6 @@ const Routing: Component = () => {
|
||||||
<Route path="/" component={TimelineHome}>
|
<Route path="/" component={TimelineHome}>
|
||||||
<Route path=""></Route>
|
<Route path=""></Route>
|
||||||
<Route path="/settings" component={Settings}></Route>
|
<Route path="/settings" component={Settings}></Route>
|
||||||
<Route path="/:acct/:id" component={TootBottomSheet}></Route>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={"/accounts"}>
|
<Route path={"/accounts"}>
|
||||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
<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 (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
|
|
|
@ -3,6 +3,18 @@ import type { mastodon } from "masto";
|
||||||
import { useSessions } from "./clients";
|
import { useSessions } from "./clients";
|
||||||
import { updateAcctInf } from "../accounts/stores";
|
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() {
|
export function useSignedInProfiles() {
|
||||||
const sessions = useSessions();
|
const sessions = useSessions();
|
||||||
const [accessor, tools] = createResource(sessions, async (all) => {
|
const [accessor, tools] = createResource(sessions, async (all) => {
|
||||||
|
@ -12,11 +24,11 @@ export function useSignedInProfiles() {
|
||||||
});
|
});
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
const value = accessor();
|
if (accessor.loading) {
|
||||||
if (!value) {
|
accessor();
|
||||||
return sessions().map((x) => ({ ...x, inf: x.account.inf }));
|
return sessions().map((x) => ({ ...x, inf: x.account.inf }));
|
||||||
}
|
}
|
||||||
return value;
|
return accessor();
|
||||||
},
|
},
|
||||||
tools,
|
tools,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -14,49 +14,55 @@ type Timeline = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTimeline(timeline: Accessor<Timeline>) {
|
export function useTimeline(timeline: Accessor<Timeline>) {
|
||||||
|
let minId: string | undefined;
|
||||||
|
let maxId: string | undefined;
|
||||||
let otl: Timeline | undefined;
|
let otl: Timeline | undefined;
|
||||||
let npager: mastodon.Paginator<mastodon.v1.Status[], unknown> | undefined;
|
const idSet = new Set<string>();
|
||||||
let opager: mastodon.Paginator<mastodon.v1.Status[], unknown> | undefined;
|
|
||||||
const [snapshot, { refetch }] = createResource<
|
const [snapshot, { refetch }] = createResource<
|
||||||
{
|
{ records: mastodon.v1.Status[]; direction: "old" | "new" },
|
||||||
records: mastodon.v1.Status[];
|
|
||||||
direction: "new" | "old";
|
|
||||||
tlChanged: boolean;
|
|
||||||
},
|
|
||||||
[Timeline],
|
[Timeline],
|
||||||
TimelineFetchTips | undefined
|
TimelineFetchTips | undefined
|
||||||
>(
|
>(
|
||||||
() => [timeline()] as const,
|
() => [timeline()] as const,
|
||||||
async ([tl], info) => {
|
async ([tl], info) => {
|
||||||
let tlChanged = false;
|
|
||||||
if (otl !== tl) {
|
if (otl !== tl) {
|
||||||
console.debug("timeline reset");
|
minId = undefined;
|
||||||
npager = opager = undefined;
|
maxId = undefined;
|
||||||
|
idSet.clear();
|
||||||
otl = tl;
|
otl = tl;
|
||||||
tlChanged = true;
|
|
||||||
}
|
}
|
||||||
const direction =
|
const direction =
|
||||||
typeof info.refetching !== "boolean"
|
typeof info.refetching !== "boolean"
|
||||||
? (info.refetching?.direction ?? "old")
|
? info.refetching?.direction
|
||||||
: "old";
|
: "old";
|
||||||
if (direction === "old") {
|
const pager = await tl.list(
|
||||||
if (!opager) {
|
direction === "old"
|
||||||
opager = tl.list({}).setDirection("next");
|
? {
|
||||||
|
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") {
|
||||||
|
minId = pager[pager.length - 1]?.id;
|
||||||
|
if (!maxId && pager.length > 0) {
|
||||||
|
maxId = pager[0].id;
|
||||||
}
|
}
|
||||||
const next = await opager.next();
|
|
||||||
return {
|
return {
|
||||||
direction,
|
direction: "old" as const,
|
||||||
records: next.value ?? [],
|
records: diff,
|
||||||
end: next.done,
|
|
||||||
tlChanged,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if (!npager) {
|
maxId = pager.length > 0 ? pager[0].id : undefined;
|
||||||
npager = tl.list({}).setDirection("prev");
|
if (!minId && pager.length > 0) {
|
||||||
|
minId = pager[pager.length - 1]?.id;
|
||||||
}
|
}
|
||||||
const next = await npager.next();
|
return { direction: "new" as const, records: diff };
|
||||||
const page = next.value ?? [];
|
|
||||||
return { direction, records: page, end: next.done, tlChanged };
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -66,10 +72,7 @@ export function useTimeline(timeline: Accessor<Timeline>) {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const shot = snapshot();
|
const shot = snapshot();
|
||||||
if (!shot) return;
|
if (!shot) return;
|
||||||
const { direction, records, tlChanged } = shot;
|
const { direction, records } = shot;
|
||||||
if (tlChanged) {
|
|
||||||
setStore(() => []);
|
|
||||||
}
|
|
||||||
if (direction == "new") {
|
if (direction == "new") {
|
||||||
setStore((x) => [...records, ...x]);
|
setStore((x) => [...records, ...x]);
|
||||||
} else if (direction == "old") {
|
} else if (direction == "old") {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { cache } from "@solidjs/router";
|
|
||||||
import type { mastodon } from "masto";
|
import type { mastodon } from "masto";
|
||||||
import { createRenderEffect, createResource, type Accessor } from "solid-js";
|
import { createRenderEffect, createResource, type Accessor } from "solid-js";
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,10 @@
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
&::backdrop {
|
|
||||||
background-color: black;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
box-shadow: var(--tutu-shadow-e16);
|
box-shadow: var(--tutu-shadow-e16);
|
||||||
|
|
||||||
:global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child {
|
:global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child {
|
||||||
|
color: white;
|
||||||
margin-left: -0.5em;
|
margin-left: -0.5em;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
}
|
}
|
||||||
|
@ -34,9 +30,4 @@
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animated {
|
|
||||||
position: absolute;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,22 @@
|
||||||
import {
|
import { createEffect, type ParentComponent } from "solid-js";
|
||||||
createEffect,
|
|
||||||
createRenderEffect,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
startTransition,
|
|
||||||
useTransition,
|
|
||||||
type ParentComponent,
|
|
||||||
} from "solid-js";
|
|
||||||
import styles from "./BottomSheet.module.css";
|
import styles from "./BottomSheet.module.css";
|
||||||
import { useHeroSignal } from "../platform/anim";
|
|
||||||
|
|
||||||
export type BottomSheetProps = {
|
export type BottomSheetProps = {
|
||||||
open?: boolean;
|
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) => {
|
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
let element: HTMLDialogElement;
|
let element: HTMLDialogElement;
|
||||||
let animation: Animation | undefined;
|
|
||||||
const hero = useHeroSignal(HERO);
|
|
||||||
|
|
||||||
const [pending] = useTransition()
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.open) {
|
if (props.open) {
|
||||||
if (!element.open && !pending()) {
|
if (!element.open) {
|
||||||
animatedOpen();
|
element.showModal();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (element.open) {
|
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();
|
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 {
|
import { createContext, useContext, type Accessor } from "solid-js";
|
||||||
createContext,
|
|
||||||
createRenderEffect,
|
|
||||||
createSignal,
|
|
||||||
untrack,
|
|
||||||
useContext,
|
|
||||||
type Accessor,
|
|
||||||
type Signal,
|
|
||||||
} from "solid-js";
|
|
||||||
|
|
||||||
export type HeroSource = {
|
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;
|
export const HeroSourceProvider = HeroSourceContext.Provider;
|
||||||
|
|
||||||
function useHeroSource() {
|
export function useHeroSource() {
|
||||||
return useContext(HeroSourceContext);
|
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={
|
topbar={
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}>
|
<Toolbar variant="dense" sx={{paddingTop: "var(--safe-area-inset-top, 0px)"}}>
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]}>
|
<IconButton onClick={[navigate, -1]}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Title>Settings</Title>
|
<Title>Settings</Title>
|
||||||
|
|
|
@ -8,9 +8,9 @@ import {
|
||||||
onMount,
|
onMount,
|
||||||
type ParentComponent,
|
type ParentComponent,
|
||||||
children,
|
children,
|
||||||
Suspense,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useDocumentTitle } from "../utils";
|
import { useDocumentTitle } from "../utils";
|
||||||
|
import { useSessions } from "../masto/clients";
|
||||||
import { type mastodon } from "masto";
|
import { type mastodon } from "masto";
|
||||||
import Scaffold from "../material/Scaffold";
|
import Scaffold from "../material/Scaffold";
|
||||||
import {
|
import {
|
||||||
|
@ -27,32 +27,23 @@ import {
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import { TimeSourceProvider, createTimeSource } from "../platform/timesrc";
|
import { TimeSourceProvider, createTimeSource } from "../platform/timesrc";
|
||||||
import TootThread from "./TootThread.js";
|
import TootThread from "./TootThread.js";
|
||||||
|
import { useAcctProfile } from "../masto/acct";
|
||||||
import ProfileMenuButton from "./ProfileMenuButton";
|
import ProfileMenuButton from "./ProfileMenuButton";
|
||||||
import Tabs from "../material/Tabs";
|
import Tabs from "../material/Tabs";
|
||||||
import Tab from "../material/Tab";
|
import Tab from "../material/Tab";
|
||||||
import { Create as CreateTootIcon } from "@suid/icons-material";
|
import { Create as CreateTootIcon } from "@suid/icons-material";
|
||||||
import { useTimeline } from "../masto/timelines";
|
import { useTimeline } from "../masto/timelines";
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
import BottomSheet, {
|
import BottomSheet from "../material/BottomSheet";
|
||||||
HERO as BOTTOM_SHEET_HERO,
|
|
||||||
} from "../material/BottomSheet";
|
|
||||||
import { $settings } from "../settings/stores";
|
import { $settings } from "../settings/stores";
|
||||||
import { useStore } from "@nanostores/solid";
|
import { useStore } from "@nanostores/solid";
|
||||||
import { vibrate } from "../platform/hardware";
|
import { vibrate } from "../platform/hardware";
|
||||||
import PullDownToRefresh from "./PullDownToRefresh";
|
import PullDownToRefresh from "./PullDownToRefresh";
|
||||||
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
|
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
import { useSignedInProfiles } from "../masto/acct";
|
|
||||||
|
|
||||||
const TimelinePanel: Component<{
|
const TimelinePanel: Component<{
|
||||||
client: mastodon.rest.Client;
|
client: mastodon.rest.Client;
|
||||||
name: "home" | "public" | "trends";
|
name: "home" | "public" | "trends";
|
||||||
prefetch?: boolean;
|
prefetch?: boolean;
|
||||||
|
|
||||||
openFullScreenToot: (
|
|
||||||
toot: mastodon.v1.Status,
|
|
||||||
srcElement?: HTMLElement,
|
|
||||||
) => void;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
|
||||||
const [
|
const [
|
||||||
|
@ -134,22 +125,18 @@ const TimelinePanel: Component<{
|
||||||
>
|
>
|
||||||
<For each={timeline}>
|
<For each={timeline}>
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
let element: HTMLElement | undefined;
|
|
||||||
return (
|
return (
|
||||||
<TootThread
|
<TootThread
|
||||||
ref={element}
|
|
||||||
status={item}
|
status={item}
|
||||||
onBoost={(...args) => onBoost(index(), ...args)}
|
onBoost={(...args) => onBoost(index(), ...args)}
|
||||||
onBookmark={(...args) => onBookmark(index(), ...args)}
|
onBookmark={(...args) => onBookmark(index(), ...args)}
|
||||||
client={props.client}
|
client={props.client}
|
||||||
expanded={item.id === expandedThreadId() ? 1 : 0}
|
expanded={item.id === expandedThreadId() ? 1 : 0}
|
||||||
onExpandChange={(x) => {
|
onExpandChange={() =>
|
||||||
if (item.id !== expandedThreadId()) {
|
setExpandedThreadId(
|
||||||
setExpandedThreadId(item.id);
|
item.id !== expandedThreadId() ? item.id : undefined,
|
||||||
} else if (x === 2) {
|
)
|
||||||
props.openFullScreenToot(item, element);
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -195,13 +182,10 @@ const Home: ParentComponent = (props) => {
|
||||||
const now = createTimeSource();
|
const now = createTimeSource();
|
||||||
|
|
||||||
const settings$ = useStore($settings);
|
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 [panelOffset, setPanelOffset] = createSignal(0);
|
||||||
const prefetching = () => !settings$().prefetchTootsDisabled;
|
const prefetching = () => !settings$().prefetchTootsDisabled;
|
||||||
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
|
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`
|
css`
|
||||||
.tab-panel {
|
.tab-panel {
|
||||||
overflow: visible auto;
|
overflow: visible auto;
|
||||||
|
@ -376,7 +344,6 @@ const Home: ParentComponent = (props) => {
|
||||||
client={client()}
|
client={client()}
|
||||||
name="home"
|
name="home"
|
||||||
prefetch={prefetching()}
|
prefetch={prefetching()}
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -386,7 +353,6 @@ const Home: ParentComponent = (props) => {
|
||||||
client={client()}
|
client={client()}
|
||||||
name="trends"
|
name="trends"
|
||||||
prefetch={prefetching()}
|
prefetch={prefetching()}
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -396,18 +362,13 @@ const Home: ParentComponent = (props) => {
|
||||||
client={client()}
|
client={client()}
|
||||||
name="public"
|
name="public"
|
||||||
prefetch={prefetching()}
|
prefetch={prefetching()}
|
||||||
openFullScreenToot={openFullScreenToot}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
</TimeSourceProvider>
|
</TimeSourceProvider>
|
||||||
<Suspense>
|
|
||||||
<HeroSourceProvider value={[heroSrc, setHeroSrc]}>
|
|
||||||
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
<BottomSheet open={!!child()}>{child()}</BottomSheet>
|
||||||
</HeroSourceProvider>
|
|
||||||
</Suspense>
|
|
||||||
</Scaffold>
|
</Scaffold>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,9 @@ import type { mastodon } from "masto";
|
||||||
import { type Component, For, createSignal } from "solid-js";
|
import { type Component, For, createSignal } from "solid-js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
import tootStyle from "./toot.module.css";
|
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<{
|
const MediaAttachmentGrid: Component<{
|
||||||
attachments: mastodon.v1.MediaAttachment[];
|
attachments: mastodon.v1.MediaAttachment[];
|
||||||
|
@ -56,6 +58,13 @@ const MediaAttachmentGrid: Component<{
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
<HeroSourceProvider
|
||||||
|
value={() => ({
|
||||||
|
[MEDIA_VIEWER_HEROSRC]: rootRef.children.item(
|
||||||
|
viewerIndex() || 0,
|
||||||
|
) as HTMLElement,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
show={viewerOpened()}
|
show={viewerOpened()}
|
||||||
index={viewerIndex() || 0}
|
index={viewerIndex() || 0}
|
||||||
|
@ -63,6 +72,7 @@ const MediaAttachmentGrid: Component<{
|
||||||
media={props.attachments}
|
media={props.attachments}
|
||||||
onClose={() => setViewerIndex(undefined)}
|
onClose={() => setViewerIndex(undefined)}
|
||||||
/>
|
/>
|
||||||
|
</HeroSourceProvider>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {
|
||||||
untrack,
|
untrack,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
|
import { useHeroSource } from "../platform/anim";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { IconButton, Toolbar } from "@suid/material";
|
import { IconButton, Toolbar } from "@suid/material";
|
||||||
import { ArrowLeft, ArrowRight, Close } from "@suid/icons-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) => {
|
const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
|
||||||
let rootRef: HTMLDialogElement;
|
let rootRef: HTMLDialogElement;
|
||||||
|
|
||||||
|
const heroSource = useHeroSource();
|
||||||
|
const heroSourceEl = () => heroSource()[MEDIA_VIEWER_HEROSRC];
|
||||||
type State = {
|
type State = {
|
||||||
ref?: HTMLElement;
|
ref?: HTMLElement;
|
||||||
media: mastodon.v1.MediaAttachment;
|
media: mastodon.v1.MediaAttachment;
|
||||||
|
|
|
@ -103,7 +103,7 @@ const PullDownToRefresh: Component<{
|
||||||
const handleTouch = (event: TouchEvent) => {
|
const handleTouch = (event: TouchEvent) => {
|
||||||
if (event.targetTouches.length > 1) {
|
if (event.targetTouches.length > 1) {
|
||||||
lastTouchId = 0;
|
lastTouchId = 0;
|
||||||
lastTouchScreenY = 0;
|
lastTouchScreenY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = event.targetTouches.item(0)!;
|
const item = event.targetTouches.item(0)!;
|
||||||
|
@ -128,12 +128,7 @@ const PullDownToRefresh: Component<{
|
||||||
lastTouchScreenY = 0;
|
lastTouchScreenY = 0;
|
||||||
holding = false;
|
holding = false;
|
||||||
if (untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) {
|
if (untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) {
|
||||||
setTimeout(props.onRefresh, 0);
|
setTimeout(props.onRefresh, 0)
|
||||||
} else {
|
|
||||||
if (released) {
|
|
||||||
released = false;
|
|
||||||
requestAnimationFrame(updatePullDown);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,80 +1,7 @@
|
||||||
import { useNavigate, useParams } from "@solidjs/router";
|
import type { Component } from "solid-js";
|
||||||
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";
|
|
||||||
|
|
||||||
const TootBottomSheet: Component = (props) => {
|
const TootBottomSheet: Component = (props) => {
|
||||||
const params = useParams<{ acct: string; id: string }>();
|
return <></>;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TootBottomSheet;
|
export default TootBottomSheet;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { mastodon } from "masto";
|
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 CompactToot from "./CompactToot";
|
||||||
import { useTimeSource } from "../platform/timesrc";
|
import { useTimeSource } from "../platform/timesrc";
|
||||||
import RegularToot from "./RegularToot";
|
import RegularToot from "./RegularToot";
|
||||||
|
@ -7,7 +7,6 @@ import cardStyle from "../material/cards.module.css";
|
||||||
import { css } from "solid-styled";
|
import { css } from "solid-styled";
|
||||||
|
|
||||||
type TootThreadProps = {
|
type TootThreadProps = {
|
||||||
ref?: Ref<HTMLElement>,
|
|
||||||
status: mastodon.v1.Status;
|
status: mastodon.v1.Status;
|
||||||
client: mastodon.rest.Client;
|
client: mastodon.rest.Client;
|
||||||
expanded?: 0 | 1 | 2;
|
expanded?: 0 | 1 | 2;
|
||||||
|
@ -71,7 +70,6 @@ const TootThread: Component<TootThreadProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={props.ref}
|
|
||||||
classList={{ "thread-line": !!inReplyTo(), expanded: expanded() > 0 }}
|
classList={{ "thread-line": !!inReplyTo(), expanded: expanded() > 0 }}
|
||||||
onClick={() => props.onExpandChange?.(nextExpandLevel[expanded()])}
|
onClick={() => props.onExpandChange?.(nextExpandLevel[expanded()])}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue