import "./NoteCreator.css"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch, useSelector } from "react-redux"; import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent } from "@snort/nostr"; import Icon from "Icons/Icon"; import useEventPublisher from "Feed/EventPublisher"; import { openFile } from "Util"; import Textarea from "Element/Textarea"; import Modal from "Element/Modal"; import ProfileImage from "Element/ProfileImage"; import useFileUpload from "Upload"; import Note from "Element/Note"; import { setShow, setNote, setError, setActive, setPreview, setShowAdvanced, setSelectedCustomRelays, setZapForward, setSensitive, reset, setPollOptions, setOtherEvents, } from "State/NoteCreator"; import type { RootState } from "State/Store"; import { LNURL } from "LNURL"; import messages from "./messages"; import { ClipboardEventHandler, useState } from "react"; import Spinner from "Icons/Spinner"; import { EventBuilder } from "System"; import { Menu, MenuItem } from "@szhsin/react-menu"; import { LoginStore } from "Login"; import { getCurrentSubscription } from "Subscription"; import useLogin from "Hooks/useLogin"; interface NotePreviewProps { note: TaggedRawEvent; } function NotePreview({ note }: NotePreviewProps) { return (
{note.content.slice(0, 136)} {note.content.length > 140 && "..."}
); } export function NoteCreator() { const { formatMessage } = useIntl(); const publisher = useEventPublisher(); const uploader = useFileUpload(); const { note, zapForward, sensitive, pollOptions, replyTo, otherEvents, preview, active, show, showAdvanced, selectedCustomRelays, error, } = useSelector((s: RootState) => s.noteCreator); const [uploadInProgress, setUploadInProgress] = useState(false); const dispatch = useDispatch(); const sub = getCurrentSubscription(LoginStore.allSubscriptions()); const login = useLogin(); const relays = login.relays; async function sendNote() { if (note && publisher) { let extraTags: Array> | undefined; if (zapForward) { try { const svc = new LNURL(zapForward); await svc.load(); extraTags = [svc.getZapTag()]; } catch { dispatch( setError( formatMessage({ defaultMessage: "Invalid LNURL", }) ) ); return; } } if (sensitive) { extraTags ??= []; extraTags.push(["content-warning", sensitive]); } const kind = pollOptions ? EventKind.Polls : EventKind.TextNote; if (pollOptions) { extraTags ??= []; extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a])); } const hk = (eb: EventBuilder) => { extraTags?.forEach(t => eb.tag(t)); eb.kind(kind); return eb; }; const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk); if (selectedCustomRelays) publisher.broadcastAll(ev, selectedCustomRelays); else publisher.broadcast(ev); dispatch(reset()); for (const oe of otherEvents) { if (selectedCustomRelays) publisher.broadcastAll(oe, selectedCustomRelays); else publisher.broadcast(oe); } dispatch(reset()); } } async function attachFile() { try { const file = await openFile(); if (file) { uploadFile(file); } } catch (error: unknown) { if (error instanceof Error) { dispatch(setError(error?.message)); } } } async function uploadFile(file: File | Blob) { setUploadInProgress(true); try { if (file) { const rx = await uploader.upload(file, file.name); if (rx.header) { const link = `nostr:${encodeTLV(rx.header.id, NostrPrefix.Event, undefined, rx.header.kind)}`; dispatch(setNote(`${note ? `${note}\n` : ""}${link}`)); dispatch(setOtherEvents([...otherEvents, rx.header])); } else if (rx.url) { dispatch(setNote(`${note ? `${note}\n` : ""}${rx.url}`)); } else if (rx?.error) { dispatch(setError(rx.error)); } } } catch (error: unknown) { if (error instanceof Error) { dispatch(setError(error?.message)); } } finally { setUploadInProgress(false); } } function onChange(ev: React.ChangeEvent) { const { value } = ev.target; dispatch(setNote(value)); if (value) { dispatch(setActive(true)); } else { dispatch(setActive(false)); } } function cancel() { dispatch(reset()); } function onSubmit(ev: React.MouseEvent) { ev.stopPropagation(); sendNote().catch(console.warn); } async function loadPreview() { if (preview) { dispatch(setPreview(undefined)); } else if (publisher) { const tmpNote = await publisher.note(note); if (tmpNote) { dispatch(setPreview(tmpNote)); } } } function getPreviewNote() { if (preview) { return ( ); } } function renderPollOptions() { if (pollOptions) { return ( <>

{pollOptions?.map((a, i) => (
changePollOption(i, e.target.value)} /> {i > 1 && ( )}
))} ); } } function changePollOption(i: number, v: string) { if (pollOptions) { const copy = [...pollOptions]; copy[i] = v; dispatch(setPollOptions(copy)); } } function removePollOption(i: number) { if (pollOptions) { const copy = [...pollOptions]; copy.splice(i, 1); dispatch(setPollOptions(copy)); } } function renderRelayCustomisation() { return (
{Object.keys(relays.item || {}) .filter(el => relays.item[el].write) .map((r, i, a) => (
{r}
dispatch( setSelectedCustomRelays( // set false if all relays selected e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1 ? false : // otherwise return selectedCustomRelays with target relay added / removed a.filter(el => el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el) ) ) ) } />
))}
); } function listAccounts() { return LoginStore.getSessions().map(a => ( { ev.stopPropagation = true; LoginStore.switchAccount(a); }}> )); } 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); } } }; return ( <> {show && ( dispatch(setShow(false))}> {replyTo && } {preview && getPreviewNote()} {!preview && (