diff --git a/src/App.tsx b/src/App.tsx
index 875f408..abdff31 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -149,7 +149,7 @@ const App: Component = () => {
return ;
}}
>
-
+
;
+ 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/theme.ts b/src/material/theme.ts
index 6527cc4..a7c2c65 100644
--- a/src/material/theme.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/profiles/Profile.tsx b/src/profiles/Profile.tsx
index 2230f3d..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,9 +57,18 @@ const Profile: Component = () => {
},
);
+ const [recentTootFilter, setRecentTootFilter] = createSignal({
+ boost: true,
+ reply: true,
+ original: true,
+ });
+
const [recentToots] = createTimeline(
() => session().client.v1.accounts.$select(params.id).statuses,
- () => ({ limit: 20 }),
+ () => {
+ const { boost, reply } = recentTootFilter();
+ return { limit: 20, excludeReblogs: !boost, excludeReplies: !reply };
+ },
);
const bannerImg = () => profile()?.header;
@@ -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;