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:
Kieran 2023-05-04 13:18:22 +00:00
commit e42325f3b5
8 changed files with 237 additions and 12 deletions

View File

@ -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>

View File

@ -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}

View 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>
)}
</>
);
}

View File

@ -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" },
});

View File

@ -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,

View 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;

View File

@ -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,
},
});

View File

@ -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() {