feat: rebroadcast
This commit is contained in:
parent
551169c2c7
commit
6fe9a27041
@ -18,6 +18,7 @@ import {
|
|||||||
setActive,
|
setActive,
|
||||||
setPreview,
|
setPreview,
|
||||||
setShowAdvanced,
|
setShowAdvanced,
|
||||||
|
setSelectedCustomRelays,
|
||||||
setZapForward,
|
setZapForward,
|
||||||
setSensitive,
|
setSensitive,
|
||||||
reset,
|
reset,
|
||||||
@ -34,6 +35,7 @@ import { EventBuilder } from "System";
|
|||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import { LoginStore } from "Login";
|
import { LoginStore } from "Login";
|
||||||
import { getCurrentSubscription } from "Subscription";
|
import { getCurrentSubscription } from "Subscription";
|
||||||
|
import useLogin from "Hooks/useLogin";
|
||||||
|
|
||||||
interface NotePreviewProps {
|
interface NotePreviewProps {
|
||||||
note: TaggedRawEvent;
|
note: TaggedRawEvent;
|
||||||
@ -55,11 +57,25 @@ export function NoteCreator() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const uploader = useFileUpload();
|
const uploader = useFileUpload();
|
||||||
const { note, zapForward, sensitive, pollOptions, replyTo, otherEvents, preview, active, show, showAdvanced, error } =
|
const {
|
||||||
useSelector((s: RootState) => s.noteCreator);
|
note,
|
||||||
|
zapForward,
|
||||||
|
sensitive,
|
||||||
|
pollOptions,
|
||||||
|
replyTo,
|
||||||
|
otherEvents,
|
||||||
|
preview,
|
||||||
|
active,
|
||||||
|
show,
|
||||||
|
showAdvanced,
|
||||||
|
selectedCustomRelays,
|
||||||
|
error,
|
||||||
|
} = useSelector((s: RootState) => s.noteCreator);
|
||||||
const [uploadInProgress, setUploadInProgress] = useState(false);
|
const [uploadInProgress, setUploadInProgress] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||||
|
const login = useLogin();
|
||||||
|
const relays = login.relays;
|
||||||
|
|
||||||
async function sendNote() {
|
async function sendNote() {
|
||||||
if (note && publisher) {
|
if (note && publisher) {
|
||||||
@ -96,10 +112,12 @@ export function NoteCreator() {
|
|||||||
return eb;
|
return eb;
|
||||||
};
|
};
|
||||||
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
||||||
publisher.broadcast(ev);
|
if (selectedCustomRelays) publisher.broadcastAll(ev, selectedCustomRelays);
|
||||||
|
else publisher.broadcast(ev);
|
||||||
dispatch(reset());
|
dispatch(reset());
|
||||||
for (const oe of otherEvents) {
|
for (const oe of otherEvents) {
|
||||||
publisher.broadcast(oe);
|
if (selectedCustomRelays) publisher.broadcastAll(oe, selectedCustomRelays);
|
||||||
|
else publisher.broadcast(oe);
|
||||||
}
|
}
|
||||||
dispatch(reset());
|
dispatch(reset());
|
||||||
}
|
}
|
||||||
@ -233,6 +251,41 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRelayCustomisation() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{Object.keys(relays.item || {})
|
||||||
|
.filter(el => relays.item[el].write)
|
||||||
|
.map((r, i, a) => (
|
||||||
|
<div className="card flex">
|
||||||
|
<div className="flex f-col f-grow">
|
||||||
|
<div>{r}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||||
|
onChange={e =>
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function listAccounts() {
|
function listAccounts() {
|
||||||
return LoginStore.getSessions().map(a => (
|
return LoginStore.getSessions().map(a => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -330,6 +383,13 @@ export function NoteCreator() {
|
|||||||
<button className="secondary" onClick={loadPreview}>
|
<button className="secondary" onClick={loadPreview}>
|
||||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||||
</button>
|
</button>
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Custom Relays" />
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||||
|
</p>
|
||||||
|
{renderRelayCustomisation()}
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Forward Zaps" />
|
<FormattedMessage defaultMessage="Forward Zaps" />
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -12,12 +12,18 @@ import { formatShort } from "Number";
|
|||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
|
import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
|
import { ReBroadcaster } from "Element/ReBroadcaster";
|
||||||
import Reactions from "Element/Reactions";
|
import Reactions from "Element/Reactions";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||||
import { useUserProfile } from "Hooks/useUserProfile";
|
import { useUserProfile } from "Hooks/useUserProfile";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
||||||
|
import {
|
||||||
|
setNote as setReBroadcastNote,
|
||||||
|
setShow as setReBroadcastShow,
|
||||||
|
reset as resetReBroadcast,
|
||||||
|
} from "State/ReBroadcast";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { SnortPubKey, TranslateHost } from "Const";
|
import { SnortPubKey, TranslateHost } from "Const";
|
||||||
import { LNURL } from "LNURL";
|
import { LNURL } from "LNURL";
|
||||||
@ -70,8 +76,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
||||||
|
const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
|
||||||
|
const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
|
||||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
||||||
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
||||||
|
const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
|
||||||
const [tip, setTip] = useState(false);
|
const [tip, setTip] = useState(false);
|
||||||
const [zapping, setZapping] = useState(false);
|
const [zapping, setZapping] = useState(false);
|
||||||
const walletState = useWallet();
|
const walletState = useWallet();
|
||||||
@ -361,10 +370,18 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
<FormattedMessage {...messages.DislikeAction} />
|
<FormattedMessage {...messages.DislikeAction} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
{ev.pubkey === publicKey && (
|
||||||
<Icon name="block" />
|
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||||
<FormattedMessage {...messages.Block} />
|
<Icon name="relay" />
|
||||||
</MenuItem>
|
<FormattedMessage {...messages.ReBroadcast} />
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{ev.pubkey !== publicKey && (
|
||||||
|
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||||
|
<Icon name="block" />
|
||||||
|
<FormattedMessage {...messages.Block} />
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem onClick={() => translate()}>
|
<MenuItem onClick={() => translate()}>
|
||||||
<Icon name="translate" />
|
<Icon name="translate" />
|
||||||
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
||||||
@ -394,6 +411,15 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
dispatch(setShow(!showNoteCreatorModal));
|
dispatch(setShow(!showNoteCreatorModal));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReBroadcastButtonClick = () => {
|
||||||
|
if (reBroadcastNote?.id !== ev.id) {
|
||||||
|
dispatch(resetReBroadcast());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setReBroadcastNote(ev));
|
||||||
|
dispatch(setReBroadcastShow(!showReBroadcastModal));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
@ -415,6 +441,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
{willRenderNoteCreator && <NoteCreator />}
|
{willRenderNoteCreator && <NoteCreator />}
|
||||||
|
{willRenderReBroadcast && <ReBroadcaster />}
|
||||||
<Reactions
|
<Reactions
|
||||||
show={showReactions}
|
show={showReactions}
|
||||||
setShow={setShowReactions}
|
setShow={setShowReactions}
|
||||||
|
87
packages/app/src/Element/ReBroadcaster.tsx
Normal file
87
packages/app/src/Element/ReBroadcaster.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
import Modal from "Element/Modal";
|
||||||
|
import type { RootState } from "State/Store";
|
||||||
|
import { setShow, reset, setSelectedCustomRelays } from "State/ReBroadcast";
|
||||||
|
import messages from "./messages";
|
||||||
|
import useLogin from "Hooks/useLogin";
|
||||||
|
|
||||||
|
export function ReBroadcaster() {
|
||||||
|
const publisher = useEventPublisher();
|
||||||
|
const { note, show, selectedCustomRelays } = useSelector((s: RootState) => s.reBroadcast);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
async function sendReBroadcast() {
|
||||||
|
if (note && publisher) {
|
||||||
|
if (selectedCustomRelays) publisher.broadcastAll(note, selectedCustomRelays);
|
||||||
|
else publisher.broadcast(note);
|
||||||
|
dispatch(reset());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
dispatch(reset());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
sendReBroadcast().catch(console.warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = useLogin();
|
||||||
|
const relays = login.relays;
|
||||||
|
|
||||||
|
function renderRelayCustomisation() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{Object.keys(relays.item || {})
|
||||||
|
.filter(el => relays.item[el].write)
|
||||||
|
.map((r, i, a) => (
|
||||||
|
<div className="card flex">
|
||||||
|
<div className="flex f-col f-grow">
|
||||||
|
<div>{r}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||||
|
onChange={e =>
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{show && (
|
||||||
|
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
||||||
|
{renderRelayCustomisation()}
|
||||||
|
<div className="note-creator-actions">
|
||||||
|
<button className="secondary" onClick={cancel}>
|
||||||
|
<FormattedMessage {...messages.Cancel} />
|
||||||
|
</button>
|
||||||
|
<button onClick={onSubmit}>
|
||||||
|
<FormattedMessage {...messages.ReBroadcast} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -104,4 +104,5 @@ export default defineMessages({
|
|||||||
ConfirmUnbookmark: { defaultMessage: "Are you sure you want to remove this note from bookmarks?" },
|
ConfirmUnbookmark: { defaultMessage: "Are you sure you want to remove this note from bookmarks?" },
|
||||||
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
||||||
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
||||||
|
ReBroadcast: { defaultMessage: "Broadcast Again" },
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ interface NoteCreatorStore {
|
|||||||
preview?: RawEvent;
|
preview?: RawEvent;
|
||||||
replyTo?: TaggedRawEvent;
|
replyTo?: TaggedRawEvent;
|
||||||
showAdvanced: boolean;
|
showAdvanced: boolean;
|
||||||
|
selectedCustomRelays: false | Array<string>;
|
||||||
zapForward: string;
|
zapForward: string;
|
||||||
sensitive: string;
|
sensitive: string;
|
||||||
pollOptions?: Array<string>;
|
pollOptions?: Array<string>;
|
||||||
@ -21,6 +22,7 @@ const InitState: NoteCreatorStore = {
|
|||||||
error: "",
|
error: "",
|
||||||
active: false,
|
active: false,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
|
selectedCustomRelays: false,
|
||||||
zapForward: "",
|
zapForward: "",
|
||||||
sensitive: "",
|
sensitive: "",
|
||||||
otherEvents: [],
|
otherEvents: [],
|
||||||
@ -51,6 +53,9 @@ const NoteCreatorSlice = createSlice({
|
|||||||
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
||||||
state.showAdvanced = action.payload;
|
state.showAdvanced = action.payload;
|
||||||
},
|
},
|
||||||
|
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||||
|
state.selectedCustomRelays = action.payload;
|
||||||
|
},
|
||||||
setZapForward: (state, action: PayloadAction<string>) => {
|
setZapForward: (state, action: PayloadAction<string>) => {
|
||||||
state.zapForward = action.payload;
|
state.zapForward = action.payload;
|
||||||
},
|
},
|
||||||
@ -75,6 +80,7 @@ export const {
|
|||||||
setPreview,
|
setPreview,
|
||||||
setReplyTo,
|
setReplyTo,
|
||||||
setShowAdvanced,
|
setShowAdvanced,
|
||||||
|
setSelectedCustomRelays,
|
||||||
setZapForward,
|
setZapForward,
|
||||||
setSensitive,
|
setSensitive,
|
||||||
setPollOptions,
|
setPollOptions,
|
||||||
|
34
packages/app/src/State/ReBroadcast.ts
Normal file
34
packages/app/src/State/ReBroadcast.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { RawEvent } from "@snort/nostr";
|
||||||
|
|
||||||
|
interface ReBroadcastStore {
|
||||||
|
show: boolean;
|
||||||
|
selectedCustomRelays: false | Array<string>;
|
||||||
|
note?: RawEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitState: ReBroadcastStore = {
|
||||||
|
show: false,
|
||||||
|
selectedCustomRelays: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReBroadcastSlice = createSlice({
|
||||||
|
name: "ReBroadcast",
|
||||||
|
initialState: InitState,
|
||||||
|
reducers: {
|
||||||
|
setShow: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.show = action.payload;
|
||||||
|
},
|
||||||
|
setNote: (state, action: PayloadAction<RawEvent>) => {
|
||||||
|
state.note = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||||
|
state.selectedCustomRelays = action.payload;
|
||||||
|
},
|
||||||
|
reset: () => InitState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setShow, setNote, setSelectedCustomRelays, reset } = ReBroadcastSlice.actions;
|
||||||
|
|
||||||
|
export const reducer = ReBroadcastSlice.reducer;
|
@ -1,9 +1,11 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import { reducer as NoteCreatorReducer } from "State/NoteCreator";
|
import { reducer as NoteCreatorReducer } from "State/NoteCreator";
|
||||||
|
import { reducer as ReBroadcastReducer } from "State/ReBroadcast";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
noteCreator: NoteCreatorReducer,
|
noteCreator: NoteCreatorReducer,
|
||||||
|
reBroadcast: ReBroadcastReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -267,10 +267,18 @@ export class NostrSystem {
|
|||||||
* Write an event to a relay then disconnect
|
* Write an event to a relay then disconnect
|
||||||
*/
|
*/
|
||||||
async WriteOnceToRelay(address: string, ev: RawEvent) {
|
async WriteOnceToRelay(address: string, ev: RawEvent) {
|
||||||
const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true);
|
return new Promise<void>((resolve, reject) => {
|
||||||
await c.Connect();
|
const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true);
|
||||||
await c.SendAsync(ev);
|
|
||||||
c.Close();
|
const t = setTimeout(reject, 5_000);
|
||||||
|
c.OnConnected = async () => {
|
||||||
|
clearTimeout(t);
|
||||||
|
await c.SendAsync(ev);
|
||||||
|
c.Close();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
c.Connect();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#changed() {
|
#changed() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user