use redux for NoteCreator state management (#494)
* use redux for NoteCreator state management This fixes a bug where the modal closes while replying to a note. This happens if the thread re-renders while you are replying. Drafts of notes are also now automatically saved unless the user clicks the cancel button. * fix modal closing bug * really fix modal closing bug * fix rebase
This commit is contained in:
parent
eab07add8c
commit
b650a1684f
@ -1,7 +1,7 @@
|
||||
import "./NoteCreator.css";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
@ -11,6 +11,18 @@ 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,
|
||||
setZapForward,
|
||||
setSensitive,
|
||||
reset,
|
||||
} from "State/NoteCreator";
|
||||
import type { RootState } from "State/Store";
|
||||
import { LNURL } from "LNURL";
|
||||
|
||||
import messages from "./messages";
|
||||
@ -31,26 +43,20 @@ function NotePreview({ note }: NotePreviewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export interface NoteCreatorProps {
|
||||
show: boolean;
|
||||
setShow: (s: boolean) => void;
|
||||
replyTo?: TaggedRawEvent;
|
||||
onSend?: () => void;
|
||||
autoFocus: boolean;
|
||||
}
|
||||
|
||||
export function NoteCreator(props: NoteCreatorProps) {
|
||||
const { show, setShow, replyTo, onSend, autoFocus } = props;
|
||||
export function NoteCreator() {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const [note, setNote] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [active, setActive] = useState(false);
|
||||
const [preview, setPreview] = useState<RawEvent>();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [zapForward, setZapForward] = useState("");
|
||||
const [sensitive, setSensitiveContent] = useState<string>();
|
||||
const uploader = useFileUpload();
|
||||
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);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function sendNote() {
|
||||
if (note) {
|
||||
@ -61,10 +67,12 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
await svc.load();
|
||||
extraTags = [svc.getZapTag()];
|
||||
} catch {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Invalid LNURL",
|
||||
})
|
||||
dispatch(
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Invalid LNURL",
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -76,12 +84,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note, extraTags) : await publisher.note(note, extraTags);
|
||||
console.debug("Sending note: ", ev);
|
||||
publisher.broadcast(ev);
|
||||
setNote("");
|
||||
setShow(false);
|
||||
if (typeof onSend === "function") {
|
||||
onSend();
|
||||
}
|
||||
setActive(false);
|
||||
dispatch(reset());
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,34 +94,30 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
if (file) {
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
if (rx.url) {
|
||||
setNote(n => `${n ? `${n}\n` : ""}${rx.url}`);
|
||||
dispatch(setNote(`${note ? `${note}\n` : ""}${rx.url}`));
|
||||
} else if (rx?.error) {
|
||||
setError(rx.error);
|
||||
dispatch(setError(rx.error));
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
setError(error?.message);
|
||||
dispatch(setError(error?.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const { value } = ev.target;
|
||||
setNote(value);
|
||||
dispatch(setNote(value));
|
||||
if (value) {
|
||||
setActive(true);
|
||||
dispatch(setActive(true));
|
||||
} else {
|
||||
setActive(false);
|
||||
dispatch(setActive(false));
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
setShow(false);
|
||||
setNote("");
|
||||
setShowAdvanced(false);
|
||||
setPreview(undefined);
|
||||
setZapForward("");
|
||||
dispatch(reset());
|
||||
}
|
||||
|
||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
@ -128,11 +127,11 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
|
||||
async function loadPreview() {
|
||||
if (preview) {
|
||||
setPreview(undefined);
|
||||
dispatch(setPreview(null));
|
||||
} else {
|
||||
const tmpNote = await publisher.note(note);
|
||||
if (tmpNote) {
|
||||
setPreview(tmpNote);
|
||||
dispatch(setPreview(tmpNote));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -155,18 +154,18 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
return (
|
||||
<>
|
||||
{show && (
|
||||
<Modal className="note-creator-modal" onClose={() => setShow(false)}>
|
||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
||||
{replyTo && <NotePreview note={replyTo} />}
|
||||
{preview && getPreviewNote()}
|
||||
{!preview && (
|
||||
<div className={`flex note-creator ${replyTo ? "note-reply" : ""}`}>
|
||||
<div className="flex f-col mr10 f-grow">
|
||||
<Textarea
|
||||
autoFocus={autoFocus}
|
||||
autoFocus
|
||||
className={`textarea ${active ? "textarea--focused" : ""}`}
|
||||
onChange={onChange}
|
||||
value={note}
|
||||
onFocus={() => setActive(true)}
|
||||
onFocus={() => dispatch(setActive(true))}
|
||||
/>
|
||||
<button type="button" className="attachment" onClick={attachFile}>
|
||||
<Icon name="attachment" />
|
||||
@ -176,7 +175,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
</div>
|
||||
)}
|
||||
<div className="note-creator-actions">
|
||||
<button className="secondary" type="button" onClick={() => setShowAdvanced(s => !s)}>
|
||||
<button className="secondary" type="button" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<FormattedMessage defaultMessage="Advanced" />
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={cancel}>
|
||||
@ -207,7 +206,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
defaultMessage: "LNURL to forward zaps to",
|
||||
})}
|
||||
value={zapForward}
|
||||
onChange={e => setZapForward(e.target.value)}
|
||||
onChange={e => dispatch(setZapForward(e.target.value))}
|
||||
/>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
||||
@ -223,7 +222,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={sensitive}
|
||||
onChange={e => setSensitiveContent(e.target.value)}
|
||||
onChange={e => dispatch(setSensitive(e.target.value))}
|
||||
maxLength={50}
|
||||
minLength={1}
|
||||
placeholder={formatMessage({
|
||||
|
@ -18,6 +18,7 @@ import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences, setPinned, setBookmarked } from "State/Login";
|
||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { SnortPubKey, TranslateHost } from "Const";
|
||||
import { LNURL } from "LNURL";
|
||||
@ -99,7 +100,9 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
||||
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
||||
const [tip, setTip] = useState(false);
|
||||
const [zapping, setZapping] = useState(false);
|
||||
const walletState = useWallet();
|
||||
@ -402,13 +405,22 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const handleReplyButtonClick = () => {
|
||||
if (replyTo?.id !== ev.id) {
|
||||
dispatch(reset());
|
||||
}
|
||||
|
||||
dispatch(setReplyTo(ev));
|
||||
dispatch(setShow(!showNoteCreatorModal));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="footer">
|
||||
<div className="footer-reactions">
|
||||
{tipButton()}
|
||||
{reactionIcons()}
|
||||
<div className={`reaction-pill ${reply ? "reacted" : ""}`} onClick={() => setReply(s => !s)}>
|
||||
<div className={`reaction-pill ${showNoteCreatorModal ? "reacted" : ""}`} onClick={handleReplyButtonClick}>
|
||||
<Icon name="reply" size={17} />
|
||||
</div>
|
||||
<Menu
|
||||
@ -421,7 +433,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
{menuItems()}
|
||||
</Menu>
|
||||
</div>
|
||||
<NoteCreator autoFocus={true} replyTo={ev} onSend={() => setReply(false)} show={reply} setShow={setReply} />
|
||||
{willRenderNoteCreator && <NoteCreator />}
|
||||
<Reactions
|
||||
show={showReactions}
|
||||
setShow={setShowReactions}
|
||||
|
@ -11,6 +11,7 @@ import { bech32ToHex, randomSample, unixNowMs, unwrap } from "Util";
|
||||
import Icon from "Icons/Icon";
|
||||
import { RootState } from "State/Store";
|
||||
import { init, setRelays } from "State/Login";
|
||||
import { setShow, reset } from "State/NoteCreator";
|
||||
import { System } from "System";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import useLoginFeed from "Feed/LoginFeed";
|
||||
@ -26,7 +27,9 @@ import { useDmCache } from "Hooks/useDmsCache";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
const [show, setShow] = useState(false);
|
||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
||||
const isNoteCreatorShowing = useSelector((s: RootState) => s.noteCreator.show);
|
||||
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { loggedOut, publicKey, relays, preferences, newUserKey } = useSelector((s: RootState) => s.login);
|
||||
@ -34,10 +37,17 @@ export default function Layout() {
|
||||
const pub = useEventPublisher();
|
||||
useLoginFeed();
|
||||
|
||||
const handleNoteCreatorButtonClick = () => {
|
||||
if (replyTo) {
|
||||
dispatch(reset());
|
||||
}
|
||||
dispatch(setShow(true));
|
||||
};
|
||||
|
||||
const shouldHideNoteCreator = useMemo(() => {
|
||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/"];
|
||||
return hideOn.some(a => location.pathname.startsWith(a));
|
||||
}, [location]);
|
||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/", "/e"];
|
||||
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||
}, [location, isReplyNoteCreatorShowing]);
|
||||
|
||||
const shouldHideHeader = useMemo(() => {
|
||||
const hideOn = ["/login", "/new"];
|
||||
@ -179,10 +189,10 @@ export default function Layout() {
|
||||
|
||||
{!shouldHideNoteCreator && (
|
||||
<>
|
||||
<button className="note-create-button" type="button" onClick={() => setShow(!show)}>
|
||||
<button className="note-create-button" type="button" onClick={handleNoteCreatorButtonClick}>
|
||||
<Icon name="plus" size={16} />
|
||||
</button>
|
||||
<NoteCreator replyTo={undefined} autoFocus={true} show={show} setShow={setShow} />
|
||||
<NoteCreator />
|
||||
</>
|
||||
)}
|
||||
{window.localStorage.getItem("debug") && <SubDebug />}
|
||||
|
76
packages/app/src/State/NoteCreator.ts
Normal file
76
packages/app/src/State/NoteCreator.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
interface NoteCreatorStore {
|
||||
show: boolean;
|
||||
note: string;
|
||||
error: string;
|
||||
active: boolean;
|
||||
preview: RawEvent | null;
|
||||
replyTo: TaggedRawEvent | null;
|
||||
showAdvanced: boolean;
|
||||
zapForward: string;
|
||||
sensitive: string;
|
||||
}
|
||||
|
||||
const InitState: NoteCreatorStore = {
|
||||
show: false,
|
||||
note: "",
|
||||
error: "",
|
||||
active: false,
|
||||
preview: null,
|
||||
replyTo: null,
|
||||
showAdvanced: false,
|
||||
zapForward: "",
|
||||
sensitive: "",
|
||||
};
|
||||
|
||||
const NoteCreatorSlice = createSlice({
|
||||
name: "NoteCreator",
|
||||
initialState: InitState,
|
||||
reducers: {
|
||||
setShow: (state, action: PayloadAction<boolean>) => {
|
||||
state.show = action.payload;
|
||||
},
|
||||
setNote: (state, action: PayloadAction<string>) => {
|
||||
state.note = action.payload;
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
setActive: (state, action: PayloadAction<boolean>) => {
|
||||
state.active = action.payload;
|
||||
},
|
||||
setPreview: (state, action: PayloadAction<RawEvent | null>) => {
|
||||
state.preview = action.payload;
|
||||
},
|
||||
setReplyTo: (state, action: PayloadAction<TaggedRawEvent | null>) => {
|
||||
state.replyTo = action.payload;
|
||||
},
|
||||
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvanced = action.payload;
|
||||
},
|
||||
setZapForward: (state, action: PayloadAction<string>) => {
|
||||
state.zapForward = action.payload;
|
||||
},
|
||||
setSensitive: (state, action: PayloadAction<string>) => {
|
||||
state.sensitive = action.payload;
|
||||
},
|
||||
reset: () => InitState,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setShow,
|
||||
setNote,
|
||||
setError,
|
||||
setActive,
|
||||
setPreview,
|
||||
setReplyTo,
|
||||
setShowAdvanced,
|
||||
setZapForward,
|
||||
setSensitive,
|
||||
reset,
|
||||
} = NoteCreatorSlice.actions;
|
||||
|
||||
export const reducer = NoteCreatorSlice.reducer;
|
@ -1,9 +1,11 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { reducer as LoginReducer } from "State/Login";
|
||||
import { reducer as NoteCreatorReducer } from "State/NoteCreator";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
login: LoginReducer,
|
||||
noteCreator: NoteCreatorReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user