import { createEffect, createMemo, createRenderEffect, createSignal, Show, type Accessor, type Component, type JSX, type Ref, } from "solid-js"; import Scaffold from "~material/Scaffold"; import { Avatar, Button, IconButton, List, ListItemButton, ListItemIcon, ListItemSecondaryAction, ListItemText, Radio, Switch, Divider, CircularProgress, Toolbar, MenuItem, ListItemAvatar, } from "@suid/material"; import { ArrowDropDown, Public as PublicIcon, Send, People as PeopleIcon, ThreeP as ThreePIcon, ListAlt as ListAltIcon, Visibility, Translate, Close, MoreVert, } from "@suid/icons-material"; import type { Account } from "../accounts/stores"; import "./TootComposer.css"; import BottomSheet from "~material/BottomSheet"; import { useAppLocale } from "~platform/i18n"; import iso639_1 from "iso-639-1"; import ChooseTootLang from "./TootLangPicker"; import type { mastodon } from "masto"; import cardStyles from "~material/cards.module.css"; 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"; const TootVisibilityPickerDialog: Component<{ open?: boolean; class?: string; 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 ( Confirm } > setDiscoverable((x) => !x)} > ); }; const TootLanguagePickerDialog: Component<{ open?: boolean; class?: string; onClose: () => void; code: string; onCodeChange: (nval: string) => void; }> = (props) => { return ( ); }; function randomChoose( rn: number, K: T, ): T extends Array ? E : never { const idx = Math.floor(rn * K.length); return K[idx]; } function useRandomChoice(choices: () => T[]): Accessor { return createMemo(() => randomChoose(Math.random(), choices())); } function cancelEvent(event: Event) { event.stopPropagation(); } const TootComposer: Component<{ ref?: Ref; style?: JSX.CSSProperties; profile?: mastodon.v1.Account; replyToDisplayName?: string; mentions?: readonly string[]; client?: mastodon.rest.Client; inReplyToId?: string; onSent?: (status: mastodon.v1.Status) => void; }> = (props) => { let inputRef: HTMLTextAreaElement; 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 { language: appLanguage } = useAppLocale(); const [openMenu, menuState] = createManagedMenuState(); const randomPlaceholder = useRandomChoice(() => [ "What's happening?", "What do you think?", ]); createEffect(() => { const lang = appLanguage().split("-")[0]; setLanguage(lang); }); createEffect(() => { if (active()) { setTimeout(() => inputRef!.focus(), 0); } }); createEffect(() => { if (inputRef!.value !== "") return; if (props.mentions) { const prepText = props.mentions.join(" ") + " "; inputRef!.value = prepText; } }); const containerStyle = () => active() || permPicker() ? { position: "sticky" as const, top: "var(--scaffold-topbar-height, 0)", bottom: "var(--safe-area-inset-bottom, 0)", "z-index": 2, ...props.style, } : undefined; const visibilityText = () => { switch (visibility()) { case "public": return "Discoverable"; case "unlisted": return "Public"; case "private": return "Only Followers"; case "direct": return "Only Mentions"; } }; const idempotencyKey = createMemo(() => window.crypto.randomUUID()); const send = async () => { const client = session()?.client; if (!client) return; setSending(true); try { const status = await client.v1.statuses.create( { status: inputRef!.value, language: language(), visibility: visibility(), inReplyToId: props.inReplyToId, }, { requestInit: { headers: { ["Idempotency-Key"]: idempotencyKey(), }, }, }, ); props.onSent?.(status); inputRef!.value = ""; } finally { setSending(false); } }; return ( openMenu(e.currentTarget.getBoundingClientRect())} > } > } endIcon={} onClick={[setLangPickerOpen, true]} disabled={sending()} > {iso639_1.getNativeName(language())} } endIcon={} onClick={[setPermPicker, true]} disabled={sending()} > {visibilityText()} setPermPicker(false)} visibility={visibility()} onVisibilityChange={setVisibility} /> setLangPickerOpen(false)} code={language()} onCodeChange={setLanguage} /> ); }; export default TootComposer;