From 16c828adace0c421ae9682db12a94d0be4f8b9fb Mon Sep 17 00:00:00 2001 From: scobru Date: Mon, 4 Dec 2023 00:39:10 +0100 Subject: [PATCH] brand new client --- packages/nextjs/components/Header.tsx | 16 +- .../nextjs/components/client/authCard.tsx | 97 +++++ .../components/client/createPostCard.tsx | 84 ++++ .../components/client/createdAccModal.tsx | 73 ++++ .../components/client/displayEventCard.tsx | 291 +++++++++++++ .../nextjs/components/client/eventLoader.tsx | 12 + .../components/client/relayCtrlCard.tsx | 120 ++++++ .../components/client/repostEventCard.tsx | 255 ++++++++++++ .../components/client/repostedEventCard.tsx | 59 +++ packages/nextjs/declaration.d.ts | 1 + packages/nextjs/hooks/scaffold-eth/index.ts | 1 + .../nextjs/hooks/scaffold-eth/useProfile.ts | 160 +++++++ packages/nextjs/nostr-tools.d.ts | 391 ++++++++++++++++++ packages/nextjs/package.json | 13 +- packages/nextjs/pages/client.tsx | 168 ++++++++ packages/nextjs/pages/index.tsx | 14 +- .../nextjs/patches/nostr-react+0.7.0.patch | 13 + packages/nextjs/services/store/store.ts | 4 + packages/nextjs/styles/globals.css | 42 ++ packages/nextjs/utils/constants.ts | 7 + packages/nextjs/utils/parsing.ts | 33 ++ packages/nextjs/utils/tailwind.ts | 3 + yarn.lock | 379 ++++++++++++++++- 23 files changed, 2214 insertions(+), 22 deletions(-) create mode 100644 packages/nextjs/components/client/authCard.tsx create mode 100644 packages/nextjs/components/client/createPostCard.tsx create mode 100644 packages/nextjs/components/client/createdAccModal.tsx create mode 100644 packages/nextjs/components/client/displayEventCard.tsx create mode 100644 packages/nextjs/components/client/eventLoader.tsx create mode 100644 packages/nextjs/components/client/relayCtrlCard.tsx create mode 100644 packages/nextjs/components/client/repostEventCard.tsx create mode 100644 packages/nextjs/components/client/repostedEventCard.tsx create mode 100644 packages/nextjs/hooks/scaffold-eth/useProfile.ts create mode 100644 packages/nextjs/nostr-tools.d.ts create mode 100644 packages/nextjs/pages/client.tsx create mode 100644 packages/nextjs/patches/nostr-react+0.7.0.patch create mode 100644 packages/nextjs/utils/constants.ts create mode 100644 packages/nextjs/utils/parsing.ts create mode 100644 packages/nextjs/utils/tailwind.ts diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 3c300b8..c1c7ff0 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -1,26 +1,32 @@ import React, { useCallback, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { Bars3Icon } from "@heroicons/react/24/outline"; +import { ArchiveBoxIcon, Bars3Icon, BugAntIcon, HomeIcon } from "@heroicons/react/24/outline"; import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; import { useOutsideClick } from "~~/hooks/scaffold-eth"; + interface HeaderMenuLink { label: string; href: string; icon?: React.ReactNode; } export const menuLinks: HeaderMenuLink[] = [ - /*{ + { label: "Home", href: "/", icon: , }, - { + { + label: "Client", + href: "/client", + icon: , + }, + { label: "Debug Contracts", href: "/debug", icon: , - }, */ + }, ]; export const HeaderMenuLinks = () => { @@ -104,4 +110,4 @@ export const Header = () => { ); -}; +}; \ No newline at end of file diff --git a/packages/nextjs/components/client/authCard.tsx b/packages/nextjs/components/client/authCard.tsx new file mode 100644 index 0000000..49d3624 --- /dev/null +++ b/packages/nextjs/components/client/authCard.tsx @@ -0,0 +1,97 @@ +import { Dispatch, SetStateAction, useRef } from "react"; +import { UnsignedEvent, generatePrivateKey, getPublicKey } from "nostr-tools"; +import { GiSkeletonKey } from "react-icons/gi"; +import { HiPencilSquare, HiUserCircle } from "react-icons/hi2"; + +interface AuthCardProps { + setSk: Dispatch>; + publishEvent: (event: UnsignedEvent, sk?: string) => void; + setShowKeysModal: Dispatch>; + privateKey: string | null; +} + +export default function AuthCard(props: AuthCardProps) { + const skField = useRef(null); + const displayNameField = useRef(null); + const profilePicField = useRef(null); + + if (props.privateKey) props.setSk(props.privateKey); + + return ( +
+
+
+
+
+ +
+ +
+
+ + +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ + +
+
+ ); +} diff --git a/packages/nextjs/components/client/createPostCard.tsx b/packages/nextjs/components/client/createPostCard.tsx new file mode 100644 index 0000000..02f3396 --- /dev/null +++ b/packages/nextjs/components/client/createPostCard.tsx @@ -0,0 +1,84 @@ +import { useRef } from "react"; +import { useProfile } from "../../hooks/scaffold-eth/useProfile"; +import { Filter, Relay, UnsignedEvent } from "nostr-tools"; +import { LazyLoadImage } from "react-lazy-load-image-component"; + +interface CreatePostCardProps { + posterPK: string; + posterSK: string; + relay: Relay; + publishEvent: (event: UnsignedEvent) => void; + getEvents: (filters: Filter[]) => void; + setEthTipAmount: (amount: string) => void; +} + +export default function CreatePostCard(props: CreatePostCardProps) { + const { data: userData } = useProfile({ pubkey: props.posterPK, relay: props.relay }); + + const textArea = useRef(null); + + return ( +
+
{ + const filter: Filter[] = [ + { + kinds: [1, 6], + authors: [props.posterPK], + }, + ]; + props.getEvents(filter); + }} + > +
+ + {userData?.name} + + + {props.posterPK?.slice(props.posterPK?.length - 6)} + +
+
+
+
+ + +
+
+ Tip with ETH + { + props.setEthTipAmount(e.target.value); + }} + /> +
+
+
+ ); +} diff --git a/packages/nextjs/components/client/createdAccModal.tsx b/packages/nextjs/components/client/createdAccModal.tsx new file mode 100644 index 0000000..4c91e3d --- /dev/null +++ b/packages/nextjs/components/client/createdAccModal.tsx @@ -0,0 +1,73 @@ +import { Dispatch, SetStateAction } from "react"; +import { Notyf } from "notyf"; +import "notyf/notyf.min.css"; +import { FaExclamationCircle } from "react-icons/fa"; + +interface CreatedAccModalProps { + sk: string; + pk: string; + setShowKeysModal: Dispatch>; +} + +export default function CreatedAccModal(props: CreatedAccModalProps) { + const notyf = new Notyf({ + duration: 1000, + position: { + x: "center", + y: "center", + }, + }); + + return ( +
+
+
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+ +
+
+
+
+
+ ); +} diff --git a/packages/nextjs/components/client/displayEventCard.tsx b/packages/nextjs/components/client/displayEventCard.tsx new file mode 100644 index 0000000..5ed826f --- /dev/null +++ b/packages/nextjs/components/client/displayEventCard.tsx @@ -0,0 +1,291 @@ +/* eslint-disable prefer-const */ +import { useEffect, useState } from "react"; +import { useProfile } from "../../hooks/scaffold-eth"; +import { extractImageLinks } from "../../utils/parsing"; +import RepostEventCard from "./repostEventCard"; +import { type Event, Filter, Relay, UnsignedEvent } from "nostr-tools"; +import { BsChatRightQuote, BsCurrencyDollar } from "react-icons/bs"; +import { FaRetweet } from "react-icons/fa"; +import { FcDislike, FcLike } from "react-icons/fc"; +import { LazyLoadImage } from "react-lazy-load-image-component"; +import { parseEther } from "viem"; +import { notification } from "~~/utils/scaffold-eth"; + +interface DisplayEventCardProps { + pk: string | null; + event: Event; + showEvent: boolean; + relay: Relay | null; + signer: any; + ethTipAmount: string; + getEvents: (filters: Filter[]) => void; + getEvent: (filter: Filter) => Promise; + publishEvent: (event: UnsignedEvent, _sk?: string) => void; +} + +interface ReactionStats { + nLikes: number; + nDislikes: number; + userLiked: boolean; + userDisliked: boolean; + userReposted: boolean; +} + +export default function DisplayEventCard(props: DisplayEventCardProps) { + function getReactionStats(reactions: Event[]): ReactionStats { + const stats: ReactionStats = { + nLikes: 0, + nDislikes: 0, + userLiked: false, + userDisliked: false, + userReposted: false, + }; + for (let i = 0; i < reactions.length; i++) { + const { content, pubkey, kind } = reactions[i]; + if (["+", "🤙", "👍", "🤍"].includes(content)) { + stats.nLikes++; + if (pubkey === props.pk) stats.userLiked = true; + } else if (["-", "👎"].includes(content)) { + stats.nDislikes++; + if (pubkey === props.pk) stats.userDisliked = true; + } else if (kind == 6) stats.userReposted = true; + } + + return stats; + } + + const [selectedRelay, setSelectedRelay] = useState(props.relay as Relay); + + useEffect(() => { + if (!props.relay) return; + setSelectedRelay(props.relay); + }, [props.relay]); + + const { data: userData } = useProfile({ pubkey: props.event.pubkey, relay: selectedRelay }); + + /* const reactionEvents = useNostrEvents({ + filter: { + "#e": [props.event.id], + kinds: [6, 7], + }, + }).events; */ + + const [reactionEvents, setReactionEvents] = useState([]); + + useEffect(() => { + const fetchReactionEvents = async () => { + const events = await selectedRelay.list([ + { + "#e": [props.event.id], + kinds: [6, 7], + }, + ]); + setReactionEvents(events); + }; + fetchReactionEvents(); + }, [selectedRelay, props.event.id]); + + const { txtContent, imgLinks } = extractImageLinks(props.event.content); + + let { nLikes, nDislikes, userLiked, userDisliked, userReposted }: ReactionStats = getReactionStats(reactionEvents); + + // If event is a simple repost + if (props.event.kind == 6) { + return ( + + ); + } + + // If event is refrencing another event + if (props.event.tags[0] && props.event.tags[0][0] == "e") { + return ( + + ); + } + + return ( + + ); +} diff --git a/packages/nextjs/components/client/eventLoader.tsx b/packages/nextjs/components/client/eventLoader.tsx new file mode 100644 index 0000000..b57b811 --- /dev/null +++ b/packages/nextjs/components/client/eventLoader.tsx @@ -0,0 +1,12 @@ +import { SpinnerDiamond } from "spinners-react"; + +export default function EventLoader() { + return ( +
+
+ Loading Events... + +
+
+ ); +} diff --git a/packages/nextjs/components/client/relayCtrlCard.tsx b/packages/nextjs/components/client/relayCtrlCard.tsx new file mode 100644 index 0000000..f253f35 --- /dev/null +++ b/packages/nextjs/components/client/relayCtrlCard.tsx @@ -0,0 +1,120 @@ +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { RELAYS } from "../../utils/constants"; +import classNames from "../../utils/tailwind"; +import { RadioGroup } from "@headlessui/react"; + +interface RelayCtrlCardProps { + relays: string[]; + curRelayName: string; + setRelay: Dispatch>; +} + +export default function RelayCtrlCard(props: RelayCtrlCardProps) { + const [selectedRelay, setSelectedRelay] = useState(RELAYS[0]); + const customRelayUrl = useRef(null); + + useEffect(() => { + props.setRelay(selectedRelay); + }, [selectedRelay]); + + return ( +
+
+
+ + Current Relay:{" "} + + + + {props.curRelayName} + +
+
+
+
+
+
+ + +
+ {RELAYS.map((relay, i) => ( + + classNames( + relay == selectedRelay ? "border-transparent" : "border-gray-300", + "relative block cursor-pointer rounded-lg border bg-white/50 px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between hover:bg-base-300/75", + ) + } + > + {({ active, checked }) => ( + <> + + + + {relay} + + + + + + )} + + ))} +
+
+
+
+ +
+ + wss:// + + +
+ +
+
+
+
+
+ ); +} diff --git a/packages/nextjs/components/client/repostEventCard.tsx b/packages/nextjs/components/client/repostEventCard.tsx new file mode 100644 index 0000000..511ace6 --- /dev/null +++ b/packages/nextjs/components/client/repostEventCard.tsx @@ -0,0 +1,255 @@ +import { useEffect, useState } from "react"; +import { useProfile } from "../../hooks/scaffold-eth"; +import { extractImageLinks } from "../../utils/parsing"; +import RepostedEventCard from "./repostedEventCard"; +import { useNostrEvents } from "nostr-react"; +import { type Event, Filter, Relay, UnsignedEvent } from "nostr-tools"; +import { BsChatRightQuote } from "react-icons/bs"; +import { FaRetweet } from "react-icons/fa"; +import { FcDislike, FcLike } from "react-icons/fc"; + +interface RepostEventCardProps { + pk: string | null; + event: Event; + showEvent: boolean; + relay: Relay | null; + getEvents: (filters: Filter[]) => void; + getEvent: (filter: Filter) => Promise; + publishEvent: (event: UnsignedEvent, _sk?: string) => void; + isQuotedRepost: boolean; +} + +interface ReactionStats { + nLikes: number; + nDislikes: number; + userLiked: boolean; + userDisliked: boolean; + userReposted: boolean; +} + +export default function RepostEventCard(props: RepostEventCardProps) { + function getReactionStats(reactions: Event[]): ReactionStats { + const stats: ReactionStats = { + nLikes: 0, + nDislikes: 0, + userLiked: false, + userDisliked: false, + userReposted: false, + }; + for (let i = 0; i < reactions.length; i++) { + const { content, pubkey, kind } = reactions[i]; + if (["+", "🤙", "👍"].includes(content)) { + stats.nLikes++; + if (pubkey === props.pk) stats.userLiked = true; + } else if (["-", "👎"].includes(content)) { + stats.nDislikes++; + if (pubkey === props.pk) stats.userDisliked = true; + } else if (kind == 6) stats.userReposted = true; + } + + return stats; + } + + const [relevantEvent, setRelevantEvent] = useState(null); + const { txtContent, imgLinks } = extractImageLinks(props.event.content); + + const { data: userData } = useProfile({ pubkey: props.event.pubkey, relay: props.relay as Relay }); + const reactionEvents = useNostrEvents({ + filter: { + "#e": [props.event.id], + kinds: [6, 7], + }, + }).events; + + // eslint-disable-next-line prefer-const + let { nLikes, nDislikes, userLiked, userDisliked, userReposted }: ReactionStats = getReactionStats(reactionEvents); + + useEffect(() => { + if (props.isQuotedRepost) { + props + .getEvent({ + ids: [props.event.tags[0][1]], + }) + .then(event => { + if (event) setRelevantEvent(event); + }); + } else { + setRelevantEvent(JSON.parse(props.event.content)); + } + }, [props.isQuotedRepost]); + + return ( + + ); +} diff --git a/packages/nextjs/components/client/repostedEventCard.tsx b/packages/nextjs/components/client/repostedEventCard.tsx new file mode 100644 index 0000000..f99f2ea --- /dev/null +++ b/packages/nextjs/components/client/repostedEventCard.tsx @@ -0,0 +1,59 @@ +import { useProfile } from "../../hooks/scaffold-eth"; +import { extractImageLinks } from "../../utils/parsing"; +import { type Event, Filter, Relay, UnsignedEvent } from "nostr-tools"; + +interface RepostedEventCardProps { + pk: string | null; + event: Event; + showEvent: boolean; + relay: Relay | null; + getEvents: (filters: Filter[]) => void; + getEvent: (filter: Filter) => Promise; + publishEvent: (event: UnsignedEvent, _sk?: string) => void; +} + +export default function RepostedEventCard(props: RepostedEventCardProps) { + const { data: userData } = useProfile({ pubkey: props.event.pubkey, relay: props.relay as Relay }); + const { txtContent, imgLinks } = extractImageLinks(props.event.content); + + return ( + + ); +} diff --git a/packages/nextjs/declaration.d.ts b/packages/nextjs/declaration.d.ts index d2d428a..a2e56cc 100644 --- a/packages/nextjs/declaration.d.ts +++ b/packages/nextjs/declaration.d.ts @@ -1,3 +1,4 @@ declare module "@scobru/crypto-ipfs"; declare module "@scobru/nostr3/dist/nostr3"; declare module "@fs-extra"; + diff --git a/packages/nextjs/hooks/scaffold-eth/index.ts b/packages/nextjs/hooks/scaffold-eth/index.ts index 10862ff..2b29a9f 100644 --- a/packages/nextjs/hooks/scaffold-eth/index.ts +++ b/packages/nextjs/hooks/scaffold-eth/index.ts @@ -14,3 +14,4 @@ export * from "./useTransactor"; export * from "./useFetchBlocks"; export * from "./useContractLogs"; export * from "./useAutoConnect"; +export * from "./useProfile"; diff --git a/packages/nextjs/hooks/scaffold-eth/useProfile.ts b/packages/nextjs/hooks/scaffold-eth/useProfile.ts new file mode 100644 index 0000000..05ccd81 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useProfile.ts @@ -0,0 +1,160 @@ +import { useEffect, useState } from "react"; +import { atom, useAtom } from "jotai"; +import { Relay } from "nostr-tools"; +import nip19 from "nostr-tools"; + +const uniqValues = (value: string, index: number, self: string[]) => { + return self.indexOf(value) === index; +}; + +export interface Metadata { + name?: string; + username?: string; + display_name?: string; + picture?: string; + banner?: string; + about?: string; + website?: string; + lud06?: string; + lud16?: string; + nip05?: string; +} + +const QUEUE_DEBOUNCE_DURATION = 1000; + +let timer: NodeJS.Timeout | undefined = undefined; + +const queuedPubkeysAtom = atom([]); +const requestedPubkeysAtom = atom([]); +const fetchedProfilesAtom = atom>({}); + +function useProfileQueue({ pubkey }: { pubkey: string }) { + const [isReadyToFetch, setIsReadyToFetch] = useState(false); + + const [queuedPubkeys, setQueuedPubkeys] = useAtom(queuedPubkeysAtom); + + const [requestedPubkeys] = useAtom(requestedPubkeysAtom); + const alreadyRequested = !!requestedPubkeys.includes(pubkey); + + useEffect(() => { + if (alreadyRequested) { + return; + } + + clearTimeout(timer); + + timer = setTimeout(() => { + setIsReadyToFetch(true); + }, QUEUE_DEBOUNCE_DURATION); + + setQueuedPubkeys((_pubkeys: string[]) => { + // Unique values only: + const arr = [..._pubkeys, pubkey].filter(uniqValues).filter(_pubkey => { + return !requestedPubkeys.includes(_pubkey); + }); + + return arr; + }); + + return () => clearTimeout(timer); + }, [pubkey, setQueuedPubkeys, alreadyRequested, requestedPubkeys]); + + return { + pubkeysToFetch: isReadyToFetch ? queuedPubkeys : [], + }; +} + +export function useProfile({ + pubkey, + relay, + enabled: _enabled = true, +}: { + pubkey: string; + relay: Relay; + enabled?: boolean; +}) { + const [, setRequestedPubkeys] = useAtom(requestedPubkeysAtom); + const { pubkeysToFetch } = useProfileQueue({ pubkey }); + const enabled = _enabled && !!pubkeysToFetch.length; + const [, setMetadata] = useState(undefined); + const [npub, setNpub] = useState(""); + + const [fetchedProfiles, setFetchedProfiles] = useAtom(fetchedProfilesAtom); + + /* const { onEvent, onSubscribe, isLoading, onDone } = useNostrEvents({ + filter: { + kinds: [0], + authors: pubkeysToFetch, + }, + enabled: true, + }); + + + + onEvent(rawMetadata => { + console.log("onEvent", rawMetadata); + try { + const metadata: Metadata = JSON.parse(rawMetadata.content); + const metaPubkey = rawMetadata.pubkey; + console.log("onEvent", metaPubkey, metadata); + + if (metadata) { + setFetchedProfiles((_profiles: Record) => { + return { + ..._profiles, + [metaPubkey]: metadata, + }; + }); + } + } catch (err) { + console.error(err, rawMetadata); + } + }); */ + + const loadProfile = async (pubkey: string) => { + try { + const result = await relay.list([{ kinds: [0], authors: [pubkey] }]); + const parsedResult = JSON.parse(result[0].content); + if (result && result[0] && result[0].content) { + return parsedResult; + } else { + console.warn("Nessun profilo trovato o dati non validi per la chiave:", pubkey); + return null; // Restituisci null se non ci sono dati validi + } + } catch (error) { + console.error("Errore nel caricamento del profilo:", error); + return null; // Gestisci l'errore restituendo null o un valore di default + } + }; + + useEffect(() => { + setRequestedPubkeys(_pubkeys => { + return [..._pubkeys, ...pubkeysToFetch].filter(uniqValues); + }); + const run = async () => { + for (const pubkey of pubkeysToFetch) { + await loadProfile(pubkey).then(data => { + setFetchedProfiles(prev => ({ + ...prev, + [pubkey]: data, + })); + }); + } + + setNpub(nip19.npubEncode(pubkey)); + setMetadata(fetchedProfiles[pubkey]); + }; + if (enabled) { + run(); + } + }, [pubkey, enabled]); + + return { + data: fetchedProfiles[pubkey] + ? { + ...fetchedProfiles[pubkey], + npub, + } + : undefined, + }; +} diff --git a/packages/nextjs/nostr-tools.d.ts b/packages/nextjs/nostr-tools.d.ts new file mode 100644 index 0000000..f00e4fa --- /dev/null +++ b/packages/nextjs/nostr-tools.d.ts @@ -0,0 +1,391 @@ +declare module "nostr-tools" { + // event + export declare enum Kind { + Metadata = 0, + Text = 1, + RecommendRelay = 2, + Contacts = 3, + EncryptedDirectMessage = 4, + EventDeletion = 5, + Repost = 6, + Reaction = 7, + BadgeAward = 8, + ChannelCreation = 40, + ChannelMetadata = 41, + ChannelMessage = 42, + ChannelHideMessage = 43, + ChannelMuteUser = 44, + Report = 1984, + ZapRequest = 9734, + Zap = 9735, + RelayList = 10002, + ClientAuth = 22242, + BadgeDefinition = 30008, + ProfileBadge = 30009, + Article = 30023, + Custom = 30078, + } + export type EventTemplate = { + kind: Kind; + tags: string[][]; + content: string; + created_at: number; + }; + export type UnsignedEvent = EventTemplate & { + pubkey: string; + }; + export type Event = UnsignedEvent & { + id: string; + sig: string; + }; + export type QuotedEvent = Event & { + quotedEvent: Event; + }; + export declare function getBlankEvent(): EventTemplate; + export declare function finishEvent(t: EventTemplate, privateKey: string): Event; + export declare function serializeEvent(evt: UnsignedEvent): string; + export declare function getEventHash(event: UnsignedEvent): string; + export declare function validateEvent(event: T): event is T & UnsignedEvent; + export declare function verifySignature(event: Event): boolean; + export declare function signEvent(event: UnsignedEvent, key: string): string; + + // Fake JSON + export declare function getHex64(json: string, field: string): string; + export declare function getInt(json: string, field: string): number; + export declare function getSubscriptionId(json: string): string | null; + export declare function matchEventId(json: string, id: string): boolean; + export declare function matchEventPubkey(json: string, pubkey: string): boolean; + export declare function matchEventKind(json: string, kind: number): boolean; + + // Filter + export type Filter = { + ids?: string[]; + kinds?: number[]; + authors?: string[]; + since?: number; + until?: number; + limit?: number; + search?: string; + [key: `#${string}`]: string[]; + }; + export declare function matchFilter(filter: Filter, event: Event): boolean; + export declare function matchFilters(filters: Filter[], event: Event): boolean; + + //Keys + export declare function generatePrivateKey(): string; + export declare function getPublicKey(privateKey: string): string; + + // Nip 04 + export declare function encrypt(privkey: string, pubkey: string, text: string): Promise; + export declare function decrypt(privkey: string, pubkey: string, data: string): Promise; + + // Nip 19 + export type ProfilePointer = { + pubkey: string; + relays?: string[]; + }; + export type EventPointer = { + id: string; + relays?: string[]; + author?: string; + }; + export type AddressPointer = { + identifier: string; + pubkey: string; + kind: number; + relays?: string[]; + }; + export type DecodeResult = + | { + type: "nprofile"; + data: ProfilePointer; + } + | { + type: "nrelay"; + data: string; + } + | { + type: "nevent"; + data: EventPointer; + } + | { + type: "naddr"; + data: AddressPointer; + } + | { + type: "nsec"; + data: string; + } + | { + type: "npub"; + data: string; + } + | { + type: "note"; + data: string; + }; + export declare function decode(nip19: string): DecodeResult; + export declare function nsecEncode(hex: string): string; + export declare function npubEncode(hex: string): string; + export declare function noteEncode(hex: string): string; + export declare function nprofileEncode(profile: ProfilePointer): string; + export declare function neventEncode(event: EventPointer): string; + export declare function naddrEncode(addr: AddressPointer): string; + export declare function nrelayEncode(url: string): string; + + // Nip 05 + export declare function useFetchImplementation(fetchImplementation: any): void; + export declare function searchDomain( + domain: string, + query?: string, + ): Promise<{ + [name: string]: string; + }>; + export declare function queryProfile(fullname: string): Promise; + + // Nip 06 + export declare function privateKeyFromSeedWords(mnemonic: string, passphrase?: string): string; + export declare function generateSeedWords(): string; + export declare function validateWords(words: string): boolean; + + // Nip 10 + export type NIP10Result = { + /** + * Pointer to the root of the thread. + */ + root: EventPointer | undefined; + /** + * Pointer to a "parent" event that parsed event replies to (responded to). + */ + reply: EventPointer | undefined; + /** + * Pointers to events which may or may not be in the reply chain. + */ + mentions: EventPointer[]; + /** + * List of pubkeys that are involved in the thread in no particular order. + */ + profiles: ProfilePointer[]; + }; + export declare function parse(event: Pick): NIP10Result; + + // Nip 13 + // /** Get POW difficulty from a Nostr hex ID. */ + export declare function getPow(id: string): number; + + // Nip 21 + /** + * Bech32 regex. + * @see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 + */ + export declare const BECH32_REGEX: RegExp; + /** Nostr URI regex, eg `nostr:npub1...` */ + export declare const NOSTR_URI_REGEX: RegExp; + /** Test whether the value is a Nostr URI. */ + export declare function test(value: unknown): value is `nostr:${string}`; + /** Parsed Nostr URI data. */ + export interface NostrURI { + /** Full URI including the `nostr:` protocol. */ + uri: `nostr:${string}`; + /** The bech32-encoded data (eg `npub1...`). */ + value: string; + /** Decoded bech32 string, according to NIP-19. */ + decoded: nip19.DecodeResult; + } + /** Parse and decode a Nostr URI. */ + // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures + export declare function parse(uri: string): NostrURI; + + // Nip 26 + export type Parameters = { + pubkey: string; + kind: number | undefined; + until: number | undefined; + since: number | undefined; + }; + export type Delegation = { + from: string; + to: string; + cond: string; + sig: string; + }; + export declare function createDelegation(privateKey: string, parameters: Parameters): Delegation; + export declare function getDelegator(event: Event): string | null; + + // Nip 27 + /** Regex to find NIP-21 URIs inside event content. */ + export declare const regex: () => RegExp; + /** Match result for a Nostr URI in event content. */ + export interface NostrURIMatch extends nip21.NostrURI { + /** Index where the URI begins in the event content. */ + start: number; + /** Index where the URI ends in the event content. */ + end: number; + } + /** Find and decode all NIP-21 URIs. */ + export declare function matchAll(content: string): Iterable; + /** + * Replace all occurrences of Nostr URIs in the text. + * + * WARNING: using this on an HTML string is potentially unsafe! + * + * @example + * ```ts + * nip27.replaceAll(event.content, ({ decoded, value }) => { + * switch(decoded.type) { + * case 'npub': + * return renderMention(decoded) + * case 'note': + * return renderNote(decoded) + * default: + * return value + * } + * }) + * ``` + */ + export declare function replaceAll(content: string, replacer: (match: nip21.NostrURI) => string): string; + + // Nip 39 + // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures + export declare function useFetchImplementation(fetchImplementation: any): void; + export declare function validateGithub(pubkey: string, username: string, proof: string): Promise; + + // Nip 42 + /** + * Authenticate via NIP-42 flow. + * + * @example + * const sign = window.nostr.signEvent + * relay.on('auth', challenge => + * authenticate({ relay, sign, challenge }) + * ) + */ + export declare const authenticate: ({ + challenge, + relay, + sign, + }: { + challenge: string; + relay: Relay; + sign: (e: EventTemplate) => Promise; + }) => Promise; + + // Nip 57 + // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures + export declare function useFetchImplementation(fetchImplementation: any): void; + export declare function getZapEndpoint(metadata: Event): Promise; + export declare function makeZapRequest({ + profile, + event, + amount, + relays, + comment, + }: { + profile: string; + event: string | null; + amount: number; + comment: string; + relays: string[]; + }): EventTemplate; + export declare function validateZapRequest(zapRequestString: string): string | null; + export declare function makeZapReceipt({ + zapRequest, + preimage, + bolt11, + paidAt, + }: { + zapRequest: string; + preimage: string | null; + bolt11: string; + paidAt: Date; + }): EventTemplate; + + // Pool + export declare class SimplePool { + private _conn; + private _seenOn; + private eoseSubTimeout; + private getTimeout; + constructor(options?: { eoseSubTimeout?: number; getTimeout?: number }); + close(relays: string[]): void; + ensureRelay(url: string): Promise; + sub(relays: string[], filters: Filter[], opts?: SubscriptionOptions): Sub; + get(relays: string[], filter: Filter, opts?: SubscriptionOptions): Promise; + list(relays: string[], filters: Filter[], opts?: SubscriptionOptions): Promise; + publish(relays: string[], event: Event): Pub; + seenOn(id: string): string[]; + } + + // Refrences + type Reference = { + text: string; + profile?: ProfilePointer; + event?: EventPointer; + address?: AddressPointer; + }; + export declare function parseReferences(evt: Event): Reference[]; + export {}; + + // Relay + type RelayEvent = { + connect: () => void | Promise; + disconnect: () => void | Promise; + error: () => void | Promise; + notice: (msg: string) => void | Promise; + auth: (challenge: string) => void | Promise; + }; + export type CountPayload = { + count: number; + }; + type SubEvent = { + event: (event: Event) => void | Promise; + count: (payload: CountPayload) => void | Promise; + eose: () => void | Promise; + }; + export type Relay = { + url: string; + status: number; + connect: () => Promise; + close: () => void; + sub: (filters: Filter[], opts?: SubscriptionOptions) => Sub; + list: (filters: Filter[], opts?: SubscriptionOptions) => Promise; + get: (filter: Filter, opts?: SubscriptionOptions) => Promise; + count: (filters: Filter[], opts?: SubscriptionOptions) => Promise; + publish: (event: Event) => Pub; + auth: (event: Event) => Pub; + off: (event: T, listener: U) => void; + on: (event: T, listener: U) => void; + }; + export type Pub = { + on: (type: "ok" | "failed", cb: any) => void; + off: (type: "ok" | "failed", cb: any) => void; + }; + export type Sub = { + sub: (filters: Filter[], opts: SubscriptionOptions) => Sub; + unsub: () => void; + on: (event: T, listener: U) => void; + off: (event: T, listener: U) => void; + }; + export type SubscriptionOptions = { + id?: string; + verb?: "REQ" | "COUNT"; + skipVerification?: boolean; + alreadyHaveEvent?: null | ((id: string, relay: string) => boolean); + }; + export declare function relayInit( + url: string, + options?: { + getTimeout?: number; + listTimeout?: number; + countTimeout?: number; + }, + ): Relay; + export {}; + + // utils + export declare const utf8Decoder: TextDecoder; + export declare const utf8Encoder: TextEncoder; + export declare function normalizeURL(url: string): string; + export declare function insertEventIntoDescendingList(sortedArray: Event[], event: Event): Event[]; + export declare function insertEventIntoAscendingList(sortedArray: Event[], event: Event): Event[]; +} diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index c701f10..f56c66d 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -11,10 +11,12 @@ "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", "check-types": "tsc --noEmit --incremental", "vercel": "vercel", - "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" + "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true", + "post-install": "npx patch-package" }, "dependencies": { "@ethersproject/providers": "^5.7.2", + "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.11", "@rainbow-me/rainbowkit": "1.1.2", "@scobru/crypto-ipfs": "^1.1.11", @@ -30,17 +32,24 @@ "ethereum-public-key-to-address": "^0.0.5", "ethers": "^6.8.1", "fs-extra": "^11.1.1", + "jotai": "^2.6.0", "js-sha3": "^0.9.2", "next": "^13.1.6", "nextjs-progressbar": "^0.0.16", "nostr-hooks": "^1.8.1", - "nostr-tools": "^1.17.0", + "nostr-react": "^0.7.0", + "nostr-tools": "^1.10.1", + "notyf": "^3.10.0", + "patch-package": "^8.0.0", "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", + "react-icons": "^4.8.0", "react-lazy-load-image-component": "^1.6.0", + "react-toggle-dark-mode": "^1.1.1", + "spinners-react": "^1.0.7", "url": "^0.11.3", "use-debounce": "^8.0.4", "usehooks-ts": "^2.7.2", diff --git a/packages/nextjs/pages/client.tsx b/packages/nextjs/pages/client.tsx new file mode 100644 index 0000000..03e5452 --- /dev/null +++ b/packages/nextjs/pages/client.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState } from "react"; +import AuthCard from "../components/client/authCard"; +import CreatePostCard from "../components/client/createPostCard"; +import CreatedAccModal from "../components/client/createdAccModal"; +import DisplayEventCard from "../components/client/displayEventCard"; +import EventLoader from "../components/client/eventLoader"; +import RelayCtrlCard from "../components/client/relayCtrlCard"; +import { RELAYS } from "../utils/constants"; +import { NextPage } from "next"; +import { + Event, + Filter, + QuotedEvent, + Relay, + UnsignedEvent, + getEventHash, + getPublicKey, + relayInit, + signEvent, +} from "nostr-tools"; +import { useWalletClient } from "wagmi"; +import { useGlobalState } from "~~/services/store/store"; + +const Client: NextPage = () => { + const [showKeysModal, setShowKeysModal] = useState(false); + const [showEventsLoader, setShowEventsLoader] = useState(true); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [events, setEvents] = useState([]); + const [curRelayName, setCurRelayName] = useState("wss://relay.damus.io"); + const [relay, setRelay] = useState(null); + const [sk, setSk] = useState(null); + const [pk, setPk] = useState(sk ? getPublicKey(sk) : null); + const privateKey = useGlobalState(state => state.privateKey); + const { data: signer } = useWalletClient(); + const [ethTipAmount, setEthTipAmount] = useState("0.01"); + + useEffect(() => { + const connectRelay = async () => { + setShowEventsLoader(true); + const relay = relayInit(curRelayName); + await relay.connect(); + + relay.on("connect", async () => { + setRelay(relay); + const events: Event[] = await relay.list([{ kinds: [1], limit: 100 }]); + setEvents(events); + + setTimeout(() => { + setShowEventsLoader(false); + }, 1000); + }); + relay.on("error", () => { + console.log("failed to connect to relay"); + }); + }; + connectRelay(); + + if (sk && !isLoggedIn) { + setPk(getPublicKey(sk)); + setIsLoggedIn(true); + } + }, [sk, pk, curRelayName]); + + const createEvent = (unsignedEvent: UnsignedEvent, sk: string): Event => { + const eventHash = getEventHash(unsignedEvent); + const signature = signEvent(unsignedEvent, sk); + return { + ...unsignedEvent, + id: eventHash, + sig: signature, + }; + }; + + const publishEvent = async (event: UnsignedEvent, _sk?: string) => { + console.log(event, _sk); + const signedEvent = createEvent(event, _sk ? _sk : sk ? sk : ""); + relay?.publish(signedEvent); + }; + + const getEvents = async (filters: Filter[]) => { + const events = await relay?.list(filters); + console.log(events); + if (events) setEvents(events); + }; + + const getQuotedEvent = async (filter: Filter): Promise => { + const e = await relay?.get(filter); + return e; + }; + + return ( +
+
+
+
+
{ + if (relay) setEvents(await relay.list([{ kinds: [1], limit: 100 }])); + }} + >
+
+ {/*
+ +
*/} +
+ +
+
+ {relay && sk && pk ? ( + + ) : relay && !sk ? ( + + ) : ( + <> + )} + + +
+
{ + // TODO: Implement fetching new/older events while scrolling ("infinite" content scroll) + }} + > + {showEventsLoader && } + + {events && ( +
+ {events.map(event => { + return ( + + ); + })} +
+ )} +
+ {sk && pk && showKeysModal ? : <>} +
+
+
+ ); +}; + +export default Client; diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index a7a8041..1b058be 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -4,7 +4,7 @@ import MecenateHelper from "@scobru/crypto-ipfs"; import { Nostr3 } from "@scobru/nostr3/dist/nostr3"; import type { NextPage } from "next"; import { finishEvent, getPublicKey, relayInit } from "nostr-tools"; -import { nip19 } from "nostr-tools"; +import nip19 from "nostr-tools"; import { LazyLoadImage } from "react-lazy-load-image-component"; import "react-lazy-load-image-component/src/effects/blur.css"; import { createWalletClient, http, parseEther, toBytes } from "viem"; @@ -13,6 +13,7 @@ import { keccak256 } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { optimism } from "viem/chains"; import { useWalletClient } from "wagmi"; +import { useGlobalState } from "~~/services/store/store"; import { notification } from "~~/utils/scaffold-eth"; declare global { @@ -26,12 +27,12 @@ declare global { const Home: NextPage = () => { const { data: signer } = useWalletClient(); - const [privateKey, setPrivateKey] = useState(""); + //const [privateKey, setPrivateKey] = useState(""); const [nostrPrivateKey, setNostrPrivateKey] = useState(""); const [publicKey, setPublicKey] = useState(""); const [nostrPublicKey, setNostrPublicKey] = useState(""); const [event, setEvent] = useState(null); - const [relayURL, setRelayURL] = useState("wss://relay.primal.net"); // Replace with a real relay URL + const [relayURL, setRelayURL] = useState("wss://relay.damus.io"); // Replace with a real relay URL const [relay, setRelay] = useState(null); const [showKeys, setShowKeys] = useState(false); // const [relayList, setRelayList] = useState([]); @@ -79,6 +80,7 @@ const Home: NextPage = () => { const [pubKeyEthAddressList, setPubKeyEthAddressList] = useState([]); const [isExtension, setIsExtension] = useState(false); const [evmAddress, setEvmAddress] = useState(""); + const privateKey = useGlobalState(state => state.privateKey); const openTipModal = () => { const tip_modal = document.getElementById("tip_modal") as HTMLDialogElement; @@ -438,7 +440,7 @@ const Home: NextPage = () => { setPublicKey(_pubKey); setNostrPublicKey(nip19.npubEncode(_pubKey)); setNProfile(nip19.nprofileEncode({ pubkey: _pubKey })); - setPrivateKey(""); + useGlobalState.setState({ privateKey: "" }); setNostrPrivateKey(""); setEvmAddress(""); setWallet(""); @@ -574,7 +576,7 @@ const Home: NextPage = () => { const nostr3 = new Nostr3(pkSlice); const nostrKeys = nostr3.generateNostrKeys(); setNostrPrivateKey(nostrKeys.nsec); - setPrivateKey(nostrKeys.sec); + useGlobalState.setState({ privateKey: nostrKeys.sec }); setPublicKey(getPublicKey(nostrKeys.sec)); setNostrPublicKey(nostrKeys.npub); setNProfile(nostrKeys.nprofile); @@ -899,7 +901,7 @@ const Home: NextPage = () => { onClick={() => { const relay_modal = document.getElementById("relay_modal") as HTMLDialogElement; if (relay_modal) relay_modal; - setRelayURL("wss://relay.primal.net"); + setRelayURL("wss://relay.damus.io"); handleConnectRelay(); }} > diff --git a/packages/nextjs/patches/nostr-react+0.7.0.patch b/packages/nextjs/patches/nostr-react+0.7.0.patch new file mode 100644 index 0000000..ce514e5 --- /dev/null +++ b/packages/nextjs/patches/nostr-react+0.7.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/nostr-react/src/useProfile.tsx b/node_modules/nostr-react/src/useProfile.tsx +index 44fd200..c919d30 100644 +--- a/node_modules/nostr-react/src/useProfile.tsx ++++ b/node_modules/nostr-react/src/useProfile.tsx +@@ -5,6 +5,8 @@ import { useEffect, useState } from "react" + import { useNostrEvents } from "./core" + import { uniqValues } from "./utils" + ++// new packages addedd ++ + export interface Metadata { + name?: string + username?: string diff --git a/packages/nextjs/services/store/store.ts b/packages/nextjs/services/store/store.ts index 041119c..44e918c 100644 --- a/packages/nextjs/services/store/store.ts +++ b/packages/nextjs/services/store/store.ts @@ -12,9 +12,13 @@ import create from "zustand"; type TGlobalState = { nativeCurrencyPrice: number; setNativeCurrencyPrice: (newNativeCurrencyPriceState: number) => void; + privateKey: string; + setPrivateKeyStorage: (newPrivateKey: string) => void; }; export const useGlobalState = create(set => ({ nativeCurrencyPrice: 0, setNativeCurrencyPrice: (newValue: number): void => set(() => ({ nativeCurrencyPrice: newValue })), + privateKey: "", + setPrivateKeyStorage: (newPrivateKey: string): void => set(() => ({ privateKey: newPrivateKey })), })); diff --git a/packages/nextjs/styles/globals.css b/packages/nextjs/styles/globals.css index 8090d38..151a538 100644 --- a/packages/nextjs/styles/globals.css +++ b/packages/nextjs/styles/globals.css @@ -30,3 +30,45 @@ p { .btn.btn-ghost { @apply shadow-none; } + +#root { + margin: 0 auto; + padding-top: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/packages/nextjs/utils/constants.ts b/packages/nextjs/utils/constants.ts new file mode 100644 index 0000000..e04770c --- /dev/null +++ b/packages/nextjs/utils/constants.ts @@ -0,0 +1,7 @@ +export const RELAYS = [ + "wss://relay.damus.io", + "wss://relay.primal.net", + "wss://nostr-pub.wellorder.net", + "wss://nostr.drss.io", + "wss://nostr.swiss-enigma.ch", +]; diff --git a/packages/nextjs/utils/parsing.ts b/packages/nextjs/utils/parsing.ts new file mode 100644 index 0000000..ad19488 --- /dev/null +++ b/packages/nextjs/utils/parsing.ts @@ -0,0 +1,33 @@ +export interface ExtractedImgLinks { + txtContent: string, + imgLinks: string[], +} + +export function extractImageLinks(textContent: string): ExtractedImgLinks { + /* F(x) to extract image links within text content data */ + + const tmpTxtContent = textContent.replaceAll("\n", " "); + const imgExtensions = ["png", "jpg", "jpeg", "webp", "gif"]; + const words = tmpTxtContent.split(" "); + let imgLinks: string[] = []; + + for (let i = 0; i < words.length; i++) { + if (words[i].slice(0, 4) != "http") continue; + + const subWords = words[i].split("."); + const ext = subWords[subWords.length - 1]; + + if (imgExtensions.includes(ext)) { + imgLinks.push(words[i]); + } else { + continue + }; + + textContent = textContent.replaceAll(words[i], ""); + } + + return { + txtContent: textContent, + imgLinks, + } +} diff --git a/packages/nextjs/utils/tailwind.ts b/packages/nextjs/utils/tailwind.ts new file mode 100644 index 0000000..b46d5de --- /dev/null +++ b/packages/nextjs/utils/tailwind.ts @@ -0,0 +1,3 @@ +export default function classNames(...classes: string[]) { + return classes.filter(Boolean).join(" "); +} diff --git a/yarn.lock b/yarn.lock index b2d0ac0..7f9a258 100644 --- a/yarn.lock +++ b/yarn.lock @@ -830,6 +830,18 @@ __metadata: languageName: node linkType: hard +"@headlessui/react@npm:^1.7.14": + version: 1.7.17 + resolution: "@headlessui/react@npm:1.7.17" + dependencies: + client-only: ^0.0.1 + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + checksum: 0cdb67747e7f606f78214dac0b48573247779e70534b4471515c094b74addda173dc6a9847d33aea9c6e6bc151016c034125328953077e32aa7947ebabed91f7 + languageName: node + linkType: hard + "@heroicons/react@npm:^2.0.11": version: 2.0.18 resolution: "@heroicons/react@npm:2.0.18" @@ -1910,6 +1922,128 @@ __metadata: languageName: node linkType: hard +"@react-spring/animated@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/animated@npm:9.7.3" + dependencies: + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 468942ca3a11c02c3e56def26b2da9dd10ddbed548004245c4ac309cce00b58d971e781abed67db0d652f72737eaa73766ea9a43b8ef3b08a7ed2eddc04d4c39 + languageName: node + linkType: hard + +"@react-spring/core@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/core@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 8a80a07276458fd14099320eda824e58a11ce3a9b03a5c9cd3f4252adb4d26da04ee5caf5cbc961199f55c2d58a99638d5ea292cdb6aa029208dbab741b5c531 + languageName: node + linkType: hard + +"@react-spring/konva@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/konva@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + konva: ">=2.6" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-konva: ^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0 + checksum: f6fc2c686ee86ccdd1e618ad1bfe99218a20ab84da7464be82b93f9f8fbdbcdf880c0433ae6964eaec6af6c8f328bd503188f9fd98e43bd0e12dd1838a835607 + languageName: node + linkType: hard + +"@react-spring/native@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/native@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || >=17.0.0 || >=18.0.0 + react-native: ">=0.58" + checksum: 8635153a696310c661a5a1d16ed57fbcd1941c230344a03fa4115e033c487f67049e7659248e3eac11d24ef407a929a8683afa5be4cc05821568b9e417c67032 + languageName: node + linkType: hard + +"@react-spring/shared@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/shared@npm:9.7.3" + dependencies: + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 912b5e567eb5345c9a6c8e8c0c2d69b1f411af72a0685b95831809c267c89846a31341ca071f284ace98b3cb5de647054dc76f6ace81d6379513eaf96b52f195 + languageName: node + linkType: hard + +"@react-spring/three@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/three@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + "@react-three/fiber": ">=6.0" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: ">=0.126" + checksum: 67cbe3ab3ed5de0389d1b13d7711ad6e63562fbd3f097927b8380846ec6c9a96c3415f7c5ce4b90b05c04545bc1d941fd7bc39c4dae2f554d31d014c20bb3c71 + languageName: node + linkType: hard + +"@react-spring/types@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/types@npm:9.7.3" + checksum: f47b81fe556464aa54a78603311cb584d6a0f03088522229afb058265bbe2ade2095a55ec7f4e960c3b9cceaa5d47865bc41fc6643c0f5f4bd3d8650203d8389 + languageName: node + linkType: hard + +"@react-spring/web@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/web@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 7f5cd05b2314b7f2f715e1926abcf9aa0a539399b222ab34e989144f48350adfcd2edab65d41425570f72c57f602fc6994d6730fbeed902171ac527b630a8a9b + languageName: node + linkType: hard + +"@react-spring/zdog@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/zdog@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-zdog: ">=1.0" + zdog: ">=1.0" + checksum: b4933e5142835fba09f127686b71208f4da9322707cde39ee87071600d819b1e3e69e2eeb0911d994df84d38344b439d8c53534bc08ffff0a583c9302fd21925 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^4.0.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" @@ -2144,6 +2278,7 @@ __metadata: resolution: "@se-2/nextjs@workspace:packages/nextjs" dependencies: "@ethersproject/providers": ^5.7.2 + "@headlessui/react": ^1.7.14 "@heroicons/react": ^2.0.11 "@rainbow-me/rainbowkit": 1.1.2 "@scobru/crypto-ipfs": ^1.1.11 @@ -2172,11 +2307,15 @@ __metadata: ethereum-public-key-to-address: ^0.0.5 ethers: ^6.8.1 fs-extra: ^11.1.1 + jotai: ^2.6.0 js-sha3: ^0.9.2 next: ^13.1.6 nextjs-progressbar: ^0.0.16 nostr-hooks: ^1.8.1 - nostr-tools: ^1.17.0 + nostr-react: ^0.7.0 + nostr-tools: ^1.10.1 + notyf: ^3.10.0 + patch-package: ^8.0.0 postcss: ^8.4.16 prettier: ^2.8.4 qrcode.react: ^3.1.0 @@ -2184,7 +2323,10 @@ __metadata: react-copy-to-clipboard: ^5.1.0 react-dom: ^18.2.0 react-hot-toast: ^2.4.0 + react-icons: ^4.8.0 react-lazy-load-image-component: ^1.6.0 + react-toggle-dark-mode: ^1.1.1 + spinners-react: ^1.0.7 tailwindcss: ^3.3.3 type-fest: ^4.6.0 typescript: ^5.1.6 @@ -4075,6 +4217,13 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/lockfile@npm:^1.1.0": + version: 1.1.0 + resolution: "@yarnpkg/lockfile@npm:1.1.0" + checksum: 05b881b4866a3546861fee756e6d3812776ea47fa6eb7098f983d6d0eefa02e12b66c3fff931574120f196286a7ad4879ce02743c8bb2be36c6a576c7852083a + languageName: node + linkType: hard + "JSONStream@npm:1.3.2": version: 1.3.2 resolution: "JSONStream@npm:1.3.2" @@ -5548,6 +5697,13 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^3.7.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 + languageName: node + linkType: hard + "cids@npm:^0.5.7, cids@npm:~0.5.2, cids@npm:~0.5.4, cids@npm:~0.5.5": version: 0.5.8 resolution: "cids@npm:0.5.8" @@ -5666,7 +5822,7 @@ __metadata: languageName: node linkType: hard -"client-only@npm:0.0.1": +"client-only@npm:0.0.1, client-only@npm:^0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 @@ -8258,6 +8414,15 @@ __metadata: languageName: node linkType: hard +"find-yarn-workspace-root@npm:^2.0.0": + version: 2.0.0 + resolution: "find-yarn-workspace-root@npm:2.0.0" + dependencies: + micromatch: ^4.0.2 + checksum: fa5ca8f9d08fe7a54ce7c0a5931ff9b7e36f9ee7b9475fb13752bcea80ec6b5f180fa5102d60b376d5526ce924ea3fc6b19301262efa0a5d248dd710f3644242 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -8462,7 +8627,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.1.0": +"fs-extra@npm:^9.0.0, fs-extra@npm:^9.1.0": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" dependencies: @@ -10219,7 +10384,7 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": +"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" dependencies: @@ -10340,6 +10505,64 @@ __metadata: languageName: node linkType: hard +"jotai@npm:^1.12.1": + version: 1.13.1 + resolution: "jotai@npm:1.13.1" + peerDependencies: + "@babel/core": "*" + "@babel/template": "*" + jotai-devtools: "*" + jotai-immer: "*" + jotai-optics: "*" + jotai-redux: "*" + jotai-tanstack-query: "*" + jotai-urql: "*" + jotai-valtio: "*" + jotai-xstate: "*" + jotai-zustand: "*" + react: ">=16.8" + peerDependenciesMeta: + "@babel/core": + optional: true + "@babel/template": + optional: true + jotai-devtools: + optional: true + jotai-immer: + optional: true + jotai-optics: + optional: true + jotai-redux: + optional: true + jotai-tanstack-query: + optional: true + jotai-urql: + optional: true + jotai-valtio: + optional: true + jotai-xstate: + optional: true + jotai-zustand: + optional: true + checksum: bc1f88ec1d52dda7c2aa39d278b15c128a6babc91fe9294862feb1b2d8467e47fc37cd193fd1358107212b9a4e5f5a8ae9a5a5ed1e845b74421d4476ce5d9efd + languageName: node + linkType: hard + +"jotai@npm:^2.6.0": + version: 2.6.0 + resolution: "jotai@npm:2.6.0" + peerDependencies: + "@types/react": ">=17.0.0" + react: ">=17.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 6e9ee397707cb012c57c7a616eaea7f401a1a3d66c0f36428e1eb33c03cc91788aa6c566daf473387bd86486a7215322efff2a29b446f8a9a59bc33648b7148f + languageName: node + linkType: hard + "js-sdsl@npm:^4.1.4": version: 4.4.2 resolution: "js-sdsl@npm:4.4.2" @@ -10469,6 +10692,18 @@ __metadata: languageName: node linkType: hard +"json-stable-stringify@npm:^1.0.2": + version: 1.1.0 + resolution: "json-stable-stringify@npm:1.1.0" + dependencies: + call-bind: ^1.0.5 + isarray: ^2.0.5 + jsonify: ^0.0.1 + object-keys: ^1.1.1 + checksum: 98e74dd45d3e93aa7cb5351b9f55475e15a8a7b57f401897373a1a1bbe41a6757f8b8d24f2bff0594893eccde616efe71bbaea2c1fdc1f67e8c39bcb9ee993e2 + languageName: node + linkType: hard + "json-text-sequence@npm:~0.1.0": version: 0.1.1 resolution: "json-text-sequence@npm:0.1.1" @@ -10533,6 +10768,13 @@ __metadata: languageName: node linkType: hard +"jsonify@npm:^0.0.1": + version: 0.0.1 + resolution: "jsonify@npm:0.0.1" + checksum: 027287e1c0294fce15f18c0ff990cfc2318e7f01fb76515f784d5cd0784abfec6fc5c2355c3a2f2cb0ad7f4aa2f5b74ebbfe4e80476c35b2d13cabdb572e1134 + languageName: node + linkType: hard + "jsonparse@npm:^1.2.0": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" @@ -10612,6 +10854,15 @@ __metadata: languageName: node linkType: hard +"klaw-sync@npm:^6.0.0": + version: 6.0.0 + resolution: "klaw-sync@npm:6.0.0" + dependencies: + graceful-fs: ^4.1.11 + checksum: 0da397f8961313c3ef8f79fb63af9002cde5a8fb2aeb1a37351feff0dd6006129c790400c3f5c3b4e757bedcabb13d21ec0a5eaef5a593d59515d4f2c291e475 + languageName: node + linkType: hard + "klaw@npm:^1.0.0": version: 1.3.1 resolution: "klaw@npm:1.3.1" @@ -12266,7 +12517,19 @@ __metadata: languageName: node linkType: hard -"nostr-tools@npm:^1.17.0, nostr-tools@npm:^1.9.0": +"nostr-react@npm:^0.7.0": + version: 0.7.0 + resolution: "nostr-react@npm:0.7.0" + dependencies: + jotai: ^1.12.1 + nostr-tools: ^1.1.0 + peerDependencies: + react: ">=16" + checksum: 314157bdd8c81102c5a5d567ca019907b3bd704f720e389c25a69eea1cac3830187ab60e54a35aaf0f50dc7b3e0383ad9cd2dc055e3df43a9f6a0282b721ca7e + languageName: node + linkType: hard + +"nostr-tools@npm:^1.1.0, nostr-tools@npm:^1.10.1, nostr-tools@npm:^1.17.0, nostr-tools@npm:^1.9.0": version: 1.17.0 resolution: "nostr-tools@npm:1.17.0" dependencies: @@ -12285,6 +12548,13 @@ __metadata: languageName: node linkType: hard +"notyf@npm:^3.10.0": + version: 3.10.0 + resolution: "notyf@npm:3.10.0" + checksum: 6cc533fccb0d74e544edf10e82d2942975adc4c993a68c966694bbb451dc06056d02e8dced4ecfce2c4586682223759cb1f9f3e3f609c83458e99c2bf5494b00 + languageName: node + linkType: hard + "npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -12541,6 +12811,16 @@ __metadata: languageName: node linkType: hard +"open@npm:^7.4.2": + version: 7.4.2 + resolution: "open@npm:7.4.2" + dependencies: + is-docker: ^2.0.0 + is-wsl: ^2.1.1 + checksum: 3333900ec0e420d64c23b831bc3467e57031461d843c801f569b2204a1acc3cd7b3ec3c7897afc9dde86491dfa289708eb92bba164093d8bd88fb2c231843c91 + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -12728,6 +13008,31 @@ __metadata: languageName: node linkType: hard +"patch-package@npm:^8.0.0": + version: 8.0.0 + resolution: "patch-package@npm:8.0.0" + dependencies: + "@yarnpkg/lockfile": ^1.1.0 + chalk: ^4.1.2 + ci-info: ^3.7.0 + cross-spawn: ^7.0.3 + find-yarn-workspace-root: ^2.0.0 + fs-extra: ^9.0.0 + json-stable-stringify: ^1.0.2 + klaw-sync: ^6.0.0 + minimist: ^1.2.6 + open: ^7.4.2 + rimraf: ^2.6.3 + semver: ^7.5.3 + slash: ^2.0.0 + tmp: ^0.0.33 + yaml: ^2.2.2 + bin: + patch-package: index.js + checksum: d23cddc4d1622e2d8c7ca31b145c6eddb24bd271f69905e766de5e1f199f0b9a5479a6a6939ea857288399d4ed249285639d539a2c00fbddb7daa39934b007a2 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -13672,6 +13977,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^4.8.0": + version: 4.12.0 + resolution: "react-icons@npm:4.12.0" + peerDependencies: + react: "*" + checksum: db82a141117edcd884ade4229f0294b2ce15d82f68e0533294db07765d6dce00b129cf504338ec7081ce364fe899b296cb7752554ea08665b1d6bad811134e79 + languageName: node + linkType: hard + "react-is@npm:^16.13.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -13727,6 +14041,23 @@ __metadata: languageName: node linkType: hard +"react-spring@npm:^9.0.0-rc.3": + version: 9.7.3 + resolution: "react-spring@npm:9.7.3" + dependencies: + "@react-spring/core": ~9.7.3 + "@react-spring/konva": ~9.7.3 + "@react-spring/native": ~9.7.3 + "@react-spring/three": ~9.7.3 + "@react-spring/web": ~9.7.3 + "@react-spring/zdog": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f763fb64b16b59b7b98816b88898d1697906aebd0d9067bdb2af61d2522b3a313d1117e139e75e39f0cd438e1d110956cfb3573bdefb5516ffba3e7aa618d87e + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -13744,6 +14075,17 @@ __metadata: languageName: node linkType: hard +"react-toggle-dark-mode@npm:^1.1.1": + version: 1.1.1 + resolution: "react-toggle-dark-mode@npm:1.1.1" + dependencies: + react-spring: ^9.0.0-rc.3 + peerDependencies: + react: ">=16" + checksum: bd24d68dd20db2c5d1af7b0e0cac6bf949e0853925065bc5b29f29c7d75dfee7e4a45bf7c78dcf6b7e4c952ab3c9a815c34c0af1f2f54e6872a62070b3d12246 + languageName: node + linkType: hard + "react@npm:>=18, react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -14111,7 +14453,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^2.2.8": +"rimraf@npm:^2.2.8, rimraf@npm:^2.6.3": version: 2.7.1 resolution: "rimraf@npm:2.7.1" dependencies: @@ -14321,7 +14663,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.4": +"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -14553,6 +14895,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^2.0.0": + version: 2.0.0 + resolution: "slash@npm:2.0.0" + checksum: 512d4350735375bd11647233cb0e2f93beca6f53441015eea241fe784d8068281c3987fbaa93e7ef1c38df68d9c60013045c92837423c69115297d6169aa85e6 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -14759,6 +15108,18 @@ __metadata: languageName: node linkType: hard +"spinners-react@npm:^1.0.7": + version: 1.0.7 + resolution: "spinners-react@npm:1.0.7" + peerDependencies: + "@types/react": ^16.x || ^17.x || ^18.x + "@types/react-dom": ^16.x || ^17.x || ^18.x + react: ^16.x || ^17.x || ^18.x + react-dom: ^16.x || ^17.x || ^18.x + checksum: a06c67b89118721fd5042ef8927e193924de18701ee65724fc3bf158733a332ad1a06179e650c87ab16ea9af09a92a425131d1b53f0a8bc059588a7231fb3d31 + languageName: node + linkType: hard + "split-ca@npm:^1.0.0, split-ca@npm:^1.0.1": version: 1.0.1 resolution: "split-ca@npm:1.0.1" @@ -15481,7 +15842,7 @@ __metadata: languageName: node linkType: hard -"tmp@npm:0.0.33": +"tmp@npm:0.0.33, tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" dependencies: @@ -17178,7 +17539,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.4": +"yaml@npm:^2.2.2, yaml@npm:^2.3.4": version: 2.3.4 resolution: "yaml@npm:2.3.4" checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8