From 53d1c242b1fd70d95eb1bbfefc34b72f12fdbde6 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 25 Sep 2023 21:45:26 -0500 Subject: [PATCH] Fixing / syncing crossposting settings, cleaning up and seperating out nostr functions --- components/adv-post-form.js | 24 +++- components/discussion-form.js | 35 ++---- components/toast.js | 10 +- lib/nostr.js | 201 +++++++++------------------------- pages/settings.js | 68 ++++++------ 5 files changed, 126 insertions(+), 212 deletions(-) diff --git a/components/adv-post-form.js b/components/adv-post-form.js index 0520aa887..2d2707b7b 100644 --- a/components/adv-post-form.js +++ b/components/adv-post-form.js @@ -1,7 +1,8 @@ import AccordianItem from './accordian-item' -import { Input, InputUserSuggest, VariableInput } from './form' +import { Input, InputUserSuggest, VariableInput, Checkbox } from './form' import InputGroup from 'react-bootstrap/InputGroup' import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS } from '../lib/constants' +import { DEFAULT_CROSSPOSTING_RELAYS } from '../lib/nostr' import Info from './info' import { numWithUnits } from '../lib/format' import styles from './adv-post-form.module.css' @@ -77,6 +78,27 @@ export default function AdvPostForm () { ) }} + crosspost to nostr + +
    +
  • crosspost this discussion item to nostr
  • +
  • requires NIP-07 extension for signing
  • +
  • we use your NIP-05 relays if set
  • +
  • otherwise we default to these relays:
  • +
      + {DEFAULT_CROSSPOSTING_RELAYS.map((relay, i) => ( +
    • {relay}
    • + ))} +
    +
+
+ + } + name='crosspost' + hint={crosspost to nostr} + /> } /> diff --git a/components/discussion-form.js b/components/discussion-form.js index 9ff0db0b1..673687aa3 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -1,5 +1,4 @@ import { Form, Input, MarkdownInput, SubmitButton } from '../components/form' -import { useRef } from 'react' import { useRouter } from 'next/router' import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client' import Countdown from './countdown' @@ -17,6 +16,7 @@ import { useCallback } from 'react' import { crosspostDiscussion } from '../lib/nostr' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' +import {DEFAULT_CROSSPOSTING_RELAYS} from '../lib/nostr' import { useMe } from './me' import { useToast } from './toast' @@ -32,7 +32,7 @@ export function DiscussionForm({ // if Web Share Target API was used const shareTitle = router.query.title const Toast = useToast() - const currentRetryRemoveToastRef = useRef(null); + const relays = [...DEFAULT_CROSSPOSTING_RELAYS, ...me?.nostrRelays || []]; const [upsertDiscussion] = useMutation( gql` @@ -43,20 +43,7 @@ export function DiscussionForm({ }` ) - let relays = [ - "wss://nostrue.com/", - "wss://relay.damus.io/", - "wss://relay.nostr.band/", - "wss://relay.snort.social/", - "wss://nostr21.com/", - "wss://nostr.mutinywallet.com" - ]; - - if (me.nostrRelays) { - relays = relays.concat(me.nostrRelays); - } - - const promptUserWithToast = (failedRelays) => { + const relayError = (failedRelays) => { return new Promise((resolve) => { const { removeToast } = Toast.danger( <> @@ -71,12 +58,12 @@ export function DiscussionForm({ - + , + () => resolve('skip') // will skip if user closes the toast ); }); }; - const handleCrosspost = async (values, id) => { let failedRelays; let allSuccessful = false; @@ -91,8 +78,7 @@ export function DiscussionForm({ failedRelays = result.failedRelays.map(relayObj => relayObj.relay); if (failedRelays.length > 0) { - console.log("failed relays", failedRelays); - const userAction = await promptUserWithToast(failedRelays); + const userAction = await relayError(failedRelays); if (userAction === 'skip') { Toast.success("Crossposting skipped."); @@ -108,7 +94,7 @@ export function DiscussionForm({ }; const onSubmit = useCallback( - async ({ boost, ...values }) => { + async ({ boost, crosspost, ...values }) => { const { data, error } = await upsertDiscussion({ variables: { sub: item?.subName || sub?.name, @@ -123,13 +109,11 @@ export function DiscussionForm({ throw new Error({ message: error.toString() }) } - const userHasCrosspostingEnabled = me?.nostrCrossposting || false; + const shouldCrosspost = me?.nostrCrossposting && crosspost - if (userHasCrosspostingEnabled && data?.upsertDiscussion?.id) { + if (shouldCrosspost && data?.upsertDiscussion?.id) { const results = await handleCrosspost(values, data.upsertDiscussion.id); if (results.allSuccessful) { - Toast.success("Crossposting succeeded."); - if (item) { await router.push(`/items/${item.id}`) } else { @@ -167,6 +151,7 @@ export function DiscussionForm({ initial={{ title: item?.title || shareTitle || '', text: item?.text || '', + crosspost: me?.nostrCrossposting, ...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }), ...SubSelectInitial({ sub: item?.subName || sub?.name }) }} diff --git a/components/toast.js b/components/toast.js index dcbe2b6a0..50c9587e4 100644 --- a/components/toast.js +++ b/components/toast.js @@ -22,7 +22,6 @@ export const ToastProvider = ({ children }) => { }, []) const removeToast = useCallback(id => { - console.log("Removing toast with id:", id); // Add this line setToasts(toasts => toasts.filter(toast => toast.id !== id)) }, []) @@ -35,14 +34,14 @@ export const ToastProvider = ({ children }) => { delay: 5000 }) }, - danger: body => { + danger: (body, onCloseCallback) => { const id = toastId.current; - console.log('id', id) dispatchToast({ id, body, variant: 'danger', autohide: false, + onCloseCallback, }) return { removeToast: () => removeToast(id) @@ -75,7 +74,10 @@ export const ToastProvider = ({ children }) => { variant={null} className='p-0 ps-2' aria-label='close' - onClick={() => removeToast(toast.id)} + onClick={() => { + if (toast.onCloseCallback) toast.onCloseCallback(); + removeToast(toast.id); + }} >
X
diff --git a/lib/nostr.js b/lib/nostr.js index d47052bfb..1d3cfc03f 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -4,6 +4,13 @@ export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/ export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/ export const NOSTR_MAX_RELAY_NUM = 20 export const NOSTR_ZAPPLE_PAY_NPUB = 'npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan' +export const DEFAULT_CROSSPOSTING_RELAYS = [ + "wss://nostrue.com/", + "wss://relay.damus.io/", + "wss://relay.nostr.band/", + "wss://relay.snort.social/", + "wss://nostr21.com/" +]; export function hexToBech32 (hex, prefix = 'npub') { return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex'))) @@ -26,109 +33,53 @@ export function nostrZapDetails (zap) { return { npub, content, note } } -export async function crosspostDiscussion(item, id, relays) { - try { - const userPubkey = await window.nostr.getPublicKey() +async function publishNostrEvent(signedEvent, relay) { + return new Promise((resolve, reject) => { + const timeout = 1000; + const wsRelay = new WebSocket(relay); + let timer; + let isMessageSentSuccessfully = false; + + function timedout() { + clearTimeout(timer); + wsRelay.close(); + reject(new Error(`relay timeout for ${relay}`)); + } - const timestamp = Math.floor(Date.now() / 1000) + timer = setTimeout(timedout, timeout); - const event = { - created_at: timestamp, - kind: 30023, - content: item.text, - tags: [ - ['d', `https://stacker.news/items/${id}`], - ['a', `30023:${userPubkey}:https://stacker.news/items/${id}`, 'wss://nostr.mutinywallet.com'], - ['title', item.title], - ['published_at', timestamp.toString()] - ], + wsRelay.onopen = function () { + clearTimeout(timer); + timer = setTimeout(timedout, timeout); + wsRelay.send(JSON.stringify(['EVENT', signedEvent])); }; - const signedEvent = await window.nostr.signEvent(event); - - let promises = []; - - if (signedEvent) { - promises = relays.map((r) => - new Promise((resolve, reject) => { - const timeout = 1000; - const relay = new WebSocket(r); - let timer; - let isMessageSentSuccessfully = false; - - function timedout() { - clearTimeout(timer); - relay.close(); - reject(new Error(`relay timeout for ${r}`)); - - return { error: `relay timeout for ${r}` }; - } - - timer = setTimeout(timedout, timeout); - - relay.onopen = function () { - clearTimeout(timer); - timer = setTimeout(timedout, timeout); - relay.send(JSON.stringify(['EVENT', signedEvent])); - }; - - relay.onmessage = function (msg) { - const m = JSON.parse(msg.data); - if (m[0] === 'OK') { - isMessageSentSuccessfully = true; - clearTimeout(timer); - relay.close(); - console.log('Successfully sent event to', r); - resolve(); - } - }; - - relay.onerror = function (error) { - clearTimeout(timer); - console.log('WebSocket Error: ', error); - reject(new Error(`relay error: Failed to send to ${r}`)); - - return { error }; - }; - - relay.onclose = function () { - clearTimeout(timer); - console.log(`Connection closed for ${r}`); - if (!isMessageSentSuccessfully) { - // Check the flag here in the onclose method - reject(new Error(`relay error: Failed to send to ${r}`)); - - return { error: `relay error: Failed to send to ${r}` }; - } - }; - }) - ) - - } else { - throw new Error('failed to sign event') - } - - const results = await Promise.allSettled(promises); - const successfulRelays = []; - const failedRelays = []; - - results.forEach((result, index) => { - if (result.status === "fulfilled") { - successfulRelays.push(relays[index]); - } else { - failedRelays.push({ relay: relays[index], error: result.reason }); + wsRelay.onmessage = function (msg) { + const m = JSON.parse(msg.data); + if (m[0] === 'OK') { + isMessageSentSuccessfully = true; + clearTimeout(timer); + wsRelay.close(); + console.log('Successfully sent event to', relay); + resolve(); } - }); + }; - return { successfulRelays, failedRelays }; + wsRelay.onerror = function (error) { + clearTimeout(timer); + reject(new Error(`relay error: Failed to send to ${relay}`)); + }; - } catch (error) { - console.error('Crosspost discussion error:', error); - return { error }; - } + wsRelay.onclose = function () { + clearTimeout(timer); + if (!isMessageSentSuccessfully) { + reject(new Error(`relay error: Failed to send to ${relay}`)); + } + }; + }); } -export async function retryCrosspost(item, id, failedRelays) { +export async function crosspostDiscussion(item, id, relays) { try { const userPubkey = await window.nostr.getPublicKey(); const timestamp = Math.floor(Date.now() / 1000); @@ -146,72 +97,28 @@ export async function retryCrosspost(item, id, failedRelays) { }; const signedEvent = await window.nostr.signEvent(event); - if (!signedEvent) throw new Error('failed to sign event'); - const promises = failedRelays.map((relayObj) => - new Promise((resolve, reject) => { - const timeout = 1000; - const relay = new WebSocket(relayObj.relay); - let timer; - let isMessageSentSuccessfully = false; - - function timedout() { - clearTimeout(timer); - relay.close(); - reject(new Error(`relay timeout for ${relayObj.relay}`)); - } - - timer = setTimeout(timedout, timeout); - - relay.onopen = function () { - clearTimeout(timer); - timer = setTimeout(timedout, timeout); - relay.send(JSON.stringify(['EVENT', signedEvent])); - }; - - relay.onmessage = function (msg) { - const m = JSON.parse(msg.data); - if (m[0] === 'OK') { - isMessageSentSuccessfully = true; - clearTimeout(timer); - relay.close(); - console.log('Successfully resent event to', relayObj.relay); - resolve(); - } - }; - - relay.onerror = function (error) { - clearTimeout(timer); - console.log('WebSocket Error: ', error); - reject(new Error(`relay error: Failed to resend to ${relayObj.relay}`)); - }; - - relay.onclose = function () { - clearTimeout(timer); - console.log(`Connection closed for ${relayObj.relay}`); - if (!isMessageSentSuccessfully) { - reject(new Error(`relay error: Failed to resend to ${relayObj.relay}`)); - } - }; - }) - ); + if (!signedEvent) throw new Error('failed to sign event'); + const promises = relays.map(r => publishNostrEvent(signedEvent, r)); const results = await Promise.allSettled(promises); const successfulRelays = []; - const stillFailedRelays = []; + const failedRelays = []; results.forEach((result, index) => { if (result.status === "fulfilled") { - successfulRelays.push(failedRelays[index].relay); + successfulRelays.push(relays[index]); } else { - stillFailedRelays.push({ relay: failedRelays[index].relay, error: result.reason }); + failedRelays.push({ relay: relays[index], error: result.reason }); } }); - return { successfulRelays, stillFailedRelays }; + const eventId = hexToBech32(signedEvent.id, 'nevent'); + + return { successfulRelays, failedRelays, eventId }; } catch (error) { - console.error('Retry crosspost error:', error); + console.error('Crosspost discussion error:', error); return { error }; } } diff --git a/pages/settings.js b/pages/settings.js index 5bd1825e8..121727520 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -18,6 +18,7 @@ import { bech32 } from 'bech32' import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32 } from '../lib/nostr' import { emailSchema, lastAuthRemovalSchema, settingsSchema } from '../lib/validate' import { SUPPORTED_CURRENCIES } from '../lib/currency' +import {DEFAULT_CROSSPOSTING_RELAYS} from '../lib/nostr' import PageLoading from '../components/page-loading' import { useShowModal } from '../components/modal' import { authErrorMessage } from '../components/login' @@ -310,41 +311,38 @@ export default function Settings ({ ssrData }) { } name='greeterMode' /> - nostr NIP-05} - body={ - <> - crosspost to nostr - -
    -
  • crosspost discussion items to nostr
  • -
  • requires NIP-07 extension for signing
  • -
  • we use your NIP-05 relays if set
  • -
  • otherwise we default to the blastr relay wss://nostr.mutinywallet.com
  • -
-
- - } - name='nostrCrossposting' - /> - pubkey optional} - name='nostrPubkey' - clear - /> - relays optional} - name='nostrRelays' - clear - min={0} - max={NOSTR_MAX_RELAY_NUM} - /> - - } +
nostr
+ crosspost to nostr + +
    +
  • crosspost discussion items to nostr
  • +
  • requires NIP-07 extension for signing
  • +
  • we use your NIP-05 relays if set
  • +
  • otherwise we default to these relays:
  • +
      + {DEFAULT_CROSSPOSTING_RELAYS.map((relay, i) => ( +
    • {relay}
    • + ))} +
    +
+
+ + } + name='nostrCrossposting' + /> + pubkey optional} + name='nostrPubkey' + clear + /> + relays optional} + name='nostrRelays' + clear + min={0} + max={NOSTR_MAX_RELAY_NUM} />
save