2023-01-16 17:48:25 +00:00
|
|
|
import "./NoteCreator.css";
|
2023-03-27 22:58:29 +00:00
|
|
|
import { FormattedMessage, useIntl } from "react-intl";
|
2023-04-08 12:48:57 +00:00
|
|
|
import { useDispatch, useSelector } from "react-redux";
|
2023-04-10 14:55:25 +00:00
|
|
|
import { EventKind, 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-04-08 12:48:57 +00:00
|
|
|
import {
|
|
|
|
setShow,
|
|
|
|
setNote,
|
|
|
|
setError,
|
|
|
|
setActive,
|
|
|
|
setPreview,
|
|
|
|
setShowAdvanced,
|
|
|
|
setZapForward,
|
|
|
|
setSensitive,
|
|
|
|
reset,
|
2023-04-10 14:55:25 +00:00
|
|
|
setPollOptions,
|
2023-04-08 12:48:57 +00:00
|
|
|
} from "State/NoteCreator";
|
|
|
|
import type { RootState } from "State/Store";
|
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-04-12 11:17:59 +00:00
|
|
|
import { ClipboardEventHandler, useState } from "react";
|
|
|
|
import Spinner from "Icons/Spinner";
|
2023-02-08 21:10:26 +00:00
|
|
|
|
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-07 20:04:50 +00:00
|
|
|
);
|
2023-02-01 22:14:30 +00:00
|
|
|
}
|
|
|
|
|
2023-04-08 12:48:57 +00:00
|
|
|
export function NoteCreator() {
|
2023-03-27 22:58:29 +00:00
|
|
|
const { formatMessage } = useIntl();
|
2023-02-05 12:32:34 +00:00
|
|
|
const publisher = useEventPublisher();
|
|
|
|
const uploader = useFileUpload();
|
2023-04-08 12:48:57 +00:00
|
|
|
const note = useSelector((s: RootState) => s.noteCreator.note);
|
|
|
|
const show = useSelector((s: RootState) => s.noteCreator.show);
|
|
|
|
const error = useSelector((s: RootState) => s.noteCreator.error);
|
|
|
|
const active = useSelector((s: RootState) => s.noteCreator.active);
|
|
|
|
const preview = useSelector((s: RootState) => s.noteCreator.preview);
|
|
|
|
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
|
|
|
const showAdvanced = useSelector((s: RootState) => s.noteCreator.showAdvanced);
|
|
|
|
const zapForward = useSelector((s: RootState) => s.noteCreator.zapForward);
|
|
|
|
const sensitive = useSelector((s: RootState) => s.noteCreator.sensitive);
|
2023-04-10 14:55:25 +00:00
|
|
|
const pollOptions = useSelector((s: RootState) => s.noteCreator.pollOptions);
|
2023-04-12 11:17:59 +00:00
|
|
|
const [uploadInProgress, setUploadInProgress] = useState(false);
|
2023-04-08 12:48:57 +00:00
|
|
|
const dispatch = useDispatch();
|
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 {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(
|
|
|
|
setError(
|
|
|
|
formatMessage({
|
|
|
|
defaultMessage: "Invalid LNURL",
|
|
|
|
})
|
|
|
|
)
|
2023-03-27 22:58:29 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2023-04-06 22:12:51 +00:00
|
|
|
if (sensitive) {
|
|
|
|
extraTags ??= [];
|
|
|
|
extraTags.push(["content-warning", sensitive]);
|
|
|
|
}
|
2023-04-10 14:55:25 +00:00
|
|
|
const kind = pollOptions ? EventKind.Polls : EventKind.TextNote;
|
|
|
|
if (pollOptions) {
|
|
|
|
extraTags ??= [];
|
|
|
|
extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
|
|
|
}
|
|
|
|
const ev = replyTo
|
|
|
|
? await publisher.reply(replyTo, note, extraTags, kind)
|
|
|
|
: await publisher.note(note, extraTags, kind);
|
2023-02-05 12:32:34 +00:00
|
|
|
publisher.broadcast(ev);
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(reset());
|
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);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
if (error instanceof Error) {
|
|
|
|
dispatch(setError(error?.message));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function uploadFile(file: File) {
|
|
|
|
setUploadInProgress(true);
|
|
|
|
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-02-05 12:32:34 +00:00
|
|
|
if (rx.url) {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setNote(`${note ? `${note}\n` : ""}${rx.url}`));
|
2023-02-05 12:32:34 +00:00
|
|
|
} else if (rx?.error) {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setError(rx.error));
|
2023-01-16 17:48:25 +00:00
|
|
|
}
|
2023-04-12 11:17:59 +00:00
|
|
|
setUploadInProgress(false);
|
2023-02-05 12:32:34 +00:00
|
|
|
}
|
2023-02-07 19:47:57 +00:00
|
|
|
} catch (error: unknown) {
|
|
|
|
if (error instanceof Error) {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setError(error?.message));
|
2023-02-07 19:47:57 +00:00
|
|
|
}
|
2023-04-12 11:17:59 +00:00
|
|
|
setUploadInProgress(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 onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
2023-02-07 20:04:50 +00:00
|
|
|
const { value } = ev.target;
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setNote(value));
|
2023-02-05 12:32:34 +00:00
|
|
|
if (value) {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setActive(true));
|
2023-02-05 12:32:34 +00:00
|
|
|
} else {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(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() {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(reset());
|
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) {
|
2023-04-10 14:55:25 +00:00
|
|
|
dispatch(setPreview(undefined));
|
2023-03-31 22:43:07 +00:00
|
|
|
} else {
|
|
|
|
const tmpNote = await publisher.note(note);
|
|
|
|
if (tmpNote) {
|
2023-04-08 12:48:57 +00:00
|
|
|
dispatch(setPreview(tmpNote));
|
2023-03-31 22:43:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPreviewNote() {
|
|
|
|
if (preview) {
|
|
|
|
return (
|
|
|
|
<Note
|
|
|
|
data={preview as TaggedRawEvent}
|
|
|
|
related={[]}
|
|
|
|
options={{
|
|
|
|
showFooter: false,
|
|
|
|
canClick: false,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-10 14:55:25 +00:00
|
|
|
function renderPollOptions() {
|
|
|
|
if (pollOptions) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<h4>
|
|
|
|
<FormattedMessage defaultMessage="Poll Options" />
|
|
|
|
</h4>
|
|
|
|
{pollOptions?.map((a, i) => (
|
|
|
|
<div className="form-group w-max" key={`po-${i}`}>
|
|
|
|
<div>
|
|
|
|
<FormattedMessage defaultMessage="Option: {n}" values={{ n: i + 1 }} />
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<input type="text" value={a} onChange={e => changePollOption(i, e.target.value)} />
|
|
|
|
{i > 1 && (
|
|
|
|
<button onClick={() => removePollOption(i)} className="ml5">
|
|
|
|
<Icon name="close" size={14} />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
<button onClick={() => dispatch(setPollOptions([...pollOptions, ""]))}>
|
|
|
|
<Icon name="plus" size={14} />
|
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-12 11:17:59 +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) {
|
|
|
|
const file = new File([blob], "filename.jpg", { type: "image/jpeg", lastModified: new Date().getTime() });
|
|
|
|
uploadFile(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-02-05 12:32:34 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{show && (
|
2023-04-08 12:48:57 +00:00
|
|
|
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
2023-02-07 20:04:50 +00:00
|
|
|
{replyTo && <NotePreview note={replyTo} />}
|
2023-03-31 22:43:07 +00:00
|
|
|
{preview && getPreviewNote()}
|
|
|
|
{!preview && (
|
2023-04-12 11:17:59 +00:00
|
|
|
<div
|
|
|
|
onPaste={handlePaste}
|
|
|
|
className={`flex note-creator${replyTo ? " note-reply" : ""}${pollOptions ? " poll" : ""}`}>
|
2023-04-10 14:55:25 +00:00
|
|
|
<div className="flex f-col f-grow">
|
2023-03-31 22:43:07 +00:00
|
|
|
<Textarea
|
2023-04-08 12:48:57 +00:00
|
|
|
autoFocus
|
2023-03-31 22:43:07 +00:00
|
|
|
className={`textarea ${active ? "textarea--focused" : ""}`}
|
|
|
|
onChange={onChange}
|
|
|
|
value={note}
|
2023-04-08 12:48:57 +00:00
|
|
|
onFocus={() => dispatch(setActive(true))}
|
2023-04-10 09:59:38 +00:00
|
|
|
onKeyDown={e => {
|
|
|
|
if (e.key === "Enter" && e.metaKey) {
|
|
|
|
sendNote().catch(console.warn);
|
|
|
|
}
|
|
|
|
}}
|
2023-03-31 22:43:07 +00:00
|
|
|
/>
|
2023-04-10 14:55:25 +00:00
|
|
|
{renderPollOptions()}
|
|
|
|
<div className="insert">
|
|
|
|
{pollOptions === undefined && !replyTo && (
|
|
|
|
<button type="button" onClick={() => dispatch(setPollOptions(["A", "B"]))}>
|
|
|
|
<Icon name="pie-chart" />
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
<button type="button" onClick={attachFile}>
|
|
|
|
<Icon name="attachment" />
|
|
|
|
</button>
|
|
|
|
</div>
|
2023-03-31 22:43:07 +00:00
|
|
|
</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-04-12 11:17:59 +00:00
|
|
|
{uploadInProgress && <Spinner />}
|
2023-04-08 12:48:57 +00:00
|
|
|
<button className="secondary" type="button" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
2023-03-27 22:58:29 +00:00
|
|
|
<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}
|
2023-04-08 12:48:57 +00:00
|
|
|
onChange={e => dispatch(setZapForward(e.target.value))}
|
2023-03-27 22:58:29 +00:00
|
|
|
/>
|
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}
|
2023-04-08 12:48:57 +00:00
|
|
|
onChange={e => dispatch(setSensitive(e.target.value))}
|
2023-04-06 22:12:51 +00:00
|
|
|
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
|
|
|
}
|