snort/packages/app/src/Components/Event/Create/NoteCreator.tsx

686 lines
22 KiB
TypeScript
Raw Normal View History

2024-01-26 10:33:07 +00:00
/* eslint-disable max-lines */
2023-01-16 17:48:25 +00:00
import "./NoteCreator.css";
2024-01-04 17:01:18 +00:00
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
2023-11-28 09:39:12 +00:00
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
2023-10-16 16:09:05 +00:00
import classNames from "classnames";
2024-01-04 17:01:18 +00:00
import { ClipboardEventHandler, DragEvent, useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl";
2023-02-08 21:10:26 +00:00
2024-01-04 17:01:18 +00:00
import AsyncButton from "@/Components/Button/AsyncButton";
import { AsyncIcon } from "@/Components/Button/AsyncIcon";
import CloseButton from "@/Components/Button/CloseButton";
import { sendEventToRelays } from "@/Components/Event/Create/util";
import Note from "@/Components/Event/EventComponent";
import Icon from "@/Components/Icons/Icon";
2024-01-04 17:01:18 +00:00
import { ToggleSwitch } from "@/Components/Icons/Toggle";
import Modal from "@/Components/Modal/Modal";
2024-01-04 17:01:18 +00:00
import Textarea from "@/Components/Textarea/Textarea";
import { Toastore } from "@/Components/Toaster/Toaster";
import ProfileImage from "@/Components/User/ProfileImage";
2024-01-04 17:01:18 +00:00
import useEventPublisher from "@/Hooks/useEventPublisher";
2023-11-17 11:52:10 +00:00
import useLogin from "@/Hooks/useLogin";
2024-04-22 13:38:14 +00:00
import usePreferences from "@/Hooks/usePreferences";
import useRelays from "@/Hooks/useRelays";
2023-11-17 11:52:10 +00:00
import { useNoteCreator } from "@/State/NoteCreator";
2024-01-09 12:35:25 +00:00
import { openFile, trackEvent } from "@/Utils";
2024-01-04 17:01:18 +00:00
import useFileUpload from "@/Utils/Upload";
import { GetPowWorker } from "@/Utils/wasm";
import { ZapTarget } from "@/Utils/Zapper";
2023-11-28 09:39:12 +00:00
import FileUploadProgress from "../FileUpload";
2023-12-02 22:44:44 +00:00
import { OkResponseRow } from "./OkResponseRow";
2023-02-01 22:14:30 +00:00
2024-01-08 13:42:25 +00:00
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() {
2023-03-27 22:58:29 +00:00
const { formatMessage } = useIntl();
2023-02-05 12:32:34 +00:00
const uploader = useFileUpload();
2024-04-22 13:38:14 +00:00
const publicKey = useLogin(s => s.publicKey);
const pow = usePreferences(s => s.pow);
const relays = useRelays();
2023-11-28 08:40:36 +00:00
const { system, publisher: pub } = useEventPublisher();
2024-04-22 13:38:14 +00:00
const publisher = pow ? pub?.pow(pow, GetPowWorker()) : pub;
2023-09-21 20:02:59 +00:00
const note = useNoteCreator();
2023-01-16 17:48:25 +00:00
useEffect(() => {
const draft = localStorage.getItem("msgDraft");
if (draft) {
note.update(n => (n.note = draft));
}
}, []);
2023-09-14 11:31:17 +00:00
async function buildNote() {
try {
2023-09-21 21:01:39 +00:00
note.update(v => (v.error = ""));
2023-09-14 11:31:17 +00:00
if (note && publisher) {
let extraTags: Array<Array<string>> | undefined;
2023-09-21 20:02:59 +00:00
if (note.zapSplits) {
2023-09-14 11:31:17 +00:00
const parsedSplits = [] as Array<ZapTarget>;
2023-09-21 20:02:59 +00:00
for (const s of note.zapSplits) {
2023-09-14 11:31:17 +00:00
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(
{
2023-11-20 19:16:47 +00:00
defaultMessage: "Failed to parse zap split: {input}",
id: "sZQzjQ",
2023-09-14 11:31:17 +00:00
},
{
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(
{
2023-11-20 19:16:47 +00:00
defaultMessage: "Failed to parse zap split: {input}",
id: "sZQzjQ",
2023-09-14 11:31:17 +00:00
},
{
input: s.value,
},
),
);
}
} else {
throw new Error(
formatMessage(
{
2023-11-20 19:16:47 +00:00
defaultMessage: "Invalid zap split: {input}",
id: "8Y6bZQ",
2023-09-14 11:31:17 +00:00
},
{
input: s.value,
},
),
);
}
}
extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]);
2023-03-27 22:58:29 +00:00
}
2023-09-21 20:02:59 +00:00
if (note.sensitive) {
2023-09-14 11:31:17 +00:00
extraTags ??= [];
2023-09-21 20:02:59 +00:00
extraTags.push(["content-warning", note.sensitive]);
2023-09-14 11:31:17 +00:00
}
2023-09-21 20:02:59 +00:00
const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote;
if (note.pollOptions) {
2023-09-14 11:31:17 +00:00
extraTags ??= [];
2023-09-21 20:02:59 +00:00
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
2023-09-14 11:31:17 +00:00
}
2023-11-16 15:42:47 +00:00
if (note.hashTags.length > 0) {
extraTags ??= [];
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
}
2023-10-13 11:40:39 +00:00
// add quote repost
if (note.quote) {
if (!note.note.endsWith("\n")) {
note.note += "\n";
}
const link = NostrLink.fromEvent(note.quote);
2023-10-18 07:01:25 +00:00
note.note += `nostr:${link.encode(CONFIG.eventLinkPrefix)}`;
2023-10-13 11:40:39 +00:00
const quoteTag = link.toEventTag();
if (quoteTag) {
extraTags ??= [];
if (quoteTag[0] === "e") {
quoteTag[0] = "q"; // how to 'q' tag replacable events?
}
extraTags.push(quoteTag);
}
}
2023-09-14 11:31:17 +00:00
const hk = (eb: EventBuilder) => {
extraTags?.forEach(t => eb.tag(t));
2023-10-16 10:07:13 +00:00
note.extraTags?.forEach(t => eb.tag(t));
2023-09-14 11:31:17 +00:00
eb.kind(kind);
return eb;
};
2023-09-21 21:01:39 +00:00
const ev = note.replyTo
? await publisher.reply(note.replyTo, note.note, hk)
: await publisher.note(note.note, hk);
2023-09-14 11:31:17 +00:00
return ev;
2023-04-06 22:12:51 +00:00
}
2023-09-14 11:31:17 +00:00
} catch (e) {
2023-09-21 20:02:59 +00:00
note.update(v => {
if (e instanceof Error) {
v.error = e.message;
} else {
v.error = e as string;
}
});
}
}
2023-09-14 11:31:17 +00:00
async function sendNote() {
const ev = await buildNote();
if (ev) {
2023-12-10 18:40:01 +00:00
let props: Record<string, boolean> | undefined = undefined;
2023-12-10 17:40:54 +00:00
if (ev.tags.find(a => a[0] === "content-warning")) {
2023-12-10 18:40:01 +00:00
props ??= {};
props["content-warning"] = true;
2023-12-10 17:40:54 +00:00
}
if (ev.tags.find(a => a[0] === "poll_option")) {
2023-12-10 18:40:01 +00:00
props ??= {};
props["poll"] = true;
2023-12-10 17:40:54 +00:00
}
if (ev.tags.find(a => a[0] === "zap")) {
2023-12-10 18:40:01 +00:00
props ??= {};
props["zap-split"] = true;
2023-12-10 17:40:54 +00:00
}
2024-01-08 10:25:31 +00:00
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;
}
2023-12-10 18:40:01 +00:00
trackEvent("PostNote", props);
2023-11-28 08:40:36 +00:00
const events = (note.otherEvents ?? []).concat(ev);
2023-12-02 22:45:52 +00:00
events.map(a =>
sendEventToRelays(system, a, note.selectedCustomRelays, r => {
if (CONFIG.noteCreatorToast) {
r.forEach(rr => {
Toastore.push({
2023-12-03 14:06:40 +00:00
element: c => <OkResponseRow rsp={rr} close={c} />,
2023-12-02 22:45:52 +00:00
expire: unixNow() + (rr.ok ? 5 : 55555),
});
});
}
}),
);
2023-12-02 22:44:44 +00:00
note.update(n => n.reset());
localStorage.removeItem("msgDraft");
2023-01-16 17:48:25 +00:00
}
2023-02-05 12:32:34 +00:00
}
2023-01-16 17:48:25 +00:00
2023-02-05 12:32:34 +00:00
async function attachFile() {
try {
2023-02-07 19:47:57 +00:00
const file = await openFile();
2023-04-12 11:17:59 +00:00
if (file) {
uploadFile(file);
}
2023-09-21 20:02:59 +00:00
} catch (e) {
note.update(v => {
if (e instanceof Error) {
v.error = e.message;
} else {
v.error = e as string;
}
});
2023-04-12 11:17:59 +00:00
}
}
2023-12-10 17:40:54 +00:00
async function uploadFile(file: File) {
2023-04-12 11:17:59 +00:00
try {
2023-02-05 12:32:34 +00:00
if (file) {
2023-02-07 19:47:57 +00:00
const rx = await uploader.upload(file, file.name);
2023-09-21 20:02:59 +00:00
note.update(v => {
if (rx.header) {
2023-10-18 08:50:26 +00:00
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode(
CONFIG.eventLinkPrefix,
)}`;
2023-09-21 20:02:59 +00:00
v.note = `${v.note ? `${v.note}\n` : ""}${link}`;
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
} else if (rx.url) {
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
2023-10-16 10:07:13 +00:00
if (rx.metadata) {
v.extraTags ??= [];
const imeta = ["imeta", `url ${rx.url}`];
if (rx.metadata.blurhash) {
imeta.push(`blurhash ${rx.metadata.blurhash}`);
}
if (rx.metadata.width && rx.metadata.height) {
imeta.push(`dim ${rx.metadata.width}x${rx.metadata.height}`);
}
2023-12-10 17:19:09 +00:00
if (rx.metadata.hash) {
imeta.push(`x ${rx.metadata.hash}`);
}
2023-10-16 10:07:13 +00:00
v.extraTags.push(imeta);
}
2023-09-21 20:02:59 +00:00
} else if (rx?.error) {
v.error = rx.error;
}
});
2023-02-07 19:47:57 +00:00
}
2023-09-21 20:02:59 +00:00
} catch (e) {
note.update(v => {
if (e instanceof Error) {
v.error = e.message;
} else {
v.error = e as string;
}
});
2023-01-16 17:48:25 +00:00
}
2023-02-05 12:32:34 +00:00
}
2023-01-16 17:48:25 +00:00
2023-02-07 19:47:57 +00:00
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
const { value } = ev.target;
2023-09-21 21:01:39 +00:00
note.update(n => (n.note = value));
localStorage.setItem("msgDraft", value);
2023-02-05 12:32:34 +00:00
}
2023-01-16 17:48:25 +00:00
2023-02-07 19:47:57 +00:00
function cancel() {
2023-09-21 20:02:59 +00:00
note.update(v => {
v.show = false;
v.reset();
});
2023-02-05 12:32:34 +00:00
}
2023-01-25 18:08:53 +00:00
2023-10-16 15:54:55 +00:00
async function onSubmit(ev: React.MouseEvent) {
2023-02-05 12:32:34 +00:00
ev.stopPropagation();
2023-08-18 18:01:34 +00:00
await sendNote();
2023-02-05 12:32:34 +00:00
}
2023-01-16 17:48:25 +00:00
2023-03-31 22:43:07 +00:00
async function loadPreview() {
2023-09-21 20:02:59 +00:00
if (note.preview) {
2023-09-21 21:01:39 +00:00
note.update(v => (v.preview = undefined));
2023-04-14 15:02:15 +00:00
} else if (publisher) {
2023-09-14 11:31:17 +00:00
const tmpNote = await buildNote();
2023-12-10 18:40:01 +00:00
trackEvent("PostNotePreview");
2023-09-21 21:01:39 +00:00
note.update(v => (v.preview = tmpNote));
2023-03-31 22:43:07 +00:00
}
}
function getPreviewNote() {
2023-09-21 20:02:59 +00:00
if (note.preview) {
2024-01-11 22:09:12 +00:00
return (
<Note className="hover:bg-transparent" data={note.preview as TaggedNostrEvent} options={previewNoteOptions} />
);
2023-03-31 22:43:07 +00:00
}
}
2023-04-10 14:55:25 +00:00
function renderPollOptions() {
2023-09-21 20:02:59 +00:00
if (note.pollOptions) {
2023-04-10 14:55:25 +00:00
return (
<>
<h4>
<FormattedMessage defaultMessage="Poll Options" id="vhlWFg" />
2023-04-10 14:55:25 +00:00
</h4>
2023-09-21 20:02:59 +00:00
{note.pollOptions?.map((a, i) => (
2023-04-10 14:55:25 +00:00
<div className="form-group w-max" key={`po-${i}`}>
<div>
<FormattedMessage defaultMessage="Option: {n}" id="mfe8RW" values={{ n: i + 1 }} />
2023-04-10 14:55:25 +00:00
</div>
<div>
<input type="text" value={a} onChange={e => changePollOption(i, e.target.value)} />
{i > 1 && <CloseButton className="ml5" onClick={() => removePollOption(i)} />}
2023-04-10 14:55:25 +00:00
</div>
</div>
))}
2023-09-21 21:01:39 +00:00
<button onClick={() => note.update(v => (v.pollOptions = [...(note.pollOptions ?? []), ""]))}>
2023-04-10 14:55:25 +00:00
<Icon name="plus" size={14} />
</button>
</>
);
}
}
function changePollOption(i: number, v: string) {
2023-09-21 20:02:59 +00:00
if (note.pollOptions) {
const copy = [...note.pollOptions];
2023-04-10 14:55:25 +00:00
copy[i] = v;
2023-09-21 21:01:39 +00:00
note.update(v => (v.pollOptions = copy));
2023-04-10 14:55:25 +00:00
}
}
function removePollOption(i: number) {
2023-09-21 20:02:59 +00:00
if (note.pollOptions) {
const copy = [...note.pollOptions];
2023-04-10 14:55:25 +00:00
copy.splice(i, 1);
2023-09-21 21:01:39 +00:00
note.update(v => (v.pollOptions = copy));
2023-04-10 14:55:25 +00:00
}
}
2023-05-04 13:16:58 +00:00
function renderRelayCustomisation() {
return (
<div className="flex flex-col g8">
2024-04-22 13:38:14 +00:00
{Object.entries(relays)
.filter(el => el[1].write)
.map(a => a[0])
2023-05-04 13:16:58 +00:00
.map((r, i, a) => (
2024-01-04 09:54:58 +00:00
<div className="p flex justify-between note-creator-relay" key={r}>
2023-08-23 12:19:48 +00:00
<div>{r}</div>
2023-05-04 13:16:58 +00:00
<div>
<input
type="checkbox"
2023-09-21 20:02:59 +00:00
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
onChange={e => {
2023-09-21 21:01:39 +00:00
note.update(
v =>
2024-01-08 10:31:49 +00:00
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
2023-09-21 21:01:39 +00:00
note.selectedCustomRelays &&
note.selectedCustomRelays.length == a.length - 1
2024-01-08 10:31:49 +00:00
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
2023-09-21 21:01:39 +00:00
);
}}
2023-05-04 13:16:58 +00:00
/>
</div>
</div>
))}
</div>
);
}
2023-09-14 11:31:17 +00:00
/*function listAccounts() {
2023-04-19 12:10:41 +00:00
return LoginStore.getSessions().map(a => (
<MenuItem
onClick={ev => {
ev.stopPropagation = true;
LoginStore.switchAccount(a);
}}>
2023-05-10 12:29:07 +00:00
<ProfileImage pubkey={a} link={""} />
2023-04-19 12:10:41 +00:00
</MenuItem>
));
2023-09-14 11:31:17 +00:00
}*/
2023-04-19 12:10:41 +00:00
2023-10-11 14:41:36 +00:00
function noteCreatorAdvanced() {
return (
<>
<div>
<h4>
<FormattedMessage defaultMessage="Custom Relays" id="EcZF24" />
2023-10-11 14:41:36 +00:00
</h4>
<p>
<FormattedMessage defaultMessage="Send note to a subset of your write relays" id="th5lxp" />
2023-10-11 14:41:36 +00:00
</p>
{renderRelayCustomisation()}
</div>
<div className="flex flex-col g8">
2023-10-11 14:41:36 +00:00
<h4>
<FormattedMessage defaultMessage="Zap Splits" id="5CB6zB" />
2023-10-11 14:41:36 +00:00
</h4>
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." id="LwYmVi" />
<div className="flex flex-col g8">
2024-01-04 09:54:58 +00:00
{[...(note.zapSplits ?? [])].map((v: ZapTarget, i, arr) => (
<div className="flex items-center g8" key={`${v.name}-${v.value}`}>
2024-01-02 18:11:44 +00:00
<div className="flex flex-col flex-4 g4">
2023-10-11 14:41:36 +00:00
<h4>
<FormattedMessage defaultMessage="Recipient" id="8Rkoyb" />
2023-10-11 14:41:36 +00:00
</h4>
<input
type="text"
value={v.value}
onChange={e =>
note.update(
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
)
}
2023-11-20 19:16:47 +00:00
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address", id: "WvGmZT" })}
2023-10-11 14:41:36 +00:00
/>
</div>
2024-01-02 18:11:44 +00:00
<div className="flex flex-col flex-1 g4">
2023-10-11 14:41:36 +00:00
<h4>
<FormattedMessage defaultMessage="Weight" id="zCb8fX" />
2023-10-11 14:41:36 +00:00
</h4>
<input
type="number"
min={0}
value={v.weight}
onChange={e =>
note.update(
v =>
2024-01-08 10:31:49 +00:00
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
2023-10-11 14:41:36 +00:00
)
}
/>
</div>
<div className="flex flex-col s g4">
2023-10-11 14:41:36 +00:00
<div>&nbsp;</div>
<Icon
name="close"
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
/>
</div>
</div>
))}
<button
type="button"
onClick={() =>
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
2023-10-11 14:41:36 +00:00
</button>
</div>
<span className="warning">
2023-11-20 19:16:47 +00:00
<FormattedMessage
defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured"
id="6bgpn+"
/>
2023-10-11 14:41:36 +00:00
</span>
</div>
<div className="flex flex-col g8">
2023-10-11 14:41:36 +00:00
<h4>
<FormattedMessage defaultMessage="Sensitive Content" id="bQdA2k" />
2023-10-11 14:41:36 +00:00
</h4>
2023-11-20 19:16:47 +00:00
<FormattedMessage
defaultMessage="Users must accept the content warning to show the content of your note."
id="UUPFlt"
/>
2023-10-11 14:41:36 +00:00
<input
className="w-max"
type="text"
value={note.sensitive}
onChange={e => note.update(v => (v.sensitive = e.target.value))}
maxLength={50}
minLength={1}
placeholder={formatMessage({
2023-11-20 19:16:47 +00:00
defaultMessage: "Reason",
id: "AkCxS/",
2023-10-11 14:41:36 +00:00
})}
2023-09-21 21:01:39 +00:00
/>
2023-10-11 14:41:36 +00:00
<span className="warning">
<FormattedMessage defaultMessage="Not all clients support this yet" id="gXgY3+" />
2023-10-11 14:41:36 +00:00
</span>
2023-09-21 21:01:39 +00:00
</div>
2023-10-11 14:41:36 +00:00
</>
);
}
function noteCreatorFooter() {
return (
2023-12-12 12:04:38 +00:00
<div className="flex justify-between">
2023-10-17 13:57:59 +00:00
<div className="flex items-center g8">
2023-09-21 21:01:39 +00:00
<ProfileImage
2024-04-22 13:38:14 +00:00
pubkey={publicKey ?? ""}
2023-09-21 21:01:39 +00:00
className="note-creator-icon"
link=""
showUsername={false}
showFollowDistance={false}
2023-12-12 12:04:38 +00:00
showProfileCard={false}
2023-09-21 21:01:39 +00:00
/>
{note.pollOptions === undefined && !note.replyTo && (
2023-10-16 15:54:55 +00:00
<AsyncIcon
2023-10-18 10:21:17 +00:00
iconName="list"
2023-10-16 15:54:55 +00:00
iconSize={24}
onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))}
2023-10-16 16:09:05 +00:00
className={classNames("note-creator-icon", { active: note.pollOptions !== undefined })}
2023-10-16 15:54:55 +00:00
/>
2023-09-21 21:01:39 +00:00
)}
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
2023-10-16 15:54:55 +00:00
<AsyncIcon
iconName="settings-04"
iconSize={24}
onClick={() => note.update(v => (v.advanced = !v.advanced))}
2023-10-16 16:09:05 +00:00
className={classNames("note-creator-icon", { active: note.advanced })}
2023-10-16 15:54:55 +00:00
/>
2023-10-21 21:26:04 +00:00
<span className="sm:inline hidden">
<FormattedMessage defaultMessage="Preview" id="TJo5E6" />
2023-10-21 21:26:04 +00:00
</span>
2023-10-18 10:13:11 +00:00
<ToggleSwitch
onClick={() => loadPreview()}
size={40}
className={classNames({ active: Boolean(note.preview) })}
/>
2023-09-21 21:01:39 +00:00
</div>
<div className="flex g8">
<button className="secondary" onClick={cancel}>
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
2023-09-21 21:01:39 +00:00
</button>
2023-10-18 10:21:17 +00:00
<AsyncButton onClick={onSubmit} className="primary">
2023-11-20 19:16:47 +00:00
{note.replyTo ? (
<FormattedMessage defaultMessage="Reply" id="9HU8vw" />
) : (
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
)}
2023-09-21 21:01:39 +00:00
</AsyncButton>
</div>
2023-09-21 20:02:59 +00:00
</div>
2023-10-11 14:41:36 +00:00
);
}
const handlePaste: ClipboardEventHandler<HTMLDivElement> = 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<HTMLTextAreaElement>) => {
event.preventDefault();
};
const handleDragLeave = (event: DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
};
const handleDrop = (event: DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
const droppedFiles = Array.from(event.dataTransfer.files);
droppedFiles.forEach(async file => {
await uploadFile(file);
2023-11-09 09:42:36 +00:00
});
};
2023-10-11 14:41:36 +00:00
function noteCreatorForm() {
return (
<>
{note.replyTo && (
2023-10-13 11:40:39 +00:00
<>
<h4>
<FormattedMessage defaultMessage="Reply To" id="8ED/4u" />
2023-10-13 11:40:39 +00:00
</h4>
<div className="max-h-64 overflow-y-auto">
2024-01-11 22:09:12 +00:00
<Note className="hover:bg-transparent" data={note.replyTo} options={replyToNoteOptions} />
2024-01-10 18:54:48 +00:00
</div>
2024-01-10 18:32:51 +00:00
<hr className="border-border-color border-1 -mx-6" />
2023-10-13 11:40:39 +00:00
</>
)}
{note.quote && (
<>
<h4>
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
2023-10-13 11:40:39 +00:00
</h4>
<div className="max-h-64 overflow-y-auto">
2024-01-11 22:09:12 +00:00
<Note className="hover:bg-transparent" data={note.quote} options={quoteNoteOptions} />
2024-01-10 18:54:48 +00:00
</div>
2024-01-10 18:32:51 +00:00
<hr className="border-border-color border-1 -mx-6" />
2023-10-13 11:40:39 +00:00
</>
2023-10-11 14:41:36 +00:00
)}
{note.preview && getPreviewNote()}
2023-11-16 15:54:02 +00:00
{!note.preview && (
<>
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
<Textarea
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
autoFocus
onChange={c => onChange(c)}
value={note.note}
onFocus={() => note.update(v => (v.active = true))}
onKeyDown={e => {
if (e.key === "Enter" && e.metaKey) {
sendNote().catch(console.warn);
}
}}
/>
{renderPollOptions()}
</div>
</>
2023-10-11 14:41:36 +00:00
)}
2023-10-16 10:07:13 +00:00
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
2023-10-11 14:41:36 +00:00
{noteCreatorFooter()}
{note.error && <span className="error">{note.error}</span>}
{note.advanced && noteCreatorAdvanced()}
</>
);
}
function reset() {
note.update(v => {
v.show = false;
});
}
2023-10-11 14:41:36 +00:00
if (!note.show) return null;
return (
2023-12-12 12:08:25 +00:00
<Modal
id="note-creator"
bodyClassName="modal-body flex flex-col gap-4"
className="note-creator-modal"
onClose={reset}>
2023-12-02 22:44:44 +00:00
{noteCreatorForm()}
2023-09-21 21:01:39 +00:00
</Modal>
2023-02-05 12:32:34 +00:00
);
2023-12-02 22:45:52 +00:00
}