diff --git a/src/App.tsx b/src/App.tsx
index 028a234..abdff31 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,7 +10,7 @@ import {
lazy,
onCleanup,
} from "solid-js";
-import { useRootTheme } from "./material/mui.js";
+import { useRootTheme } from "./material/theme.js";
import {
Provider as ClientProvider,
createMastoClientFor,
@@ -149,7 +149,7 @@ const App: Component = () => {
return ;
}}
>
-
+
;
+type Timeline = {
+ list(params?: T): mastodon.Paginator;
};
-export function createTimelineSnapshot(
- timeline: Accessor,
- limit: Accessor,
-) {
+type TimelineParamsOf = T extends Timeline ? P : never;
+
+export function createTimelineSnapshot<
+ T extends Timeline,
+>(timeline: Accessor, limit: Accessor) {
const [shot, { refetch }] = createResource(
() => [timeline(), limit()] as const,
async ([tl, limit]) => {
@@ -80,13 +72,13 @@ export function createTimelineSnapshot(
export type TimelineFetchDirection = mastodon.Direction;
-export type TimelineChunk = {
- tl: Timeline;
+export type TimelineChunk = {
+ tl: Timeline;
rebuilt: boolean;
chunk: readonly mastodon.v1.Status[];
done?: boolean;
direction: TimelineFetchDirection;
- limit: number;
+ params: T;
};
type TreeNode = {
@@ -108,21 +100,21 @@ function collectPath(node: TreeNode) {
return path;
}
-function createTimelineChunk(
- timeline: Accessor,
- limit: Accessor,
+function createTimelineChunk>(
+ timeline: Accessor,
+ params: Accessor>,
) {
let vpMaxId: string | undefined, vpMinId: string | undefined;
const fetchExtendingPage = async (
- tl: Timeline,
+ tl: T,
direction: TimelineFetchDirection,
- limit: number,
+ params: TimelineParamsOf,
) => {
switch (direction) {
case "next": {
const page = await tl
- .list({ limit, sinceId: vpMaxId })
+ .list({ ...params, sinceId: vpMaxId })
.setDirection(direction)
.next();
if ((page.value?.length ?? 0) > 0) {
@@ -133,7 +125,7 @@ function createTimelineChunk(
case "prev": {
const page = await tl
- .list({ limit, maxId: vpMinId })
+ .list({ ...params, maxId: vpMinId })
.setDirection(direction)
.next();
if ((page.value?.length ?? 0) > 0) {
@@ -145,11 +137,11 @@ function createTimelineChunk(
};
return createResource(
- () => [timeline(), limit()] as const,
+ () => [timeline(), params()] as const,
async (
- [tl, limit],
+ [tl, params],
info: ResourceFetcherInfo<
- Readonly,
+ Readonly>>,
TimelineFetchDirection
>,
) => {
@@ -160,27 +152,26 @@ function createTimelineChunk(
vpMaxId = undefined;
vpMinId = undefined;
}
- const posts = await fetchExtendingPage(tl, direction, limit);
+ const posts = await fetchExtendingPage(tl, direction, params);
return {
tl,
rebuilt: rebuildTimeline,
chunk: posts.value ?? [],
done: posts.done,
direction,
- limit,
+ params,
};
},
);
}
-export function createTimeline(
- timeline: Accessor,
- limit: Accessor,
-) {
+export function createTimeline<
+ T extends Timeline,
+>(timeline: Accessor, params: Accessor>) {
const lookup = new ReactiveMap>();
const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]);
- const [chunk, { refetch }] = createTimelineChunk(timeline, limit);
+ const [chunk, { refetch }] = createTimelineChunk(timeline, params);
createEffect(() => {
const chk = catchError(chunk, (e) => console.error(e));
diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx
new file mode 100644
index 0000000..365d090
--- /dev/null
+++ b/src/material/Menu.tsx
@@ -0,0 +1,136 @@
+import { useWindowSize } from "@solid-primitives/resize-observer";
+import { MenuList } from "@suid/material";
+import {
+ createEffect,
+ createSignal,
+ type JSX,
+ type ParentComponent,
+} from "solid-js";
+import { ANIM_CURVE_STD } from "./theme";
+
+type Props = {
+ open?: boolean;
+ onClose?: JSX.EventHandlerUnion;
+ anchor: () => DOMRect;
+};
+
+function adjustMenuPosition(
+ rect: DOMRect,
+ [left, top]: [number, number],
+ { width, height }: { width: number; height: number },
+) {
+ const ntop = rect.bottom > height ? top - (rect.bottom - height) : top;
+ const nleft = rect.right > width ? left - (rect.right - width) : left;
+ return [nleft, ntop] as [number, number];
+}
+
+const Menu: ParentComponent = (props) => {
+ let root: HTMLDialogElement;
+ const [pos, setPos] = createSignal<[number, number]>([0, 0]);
+ const windowSize = useWindowSize();
+
+ createEffect(() => {
+ if (props.open) {
+ const a = props.anchor();
+ if (!root.open) {
+ root.showModal();
+ const rend = root.getBoundingClientRect();
+
+ setPos(adjustMenuPosition(rend, [a.left, a.top], windowSize));
+
+ const overflow = root.style.overflow;
+ root.style.overflow = "hidden";
+ const duration = (rend.height / 1600) * 1000;
+ const easing = ANIM_CURVE_STD;
+ const animation = root.animate(
+ {
+ height: [`${rend.height / 2}px`, `${rend.height}px`],
+ width: [`${rend.width / 4 * 3}px`, `${rend.width}px`],
+ },
+ {
+ duration,
+ easing,
+ },
+ );
+ animation.addEventListener(
+ "finish",
+ () => (root.style.overflow = overflow),
+ );
+ } else {
+ setPos(
+ adjustMenuPosition(
+ root.getBoundingClientRect(),
+ [a.left, a.top],
+ windowSize,
+ ),
+ );
+ }
+ } else {
+ animateClose();
+ }
+ });
+
+ const animateClose = () => {
+ const rend = root.getBoundingClientRect();
+ const overflow = root.style.overflow;
+ root.style.overflow = "hidden";
+ const animation = root.animate(
+ {
+ height: [`${rend.height}px`, `${rend.height / 2}px`],
+ width: [`${rend.width}px`, `${rend.width / 4 * 3}px`],
+ },
+ {
+ duration: (rend.height / 2 / 1600) * 1000,
+ easing: ANIM_CURVE_STD,
+ },
+ );
+ animation.addEventListener("finish", () => {
+ root.style.overflow = overflow;
+ root.close();
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default Menu;
diff --git a/src/material/mui.ts b/src/material/theme.ts
similarity index 57%
rename from src/material/mui.ts
rename to src/material/theme.ts
index 6527cc4..a7c2c65 100644
--- a/src/material/mui.ts
+++ b/src/material/theme.ts
@@ -2,6 +2,9 @@ import { Theme, createTheme } from "@suid/material/styles";
import { deepPurple, amber } from "@suid/material/colors";
import { Accessor } from "solid-js";
+/**
+ * The MUI theme.
+ */
export function useRootTheme(): Accessor {
return () =>
createTheme({
@@ -15,3 +18,8 @@ export function useRootTheme(): Accessor {
},
});
}
+
+export const ANIM_CURVE_STD = "cubic-bezier(0.4, 0, 0.2, 1)";
+export const ANIM_CURVE_DECELERATION = "cubic-bezier(0, 0, 0.2, 1)";
+export const ANIM_CURVE_ACELERATION = "cubic-bezier(0.4, 0, 1, 1)";
+export const ANIM_CURVE_SHARP = "cubic-bezier(0.4, 0, 0.6, 1)";
diff --git a/src/platform/share.tsx b/src/platform/share.tsx
index 5f80dc0..7c98cb9 100644
--- a/src/platform/share.tsx
+++ b/src/platform/share.tsx
@@ -16,7 +16,7 @@ import {
import { Close as CloseIcon, ContentCopy } from "@suid/icons-material";
import { Title } from "../material/typography";
import { render } from "solid-js/web";
-import { useRootTheme } from "../material/mui";
+import { useRootTheme } from "../material/theme";
const ShareBottomSheet: Component<{
data?: ShareData;
diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx
index cdea9d5..64cf0b5 100644
--- a/src/profiles/Profile.tsx
+++ b/src/profiles/Profile.tsx
@@ -19,8 +19,8 @@ import { useWindowSize } from "@solid-primitives/resize-observer";
import { css } from "solid-styled";
import { createTimeline } from "../masto/timelines";
import TootList from "../timelines/TootList";
-import { createIntersectionObserver } from "@solid-primitives/intersection-observer";
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
+import TootFilterButton from "./TootFilterButton";
const Profile: Component = () => {
const navigate = useNavigate();
@@ -48,7 +48,6 @@ const Profile: Component = () => {
threshold: 0.1,
},
);
-
onCleanup(() => obx.disconnect());
const [profile] = createResource(
@@ -58,16 +57,25 @@ const Profile: Component = () => {
},
);
+ const [recentTootFilter, setRecentTootFilter] = createSignal({
+ boost: true,
+ reply: true,
+ original: true,
+ });
+
const [recentToots] = createTimeline(
() => session().client.v1.accounts.$select(params.id).statuses,
- () => 20,
+ () => {
+ const { boost, reply } = recentTootFilter();
+ return { limit: 20, excludeReblogs: !boost, excludeReplies: !reply };
+ },
);
const bannerImg = () => profile()?.header;
const avatarImg = () => profile()?.avatar;
const displayName = () =>
resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
- const fullUsername = () => `@${profile()?.acct ?? "..."}`; // TODO: full user name
+ const fullUsername = () => `@${profile()?.acct ?? ""}`; // TODO: full user name
const description = () => profile()?.note;
css`
@@ -126,7 +134,9 @@ const Profile: Component = () => {
variant="dense"
sx={{
display: "flex",
- color: scrolledPastBanner() ? undefined : bannerSampledColors()?.text,
+ color: scrolledPastBanner()
+ ? undefined
+ : bannerSampledColors()?.text,
paddingTop: "var(--safe-area-inset-top)",
}}
>
@@ -238,6 +248,19 @@ const Profile: Component = () => {
+
+
+
+
> = {
+ options: Filters;
+ applied: Record;
+ disabledKeys?: (keyof Filters)[];
+
+ onApply(value: Record): void;
+};
+
+function TootFilterButton>(props: Props) {
+ const buttonId = createUniqueId();
+ const [open, setOpen] = createSignal(false);
+
+ const getTextForMultipleEntities = (texts: string[]) => {
+ switch (texts.length) {
+ case 0:
+ return "Nothing";
+ case 1:
+ return texts[0];
+ case 2:
+ return `${texts[0]} and ${texts[1]}`;
+ case 3:
+ return `${texts[0]}, ${texts[1]} and ${texts[2]}`;
+ default:
+ return `${texts[0]} and ${texts.length - 1} other${texts.length > 2 ? "s" : ""}`;
+ }
+ };
+
+ const optionKeys = () => Object.keys(props.options);
+
+ const appliedKeys = createMemo(() => {
+ const applied = props.applied;
+ return optionKeys().filter((k) => applied[k]);
+ });
+
+ const text = () => {
+ const keys = optionKeys();
+ const napplied = appliedKeys().length;
+ switch (napplied) {
+ case keys.length:
+ return "All";
+ default:
+ return getTextForMultipleEntities(
+ appliedKeys().map((k) => props.options[k]),
+ );
+ }
+ };
+
+ const toggleKey = (key: keyof F) => {
+ props.onApply(
+ Object.assign({}, props.applied, {
+ [key]: !props.applied[key],
+ }),
+ );
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default TootFilterButton;
diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx
index b1a014d..5c3e150 100644
--- a/src/timelines/TimelinePanel.tsx
+++ b/src/timelines/TimelinePanel.tsx
@@ -32,7 +32,7 @@ const TimelinePanel: Component<{
const [timeline, snapshot, { refetch: refetchTimeline }] = createTimeline(
() => props.client.v1.timelines[props.name],
- () => 20,
+ () => ({limit: 20}),
);
const [expandedThreadId, setExpandedThreadId] = createSignal();
const [typing, setTyping] = createSignal(false);
diff --git a/vite.config.ts b/vite.config.ts
index 7881869..466d139 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -64,7 +64,7 @@ export default defineConfig(({ mode }) => ({
strategies: "injectManifest",
registerType: "autoUpdate",
devOptions: {
- enabled: mode === "staging" || mode === "dev",
+ enabled: !["production", "staging"].includes(mode),
},
srcDir: "src/serviceworker",
filename: "main.ts",