diff --git a/src/material/BottomSheet.module.css b/src/material/BottomSheet.module.css index d096fff..6c31a44 100644 --- a/src/material/BottomSheet.module.css +++ b/src/material/BottomSheet.module.css @@ -1,5 +1,7 @@ .bottomSheet { - composes: surface from "material.module.css"; + composes: surface from "./material.module.css"; + composes: cardGutSkip from "./cards.module.css"; + composes: cardNoPad from "./cards.module.css"; border: none; position: absolute; left: 50%; @@ -47,4 +49,10 @@ opacity: 0; } } + + &.bottom { + top: unset; + transform: translateX(-50%); + bottom: 0; + } } diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index d37a0e2..cb3adc4 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -13,9 +13,12 @@ import { } from "solid-js"; import styles from "./BottomSheet.module.css"; import { useHeroSignal } from "../platform/anim"; +import { makeEventListener } from "@solid-primitives/event-listener"; export type BottomSheetProps = { open?: boolean; + bottomUp?: boolean; + onClose?(reason: "backdrop"): void; }; export const HERO = Symbol("BottomSheet Hero Symbol"); @@ -123,8 +126,28 @@ const BottomSheet: ParentComponent = (props) => { } }); + onMount(() => { + makeEventListener(element, "click", (event) => { + const rect = element.getBoundingClientRect(); + const isInDialog = + rect.top <= event.clientY && + event.clientY <= rect.top + rect.height && + rect.left <= event.clientX && + event.clientX <= rect.left + rect.width; + if (!isInDialog) { + props.onClose?.("backdrop"); + } + }); + }); + return ( - + {ochildren() ?? cache()} ); diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index f1f9d34..cebcda7 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -12,6 +12,7 @@ import { css } from "solid-styled"; interface ScaffoldProps { topbar?: JSX.Element; fab?: JSX.Element; + bottom?: JSX.Element; } const Scaffold: ParentComponent = (props) => { @@ -36,6 +37,16 @@ const Scaffold: ParentComponent = (props) => { right: 40px; z-index: var(--tutu-zidx-nav, auto); } + + .bottom-dock { + position: sticky; + bottom: 0; + left: 0; + right: 0; + z-index: var(--tutu-zidx-nav, auto); + padding-bottom: var(--safe-area-inset-bottom, 0); + + } `; return ( <> @@ -48,6 +59,9 @@ const Scaffold: ParentComponent = (props) => {
{props.fab}
{props.children}
+ +
{props.bottom}
+
); }; diff --git a/src/timelines/ReplyEditor.tsx b/src/timelines/ReplyEditor.tsx new file mode 100644 index 0000000..d40ac77 --- /dev/null +++ b/src/timelines/ReplyEditor.tsx @@ -0,0 +1,236 @@ +import { + createSignal, + createUniqueId, + onMount, + type Component, + type Setter, +} from "solid-js"; +import Scaffold from "../material/Scaffold"; +import { + Avatar, + Button, + IconButton, + List, + ListItemButton, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + Radio, + Switch, + Divider, +} from "@suid/material"; +import { + ArrowDropDown, + Public as PublicIcon, + Send, + People as PeopleIcon, + ThreeP as ThreePIcon, + ListAlt as ListAltIcon, +} from "@suid/icons-material"; +import type { Account } from "../accounts/stores"; +import tootComposers from "./TootComposer.module.css"; +import { makeEventListener } from "@solid-primitives/event-listener"; +import BottomSheet from "../material/BottomSheet"; + +type TootVisibility = "public" | "unlisted" | "private" | "direct"; + +const TootVisibilityPickerDialog: Component<{ + open?: boolean; + onClose: () => void; + visibility: TootVisibility; + onVisibilityChange: (value: TootVisibility) => void; +}> = (props) => { + type Kind = "public" | "private" | "direct"; + + const kind = () => + props.visibility === "public" || props.visibility === "unlisted" + ? "public" + : props.visibility; + + const setKind = (nv: Kind) => { + if (nv == "public") { + props.onVisibilityChange(discoverable() ? "public" : "unlisted"); + } else { + props.onVisibilityChange(nv); + } + }; + + const discoverable = () => { + return props.visibility === "public"; + } + + const setDiscoverable = (setter: (v: boolean) => boolean) => { + const nval = setter(discoverable()) + props.onVisibilityChange(nval ? "public" : "unlisted"); // trigger change + }; + + + + return ( + + + + + } + > + + + + + + + + + + + + setDiscoverable((x) => !x)} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ReplyEditor: Component<{ + profile: Account; + replyToDisplayName: string; +}> = (props) => { + let inputRef: HTMLTextAreaElement; + const buttonId = createUniqueId(); + const menuId = createUniqueId(); + + const [typing, setTyping] = createSignal(false); + const [visibility, setVisibility] = createSignal("public"); + const [permPicker, setPermPicker] = createSignal(false); + + onMount(() => { + makeEventListener(inputRef, "focus", () => setTyping(true)); + makeEventListener(inputRef, "blur", () => setTyping(false)); + }); + + const containerStyle = () => + typing() + ? { + position: "sticky" as const, + top: "var(--scaffold-topbar-height, 0)", + bottom: "var(--safe-area-inset-bottom, 0)", + "z-index": 1, + } + : undefined; + + const visibilityText = () => { + switch (visibility()) { + case "public": + return "Discoverable"; + case "unlisted": + return "Public"; + case "private": + return "Only Followers"; + case "direct": + return "Only Mentions"; + } + }; + + return ( +
setTyping(true)} + > +
+ + + + + +
+ +
+ +
+ + setPermPicker(false)} + visibility={visibility()} + onVisibilityChange={setVisibility} + /> +
+ ); +}; + +export default ReplyEditor; diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index 18a83dc..07287c9 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -8,10 +8,8 @@ import { type Component, } from "solid-js"; import Scaffold from "../material/Scaffold"; -import TootThread from "./TootThread"; import { AppBar, - Avatar, CircularProgress, IconButton, Toolbar, @@ -20,9 +18,7 @@ import { Title } from "../material/typography"; import { ArrowBack as BackIcon, Close as CloseIcon, - Send, } from "@suid/icons-material"; -import { isiOS } from "../platform/host"; import { createUnauthorizedClient, useSessions } from "../masto/clients"; import { resolveCustomEmoji } from "../masto/toot"; import RegularToot from "./RegularToot"; @@ -30,6 +26,8 @@ import type { mastodon } from "masto"; import cards from "../material/cards.module.css"; import { css } from "solid-styled"; import { vibrate } from "../platform/hardware"; +import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; +import ReplyEditor from "./ReplyEditor"; let cachedEntry: [string, mastodon.v1.Status] | undefined; @@ -43,11 +41,14 @@ function getCache(acct: string, id: string) { } } + + const TootBottomSheet: Component = (props) => { const params = useParams<{ acct: string; id: string }>(); const location = useLocation<{ tootBottomSheetPushedCount?: number }>(); const navigate = useNavigate(); const allSession = useSessions(); + const time = createTimeSource(); const acctText = () => decodeURIComponent(params.acct); const session = () => { const [inputUsername, inputSite] = acctText().split("@", 2); @@ -162,12 +163,6 @@ const TootBottomSheet: Component = (props) => { }; css` - .bottom-dock { - position: sticky; - bottom: 0; - z-index: var(--tutu-zidx-nav); - } - .name :global(img) { max-height: 1em; } @@ -201,74 +196,69 @@ const TootBottomSheet: Component = (props) => { } > - - {(item) => ( - - )} - + + + {(item) => ( + + )} + -
- - - -
+
+ + + +
- -
- -
-
- - - {(item) => ( - - )} - - -
-
- - - - - + + + + +
+
-
+ + + {(item) => ( + + )} + + ); }; diff --git a/src/timelines/TootComposer.module.css b/src/timelines/TootComposer.module.css new file mode 100644 index 0000000..f752e7c --- /dev/null +++ b/src/timelines/TootComposer.module.css @@ -0,0 +1,11 @@ + +.composer { + composes: card from "../material/cards.module.css"; + --card-gut: 8px; +} + +.replyInput { + display: flex; + align-items: flex-start; + gap: 8px; +}