Merge pull request 'feat: broadcast or rebroadcast note to specific relays' (#536) from DanConwayDev/main into main
Reviewed-on: #536
This commit is contained in:
commit
e42325f3b5
@ -18,6 +18,7 @@ import {
|
||||
setActive,
|
||||
setPreview,
|
||||
setShowAdvanced,
|
||||
setSelectedCustomRelays,
|
||||
setZapForward,
|
||||
setSensitive,
|
||||
reset,
|
||||
@ -34,6 +35,7 @@ import { EventBuilder } from "System";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { LoginStore } from "Login";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
interface NotePreviewProps {
|
||||
note: TaggedRawEvent;
|
||||
@ -55,11 +57,25 @@ export function NoteCreator() {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
const { note, zapForward, sensitive, pollOptions, replyTo, otherEvents, preview, active, show, showAdvanced, error } =
|
||||
useSelector((s: RootState) => s.noteCreator);
|
||||
const {
|
||||
note,
|
||||
zapForward,
|
||||
sensitive,
|
||||
pollOptions,
|
||||
replyTo,
|
||||
otherEvents,
|
||||
preview,
|
||||
active,
|
||||
show,
|
||||
showAdvanced,
|
||||
selectedCustomRelays,
|
||||
error,
|
||||
} = useSelector((s: RootState) => s.noteCreator);
|
||||
const [uploadInProgress, setUploadInProgress] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||
const login = useLogin();
|
||||
const relays = login.relays;
|
||||
|
||||
async function sendNote() {
|
||||
if (note && publisher) {
|
||||
@ -96,10 +112,12 @@ export function NoteCreator() {
|
||||
return eb;
|
||||
};
|
||||
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());
|
||||
for (const oe of otherEvents) {
|
||||
publisher.broadcast(oe);
|
||||
if (selectedCustomRelays) publisher.broadcastAll(oe, selectedCustomRelays);
|
||||
else publisher.broadcast(oe);
|
||||
}
|
||||
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() {
|
||||
return LoginStore.getSessions().map(a => (
|
||||
<MenuItem
|
||||
@ -330,6 +383,13 @@ export function NoteCreator() {
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Custom Relays" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||
</p>
|
||||
{renderRelayCustomisation()}
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Forward Zaps" />
|
||||
</h4>
|
||||
|
@ -12,12 +12,18 @@ import { formatShort } from "Number";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import { ReBroadcaster } from "Element/ReBroadcaster";
|
||||
import Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { RootState } from "State/Store";
|
||||
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 { SnortPubKey, TranslateHost } from "Const";
|
||||
import { LNURL } from "LNURL";
|
||||
@ -70,8 +76,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const publisher = useEventPublisher();
|
||||
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 willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
||||
const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
|
||||
const [tip, setTip] = useState(false);
|
||||
const [zapping, setZapping] = useState(false);
|
||||
const walletState = useWallet();
|
||||
@ -361,10 +370,18 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
<FormattedMessage {...messages.DislikeAction} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||
<Icon name="block" />
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</MenuItem>
|
||||
{ev.pubkey === publicKey && (
|
||||
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage {...messages.ReBroadcast} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{ev.pubkey !== publicKey && (
|
||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||
<Icon name="block" />
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => translate()}>
|
||||
<Icon name="translate" />
|
||||
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
||||
@ -394,6 +411,15 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
dispatch(setShow(!showNoteCreatorModal));
|
||||
};
|
||||
|
||||
const handleReBroadcastButtonClick = () => {
|
||||
if (reBroadcastNote?.id !== ev.id) {
|
||||
dispatch(resetReBroadcast());
|
||||
}
|
||||
|
||||
dispatch(setReBroadcastNote(ev));
|
||||
dispatch(setReBroadcastShow(!showReBroadcastModal));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="footer">
|
||||
@ -415,6 +441,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
</Menu>
|
||||
</div>
|
||||
{willRenderNoteCreator && <NoteCreator />}
|
||||
{willRenderReBroadcast && <ReBroadcaster />}
|
||||
<Reactions
|
||||
show={showReactions}
|
||||
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?" },
|
||||
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
||||
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
||||
ReBroadcast: { defaultMessage: "Broadcast Again" },
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ interface NoteCreatorStore {
|
||||
preview?: RawEvent;
|
||||
replyTo?: TaggedRawEvent;
|
||||
showAdvanced: boolean;
|
||||
selectedCustomRelays: false | Array<string>;
|
||||
zapForward: string;
|
||||
sensitive: string;
|
||||
pollOptions?: Array<string>;
|
||||
@ -21,6 +22,7 @@ const InitState: NoteCreatorStore = {
|
||||
error: "",
|
||||
active: false,
|
||||
showAdvanced: false,
|
||||
selectedCustomRelays: false,
|
||||
zapForward: "",
|
||||
sensitive: "",
|
||||
otherEvents: [],
|
||||
@ -51,6 +53,9 @@ const NoteCreatorSlice = createSlice({
|
||||
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvanced = action.payload;
|
||||
},
|
||||
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||
state.selectedCustomRelays = action.payload;
|
||||
},
|
||||
setZapForward: (state, action: PayloadAction<string>) => {
|
||||
state.zapForward = action.payload;
|
||||
},
|
||||
@ -75,6 +80,7 @@ export const {
|
||||
setPreview,
|
||||
setReplyTo,
|
||||
setShowAdvanced,
|
||||
setSelectedCustomRelays,
|
||||
setZapForward,
|
||||
setSensitive,
|
||||
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 { reducer as NoteCreatorReducer } from "State/NoteCreator";
|
||||
import { reducer as ReBroadcastReducer } from "State/ReBroadcast";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
noteCreator: NoteCreatorReducer,
|
||||
reBroadcast: ReBroadcastReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -267,10 +267,18 @@ export class NostrSystem {
|
||||
* Write an event to a relay then disconnect
|
||||
*/
|
||||
async WriteOnceToRelay(address: string, ev: RawEvent) {
|
||||
const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true);
|
||||
await c.Connect();
|
||||
await c.SendAsync(ev);
|
||||
c.Close();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true);
|
||||
|
||||
const t = setTimeout(reject, 5_000);
|
||||
c.OnConnected = async () => {
|
||||
clearTimeout(t);
|
||||
await c.SendAsync(ev);
|
||||
c.Close();
|
||||
resolve();
|
||||
};
|
||||
c.Connect();
|
||||
});
|
||||
}
|
||||
|
||||
#changed() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user