snort/packages/app/src/Element/NoteCreator.tsx

241 lines
7.2 KiB
TypeScript
Raw Normal View History

2023-01-16 17:48:25 +00:00
import "./NoteCreator.css";
2023-02-01 22:14:30 +00:00
import { useState } from "react";
2023-03-27 22:58:29 +00:00
import { FormattedMessage, useIntl } from "react-intl";
2023-03-31 22:43:07 +00:00
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
2023-02-08 21:10:26 +00:00
2023-03-02 17:47:02 +00:00
import Icon from "Icons/Icon";
2023-01-20 11:11:50 +00:00
import useEventPublisher from "Feed/EventPublisher";
import { openFile } from "Util";
import Textarea from "Element/Textarea";
2023-01-25 18:08:53 +00:00
import Modal from "Element/Modal";
2023-02-01 22:14:30 +00:00
import ProfileImage from "Element/ProfileImage";
2023-01-31 19:08:11 +00:00
import useFileUpload from "Upload";
2023-03-31 22:43:07 +00:00
import Note from "Element/Note";
2023-03-27 22:58:29 +00:00
import { LNURL } from "LNURL";
2023-01-16 17:48:25 +00:00
2023-02-08 21:10:26 +00:00
import messages from "./messages";
2023-02-01 22:14:30 +00:00
interface NotePreviewProps {
2023-03-28 14:34:01 +00:00
note: TaggedRawEvent;
2023-02-01 22:14:30 +00:00
}
function NotePreview({ note }: NotePreviewProps) {
return (
<div className="note-preview">
2023-03-28 14:34:01 +00:00
<ProfileImage pubkey={note.pubkey} />
2023-02-01 22:14:30 +00:00
<div className="note-preview-body">
2023-03-28 14:34:01 +00:00
{note.content.slice(0, 136)}
{note.content.length > 140 && "..."}
2023-02-01 22:14:30 +00:00
</div>
</div>
);
2023-02-01 22:14:30 +00:00
}
2023-01-16 17:48:25 +00:00
export interface NoteCreatorProps {
show: boolean;
setShow: (s: boolean) => void;
2023-03-28 14:34:01 +00:00
replyTo?: TaggedRawEvent;
2023-02-07 19:47:57 +00:00
onSend?: () => void;
autoFocus: boolean;
2023-01-16 17:48:25 +00:00
}
export function NoteCreator(props: NoteCreatorProps) {
const { show, setShow, replyTo, onSend, autoFocus } = props;
2023-03-27 22:58:29 +00:00
const { formatMessage } = useIntl();
2023-02-05 12:32:34 +00:00
const publisher = useEventPublisher();
2023-03-31 22:43:07 +00:00
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [active, setActive] = useState(false);
const [preview, setPreview] = useState<RawEvent>();
2023-03-27 22:58:29 +00:00
const [showAdvanced, setShowAdvanced] = useState(false);
const [zapForward, setZapForward] = useState("");
2023-04-06 22:12:51 +00:00
const [sensitive, setSensitiveContent] = useState<string>();
2023-02-05 12:32:34 +00:00
const uploader = useFileUpload();
2023-01-16 17:48:25 +00:00
2023-02-05 12:32:34 +00:00
async function sendNote() {
if (note) {
2023-03-27 22:58:29 +00:00
let extraTags: Array<Array<string>> | undefined;
if (zapForward) {
try {
const svc = new LNURL(zapForward);
await svc.load();
2023-04-05 10:58:26 +00:00
extraTags = [svc.getZapTag()];
2023-03-27 22:58:29 +00:00
} catch {
setError(
formatMessage({
defaultMessage: "Invalid LNURL",
})
);
return;
}
}
2023-04-06 22:12:51 +00:00
if (sensitive) {
extraTags ??= [];
extraTags.push(["content-warning", sensitive]);
}
2023-03-27 22:58:29 +00:00
const ev = replyTo ? await publisher.reply(replyTo, note, extraTags) : await publisher.note(note, extraTags);
2023-02-05 12:32:34 +00:00
console.debug("Sending note: ", ev);
publisher.broadcast(ev);
setNote("");
setShow(false);
if (typeof onSend === "function") {
onSend();
}
setActive(false);
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-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-02-05 12:32:34 +00:00
if (rx.url) {
2023-02-09 12:26:54 +00:00
setNote(n => `${n ? `${n}\n` : ""}${rx.url}`);
2023-02-05 12:32:34 +00:00
} else if (rx?.error) {
setError(rx.error);
2023-01-16 17:48:25 +00:00
}
2023-02-05 12:32:34 +00:00
}
2023-02-07 19:47:57 +00:00
} catch (error: unknown) {
if (error instanceof Error) {
setError(error?.message);
}
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;
setNote(value);
2023-02-05 12:32:34 +00:00
if (value) {
setActive(true);
2023-02-05 12:32:34 +00:00
} else {
setActive(false);
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 cancel() {
setShow(false);
setNote("");
2023-04-05 11:06:07 +00:00
setShowAdvanced(false);
setPreview(undefined);
setZapForward("");
2023-02-05 12:32:34 +00:00
}
2023-01-25 18:08:53 +00:00
2023-02-05 12:32:34 +00:00
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
ev.stopPropagation();
sendNote().catch(console.warn);
}
2023-01-16 17:48:25 +00:00
2023-03-31 22:43:07 +00:00
async function loadPreview() {
if (preview) {
setPreview(undefined);
} else {
const tmpNote = await publisher.note(note);
if (tmpNote) {
setPreview(tmpNote);
}
}
}
function getPreviewNote() {
if (preview) {
return (
<Note
data={preview as TaggedRawEvent}
related={[]}
options={{
showFooter: false,
canClick: false,
}}
/>
);
}
}
2023-02-05 12:32:34 +00:00
return (
<>
{show && (
<Modal className="note-creator-modal" onClose={() => setShow(false)}>
{replyTo && <NotePreview note={replyTo} />}
2023-03-31 22:43:07 +00:00
{preview && getPreviewNote()}
{!preview && (
<div className={`flex note-creator ${replyTo ? "note-reply" : ""}`}>
<div className="flex f-col mr10 f-grow">
<Textarea
autoFocus={autoFocus}
className={`textarea ${active ? "textarea--focused" : ""}`}
onChange={onChange}
value={note}
onFocus={() => setActive(true)}
/>
<button type="button" className="attachment" onClick={attachFile}>
<Icon name="attachment" />
</button>
</div>
{error && <span className="error">{error}</span>}
2023-01-16 17:48:25 +00:00
</div>
2023-03-31 22:43:07 +00:00
)}
2023-02-05 12:32:34 +00:00
<div className="note-creator-actions">
2023-03-27 22:58:29 +00:00
<button className="secondary" type="button" onClick={() => setShowAdvanced(s => !s)}>
<FormattedMessage defaultMessage="Advanced" />
</button>
2023-02-05 12:32:34 +00:00
<button className="secondary" type="button" onClick={cancel}>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.Cancel} />
2023-02-05 12:32:34 +00:00
</button>
<button type="button" onClick={onSubmit}>
2023-02-09 12:26:54 +00:00
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
2023-02-05 12:32:34 +00:00
</button>
</div>
2023-03-27 22:58:29 +00:00
{showAdvanced && (
2023-04-05 11:06:07 +00:00
<div>
<button className="secondary" type="button" onClick={loadPreview}>
<FormattedMessage defaultMessage="Toggle Preview" />
</button>
2023-03-27 22:58:29 +00:00
<h4>
<FormattedMessage defaultMessage="Forward Zaps" />
</h4>
<p>
<FormattedMessage defaultMessage="All zaps sent to this note will be received by the following LNURL" />
</p>
2023-04-06 22:12:51 +00:00
<b className="warning">
<FormattedMessage defaultMessage="Not all clients support this yet" />
</b>
2023-03-27 22:58:29 +00:00
<input
type="text"
className="w-max"
placeholder={formatMessage({
defaultMessage: "LNURL to forward zaps to",
})}
value={zapForward}
onChange={e => setZapForward(e.target.value)}
/>
2023-04-06 22:12:51 +00:00
<h4>
<FormattedMessage defaultMessage="Sensitive Content" />
</h4>
<p>
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
</p>
<b className="warning">
<FormattedMessage defaultMessage="Not all clients support this yet" />
</b>
<div className="flex">
<input
className="w-max"
type="text"
value={sensitive}
onChange={e => setSensitiveContent(e.target.value)}
maxLength={50}
minLength={1}
placeholder={formatMessage({
defaultMessage: "Reason",
})}
/>
</div>
2023-04-05 11:06:07 +00:00
</div>
2023-03-27 22:58:29 +00:00
)}
2023-02-05 12:32:34 +00:00
</Modal>
)}
</>
);
2023-01-18 23:31:34 +00:00
}