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 ( + { + if (e.target === root) { + if (props.onClose) { + if (Array.isArray(props.onClose)) { + props.onClose[0](props.onClose[1], e); + } else { + ( + props.onClose as ( + event: Event & { currentTarget: HTMLDialogElement }, + ) => void + )(e); + } + } + e.stopPropagation(); + } + }} + style={{ + position: "absolute", + left: `${pos()[0]}px`, + top: `${pos()[1]}px`, + border: "none", + padding: 0, + "max-width": "560px", + width: "max-content", + /*"min-width": "20vw", */ + "box-shadow": "var(--tutu-shadow-e8)", + }} + > +
+ {props.children} +
+
+ ); +}; + +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 ( + <> + + + document.getElementById(buttonId)!.getBoundingClientRect() + } + > + + {(item, idx) => ( + <> + + {props.options[item]} + + + + )} + + + + ); +} + +export default TootFilterButton;