/* eslint-disable max-lines */ import { fetchNip05Pubkey, unixNow } from "@snort/shared"; import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { Menu, MenuItem } from "@szhsin/react-menu"; import classNames from "classnames"; import { ClipboardEventHandler, DragEvent, useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import AsyncButton from "@/Components/Button/AsyncButton"; import { AsyncIcon } from "@/Components/Button/AsyncIcon"; import CloseButton from "@/Components/Button/CloseButton"; import IconButton from "@/Components/Button/IconButton"; import { sendEventToRelays } from "@/Components/Event/Create/util"; import Note from "@/Components/Event/EventComponent"; import Flyout from "@/Components/flyout"; import Icon from "@/Components/Icons/Icon"; import { ToggleSwitch } from "@/Components/Icons/Toggle"; import Modal from "@/Components/Modal/Modal"; import Textarea from "@/Components/Textarea/Textarea"; import { Toastore } from "@/Components/Toaster/Toaster"; import { MediaServerFileList } from "@/Components/Upload/file-picker"; import Avatar from "@/Components/User/Avatar"; import useEventPublisher from "@/Hooks/useEventPublisher"; import useLogin from "@/Hooks/useLogin"; import usePreferences from "@/Hooks/usePreferences"; import useRelays from "@/Hooks/useRelays"; import { useNoteCreator } from "@/State/NoteCreator"; import { openFile, trackEvent } from "@/Utils"; import useFileUpload, { addExtensionToNip94Url, nip94TagsToIMeta, readNip94Tags } from "@/Utils/Upload"; import { GetPowWorker } from "@/Utils/wasm"; import { ZapTarget } from "@/Utils/Zapper"; import FileUploadProgress from "../FileUpload"; import { OkResponseRow } from "./OkResponseRow"; const previewNoteOptions = { showContextMenu: false, showFooter: false, canClick: false, showTime: false, }; const replyToNoteOptions = { showFooter: false, showContextMenu: false, showProfileCard: false, showTime: false, canClick: false, longFormPreview: true, }; const quoteNoteOptions = { showFooter: false, showContextMenu: false, showTime: false, canClick: false, longFormPreview: true, }; export function NoteCreator() { const { formatMessage } = useIntl(); const uploader = useFileUpload(); const publicKey = useLogin(s => s.publicKey); const profile = useUserProfile(publicKey); const pow = usePreferences(s => s.pow); const relays = useRelays(); const { system, publisher: pub } = useEventPublisher(); const publisher = pow ? pub?.pow(pow, GetPowWorker()) : pub; const note = useNoteCreator(); useEffect(() => { const draft = localStorage.getItem("msgDraft"); if (draft) { note.update(n => (n.note = draft)); } }, []); async function buildNote() { try { note.update(v => (v.error = "")); if (note && publisher) { let extraTags: Array> | undefined; if (note.zapSplits) { const parsedSplits = [] as Array; for (const s of note.zapSplits) { if (s.value.startsWith(NostrPrefix.PublicKey) || s.value.startsWith(NostrPrefix.Profile)) { const link = tryParseNostrLink(s.value); if (link) { parsedSplits.push({ ...s, value: link.id }); } else { throw new Error( formatMessage( { defaultMessage: "Failed to parse zap split: {input}", id: "sZQzjQ", }, { input: s.value, }, ), ); } } else if (s.value.includes("@")) { const [name, domain] = s.value.split("@"); const pubkey = await fetchNip05Pubkey(name, domain); if (pubkey) { parsedSplits.push({ ...s, value: pubkey }); } else { throw new Error( formatMessage( { defaultMessage: "Failed to parse zap split: {input}", id: "sZQzjQ", }, { input: s.value, }, ), ); } } else { throw new Error( formatMessage( { defaultMessage: "Invalid zap split: {input}", id: "8Y6bZQ", }, { input: s.value, }, ), ); } } extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]); } if (note.sensitive) { extraTags ??= []; extraTags.push(["content-warning", note.sensitive]); } const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote; if (note.pollOptions) { extraTags ??= []; extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a])); } if (note.hashTags.length > 0) { extraTags ??= []; extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()])); } for (const ex of note.otherEvents ?? []) { const meta = readNip94Tags(ex.tags); if (!meta.url) continue; if (!note.note.endsWith("\n")) { note.note += "\n"; } note.note += addExtensionToNip94Url(meta); extraTags ??= []; extraTags.push(nip94TagsToIMeta(meta)); } // add quote repost if (note.quote) { if (!note.note.endsWith("\n")) { note.note += "\n"; } const link = NostrLink.fromEvent(note.quote); note.note += `nostr:${link.encode(CONFIG.eventLinkPrefix)}`; const quoteTag = link.toEventTag(); if (quoteTag) { extraTags ??= []; if (quoteTag[0] === "e") { quoteTag[0] = "q"; // how to 'q' tag replacable events? } extraTags.push(quoteTag); } } const hk = (eb: EventBuilder) => { extraTags?.forEach(t => eb.tag(t)); note.extraTags?.forEach(t => eb.tag(t)); eb.kind(kind); return eb; }; const ev = note.replyTo ? await publisher.reply(note.replyTo, note.note, hk) : await publisher.note(note.note, hk); return ev; } } catch (e) { note.update(v => { if (e instanceof Error) { v.error = e.message; } else { v.error = e as string; } }); } } async function sendNote() { const ev = await buildNote(); if (ev) { let props: Record | undefined = undefined; if (ev.tags.find(a => a[0] === "content-warning")) { props ??= {}; props["content-warning"] = true; } if (ev.tags.find(a => a[0] === "poll_option")) { props ??= {}; props["poll"] = true; } if (ev.tags.find(a => a[0] === "zap")) { props ??= {}; props["zap-split"] = true; } if (note.hashTags.length > 0) { props ??= {}; props["hashtags"] = true; } if (props) { props["content-warning"] ??= false; props["poll"] ??= false; props["zap-split"] ??= false; props["hashtags"] ??= false; } trackEvent("PostNote", props); sendEventToRelays(system, ev, note.selectedCustomRelays, r => { if (CONFIG.noteCreatorToast) { r.forEach(rr => { Toastore.push({ element: c => , expire: unixNow() + (rr.ok ? 5 : 55555), }); }); } }); note.update(n => n.reset()); localStorage.removeItem("msgDraft"); } } async function attachFile() { try { const file = await openFile(); if (file) { uploadFile(file); } } catch (e) { note.update(v => { if (e instanceof Error) { v.error = e.message; } else { v.error = e as string; } }); } } async function uploadFile(file: File) { try { if (file && uploader) { const rx = await uploader.upload(file, file.name); note.update(v => { if (rx.header) { v.otherEvents ??= []; v.otherEvents.push(rx.header); } else if (rx.url) { v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`; if (rx.metadata) { v.extraTags ??= []; const imeta = nip94TagsToIMeta(rx.metadata); v.extraTags.push(imeta); } } else if (rx?.error) { v.error = rx.error; } }); } } catch (e) { note.update(v => { if (e instanceof Error) { v.error = e.message; } else { v.error = e as string; } }); } } function onChange(ev: React.ChangeEvent) { const { value } = ev.target; note.update(n => (n.note = value)); localStorage.setItem("msgDraft", value); } function cancel() { note.update(v => { v.show = false; v.reset(); }); } async function onSubmit(ev: React.MouseEvent) { ev.stopPropagation(); await sendNote(); } async function loadPreview() { if (note.preview) { note.update(v => (v.preview = undefined)); } else if (publisher) { const tmpNote = await buildNote(); trackEvent("PostNotePreview"); note.update(v => (v.preview = tmpNote)); } } function getPreviewNote() { if (note.preview) { return ( ); } } function renderPollOptions() { if (note.pollOptions) { return ( <>

{note.pollOptions?.map((a, i) => (
changePollOption(i, e.target.value)} /> {i > 1 && removePollOption(i)} />}
))} ); } } function changePollOption(i: number, v: string) { if (note.pollOptions) { const copy = [...note.pollOptions]; copy[i] = v; note.update(v => (v.pollOptions = copy)); } } function removePollOption(i: number) { if (note.pollOptions) { const copy = [...note.pollOptions]; copy.splice(i, 1); note.update(v => (v.pollOptions = copy)); } } function renderRelayCustomisation() { return (
{Object.entries(relays) .filter(el => el[1].write) .map(a => a[0]) .map((r, i, a) => (
{r}
{ note.update( v => (v.selectedCustomRelays = // set false if all relays selected e.target.checked && note.selectedCustomRelays && note.selectedCustomRelays.length == a.length - 1 ? undefined : // otherwise return selectedCustomRelays with target relay added / removed a.filter(el => el === r ? e.target.checked : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el), )), ); }} />
))}
); } /*function listAccounts() { return LoginStore.getSessions().map(a => ( { ev.stopPropagation = true; LoginStore.switchAccount(a); }}> )); }*/ function noteCreatorAdvanced() { return ( <>

{renderRelayCustomisation()}

{[...(note.zapSplits ?? [])].map((v: ZapTarget, i, arr) => (

note.update( v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))), ) } placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address", id: "WvGmZT" })} />

note.update( v => (v.zapSplits = arr.map((vv, ii) => ii === i ? { ...vv, weight: Number(e.target.value) } : vv, )), ) } />
 
note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))} />
))}

note.update(v => (v.sensitive = e.target.value))} maxLength={50} minLength={1} placeholder={formatMessage({ defaultMessage: "Reason", id: "AkCxS/", })} />
); } function noteCreatorFooter() { return (
} menuClassName="ctx-menu no-icons">
{/* This menu item serves as a "close menu" button; it allows the user to click anywhere nearby the menu to close it. */}
note.update(s => (s.filePicker = "compact"))}> attachFile()}>
{note.pollOptions === undefined && !note.replyTo && ( note.update(v => (v.pollOptions = ["A", "B"]))} className={classNames("hover:text-gray-superlight transition", { "text-white": note.pollOptions !== undefined, })} /> )} note.update(v => (v.advanced = !v.advanced))} className={classNames("hover:text-gray-superlight transition", { "text-white": note.advanced })} /> loadPreview()} size={40} className={classNames({ active: Boolean(note.preview) })} />
{note.replyTo ? : }
); } const handlePaste: ClipboardEventHandler = evt => { if (evt.clipboardData) { const clipboardItems = evt.clipboardData.items; const items: DataTransferItem[] = Array.from(clipboardItems).filter(function (item: DataTransferItem) { // Filter the image items only return /^image\//.test(item.type); }); if (items.length === 0) { return; } const item = items[0]; const blob = item.getAsFile(); if (blob) { uploadFile(blob); } } }; const handleDragOver = (event: DragEvent) => { event.preventDefault(); }; const handleDragLeave = (event: DragEvent) => { event.preventDefault(); }; const handleDrop = (event: DragEvent) => { event.preventDefault(); const droppedFiles = Array.from(event.dataTransfer.files); droppedFiles.forEach(async file => { await uploadFile(file); }); }; function noteCreatorForm() { return ( <> {note.replyTo && ( <>


)} {note.quote && ( <>


)} {note.preview && getPreviewNote()} {!note.preview && (