From 7883cd8c3c0ac8a00d77e26533028457856397ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?calvadev=E2=9A=A1=EF=B8=8F?= <32919103+calvadev@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:44:26 +0000 Subject: [PATCH 01/12] Added relay list fetching upon sign in --- components/display-products.tsx | 11 ++-- components/sign-in/SignInModal.tsx | 46 +++++++++++---- .../utility-components/shopstr-slider.tsx | 9 +-- pages/_app.tsx | 32 +++++++---- pages/api/nostr/fetch-service.ts | 16 +++++- pages/settings/user-profile.tsx | 2 +- pages/sign-in/index.tsx | 57 +++++++++++++------ utils/context/context.ts | 8 ++- 8 files changed, 128 insertions(+), 53 deletions(-) diff --git a/components/display-products.tsx b/components/display-products.tsx index 0101269..a9c065f 100644 --- a/components/display-products.tsx +++ b/components/display-products.tsx @@ -5,7 +5,7 @@ import { NostrEvent } from "../utils/types/types"; import { ProductContext, ProfileMapContext, - FollowsContext, + FollowsAndRelaysContext, } from "../utils/context/context"; import ProductCard from "./utility-components/product-card"; import DisplayProductModal from "./display-product-modal"; @@ -38,7 +38,7 @@ const DisplayEvents = ({ const [isProductsLoading, setIsProductLoading] = useState(true); const productEventContext = useContext(ProductContext); const profileMapContext = useContext(ProfileMapContext); - const followsContext = useContext(FollowsContext); + const followsAndRelaysContext = useContext(FollowsAndRelaysContext); const [focusedProduct, setFocusedProduct] = useState(""); // product being viewed in modal const [showModal, setShowModal] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); @@ -58,8 +58,11 @@ const DisplayEvents = ({ let parsedProductData: ProductData[] = []; sortedProductEvents.forEach((event) => { if (wotFilter) { - if (!followsContext.isLoading && followsContext.followList) { - const followList = followsContext.followList; + if ( + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.followList + ) { + const followList = followsAndRelaysContext.followList; if (followList.length > 0 && followList.includes(event.pubkey)) { let parsedData = parseTags(event); if (parsedData) parsedProductData.push(parsedData); diff --git a/components/sign-in/SignInModal.tsx b/components/sign-in/SignInModal.tsx index ab5b968..2b23d7a 100644 --- a/components/sign-in/SignInModal.tsx +++ b/components/sign-in/SignInModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useContext } from "react"; import { Modal, ModalContent, @@ -13,6 +13,7 @@ import { setLocalStorageDataOnSignIn, validateNSecKey, } from "@/components/utility/nostr-helper-functions"; +import { FollowsAndRelaysContext } from "../../utils/context/context"; import { getPublicKey, nip19 } from "nostr-tools"; import CryptoJS from "crypto-js"; import { useRouter } from "next/router"; @@ -31,16 +32,29 @@ export default function SignInModal({ const [showNsecSignIn, setShowNsecSignIn] = useState(false); + const followsAndRelaysContext = useContext(FollowsAndRelaysContext); + const router = useRouter(); const startExtensionLogin = async () => { try { // @ts-ignore var pk = await window.nostr.getPublicKey(); - setLocalStorageDataOnSignIn({ - signInMethod: "extension", - pubkey: pk, - }); + if ( + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.relayList.length >= 0 + ) { + setLocalStorageDataOnSignIn({ + signInMethod: "nsec", + pubkey: pk, + relays: followsAndRelaysContext.relayList, + }); + } else { + setLocalStorageDataOnSignIn({ + signInMethod: "nsec", + pubkey: pk, + }); + } onClose(); } catch (error) { alert("Extension sign in failed!"); @@ -67,11 +81,23 @@ export default function SignInModal({ onClose(); // avoids tree walker issue by closing modal }, 500); - setLocalStorageDataOnSignIn({ - signInMethod: "nsec", - pubkey: pk, - encryptedPrivateKey: encryptedPrivateKey, - }); + if ( + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.relayList.length >= 0 + ) { + setLocalStorageDataOnSignIn({ + signInMethod: "nsec", + pubkey: pk, + encryptedPrivateKey: encryptedPrivateKey, + relays: followsAndRelaysContext.relayList, + }); + } else { + setLocalStorageDataOnSignIn({ + signInMethod: "nsec", + pubkey: pk, + encryptedPrivateKey: encryptedPrivateKey, + }); + } } } else { alert( diff --git a/components/utility-components/shopstr-slider.tsx b/components/utility-components/shopstr-slider.tsx index b07c983..09f0754 100644 --- a/components/utility-components/shopstr-slider.tsx +++ b/components/utility-components/shopstr-slider.tsx @@ -2,14 +2,14 @@ import { useState, useEffect, useContext } from "react"; import { Button } from "@nextui-org/react"; import { Slider } from "@nextui-org/react"; import { useTheme } from "next-themes"; -import { FollowsContext } from "../../utils/context/context"; +import { FollowsAndRelaysContext } from "../../utils/context/context"; import { getLocalStorageData } from "../../components/utility/nostr-helper-functions"; import { SHOPSTRBUTTONCLASSNAMES } from "../../components/utility/STATIC-VARIABLES"; const ShopstrSlider = () => { const { theme, setTheme } = useTheme(); - const followsContext = useContext(FollowsContext); + const followsAndRelaysContext = useContext(FollowsAndRelaysContext); const [wot, setWot] = useState(getLocalStorageData().wot); const [wotIsChanged, setWotIsChanged] = useState(false); @@ -33,8 +33,9 @@ const ShopstrSlider = () => { label="Minimum Follower Count:" showSteps={true} maxValue={ - !followsContext.isLoading && followsContext.firstDegreeFollowsLength - ? followsContext.firstDegreeFollowsLength + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.firstDegreeFollowsLength + ? followsAndRelaysContext.firstDegreeFollowsLength : wot } minValue={1} diff --git a/pages/_app.tsx b/pages/_app.tsx index 7654bc3..4bef1c2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -11,8 +11,8 @@ import { ChatsContextInterface, ChatsContext, ChatsMap, - FollowsContextInterface, - FollowsContext, + FollowsAndRelaysContextInterface, + FollowsAndRelaysContext, } from "../utils/context/context"; import { getLocalStorageData, @@ -24,7 +24,7 @@ import { fetchAllPosts, fetchChatsAndMessages, fetchProfile, - fetchAllFollows, + fetchAllFollowsAndRelays, } from "./api/nostr/fetch-service"; import { NostrEvent, ProfileData } from "../utils/types/types"; import BottomNav from "@/components/nav-bottom"; @@ -86,13 +86,13 @@ function App({ Component, pageProps }: AppProps) { chatsMap: new Map(), isLoading: true, }); - const [followsContext, setFollowsContext] = useState( - { + const [followsAndRelaysContext, setFollowsAndRelaysContext] = + useState({ followList: [], firstDegreeFollowsLength: 0, + relayList: [], isLoading: true, - }, - ); + }); const editProductContext = ( productEvents: NostrEvent[], @@ -125,12 +125,18 @@ function App({ Component, pageProps }: AppProps) { setChatsContext({ chatsMap, isLoading }); }; - const editFollowsContext = ( + const editFollowsAndRelaysContext = ( followList: string[], firstDegreeFollowsLength: number, + relayList: string[], isLoading: boolean, ) => { - setFollowsContext({ followList, firstDegreeFollowsLength, isLoading }); + setFollowsAndRelaysContext({ + followList, + firstDegreeFollowsLength, + relayList, + isLoading, + }); }; /** FETCH initial PRODUCTS and PROFILES **/ @@ -139,7 +145,9 @@ function App({ Component, pageProps }: AppProps) { const relays = getLocalStorageData().relays; const userPubkey = getLocalStorageData().userPubkey; try { - let { followList } = await fetchAllFollows(editFollowsContext); + let { followList } = await fetchAllFollowsAndRelays( + editFollowsAndRelaysContext, + ); let pubkeysToFetchProfilesFor: string[] = []; let { profileSetFromProducts } = await fetchAllPosts( relays, @@ -196,7 +204,7 @@ function App({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> - + @@ -214,7 +222,7 @@ function App({ Component, pageProps }: AppProps) { - + ); } diff --git a/pages/api/nostr/fetch-service.ts b/pages/api/nostr/fetch-service.ts index 5e277f9..4456ad5 100644 --- a/pages/api/nostr/fetch-service.ts +++ b/pages/api/nostr/fetch-service.ts @@ -276,10 +276,11 @@ export const fetchChatsAndMessages = async ( }); }; -export const fetchAllFollows = async ( +export const fetchAllFollowsAndRelays = async ( editFollowsContext: ( followList: string[], firstDegreeFollowsLength: number, + relayList: string[], isLoading: boolean, ) => void, ): Promise<{ @@ -293,14 +294,24 @@ export const fetchAllFollows = async ( let followsArrayFromRelay: string[] = []; const followsSet: Set = new Set(); let firstDegreeFollowsLength = 0; + let relayList: string[] = []; + const relaySet: Set = new Set(); const firstFollowfilter: Filter = { - kinds: [3], + kinds: [3, 10002], authors: [getLocalStorageData().userPubkey], }; let first = relay.subscribe([firstFollowfilter], { onevent(event) { + if (event.kind === 10002) { + const validRelays = event.tags.filter( + (tag) => tag[0] === "r" && (tag[2] === "write" || !tag[2]), + ); + + validRelays.forEach((tag) => relaySet.add(tag[1])); + relayList.push(...validRelays.map((tag) => tag[1])); + } const validTags = event.tags .map((tag) => tag[1]) .filter((pubkey) => isHexString(pubkey) && !followsSet.has(pubkey)); @@ -417,6 +428,7 @@ export const fetchAllFollows = async ( editFollowsContext( followsArrayFromRelay, firstDegreeFollowsLength, + relayList, false, ); }; diff --git a/pages/settings/user-profile.tsx b/pages/settings/user-profile.tsx index a90355a..3625aa3 100644 --- a/pages/settings/user-profile.tsx +++ b/pages/settings/user-profile.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useContext, useMemo } from "react"; import { SettingsBreadCrumbs } from "@/components/settings/settings-bread-crumbs"; import { ProfileMapContext } from "@/utils/context/context"; -import { useForm, Controller, set } from "react-hook-form"; +import { useForm, Controller } from "react-hook-form"; import { Button, Textarea, diff --git a/pages/sign-in/index.tsx b/pages/sign-in/index.tsx index 4c6c33e..1d1ee72 100644 --- a/pages/sign-in/index.tsx +++ b/pages/sign-in/index.tsx @@ -1,8 +1,9 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useContext } from "react"; import { withRouter, NextRouter } from "next/router"; import { nip19, getPublicKey } from "nostr-tools"; import * as CryptoJS from "crypto-js"; import { validateNSecKey } from "../../components/utility/nostr-helper-functions"; +import { FollowsAndRelaysContext } from "../../utils/context/context"; import { Card, CardBody, Button, Input, Image } from "@nextui-org/react"; import { SHOPSTRBUTTONCLASSNAMES } from "../../components/utility/STATIC-VARIABLES"; @@ -12,6 +13,8 @@ const LoginPage = ({ router }: { router: NextRouter }) => { const [validPrivateKey, setValidPrivateKey] = useState(false); const [passphrase, setPassphrase] = useState(""); + const followsAndRelaysContext = useContext(FollowsAndRelaysContext); + const handleSignIn = async () => { if (validPrivateKey) { if (passphrase === "" || passphrase === null) { @@ -31,14 +34,24 @@ const LoginPage = ({ router }: { router: NextRouter }) => { localStorage.setItem("signIn", "nsec"); - localStorage.setItem( - "relays", - JSON.stringify([ - "wss://relay.damus.io", - "wss://nos.lol", - "wss://nostr.mutinywallet.com", - ]), - ); + if ( + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.relayList.length >= 0 + ) { + localStorage.setItem( + "relays", + JSON.stringify(followsAndRelaysContext.relayList), + ); + } else { + localStorage.setItem( + "relays", + JSON.stringify([ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://nostr.mutinywallet.com", + ]), + ); + } localStorage.setItem( "mints", @@ -67,14 +80,24 @@ const LoginPage = ({ router }: { router: NextRouter }) => { let npub = nip19.npubEncode(pk); localStorage.setItem("npub", npub); localStorage.setItem("signIn", "extension"); - localStorage.setItem( - "relays", - JSON.stringify([ - "wss://relay.damus.io", - "wss://nos.lol", - "wss://nostr.mutinywallet.com", - ]), - ); + if ( + !followsAndRelaysContext.isLoading && + followsAndRelaysContext.relayList.length >= 0 + ) { + localStorage.setItem( + "relays", + JSON.stringify(followsAndRelaysContext.relayList), + ); + } else { + localStorage.setItem( + "relays", + JSON.stringify([ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://nostr.mutinywallet.com", + ]), + ); + } localStorage.setItem( "mints", JSON.stringify(["https://mint.minibits.cash/Bitcoin"]), diff --git a/utils/context/context.ts b/utils/context/context.ts index 0a8c16a..eb27a9f 100644 --- a/utils/context/context.ts +++ b/utils/context/context.ts @@ -38,14 +38,16 @@ export const ChatsContext = createContext({ isLoading: true, } as ChatsContextInterface); -export interface FollowsContextInterface { +export interface FollowsAndRelaysContextInterface { followList: string[]; firstDegreeFollowsLength: number; + relayList: string[]; isLoading: boolean; } -export const FollowsContext = createContext({ +export const FollowsAndRelaysContext = createContext({ followList: [], firstDegreeFollowsLength: 0, + relayList: [], isLoading: true, -} as FollowsContextInterface); +} as FollowsAndRelaysContextInterface); From 862371b07f476066cc9764778cfe278cb831f5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?calvadev=E2=9A=A1=EF=B8=8F?= <32919103+calvadev@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:49:51 +0000 Subject: [PATCH 02/12] Bugfixes --- components/sign-in/SignInModal.tsx | 4 ++-- components/utility/nostr-helper-functions.ts | 3 +-- pages/_app.tsx | 7 +++++-- pages/api/nostr/fetch-service.ts | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/components/sign-in/SignInModal.tsx b/components/sign-in/SignInModal.tsx index 2b23d7a..037dff6 100644 --- a/components/sign-in/SignInModal.tsx +++ b/components/sign-in/SignInModal.tsx @@ -45,13 +45,13 @@ export default function SignInModal({ followsAndRelaysContext.relayList.length >= 0 ) { setLocalStorageDataOnSignIn({ - signInMethod: "nsec", + signInMethod: "extension", pubkey: pk, relays: followsAndRelaysContext.relayList, }); } else { setLocalStorageDataOnSignIn({ - signInMethod: "nsec", + signInMethod: "extension", pubkey: pk, }); } diff --git a/components/utility/nostr-helper-functions.ts b/components/utility/nostr-helper-functions.ts index 01dd196..985a10d 100644 --- a/components/utility/nostr-helper-functions.ts +++ b/components/utility/nostr-helper-functions.ts @@ -1,7 +1,6 @@ import * as CryptoJS from "crypto-js"; import { finalizeEvent, - getPublicKey, nip04, nip19, nip98, @@ -333,7 +332,7 @@ export const setLocalStorageDataOnSignIn = ({ localStorage.setItem( LOCALSTORAGECONSTANTS.relays, JSON.stringify( - relays + (relays && relays.length != 0) ? relays : [ "wss://relay.damus.io", diff --git a/pages/_app.tsx b/pages/_app.tsx index 4bef1c2..f683476 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -139,15 +139,18 @@ function App({ Component, pageProps }: AppProps) { }); }; - /** FETCH initial PRODUCTS and PROFILES **/ + /** FETCH initial FOLLOWS, RELAYS, PRODUCTS, and PROFILES **/ useEffect(() => { async function fetchData() { const relays = getLocalStorageData().relays; const userPubkey = getLocalStorageData().userPubkey; try { - let { followList } = await fetchAllFollowsAndRelays( + let { relayList } = await fetchAllFollowsAndRelays( editFollowsAndRelaysContext, ); + if (getLocalStorageData().relays.length != 0) { + localStorage.setItem("relays", JSON.stringify(relayList)); + } let pubkeysToFetchProfilesFor: string[] = []; let { profileSetFromProducts } = await fetchAllPosts( relays, diff --git a/pages/api/nostr/fetch-service.ts b/pages/api/nostr/fetch-service.ts index 4456ad5..c9d7f0a 100644 --- a/pages/api/nostr/fetch-service.ts +++ b/pages/api/nostr/fetch-service.ts @@ -284,7 +284,7 @@ export const fetchAllFollowsAndRelays = async ( isLoading: boolean, ) => void, ): Promise<{ - followList: string[]; + relayList: string[]; }> => { return new Promise(async function (resolve, reject) { const wot = getLocalStorageData().wot; @@ -423,7 +423,7 @@ export const fetchAllFollowsAndRelays = async ( }); } resolve({ - followList: followsArrayFromRelay, + relayList: relayList, }); editFollowsContext( followsArrayFromRelay, From ec5ea4f403d892eb2f0b8cf33ca533656cbfa50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?calvadev=E2=9A=A1=EF=B8=8F?= <32919103+calvadev@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:04:46 +0000 Subject: [PATCH 03/12] Added outbox relay support --- components/sign-in/SignInModal.tsx | 22 +++++- components/utility/nostr-helper-functions.ts | 78 +++++++++++++++----- pages/_app.tsx | 13 +++- pages/api/nostr/fetch-service.ts | 30 +++++++- pages/sign-in/index.tsx | 24 +++++- utils/context/context.ts | 4 + 6 files changed, 142 insertions(+), 29 deletions(-) diff --git a/components/sign-in/SignInModal.tsx b/components/sign-in/SignInModal.tsx index 037dff6..e4de77a 100644 --- a/components/sign-in/SignInModal.tsx +++ b/components/sign-in/SignInModal.tsx @@ -42,12 +42,19 @@ export default function SignInModal({ var pk = await window.nostr.getPublicKey(); if ( !followsAndRelaysContext.isLoading && - followsAndRelaysContext.relayList.length >= 0 + followsAndRelaysContext.relayList.length >= 0 && + followsAndRelaysContext.readRelayList && + followsAndRelaysContext.writeRelayList ) { + const allRelays = [ + ...followsAndRelaysContext.relayList, + ...followsAndRelaysContext.readRelayList, + ...followsAndRelaysContext.writeRelayList, + ]; setLocalStorageDataOnSignIn({ signInMethod: "extension", pubkey: pk, - relays: followsAndRelaysContext.relayList, + relays: allRelays, }); } else { setLocalStorageDataOnSignIn({ @@ -83,13 +90,20 @@ export default function SignInModal({ if ( !followsAndRelaysContext.isLoading && - followsAndRelaysContext.relayList.length >= 0 + followsAndRelaysContext.relayList.length >= 0 && + followsAndRelaysContext.readRelayList && + followsAndRelaysContext.writeRelayList ) { + const allRelays = [ + ...followsAndRelaysContext.relayList, + ...followsAndRelaysContext.readRelayList, + ...followsAndRelaysContext.writeRelayList, + ]; setLocalStorageDataOnSignIn({ signInMethod: "nsec", pubkey: pk, encryptedPrivateKey: encryptedPrivateKey, - relays: followsAndRelaysContext.relayList, + relays: allRelays, }); } else { setLocalStorageDataOnSignIn({ diff --git a/components/utility/nostr-helper-functions.ts b/components/utility/nostr-helper-functions.ts index 985a10d..80674c5 100644 --- a/components/utility/nostr-helper-functions.ts +++ b/components/utility/nostr-helper-functions.ts @@ -1,11 +1,5 @@ import * as CryptoJS from "crypto-js"; -import { - finalizeEvent, - nip04, - nip19, - nip98, - SimplePool, -} from "nostr-tools"; +import { finalizeEvent, nip04, nip19, nip98, SimplePool } from "nostr-tools"; import axios from "axios"; import { NostrEvent } from "@/utils/types/types"; import { ProductFormValues } from "@/pages/api/nostr/post-event"; @@ -14,7 +8,8 @@ export async function PostListing( values: ProductFormValues, passphrase: string, ) { - const { signInMethod, userPubkey, relays } = getLocalStorageData(); + const { signInMethod, userPubkey, relays, writeRelays } = + getLocalStorageData(); const summary = values.find(([key]) => key === "summary")?.[1] || ""; const dValue = values.find(([key]) => key === "d")?.[1] || undefined; @@ -64,11 +59,14 @@ export async function PostListing( const pool = new SimplePool(); - await Promise.any(pool.publish(relays, signedEvent)); - await Promise.any(pool.publish(relays, signedRecEvent)); - await Promise.any(pool.publish(relays, signedHandlerEvent)); + const allWriteRelays = [...writeRelays, ...relays]; + + await Promise.any(pool.publish(allWriteRelays, signedEvent)); + await Promise.any(pool.publish(allWriteRelays, signedRecEvent)); + await Promise.any(pool.publish(allWriteRelays, signedHandlerEvent)); return signedEvent; } else { + const allWriteRelays = [...writeRelays, ...relays]; const res = await axios({ method: "POST", url: "/api/nostr/post-event", @@ -83,7 +81,7 @@ export async function PostListing( // kind: 30018, tags: updatedValues, content: summary, - relays: relays, + relays: allWriteRelays, }, }); return { @@ -143,7 +141,7 @@ export async function sendEncryptedMessage( encryptedMessageEvent: EncryptedMessageEvent, passphrase?: string, ) { - const { signInMethod, relays } = getLocalStorageData(); + const { signInMethod, relays, writeRelays } = getLocalStorageData(); let signedEvent; if (signInMethod === "extension") { signedEvent = await window.nostr.signEvent(encryptedMessageEvent); @@ -153,7 +151,8 @@ export async function sendEncryptedMessage( signedEvent = finalizeEvent(encryptedMessageEvent, senderPrivkey); } const pool = new SimplePool(); - await Promise.any(pool.publish(relays, signedEvent)); + const allWriteRelays = [...writeRelays, ...relays]; + await Promise.any(pool.publish(allWriteRelays, signedEvent)); } export async function finalizeAndSendNostrEvent( @@ -161,7 +160,7 @@ export async function finalizeAndSendNostrEvent( passphrase?: string, ) { try { - const { signInMethod, relays } = getLocalStorageData(); + const { signInMethod, relays, writeRelays } = getLocalStorageData(); let signedEvent; if (signInMethod === "extension") { signedEvent = await window.nostr.signEvent(nostrEvent); @@ -171,7 +170,8 @@ export async function finalizeAndSendNostrEvent( signedEvent = finalizeEvent(nostrEvent, senderPrivkey); } const pool = new SimplePool(); - await Promise.any(pool.publish(relays, signedEvent)); + const allWriteRelays = [...writeRelays, ...relays]; + await Promise.any(pool.publish(allWriteRelays, signedEvent)); } catch (e: any) { console.log("Error: ", e); alert("Failed to send event: " + e.message); @@ -295,6 +295,8 @@ const LOCALSTORAGECONSTANTS = { userPubkey: "userPubkey", encryptedPrivateKey: "encryptedPrivateKey", relays: "relays", + readRelays: "readRelays", + writeRelays: "writeRelays", mints: "mints", tokens: "tokens", history: "history", @@ -306,6 +308,8 @@ export const setLocalStorageDataOnSignIn = ({ pubkey, encryptedPrivateKey, relays, + readRelays, + writeRelays, mints, wot, }: { @@ -313,6 +317,8 @@ export const setLocalStorageDataOnSignIn = ({ pubkey: string; encryptedPrivateKey?: string; relays?: string[]; + readRelays?: string[]; + writeRelays?: string[]; mints?: string[]; wot?: number; }) => { @@ -332,7 +338,7 @@ export const setLocalStorageDataOnSignIn = ({ localStorage.setItem( LOCALSTORAGECONSTANTS.relays, JSON.stringify( - (relays && relays.length != 0) + relays && relays.length != 0 ? relays : [ "wss://relay.damus.io", @@ -342,6 +348,16 @@ export const setLocalStorageDataOnSignIn = ({ ), ); + localStorage.setItem( + LOCALSTORAGECONSTANTS.readRelays, + JSON.stringify(readRelays && readRelays.length != 0 ? readRelays : []), + ); + + localStorage.setItem( + LOCALSTORAGECONSTANTS.writeRelays, + JSON.stringify(writeRelays && writeRelays.length != 0 ? writeRelays : []), + ); + localStorage.setItem( LOCALSTORAGECONSTANTS.mints, JSON.stringify(mints ? mints : ["https://mint.minibits.cash/Bitcoin"]), @@ -363,6 +379,8 @@ export interface LocalStorageInterface { userNPub: string; userPubkey: string; relays: string[]; + readRelays: string[]; + writeRelays: string[]; mints: string[]; tokens: []; history: []; @@ -376,6 +394,8 @@ export const getLocalStorageData = (): LocalStorageInterface => { let userNPub; let userPubkey; let relays; + let readRelays; + let writeRelays; let mints; let tokens; let history; @@ -412,14 +432,31 @@ export const getLocalStorageData = (): LocalStorageInterface => { if (!relays) { relays = defaultRelays; + localStorage.setItem("relays", JSON.stringify(relays)); } else { try { relays = (JSON.parse(relays) as string[]).filter((r) => r); } catch { relays = defaultRelays; + localStorage.setItem("relays", JSON.stringify(relays)); } } - localStorage.setItem("relays", JSON.stringify(relays)); + + readRelays = localStorage.getItem(LOCALSTORAGECONSTANTS.readRelays) + ? ( + JSON.parse( + localStorage.getItem(LOCALSTORAGECONSTANTS.readRelays) as string, + ) as string[] + ).filter((r) => r) + : []; + + writeRelays = localStorage.getItem(LOCALSTORAGECONSTANTS.writeRelays) + ? ( + JSON.parse( + localStorage.getItem(LOCALSTORAGECONSTANTS.writeRelays) as string, + ) as string[] + ).filter((r) => r) + : []; mints = localStorage.getItem(LOCALSTORAGECONSTANTS.mints) ? JSON.parse(localStorage.getItem("mints") as string) @@ -454,6 +491,8 @@ export const getLocalStorageData = (): LocalStorageInterface => { userNPub: userNPub as string, userPubkey: userPubkey as string, relays: relays || [], + readRelays: readRelays || [], + writeRelays: writeRelays || [], mints, tokens: tokens || [], history: history || [], @@ -471,6 +510,9 @@ export const LogOut = () => { localStorage.removeItem(LOCALSTORAGECONSTANTS.userNPub); localStorage.removeItem(LOCALSTORAGECONSTANTS.userPubkey); localStorage.removeItem(LOCALSTORAGECONSTANTS.encryptedPrivateKey); + localStorage.removeItem(LOCALSTORAGECONSTANTS.relays); + localStorage.removeItem(LOCALSTORAGECONSTANTS.readRelays); + localStorage.removeItem(LOCALSTORAGECONSTANTS.writeRelays); window.dispatchEvent(new Event("storage")); }; diff --git a/pages/_app.tsx b/pages/_app.tsx index f683476..1fb06af 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -91,6 +91,8 @@ function App({ Component, pageProps }: AppProps) { followList: [], firstDegreeFollowsLength: 0, relayList: [], + readRelayList: [], + writeRelayList: [], isLoading: true, }); @@ -129,12 +131,16 @@ function App({ Component, pageProps }: AppProps) { followList: string[], firstDegreeFollowsLength: number, relayList: string[], + readRelayList: string[], + writeRelayList: string[], isLoading: boolean, ) => { setFollowsAndRelaysContext({ followList, firstDegreeFollowsLength, relayList, + readRelayList, + writeRelayList, isLoading, }); }; @@ -145,11 +151,12 @@ function App({ Component, pageProps }: AppProps) { const relays = getLocalStorageData().relays; const userPubkey = getLocalStorageData().userPubkey; try { - let { relayList } = await fetchAllFollowsAndRelays( - editFollowsAndRelaysContext, - ); + let { relayList, readRelayList, writeRelayList } = + await fetchAllFollowsAndRelays(editFollowsAndRelaysContext); if (getLocalStorageData().relays.length != 0) { localStorage.setItem("relays", JSON.stringify(relayList)); + localStorage.setItem("readRelays", JSON.stringify(readRelayList)); + localStorage.setItem("writeRelays", JSON.stringify(writeRelayList)); } let pubkeysToFetchProfilesFor: string[] = []; let { profileSetFromProducts } = await fetchAllPosts( diff --git a/pages/api/nostr/fetch-service.ts b/pages/api/nostr/fetch-service.ts index c9d7f0a..d265e44 100644 --- a/pages/api/nostr/fetch-service.ts +++ b/pages/api/nostr/fetch-service.ts @@ -1,4 +1,4 @@ -import { Filter, Nostr, Relay, SimplePool } from "nostr-tools"; +import { Filter, Relay, SimplePool } from "nostr-tools"; import { addChatMessageToCache, addProductToCache, @@ -281,10 +281,14 @@ export const fetchAllFollowsAndRelays = async ( followList: string[], firstDegreeFollowsLength: number, relayList: string[], + readRelayList: string[], + writeRelayList: string[], isLoading: boolean, ) => void, ): Promise<{ relayList: string[]; + readRelayList: string[]; + writeRelayList: string[]; }> => { return new Promise(async function (resolve, reject) { const wot = getLocalStorageData().wot; @@ -296,6 +300,10 @@ export const fetchAllFollowsAndRelays = async ( let firstDegreeFollowsLength = 0; let relayList: string[] = []; const relaySet: Set = new Set(); + let readRelayList: string[] = []; + const readRelaySet: Set = new Set(); + let writeRelayList: string[] = []; + const writeRelaySet: Set = new Set(); const firstFollowfilter: Filter = { kinds: [3, 10002], @@ -306,11 +314,25 @@ export const fetchAllFollowsAndRelays = async ( onevent(event) { if (event.kind === 10002) { const validRelays = event.tags.filter( - (tag) => tag[0] === "r" && (tag[2] === "write" || !tag[2]), + (tag) => tag[0] === "r" && !tag[2], + ); + + const validReadRelays = event.tags.filter( + (tag) => tag[0] === "r" && tag[2] === "read", + ); + + const validWriteRelays = event.tags.filter( + (tag) => tag[0] === "r" && tag[2] === "write", ); validRelays.forEach((tag) => relaySet.add(tag[1])); relayList.push(...validRelays.map((tag) => tag[1])); + + validReadRelays.forEach((tag) => readRelaySet.add(tag[1])); + readRelayList.push(...validReadRelays.map((tag) => tag[1])); + + validWriteRelays.forEach((tag) => writeRelaySet.add(tag[1])); + writeRelayList.push(...validWriteRelays.map((tag) => tag[1])); } const validTags = event.tags .map((tag) => tag[1]) @@ -424,11 +446,15 @@ export const fetchAllFollowsAndRelays = async ( } resolve({ relayList: relayList, + readRelayList: readRelayList, + writeRelayList: writeRelayList, }); editFollowsContext( followsArrayFromRelay, firstDegreeFollowsLength, relayList, + readRelayList, + writeRelayList, false, ); }; diff --git a/pages/sign-in/index.tsx b/pages/sign-in/index.tsx index 1d1ee72..da0d9d5 100644 --- a/pages/sign-in/index.tsx +++ b/pages/sign-in/index.tsx @@ -36,12 +36,22 @@ const LoginPage = ({ router }: { router: NextRouter }) => { if ( !followsAndRelaysContext.isLoading && - followsAndRelaysContext.relayList.length >= 0 + followsAndRelaysContext.relayList.length != 0 && + followsAndRelaysContext.readRelayList && + followsAndRelaysContext.writeRelayList ) { localStorage.setItem( "relays", JSON.stringify(followsAndRelaysContext.relayList), ); + localStorage.setItem( + "readRelays", + JSON.stringify(followsAndRelaysContext.readRelayList), + ); + localStorage.setItem( + "writeRelays", + JSON.stringify(followsAndRelaysContext.writeRelayList), + ); } else { localStorage.setItem( "relays", @@ -82,12 +92,22 @@ const LoginPage = ({ router }: { router: NextRouter }) => { localStorage.setItem("signIn", "extension"); if ( !followsAndRelaysContext.isLoading && - followsAndRelaysContext.relayList.length >= 0 + followsAndRelaysContext.relayList.length != 0 && + followsAndRelaysContext.readRelayList && + followsAndRelaysContext.writeRelayList ) { localStorage.setItem( "relays", JSON.stringify(followsAndRelaysContext.relayList), ); + localStorage.setItem( + "readRelays", + JSON.stringify(followsAndRelaysContext.readRelayList), + ); + localStorage.setItem( + "writeRelays", + JSON.stringify(followsAndRelaysContext.writeRelayList), + ); } else { localStorage.setItem( "relays", diff --git a/utils/context/context.ts b/utils/context/context.ts index eb27a9f..acb45e0 100644 --- a/utils/context/context.ts +++ b/utils/context/context.ts @@ -42,6 +42,8 @@ export interface FollowsAndRelaysContextInterface { followList: string[]; firstDegreeFollowsLength: number; relayList: string[]; + readRelayList: string[]; + writeRelayList: string[]; isLoading: boolean; } @@ -49,5 +51,7 @@ export const FollowsAndRelaysContext = createContext({ followList: [], firstDegreeFollowsLength: 0, relayList: [], + readRelayList: [], + writeRelayList: [], isLoading: true, } as FollowsAndRelaysContextInterface); From f3dce248bca4dfd4bde15ed0ba52b754d0a831ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?calvadev=E2=9A=A1=EF=B8=8F?= <32919103+calvadev@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:09:41 +0000 Subject: [PATCH 04/12] Added inbox rendering support --- pages/_app.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index 1fb06af..a978423 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -149,6 +149,8 @@ function App({ Component, pageProps }: AppProps) { useEffect(() => { async function fetchData() { const relays = getLocalStorageData().relays; + const readRelays = getLocalStorageData().readRelays; + const allRelays = [...relays, ...readRelays]; const userPubkey = getLocalStorageData().userPubkey; try { let { relayList, readRelayList, writeRelayList } = @@ -160,12 +162,12 @@ function App({ Component, pageProps }: AppProps) { } let pubkeysToFetchProfilesFor: string[] = []; let { profileSetFromProducts } = await fetchAllPosts( - relays, + allRelays, editProductContext, ); pubkeysToFetchProfilesFor = [...profileSetFromProducts]; let { profileSetFromChats } = await fetchChatsAndMessages( - relays, + allRelays, userPubkey, editChatContext, ); @@ -175,7 +177,7 @@ function App({ Component, pageProps }: AppProps) { ...profileSetFromChats, ]; let { profileMap } = await fetchProfile( - relays, + allRelays, pubkeysToFetchProfilesFor, editProfileContext, ); From 16e66c1f330680a5f6c34a0f14b5f540fd1d8faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?calvadev=E2=9A=A1=EF=B8=8F?= <32919103+calvadev@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:10:38 +0000 Subject: [PATCH 05/12] Added publishing of relay list metadata --- pages/api/nostr/crud-service.ts | 18 - pages/settings/preferences.tsx | 975 ++++++++++++++++---------------- 2 files changed, 496 insertions(+), 497 deletions(-) diff --git a/pages/api/nostr/crud-service.ts b/pages/api/nostr/crud-service.ts index dcfef41..ee63eb6 100644 --- a/pages/api/nostr/crud-service.ts +++ b/pages/api/nostr/crud-service.ts @@ -66,8 +66,6 @@ export async function createNostrProfileEvent( export async function createNostrRelayEvent( pubkey: string, - relays: string[], - type: string, passphrase: string, ) { const relayList = getLocalStorageData().relays; @@ -96,22 +94,6 @@ export async function createNostrRelayEvent( relayTags.push(relayTag); } } - if (type === "read") { - for (const relay of relays) { - const newTag = ["r", relay, "read"]; - relayTags.push(newTag); - } - } else if (type === "write") { - for (const relay of relays) { - const newTag = ["r", relay, "write"]; - relayTags.push(newTag); - } - } else { - for (const relay of relays) { - const newTag = ["r", relay]; - relayTags.push(newTag); - } - } let relayEvent = { kind: 10002, // NIP-65 - Relay List Metadata content: "", diff --git a/pages/settings/preferences.tsx b/pages/settings/preferences.tsx index 93c1536..3228848 100644 --- a/pages/settings/preferences.tsx +++ b/pages/settings/preferences.tsx @@ -20,19 +20,22 @@ import { } from "@nextui-org/react"; import { relayConnect } from "nostr-tools"; import { SHOPSTRBUTTONCLASSNAMES } from "../../components/utility/STATIC-VARIABLES"; -import { getLocalStorageData } from "../../components/utility/nostr-helper-functions"; +import { getLocalStorageData, validPassphrase } from "../../components/utility/nostr-helper-functions"; import { createNostrRelayEvent } from "../api/nostr/crud-service"; import { useTheme } from "next-themes"; import { SettingsBreadCrumbs } from "@/components/settings/settings-bread-crumbs"; import ShopstrSlider from "../../components/utility-components/shopstr-slider"; +import RequestPassphraseModal from "@/components/utility-components/request-passphrase-modal"; const PreferencesPage = () => { + const [enterPassphrase, setEnterPassphrase] = useState(false); + const [passphrase, setPassphrase] = useState(""); + const [relays, setRelays] = useState(Array(0)); const [readRelays, setReadRelays] = useState(Array(0)); const [writeRelays, setWriteRelays] = useState(Array(0)); const [showRelayModal, setShowRelayModal] = useState(false); const [relaysAreChanged, setRelaysAreChanged] = useState(false); - const [relayType, setRelayType] = useState(""); const [mints, setMints] = useState(Array(0)); const [showMintModal, setShowMintModal] = useState(false); @@ -42,8 +45,12 @@ const PreferencesPage = () => { const [pubkey, setPubkey] = useState(""); + const { signInMethod } = getLocalStorageData(); + useEffect(() => { - if (typeof window !== "undefined") { + if (signInMethod === "nsec" && !validPassphrase(passphrase)) { + setEnterPassphrase(true); // prompt for passphrase when chatsContext is loaded + } else if (typeof window !== "undefined") { setMints(getLocalStorageData().mints); setRelays(getLocalStorageData().relays); setReadRelays(getLocalStorageData().readRelays); @@ -51,7 +58,7 @@ const PreferencesPage = () => { setPubkey(getLocalStorageData().userPubkey); } setIsLoaded(true); - }, []); + }, [signInMethod, passphrase]); useEffect(() => { if (mints.length != 0) { @@ -153,77 +160,207 @@ const PreferencesPage = () => { setRelaysAreChanged(true); }; - const publishRelays = (relays: string[], type: string) { - createNostrRelayEvent(pubkey, relays, type, ""); // passphrase required in final param + const publishRelays = () => { + createNostrRelayEvent(pubkey, passphrase); + setRelaysAreChanged(false); } return ( -
-
- - - Mint - - -
- {mints.length === 0 ? ( -
-

- No mint added . . . -

-
- ) : ( -
-
-
- {mints[0]} + <> +
+
+ + + Mint + + +
+ {mints.length === 0 ? ( +
+

+ No mint added . . . +

+
+ ) : ( +
+
+
+ {mints[0]} +
+ +
- -
+ )} + {mints.length > 0 && ( +
+ +

+ This mint is used to handle{" "} + + + Cashu + + {" "} + tokens within your wallet and to send to the seller upon + purchase. +

+
+ )} + +
+
- )} - {mints.length > 0 && ( -
- -

- This mint is used to handle{" "} - - - Cashu - - {" "} - tokens within your wallet and to send to the seller upon - purchase. + + + + Change Mint + +

+ + + /^(https:\/\/|http:\/\/)/.test(value) || + "Invalid mint URL, must start with https:// or http://.", + }} + render={({ + field: { onChange, onBlur, value }, + fieldState: { error }, + }) => { + let isErrored = error !== undefined; + let errorMessage: string = error?.message + ? error.message + : ""; + return ( +