diff --git a/src/platform/SizedTextarea.css b/src/platform/SizedTextarea.css new file mode 100644 index 0000000..14184f6 --- /dev/null +++ b/src/platform/SizedTextarea.css @@ -0,0 +1,5 @@ +.SizedTextarea { + overflow-y: hidden; + width: 100%; + resize: vertical; +} diff --git a/src/platform/SizedTextarea.tsx b/src/platform/SizedTextarea.tsx new file mode 100644 index 0000000..4f97ff1 --- /dev/null +++ b/src/platform/SizedTextarea.tsx @@ -0,0 +1,63 @@ +import { splitProps, type Component, type JSX } from "solid-js"; +import "./SizedTextarea.css"; + +function isBoundEventHandler( + handler: JSX.EventHandlerUnion, +): handler is JSX.BoundEventHandler { + return Array.isArray(handler); +} + +function callEventHandlerUnion( + handler: JSX.EventHandlerUnion, + event: E & { currentTarget: T; target: Element }, +) { + if (isBoundEventHandler(handler)) { + const fn = handler[0], + value = handler[1]; + fn(value, event); + } else { + (handler as (e: typeof event) => void).bind(event.target)(event); + } +} + +function onTextareaRefreshHeight< + E extends Event & { + currentTarget: HTMLTextAreaElement; + target: HTMLTextAreaElement; + }, +>( + ocallback: JSX.EventHandlerUnion | undefined, + event: E, +) { + const element = event.currentTarget; + element.style.removeProperty("height"); + element.style.height = `${element.scrollHeight + 2}px`; + + if (ocallback) { + callEventHandlerUnion(ocallback, event); + } +} + +/** + * The + ); +}; + +export default SizedTextarea; diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx index edd1806..150ca0e 100644 --- a/src/timelines/TimelinePanel.tsx +++ b/src/timelines/TimelinePanel.tsx @@ -33,7 +33,6 @@ const TimelinePanel: Component<{ () => props.client.v1.timelines[props.name], () => ({ limit: 20 }), ); - const [typing, setTyping] = createSignal(false); const tlEndObserver = new IntersectionObserver(() => { if (untrack(() => props.prefetch) && !snapshot.loading) @@ -65,8 +64,6 @@ const TimelinePanel: Component<{ style={{ "--scaffold-topbar-height": "0px", }} - isTyping={typing()} - onTypingChange={setTyping} client={props.client} onSent={() => refetchTimeline("prev")} /> diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index 40d04ad..92a44c3 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -46,7 +46,6 @@ const TootBottomSheet: Component = (props) => { }>(); const navigate = useNavigate(); const time = createTimeSource(); - const [isInTyping, setInTyping] = createSignal(false); const acctText = () => decodeURIComponent(params.acct); const session = useSessionForAcctStr(acctText); @@ -70,12 +69,6 @@ const TootBottomSheet: Component = (props) => { return tootId; }); - createEffect(() => { - if (location.state?.tootReply) { - setInTyping(true); - } - }); - const [tootContextErrorUncaught, { refetch: refetchContext }] = createResource( () => [session().client, params.id] as const, @@ -282,8 +275,6 @@ const TootBottomSheet: Component = (props) => { .MuiToolbar-root { + justify-content: space-between; + + > :first-child { + margin-left: -0.5em; + } + + > :last-child { + margin-right: -0.5em; + } + } + + .reply-input { + display: flex; + align-items: flex-start; + gap: 8px; + } + + .options { + display: flex; + justify-content: flex-end; + gap: 16px; + flex-flow: row wrap; + padding-top: 16px; + padding-bottom: 8px; + margin-left: -0.5em; + margin-right: -0.5em; + + animation: TootComposerFadeIn 110ms var(--tutu-anim-curve-sharp) both; + } +} + +@keyframes TootComposerFadeIn { + 0% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } +} diff --git a/src/timelines/TootComposer.module.css b/src/timelines/TootComposer.module.css deleted file mode 100644 index f752e7c..0000000 --- a/src/timelines/TootComposer.module.css +++ /dev/null @@ -1,11 +0,0 @@ - -.composer { - composes: card from "../material/cards.module.css"; - --card-gut: 8px; -} - -.replyInput { - display: flex; - align-items: flex-start; - gap: 8px; -} diff --git a/src/timelines/TootComposer.tsx b/src/timelines/TootComposer.tsx index 01e0b81..60e6bc5 100644 --- a/src/timelines/TootComposer.tsx +++ b/src/timelines/TootComposer.tsx @@ -1,10 +1,12 @@ import { createEffect, createMemo, + createRenderEffect, createSignal, createUniqueId, onMount, Show, + type Accessor, type Component, type JSX, type Ref, @@ -23,6 +25,9 @@ import { Switch, Divider, CircularProgress, + Toolbar, + MenuItem, + ListItemAvatar, } from "@suid/material"; import { ArrowDropDown, @@ -33,9 +38,11 @@ import { ListAlt as ListAltIcon, Visibility, Translate, + Close, + MoreVert, } from "@suid/icons-material"; import type { Account } from "../accounts/stores"; -import tootComposers from "./TootComposer.module.css"; +import "./TootComposer.css"; import { makeEventListener } from "@solid-primitives/event-listener"; import BottomSheet from "../material/BottomSheet"; import { useLanguage } from "../platform/i18n"; @@ -43,6 +50,11 @@ import iso639_1 from "iso-639-1"; import ChooseTootLang from "./ChooseTootLang"; import type { mastodon } from "masto"; import cardStyles from "../material/cards.module.css"; +import { Title } from "../material/typography"; +import Menu, { createManagedMenuState } from "../material/Menu"; +import { useDefaultSession } from "../masto/clients"; +import { resolveCustomEmoji } from "../masto/toot"; +import SizedTextarea from "../platform/SizedTextarea"; type TootVisibility = "public" | "unlisted" | "private" | "direct"; @@ -196,6 +208,10 @@ function randomChoose( return K[idx]; } +function useRandomChoice(choices: () => T[]): Accessor { + return createMemo(() => randomChoose(Math.random(), choices())); +} + function cancelEvent(event: Event) { event.stopPropagation(); } @@ -206,27 +222,27 @@ const TootComposer: Component<{ profile?: Account; replyToDisplayName?: string; mentions?: readonly string[]; - isTyping?: boolean; - onTypingChange: (value: boolean) => void; client?: mastodon.rest.Client; inReplyToId?: string; onSent?: (status: mastodon.v1.Status) => void; }> = (props) => { let inputRef: HTMLTextAreaElement; - let sendKey: string | undefined; - const typing = () => props.isTyping; - const setTyping = (v: boolean) => props.onTypingChange(v); + const session = useDefaultSession(); + + const [active, setActive] = createSignal(false); const [sending, setSending] = createSignal(false); const [visibility, setVisibility] = createSignal("public"); const [permPicker, setPermPicker] = createSignal(false); const [language, setLanguage] = createSignal("en"); const [langPickerOpen, setLangPickerOpen] = createSignal(false); const appLanguage = useLanguage(); + const [openMenu, menuState] = createManagedMenuState(); - const randomPlaceholder = createMemo(() => - randomChoose(Math.random(), ["What's happening?", "What do your think?"]), - ); + const randomPlaceholder = useRandomChoice(() => [ + "What's happening?", + "What do you think?", + ]); createEffect(() => { const lang = appLanguage().split("-")[0]; @@ -234,15 +250,11 @@ const TootComposer: Component<{ }); createEffect(() => { - if (typing()) { + if (active()) { setTimeout(() => inputRef.focus(), 0); } }); - onMount(() => { - makeEventListener(inputRef, "focus", () => setTyping(true)); - }); - createEffect(() => { if (inputRef.value !== "") return; if (props.mentions) { @@ -252,7 +264,7 @@ const TootComposer: Component<{ }); const containerStyle = () => - typing() || permPicker() + active() || permPicker() ? { position: "sticky" as const, top: "var(--scaffold-topbar-height, 0)", @@ -275,17 +287,15 @@ const TootComposer: Component<{ } }; - const getOrGenSendKey = () => { - if (sendKey === undefined) { - sendKey = window.crypto.randomUUID(); - } - return sendKey; - }; + const idempotencyKey = createMemo(() => window.crypto.randomUUID()); const send = async () => { + const client = session()?.client; + if (!client) return; + setSending(true); try { - const status = await props.client!.v1.statuses.create( + const status = await client.v1.statuses.create( { status: inputRef.value, language: language(), @@ -295,7 +305,7 @@ const TootComposer: Component<{ { requestInit: { headers: { - ["Idempotency-Key"]: getOrGenSendKey(), + ["Idempotency-Key"]: idempotencyKey(), }, }, }, @@ -311,11 +321,8 @@ const TootComposer: Component<{ return (
{ - inputRef.focus(); - }} on:touchend={ cancelEvent /* on: is required to register the event handler on the exact element */ @@ -323,23 +330,63 @@ const TootComposer: Component<{ on:touchmove={cancelEvent} on:wheel={cancelEvent} > -
+ + + + + + openMenu(e.currentTarget.getBoundingClientRect())} + > + + + +
+ + + + + + + { + createRenderEffect(() => { + const inf = session()?.account.inf; + return (e.innerHTML = resolveCustomEmoji( + inf?.displayName || "", + inf?.emojis ?? [], + )); + }); + }} + > + + + +
+
+ +
- + autocomplete="off" + > } > - +
- -
- -