From 0bf2f7c307238baaa5e8cfda273d09a3a6b803e6 Mon Sep 17 00:00:00 2001 From: highperfocused <24775431+mroxso@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:57:18 +0100 Subject: [PATCH] AI made all of this lol --- app/layout.tsx | 37 +- components/CommentCard.tsx | 77 ++- components/CommentsCompontent.tsx | 15 +- components/FollowButton.tsx | 99 ++-- components/FollowerFeed.tsx | 57 ++- components/FollowerQuickViewFeed.tsx | 65 +-- components/GlobalFeed.tsx | 7 +- components/KIND20Card.tsx | 76 +-- components/LoginForm.tsx | 201 ++++---- components/NoteCard.tsx | 119 ++--- components/NotePageComponent.tsx | 59 ++- components/Notification.tsx | 112 ++-- components/Notifications.tsx | 54 +- components/ProfileFeed.tsx | 7 +- components/ProfileGalleryViewFeed.tsx | 18 +- components/ProfileInfoCard.tsx | 9 +- components/ProfileQuickViewFeed.tsx | 22 +- components/ProfileTextFeed.tsx | 8 +- components/QuickViewKind20NoteCard.tsx | 68 ++- components/ReactionButton.tsx | 49 +- components/ReactionButtonReactionList.tsx | 15 +- components/ReactionButtonReactionListItem.tsx | 31 +- components/ReelFeed.tsx | 32 +- components/TagFeed.tsx | 21 +- components/TrendingAccounts.tsx | 12 +- components/TrendingImage.tsx | 77 ++- components/TrendingImages.tsx | 21 +- components/UpdateProfileForm.tsx | 63 +-- components/UploadComponent.tsx | 481 +++++++----------- components/ZapButton.tsx | 122 ++--- components/ZapButtonList.tsx | 20 +- components/ZapButtonListItem.tsx | 41 +- components/dashboard/RecentFollower.tsx | 26 +- components/dashboard/RecentFollowerCard.tsx | 64 ++- components/dashboard/RecentZap.tsx | 39 +- components/dashboard/RecentZapsCard.tsx | 65 ++- components/dashboard/Statistics.tsx | 70 +-- hooks/useNDK.ts | 72 +++ utils/utils.ts | 106 ++-- 39 files changed, 1179 insertions(+), 1358 deletions(-) create mode 100644 hooks/useNDK.ts diff --git a/app/layout.tsx b/app/layout.tsx index f5595eb..05a8297 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import { Metadata } from "next"; import "./globals.css"; -import { NostrProvider } from "nostr-react"; import { ThemeProvider } from "@/components/theme-provider"; import { TopNavigation } from "@/components/headerComponents/TopNavigation"; import BottomBar from "@/components/BottomBar"; @@ -10,19 +9,29 @@ import { Inter } from "next/font/google"; import { Toaster } from "@/components/ui/toaster" import Script from "next/script"; import Umami from "@/components/Umami"; +import NDK from '@nostr-dev-kit/ndk'; +import { createContext, useMemo } from 'react'; const inter = Inter({ subsets: ["latin"] }); +// Create NDK context +export const NDKContext = createContext(null); + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - - const relayUrls = [ - "wss://relay.nostr.band", - "wss://relay.damus.io", - ]; + const ndk = useMemo(() => { + const ndk = new NDK({ + explicitRelayUrls: [ + "wss://relay.nostr.band", + "wss://relay.damus.io", + ] + }); + ndk.connect(); + return ndk; + }, []); return ( @@ -39,15 +48,15 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - - - -
- + + + + +
{children} - -
- +
+ + diff --git a/components/CommentCard.tsx b/components/CommentCard.tsx index 6a35025..cc4d5da 100644 --- a/components/CommentCard.tsx +++ b/components/CommentCard.tsx @@ -1,8 +1,5 @@ import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; +import { nip19 } from "nostr-tools"; import { Card, CardContent, @@ -26,8 +23,8 @@ import { import ReactionButton from '@/components/ReactionButton'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; import ViewRawButton from '@/components/ViewRawButton'; -import ViewNoteButton from './ViewNoteButton'; import Link from 'next/link'; +import { useProfile } from '@/hooks/useNDK'; interface CommentCardProps { pubkey: string; @@ -37,17 +34,15 @@ interface CommentCardProps { event: any; } -const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event }) => { - const { data: userData } = useProfile({ - pubkey, - }); +const CommentCard: React.FC = ({ pubkey, text, eventId, tags, event }) => { + const { data: userData } = useProfile(pubkey); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); + const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(pubkey); const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g, ''); const createdAt = new Date(event.created_at * 1000); const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const profileImageSrc = userData?.image || "https://robohash.org/" + pubkey; return ( <> @@ -74,50 +69,38 @@ const NoteCard: React.FC = ({ pubkey, text, eventId, tags, eve -
- { -
- {imageSrc && imageSrc.length > 1 ? ( - - - {imageSrc.map((src, index) => ( - - - - ))} - - - - - ) : ( - imageSrc ? : "" - )} -
- } -
-
- {textWithoutImage} -
+
+

{textWithoutImage}

+ {imageSrc && imageSrc.length > 0 && ( + + + {imageSrc.map((image, index) => ( + + {`Image + + ))} + + + + + )}
-
-
-
- +
+
+ +
+
-
- {createdAt.toLocaleString()} + + {createdAt.toLocaleString()} + ); } -export default NoteCard; \ No newline at end of file +export default CommentCard; \ No newline at end of file diff --git a/components/CommentsCompontent.tsx b/components/CommentsCompontent.tsx index 8f913a2..f68af99 100644 --- a/components/CommentsCompontent.tsx +++ b/components/CommentsCompontent.tsx @@ -1,8 +1,5 @@ import React from 'react'; -import { useNostrEvents } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; +import { useNostrEvents } from "@/hooks/useNDK"; import CommentCard from '@/components/CommentCard'; interface CommentsCompontentProps { @@ -11,7 +8,6 @@ interface CommentsCompontentProps { } const CommentsCompontent: React.FC = ({ pubkey, event }) => { - const { events } = useNostrEvents({ filter: { kinds: [1], @@ -24,7 +20,14 @@ const CommentsCompontent: React.FC = ({ pubkey, event }

Comments

{events.map((event) => (
- +
))} diff --git a/components/FollowButton.tsx b/components/FollowButton.tsx index a70001c..3022ca3 100644 --- a/components/FollowButton.tsx +++ b/components/FollowButton.tsx @@ -1,10 +1,6 @@ - import React, { useEffect, useState } from 'react'; import { Button } from './ui/button'; -import { useNostr, useNostrEvents } from 'nostr-react'; -import { finalizeEvent } from 'nostr-tools'; -import { sign } from 'crypto'; -import { SignalMedium } from 'lucide-react'; +import { useNDK, useNostrEvents } from '@/hooks/useNDK'; interface FollowButtonProps { pubkey: string; @@ -12,18 +8,9 @@ interface FollowButtonProps { } const FollowButton: React.FC = ({ pubkey, userPubkey }) => { - // const { publish } = useNostr(); + const ndk = useNDK(); const [isFollowing, setIsFollowing] = useState(false); - let storedPubkey: string | null = null; - let storedNsec: string | null = null; - let isLoggedIn = false; - if (typeof window !== 'undefined') { - storedPubkey = window.localStorage.getItem('pubkey'); - storedNsec = window.localStorage.getItem('nsec'); - isLoggedIn = storedPubkey !== null; - } - const { events } = useNostrEvents({ filter: { kinds: [3], @@ -33,65 +20,59 @@ const FollowButton: React.FC = ({ pubkey, userPubkey }) => { }); let followingPubkeys = events.flatMap((event) => event.tags.map(tag => tag[1])); - // filter out all null or undefined followingPubkeys = followingPubkeys.filter((tag) => tag); - useEffect(() => { if (followingPubkeys.includes(pubkey)) { setIsFollowing(true); } - }, [followingPubkeys, isFollowing, setIsFollowing]); + }, [followingPubkeys, isFollowing, pubkey]); const handleFollow = async () => { - // if (isLoggedIn) { + const ndkEvent = ndk.getEvent(); + ndkEvent.kind = 3; + ndkEvent.created_at = Math.floor(Date.now() / 1000); + ndkEvent.content = ''; - // let eventTemplate = { - // kind: 3, - // created_at: Math.floor(Date.now() / 1000), - // tags: [followingPubkeys], - // content: '', - // } + // Get current following list and update it + const currentList = [...followingPubkeys]; + if (isFollowing) { + ndkEvent.tags = currentList.filter(p => p !== pubkey).map(p => ['p', p]); + } else { + currentList.push(pubkey); + ndkEvent.tags = currentList.map(p => ['p', p]); + } - // console.log(eventTemplate); + try { + const loginType = window.localStorage.getItem("loginType"); + if (loginType === "extension") { + const signedEvent = await window.nostr.signEvent(ndkEvent.rawEvent()); + Object.assign(ndkEvent, signedEvent); + } else if (loginType === "amber") { + alert("Signing with Amber is not implemented yet, sorry!"); + return; + } else if (loginType === "raw_nsec") { + const nsecStr = window.localStorage.getItem("nsec"); + if (!nsecStr) throw new Error("No nsec found"); + await ndkEvent.sign(); + } - // if (isFollowing) { - // eventTemplate.tags = eventTemplate.tags.filter(tag => tag[1] !== pubkey); - // } else { - // eventTemplate.tags[0].push(pubkey); - // } - - // console.log(eventTemplate); - - // let signedEvent = null; - // if (storedNsec != null) { - // // TODO: Sign Nostr Event with nsec - // const nsecArray = storedNsec ? new TextEncoder().encode(storedNsec) : new Uint8Array(); - // signedEvent = finalizeEvent(eventTemplate, nsecArray); - // console.log(signedEvent); - // } else if (storedPubkey != null) { - // // TODO: Request Extension to sign Nostr Event - // console.log('Requesting Extension to sign Nostr Event..'); - // try { - // signedEvent = await window.nostr.signEvent(eventTemplate); - // } catch (error) { - // console.error('Nostr Extension not found or aborted.'); - // } - // } - - // if (signedEvent !== null) { - // console.log(signedEvent); - // publish(signedEvent); - // setIsFollowing(!isFollowing); - // } - // } + await ndkEvent.publish(); + setIsFollowing(!isFollowing); + } catch (error) { + console.error("Failed to follow/unfollow:", error); + } }; return ( - ); -}; +} export default FollowButton; \ No newline at end of file diff --git a/components/FollowerFeed.tsx b/components/FollowerFeed.tsx index 0b97d5a..81f0b82 100644 --- a/components/FollowerFeed.tsx +++ b/components/FollowerFeed.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import KIND20Card from "./KIND20Card"; import { getImageUrl } from "@/utils/utils"; import { Button } from "@/components/ui/button"; @@ -12,7 +12,7 @@ const FollowerFeed: React.FC = ({ pubkey }) => { const now = useRef(new Date()); const [limit, setLimit] = useState(20); - const { events: following, isLoading: followingLoading } = useNostrEvents({ + const { events: following } = useNostrEvents({ filter: { kinds: [3], authors: [pubkey], @@ -40,27 +40,44 @@ const FollowerFeed: React.FC = ({ pubkey }) => { return ( <> -
- {events.map((event) => ( -
- +
+ {events.length === 0 && isLoading ? ( +
+ +
+ + +
- ))} + ) : events.some(event => getImageUrl(event.tags)) ? ( + <> + {events.map((event) => { + const imageUrl = getImageUrl(event.tags); + return imageUrl ? ( + + ) : null; + })} + + ) : ( +
+

No posts found :(

+
+ )}
- {!isLoading && ( -
- + {!isLoading && events.some(event => getImageUrl(event.tags)) ? ( +
+
- )} + ) : null} ); } diff --git a/components/FollowerQuickViewFeed.tsx b/components/FollowerQuickViewFeed.tsx index 900e3c4..93dbdcb 100644 --- a/components/FollowerQuickViewFeed.tsx +++ b/components/FollowerQuickViewFeed.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import QuickViewNoteCard from "./QuickViewNoteCard"; @@ -11,10 +11,10 @@ interface FollowerQuickViewFeedProps { } const FollowerQuickViewFeed: React.FC = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const [limit, setLimit] = useState(25); - const { events: following, isLoading: followingLoading } = useNostrEvents({ + const { events: following } = useNostrEvents({ filter: { kinds: [3], authors: [pubkey], @@ -27,12 +27,13 @@ const FollowerQuickViewFeed: React.FC = ({ pubkey }) .filter(tag => tag[0] === 'p') .map(tag => tag[1]) ); - + const { events, isLoading } = useNostrEvents({ filter: { - limit: limit, kinds: [20], authors: followingPubkeys, + limit, + since: Math.floor(now.current.getTime() / 1000) - 7 * 24 * 60 * 60, // Last week }, }); @@ -46,44 +47,44 @@ const FollowerQuickViewFeed: React.FC = ({ pubkey }) {events.length === 0 && isLoading ? ( <>
- +
- +
- -
-
- -
-
- -
-
- +
+ ) : events.some(event => getImageUrl(event.tags)) ? ( + <> + {events.map((event) => { + const imageUrl = getImageUrl(event.tags); + return imageUrl ? ( + + ) : null; + })} + ) : ( - events.map((event) => ( - - )) +
+

No posts found :(

+
)}
- {!isLoading && ( + {!isLoading && events.some(event => getImageUrl(event.tags)) ? (
- +
- )} + ) : null} ); } diff --git a/components/GlobalFeed.tsx b/components/GlobalFeed.tsx index 99ea636..2cd4e09 100644 --- a/components/GlobalFeed.tsx +++ b/components/GlobalFeed.tsx @@ -1,4 +1,4 @@ -import { useNostrEvents } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import KIND20Card from "./KIND20Card"; import { getImageUrl } from "@/utils/utils"; import { useState, useRef } from "react"; @@ -10,8 +10,9 @@ const GlobalFeed: React.FC = () => { const { events, isLoading } = useNostrEvents({ filter: { - limit: limit, + limit, kinds: [20], + since: Math.floor(now.current.getTime() / 1000) - 24 * 60 * 60, // Last 24 hours }, }); @@ -34,7 +35,7 @@ const GlobalFeed: React.FC = () => { image={imageUrl} eventId={event.id} tags={event.tags} - event={event} + event={event.rawEvent()} showViewNoteCardButton={true} />
diff --git a/components/KIND20Card.tsx b/components/KIND20Card.tsx index c9dbd96..832f776 100644 --- a/components/KIND20Card.tsx +++ b/components/KIND20Card.tsx @@ -1,7 +1,6 @@ import type React from "react" -import { useProfile } from "nostr-react" -import { nip19 } from "nostr-tools" import { useState } from "react" +import { nip19 } from "nostr-tools" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel" @@ -11,18 +10,18 @@ import ViewRawButton from "@/components/ViewRawButton" import ViewNoteButton from "./ViewNoteButton" import Link from "next/link" import ViewCopyButton from "./ViewCopyButton" -import type { Event as NostrEvent } from "nostr-tools" import ZapButton from "./ZapButton" import Image from "next/image" +import { useProfile } from "@/hooks/useNDK" interface KIND20CardProps { - pubkey: string - text: string - image: string - eventId: string - tags: string[][] - event: NostrEvent - showViewNoteCardButton: boolean + pubkey: string; + text: string; + image: string; + eventId: string; + tags: string[][]; + event: any; + showViewNoteCardButton: boolean; } const KIND20Card: React.FC = ({ @@ -34,24 +33,20 @@ const KIND20Card: React.FC = ({ event, showViewNoteCardButton, }) => { - const { data: userData } = useProfile({ - pubkey, - }) + const { data: userData } = useProfile(pubkey); const [imageError, setImageError] = useState(false); if (!image || imageError) return null; - const title = - userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey) - text = text.replaceAll("\n", " ") - const createdAt = new Date(event.created_at * 1000) - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}` - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey - const uploadedVia = tags.find((tag) => tag[0] === "client")?.[1] + const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(pubkey); + text = text.replaceAll("\n", " "); + const createdAt = new Date(event.created_at * 1000); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.image || "https://robohash.org/" + pubkey; + const uploadedVia = tags.find((tag) => tag[0] === "client")?.[1]; return ( <> -
@@ -64,9 +59,7 @@ const KIND20Card: React.FC = ({ - - {title} - + {title}
@@ -77,22 +70,29 @@ const KIND20Card: React.FC = ({ - -
-
-
- {text} setImageError(true)} - /> + +
+
+ {text &&

{text}

} +
+
+
+ {text} setImageError(true)} + /> +
+
-
-
-
{text}

diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 979003c..ceb1c30 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -1,10 +1,11 @@ -declare global { - interface Window { - nostr: any; - } -} +'use client'; +import React, { useEffect, useRef } from 'react'; import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { generatePrivateKey, getPublicKey } from 'nostr-tools/pure' +import { nip19 } from "nostr-tools" +import { Label } from "./ui/label" import { Card, CardContent, @@ -13,22 +14,25 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion" -import { useEffect, useRef } from "react" -import { getPublicKey, generateSecretKey, nip19 } from 'nostr-tools' import { InfoIcon } from "lucide-react"; import Link from "next/link"; -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { useNDK } from '@/hooks/useNDK'; + +declare global { + interface Window { + nostr: any; + } +} export function LoginForm() { - + const ndk = useNDK(); let publicKey = useRef(null); let nsecInput = useRef(null); let npubInput = useRef(null); @@ -38,70 +42,52 @@ export function LoginForm() { const urlParams = new URLSearchParams(window.location.search); const amberResponse = urlParams.get('amberResponse'); if (amberResponse !== null) { - // localStorage.setItem("pubkey", nip19.npubEncode(amberResponse).toString()); localStorage.setItem("pubkey", amberResponse); localStorage.setItem("loginType", "amber"); window.location.href = `/profile/${amberResponse}`; } }, []); - const handleAmber = async () => { const hostname = window.location.host; - console.log(hostname); if (!hostname) { throw new Error("Hostname is null or undefined"); } const intent = `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=http://${hostname}/login?amberResponse=;end`; window.location.href = intent; - // window.location.href = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=http://${hostname}/login?amberResponse=`; - } + }; - const handleExtensionLogin = async () => { - // eslint-disable-next-line - if (window.nostr !== undefined) { - publicKey.current = await window.nostr.getPublicKey() - console.log("Logged in with pubkey: ", publicKey.current); - if (publicKey.current !== null) { - localStorage.setItem("pubkey", publicKey.current); + const handleExtension = async () => { + if (typeof window !== 'undefined') { + try { + const pubkey = await window.nostr.getPublicKey(); + localStorage.setItem("pubkey", pubkey); localStorage.setItem("loginType", "extension"); - // window.location.reload(); - window.location.href = `/profile/${nip19.npubEncode(publicKey.current)}`; + window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; + } catch (e) { + console.error(e); } } }; - // const handleNsecSignUp = async () => { - // let nsec = generateSecretKey(); - // console.log('nsec: ' + nsec); - - // let nsecHex = bytesToHex(nsec); - // console.log('bytesToHex nsec: ' + nsecHex); - - // let pubkey = getPublicKey(nsec); - // console.log('pubkey: ' + pubkey); - - // localStorage.setItem("nsec", nsecHex); - // localStorage.setItem("pubkey", pubkey); - // localStorage.setItem("loginType", "raw_nsec") - // window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; - // }; - const handleNsecLogin = async () => { if (nsecInput.current !== null) { try { let input = nsecInput.current.value; - if(input.includes("nsec")) { - input = bytesToHex(nip19.decode(input).data as Uint8Array); - console.log('decoded nsec: ' + input); - } - let nsecBytes = hexToBytes(input); - let nsecHex = bytesToHex(nsecBytes); - let pubkey = getPublicKey(nsecBytes); + let nsec = null; + let pubkey = null; - localStorage.setItem("nsec", nsecHex); + if (input.startsWith("nsec1")) { + nsec = nip19.decode(input).data.toString(); + pubkey = getPublicKey(hexToBytes(nsec)); + } else { + nsec = input; + pubkey = getPublicKey(hexToBytes(input)); + } + + localStorage.setItem("nsec", nsec); localStorage.setItem("pubkey", pubkey); - localStorage.setItem("loginType", "raw_nsec") + localStorage.setItem("loginType", "raw_nsec"); window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; } catch (e) { @@ -114,77 +100,90 @@ export function LoginForm() { if (npubInput.current !== null) { try { let input = npubInput.current.value; - let npub = null; let pubkey = null; - if(input.startsWith("npub1")) { - npub = input; + + if (input.startsWith("npub1")) { pubkey = nip19.decode(input).data.toString(); } else { pubkey = input; - npub = nip19.npubEncode(input); } - localStorage.setItem("pubkey", pubkey); - localStorage.setItem("loginType", "readOnly_npub") - - window.location.href = `/profile/${npub}`; + // Verify the pubkey exists by trying to fetch their profile + const user = ndk.getUser({ pubkey }); + const profile = await user.fetchProfile(); + + if (profile || confirm("No profile found for this key. Continue anyway?")) { + localStorage.setItem("pubkey", pubkey); + localStorage.setItem("loginType", "readOnly_npub"); + window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; + } } catch (e) { console.error(e); + alert("Invalid public key"); } } }; - return ( - Login to Lumina - - Login to your account either with a nostr extension or with your nsec. - + Login + Login with your preferred method. - -
- - - - -
-
- - - - -
-
- or - - - Login with npub (read-only) - -
- - - + +
+
+ + Login with Extension + +
+
+ +
+ + + +
+ Login with Private Key (nsec) +
- -
-
- or - - - Login with nsec (not recommended) - -
- - - + + +
+

Warning: This is not recommended for security reasons.

+
+ + +
-
- - + + + + +
+ Login with Public Key (npub) + +
+
+ +
+

Read-only mode - you won't be able to post.

+
+ + +
+
+
+
+ +
+
+

Don't have an Account? Create Account

+
) diff --git a/components/NoteCard.tsx b/components/NoteCard.tsx index b58f43a..d2070c0 100644 --- a/components/NoteCard.tsx +++ b/components/NoteCard.tsx @@ -1,8 +1,5 @@ import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; +import { nip19 } from "nostr-tools"; import { Card, CardContent, @@ -29,32 +26,31 @@ import ViewRawButton from '@/components/ViewRawButton'; import ViewNoteButton from './ViewNoteButton'; import Link from 'next/link'; import ViewCopyButton from './ViewCopyButton'; -import { Event as NostrEvent } from "nostr-tools"; +import { NDKEvent } from '@nostr-dev-kit/ndk'; import ZapButton from './ZapButton'; +import { useProfile } from '@/hooks/useNDK'; interface NoteCardProps { pubkey: string; text: string; eventId: string; tags: string[][]; - event: NostrEvent; - showViewNoteCardButton: boolean; + event: any; + showViewNoteCardButton?: boolean; } const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event, showViewNoteCardButton }) => { - const { data: userData } = useProfile({ - pubkey, - }); + const { data: userData } = useProfile(pubkey); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - // text = text.replaceAll('\n', '
'); + const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(pubkey); text = text.replaceAll('\n', ' '); const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); const videoSrc = text.match(/https?:\/\/[^ ]*\.(mp4|webm|mov)/g); const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov|jpeg)/g, ''); const createdAt = new Date(event.created_at * 1000); const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const profileImageSrc = userData?.image || "https://robohash.org/" + pubkey; + const uploadedVia = tags.find((tag) => tag[0] === "client")?.[1]; return ( <> @@ -81,76 +77,55 @@ const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event, -
- { -
-
- {imageSrc && imageSrc.length > 1 ? ( - - - {imageSrc.map((src, index) => ( - - - - ))} - - - - - ) : ( - imageSrc ? : "" - )} -
-
- {videoSrc && videoSrc.length > 1 ? ( - - - {videoSrc.map((src, index) => ( - - - ))} - - - - - ) : ( - videoSrc ?
-
- } -
-
- {textWithoutImage} -
+
+ {textWithoutImage &&

{textWithoutImage}

} + {imageSrc && imageSrc.length > 0 && ( + + + {imageSrc.map((image, index) => ( + + {`Image + + ))} + + + + + )} + {videoSrc && videoSrc.length > 0 && ( + + + {videoSrc.map((video, index) => ( + + + + ))} + + + + + )}
-
-
-
+
+
+
{showViewNoteCardButton && }
-
+
- {createdAt.toLocaleString()} +
+ {createdAt.toLocaleString()} + {uploadedVia && Uploaded via {uploadedVia}} +
diff --git a/components/NotePageComponent.tsx b/components/NotePageComponent.tsx index dbfc24a..e58ebbb 100644 --- a/components/NotePageComponent.tsx +++ b/components/NotePageComponent.tsx @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { useNostrEvents } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import NoteCard from '@/components/NoteCard'; import CommentsCompontent from "@/components/CommentsCompontent"; import KIND20Card from "./KIND20Card"; @@ -10,7 +10,7 @@ interface NotePageComponentProps { } const NotePageComponent: React.FC = ({ id }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const { events } = useNostrEvents({ filter: { @@ -19,42 +19,41 @@ const NotePageComponent: React.FC = ({ id }) => { }, }); - // filter out all events that also have another e tag with another id + // Filter out events that have other e tags (replies to other notes) const filteredEvents = events.filter((event) => { return event.tags.filter((tag) => { - return tag[0] === '#e' && tag[1] !== id; + return tag[0] === 'e' && tag[1] !== id; }).length === 0; }); return ( <> {filteredEvents.map((event) => ( -
- {event.kind === 1 && ( - - )} - {event.kind === 20 && ( - - )} -
- +
+ {event.kind === 20 ? ( + + ) : ( + + )} +
+
))} diff --git a/components/Notification.tsx b/components/Notification.tsx index 3351f41..4dc0f74 100644 --- a/components/Notification.tsx +++ b/components/Notification.tsx @@ -1,16 +1,12 @@ import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; +import { useProfile } from "@/hooks/useNDK"; import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; -import { - NostrEvent, - Event, - nip19, -} from "nostr-tools"; import { Avatar, AvatarImage } from './ui/avatar'; import Link from 'next/link'; +import { nip19 } from "nostr-tools"; interface NotificationProps { - event: NostrEvent; + event: any; } const Notification: React.FC = ({ event }) => { @@ -18,9 +14,7 @@ const Notification: React.FC = ({ event }) => { let sats = 0; let reactedToId = ''; - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey: sender, - }); + const { data: userData } = useProfile(sender); if (!event) { return null; @@ -32,80 +26,52 @@ const Notification: React.FC = ({ event }) => { sender = tag[1]; } if (tag[0] === 'bolt11') { - let bolt11decoded = require('light-bolt11-decoder').decode(tag[1]); - for (let field of bolt11decoded.sections) { - if (field.name === 'amount') { - sats = field.value / 1000; - } - } + const lightningPayReq = require('bolt11'); + const decoded = lightningPayReq.decode(tag[1]); + sats = decoded.satoshis; } } } - if (event.kind === 7) { - for (let tag of event.tags) { - if (tag[0] === 'e') { - reactedToId = tag[1]; - } - } - } - - let name = userData?.name ?? nip19.npubEncode(event.pubkey).slice(0, 8) + ':' + nip19.npubEncode(event.pubkey).slice(-3); - let createdAt = new Date(event.created_at * 1000); + const name = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(sender).slice(0, 8) + ':' + nip19.npubEncode(sender).slice(-3); + const profileImageSrc = userData?.image || "https://robohash.org/" + sender; + const createdAt = new Date(event.created_at * 1000); return ( - <> -
- {/* ZAP */} - {event.kind === 9735 && ( -
-

{sats} sats ⚡️

-
+ + + + +
- + -
-
-

{name} zapped you

-

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

-
-
- )} - {/* FOLLOW */} - {event.kind === 3 && ( -
-

{event.content}

-
- - - -
-
-

{name} started following you

-

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

-
-
- )} - {/* REACTION */} - {event.kind === 7 && ( - -
-

{event.content}

-
- - - -
-
-

{name} reacted to you

-

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

-
+ {name}
+ + + + {event.kind === 9735 && ( +
+

{name} zapped you with {sats} sats

+

{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}

+
)} -
-
- + {event.kind === 3 && ( +
+

{name} started following you

+

{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}

+
+ )} + {event.kind === 7 && ( + +

{name} reacted to your note

+

{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}

+ + )} + + ); } diff --git a/components/Notifications.tsx b/components/Notifications.tsx index 4920166..ba7bdc6 100644 --- a/components/Notifications.tsx +++ b/components/Notifications.tsx @@ -1,13 +1,6 @@ import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; -import { AvatarImage } from '@radix-ui/react-avatar'; -import { Avatar } from '@/components/ui/avatar'; -import NIP05 from '@/components/nip05'; -import { - nip19, -} from "nostr-tools"; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { useNostrEvents, useProfile } from '@/hooks/useNDK'; import Notification from './Notification'; interface NotificationsProps { @@ -15,20 +8,10 @@ interface NotificationsProps { } const Notifications: React.FC = ({ pubkey }) => { - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey, - }); + const { data: userData } = useProfile(pubkey); - - // const { events: followers, isLoading: followersLoading } = useNostrEvents({ - // filter: { - // kinds: [3], - // '#p': [pubkey], - // limit: 50, - // }, - // }); - - const { events: zaps, isLoading: zapsLoading } = useNostrEvents({ + // Get zaps + const { events: zaps } = useNostrEvents({ filter: { kinds: [9735], '#p': [pubkey], @@ -36,7 +19,8 @@ const Notifications: React.FC = ({ pubkey }) => { }, }); - const { events: reactions, isLoading: reactionsLoading } = useNostrEvents({ + // Get reactions + const { events: reactions } = useNostrEvents({ filter: { kinds: [7], '#p': [pubkey], @@ -44,37 +28,19 @@ const Notifications: React.FC = ({ pubkey }) => { }, }); - // const { events: following, isLoading: followingLoading } = useNostrEvents({ - // filter: { - // kinds: [3], - // authors: [pubkey], - // limit: 1, - // }, - // }); - - // filter for only new followings (latest in a users followers list) - // const filteredFollowers = followers.filter(follower => { - // const lastPTag = follower.tags[follower.tags.length - 1]; - // if (lastPTag[0] === "p" && lastPTag[1] === pubkey.toString()) { - // // console.log(follower.tags[follower.tags.length - 1]); - // return true; - // } - // }); - - // let allNotifications = [...filteredFollowers, ...zaps].sort((a, b) => b.created_at - a.created_at); - let allNotifications = [...zaps, ...reactions].sort((a, b) => b.created_at - a.created_at); + // Combine and sort notifications + const allNotifications = [...zaps, ...reactions].sort((a, b) => b.created_at - a.created_at); return ( <>
- {/* */} Notifications {allNotifications.map((notification, index) => ( - + ))} diff --git a/components/ProfileFeed.tsx b/components/ProfileFeed.tsx index 5d284d3..b4e9d00 100644 --- a/components/ProfileFeed.tsx +++ b/components/ProfileFeed.tsx @@ -1,10 +1,9 @@ import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; -import NoteCard from '@/components/NoteCard'; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import KIND20Card from "./KIND20Card"; import { getImageUrl } from "@/utils/utils"; +import { useNostrEvents } from "@/hooks/useNDK"; interface ProfileFeedProps { pubkey: string; @@ -18,7 +17,7 @@ const ProfileFeed: React.FC = ({ pubkey }) => { filter: { authors: [pubkey], kinds: [20], - limit: limit, + limit, }, }); @@ -46,7 +45,7 @@ const ProfileFeed: React.FC = ({ pubkey }) => { pubkey={event.pubkey} text={event.content} image={imageUrl} - event={event} + event={event.rawEvent()} tags={event.tags} eventId={event.id} showViewNoteCardButton={true} diff --git a/components/ProfileGalleryViewFeed.tsx b/components/ProfileGalleryViewFeed.tsx index 361408f..e1c7563 100644 --- a/components/ProfileGalleryViewFeed.tsx +++ b/components/ProfileGalleryViewFeed.tsx @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { useNostrEvents } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import { Skeleton } from "@/components/ui/skeleton"; import GalleryCard from "./GalleryCard"; @@ -8,7 +8,7 @@ interface ProfileGalleryViewFeedProps { } const ProfileGalleryViewFeed: React.FC = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const { isLoading, events } = useNostrEvents({ filter: { @@ -28,7 +28,7 @@ const ProfileGalleryViewFeed: React.FC = ({ pubkey return ( <>
- {imagesAndIds.length === 0 && isLoading ? ( + {isLoading ? ( <>
@@ -41,15 +41,9 @@ const ProfileGalleryViewFeed: React.FC = ({ pubkey
) : ( - imagesAndIds.map((galleryEntry) => ( - galleryEntry.images.map((imageUrl, index) => ( - + imagesAndIds.map(({ id, images }) => ( + images.map((image, index) => ( + )) )) )} diff --git a/components/ProfileInfoCard.tsx b/components/ProfileInfoCard.tsx index 68ae0a1..53f6c56 100644 --- a/components/ProfileInfoCard.tsx +++ b/components/ProfileInfoCard.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { useProfile } from "nostr-react"; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { AvatarImage } from '@radix-ui/react-avatar'; import { Avatar } from '@/components/ui/avatar'; @@ -22,13 +21,13 @@ import { import { Input } from './ui/input'; import { Share1Icon } from '@radix-ui/react-icons'; import { toast } from './ui/use-toast'; +import { useProfile } from '@/hooks/useNDK'; interface ProfileInfoCardProps { pubkey: string; } const ProfileInfoCard: React.FC = React.memo(({ pubkey }) => { - let userPubkey = ''; let host = ''; if (typeof window !== 'undefined') { @@ -36,7 +35,7 @@ const ProfileInfoCard: React.FC = React.memo(({ pubkey }) host = window.location.host; } - const { data: userData, isLoading } = useProfile({ pubkey }); + const { data: userData, isLoading } = useProfile(pubkey); const npubShortened = useMemo(() => { let encoded = nip19.npubEncode(pubkey); @@ -44,7 +43,7 @@ const ProfileInfoCard: React.FC = React.memo(({ pubkey }) return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); }, [pubkey]); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; + const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || npubShortened; const description = userData?.about?.replace(/(?:\r\n|\r|\n)/g, '
'); const nip05 = userData?.nip05; @@ -88,7 +87,7 @@ const ProfileInfoCard: React.FC = React.memo(({ pubkey })
- + {title}
diff --git a/components/ProfileQuickViewFeed.tsx b/components/ProfileQuickViewFeed.tsx index 4ea7f8c..0d01937 100644 --- a/components/ProfileQuickViewFeed.tsx +++ b/components/ProfileQuickViewFeed.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; @@ -10,7 +10,7 @@ interface ProfileQuickViewFeedProps { } const ProfileQuickViewFeed: React.FC = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const [limit, setLimit] = useState(20); const { isLoading, events } = useNostrEvents({ @@ -45,15 +45,15 @@ const ProfileQuickViewFeed: React.FC = ({ pubkey }) = {events.map((event) => { const imageUrl = getImageUrl(event.tags); return imageUrl ? ( - ) : null; })} diff --git a/components/ProfileTextFeed.tsx b/components/ProfileTextFeed.tsx index 347bc7c..5cf483e 100644 --- a/components/ProfileTextFeed.tsx +++ b/components/ProfileTextFeed.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import NoteCard from '@/components/NoteCard'; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; @@ -20,9 +20,9 @@ const ProfileTextFeed: React.FC = ({ pubkey }) => { }, }); - // filter out all images since we only want text messages + // Filter out all images since we only want text messages let filteredEvents = events.filter((event) => !event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g)?.[0]); - // filter out all replies (tag[0] == e) + // Filter out all replies (tag[0] == e) filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); const loadMore = () => { @@ -47,7 +47,7 @@ const ProfileTextFeed: React.FC = ({ pubkey }) => { key={event.id} pubkey={event.pubkey} text={event.content} - event={event} + event={event.rawEvent()} tags={event.tags} eventId={event.id} showViewNoteCardButton={true} diff --git a/components/QuickViewKind20NoteCard.tsx b/components/QuickViewKind20NoteCard.tsx index d288c4c..04041b3 100644 --- a/components/QuickViewKind20NoteCard.tsx +++ b/components/QuickViewKind20NoteCard.tsx @@ -1,14 +1,11 @@ import React, { useState } from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; +import { useProfile } from "@/hooks/useNDK"; +import { nip19 } from "nostr-tools"; import { Card, SmallCardContent, } from "@/components/ui/card" import Link from 'next/link'; -import Image from 'next/image'; import { extractDimensions } from '@/utils/utils'; interface QuickViewKind20NoteCardProps { @@ -21,50 +18,51 @@ interface QuickViewKind20NoteCardProps { linkToNote: boolean; } -const QuickViewKind20NoteCard: React.FC = ({ pubkey, text, image, eventId, tags, event, linkToNote }) => { - const {data, isLoading} = useProfile({ - pubkey, - }); +const QuickViewKind20NoteCard: React.FC = ({ + pubkey, + text, + image, + eventId, + tags, + event, + linkToNote +}) => { + const { data: userData } = useProfile(pubkey); const [imageError, setImageError] = useState(false); if (!image || imageError) return null; text = text.replaceAll('\n', ' '); - const encodedNoteId = nip19.noteEncode(event.id) - + const encodedNoteId = nip19.noteEncode(event.id); const { width, height } = extractDimensions(event); const card = ( -
-
- {text} setImageError(true)} - /> -
-
+ {text} setImageError(true)} + />
); - return ( - <> - {linkToNote ? ( - - {card} - - ) : ( - card - )} - - ); + if (linkToNote) { + return ( + + {card} + + ); + } + + return card; } export default QuickViewKind20NoteCard; \ No newline at end of file diff --git a/components/ReactionButton.tsx b/components/ReactionButton.tsx index 79d75e8..9463e18 100644 --- a/components/ReactionButton.tsx +++ b/components/ReactionButton.tsx @@ -1,5 +1,4 @@ -import { useNostr, useNostrEvents } from "nostr-react" -import type { Event as NostrEvent } from "nostr-tools" +import { useNostrEvents, useNDK } from "@/hooks/useNDK" import { Button } from "@/components/ui/button" import { Drawer, @@ -12,12 +11,10 @@ import { } from "@/components/ui/drawer" import { ReloadIcon } from "@radix-ui/react-icons" import ReactionButtonReactionList from "./ReactionButtonReactionList" -import { signEvent } from "@/utils/utils" import { useState, useEffect, useMemo } from "react" export default function ReactionButton({ event }: { event: any }) { - const { publish } = useNostr() - + const ndk = useNDK() const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null const loggedInUserPublicKey = typeof window !== "undefined" ? window.localStorage.getItem("pubkey") : null @@ -53,30 +50,32 @@ export default function ReactionButton({ event }: { event: any }) { const onPost = async (icon: string) => { const message = icon || "+" - const likeEvent: NostrEvent = { - content: message, - kind: 7, - tags: [], - created_at: Math.floor(Date.now() / 1000), - pubkey: "", - id: "", - sig: "", - } + const ndkEvent = ndk.getEvent() + ndkEvent.kind = 7 + ndkEvent.tags.push(["e", event.id]) + ndkEvent.tags.push(["p", event.pubkey]) + ndkEvent.tags.push(["k", event.kind.toString()]) + ndkEvent.content = message + ndkEvent.created_at = Math.floor(Date.now() / 1000) - likeEvent.tags.push(["e", event.id]) - likeEvent.tags.push(["p", event.pubkey]) - likeEvent.tags.push(["k", event.kind.toString()]) + try { + if (loginType === "extension") { + await window.nostr.signEvent(ndkEvent.rawEvent()) + } else if (loginType === "amber") { + alert("Signing with Amber is not implemented yet, sorry!") + return + } else if (loginType === "raw_nsec") { + const nsecStr = window.localStorage.getItem("nsec") + if (!nsecStr) throw new Error("No nsec found") + await ndkEvent.sign() + } - const signedEvent = await signEvent(loginType, likeEvent) - - if (signedEvent) { - publish(signedEvent) + await ndkEvent.publish() setLiked(true) setLikeIcon(message) - filteredEvents.push(signedEvent) - } else { - console.error("Failed to sign event") - alert("Failed to sign event") + filteredEvents.push(ndkEvent) + } catch (error) { + console.error("Failed to sign/publish event", error) } } diff --git a/components/ReactionButtonReactionList.tsx b/components/ReactionButtonReactionList.tsx index 13d0a7f..a33a066 100644 --- a/components/ReactionButtonReactionList.tsx +++ b/components/ReactionButtonReactionList.tsx @@ -1,12 +1,15 @@ import { ScrollArea } from "@/components/ui/scroll-area" import ReactionButtonReactionListItem from "./ReactionButtonReactionListItem"; +import { NDKEvent } from '@nostr-dev-kit/ndk'; -export default function ReactionButtonReactionList({ filteredEvents }: { filteredEvents: any }) { +export default function ReactionButtonReactionList({ filteredEvents }: { filteredEvents: NDKEvent[] }) { return ( - - {filteredEvents.map((event: any) => ( - - ))} - +
+
+ {filteredEvents.map((event) => ( + + ))} +
+
); } \ No newline at end of file diff --git a/components/ReactionButtonReactionListItem.tsx b/components/ReactionButtonReactionListItem.tsx index 6dff4cc..02d8c2b 100644 --- a/components/ReactionButtonReactionListItem.tsx +++ b/components/ReactionButtonReactionListItem.tsx @@ -1,36 +1,21 @@ import Link from "next/link"; -import { useNostr, dateToUnix, useNostrEvents, useProfile } from "nostr-react"; - -import { - type Event as NostrEvent, - getEventHash, - getPublicKey, - finalizeEvent, - nip19, -} from "nostr-tools"; +import { useProfile } from "@/hooks/useNDK"; +import { nip19, type Event as NostrEvent } from "nostr-tools"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; export default function ReactionButtonReactionListItem({ event }: { event: NostrEvent }) { + const { data: userData } = useProfile(event.pubkey); - let pubkey = event.pubkey; - - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || nip19.npubEncode(pubkey).slice(0, 8) + ':' + nip19.npubEncode(pubkey).slice(-3);; - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const title = userData?.displayName || userData?.name || userData?.nip05 || + nip19.npubEncode(event.pubkey).slice(0, 8) + ':' + nip19.npubEncode(event.pubkey).slice(-3); + const hrefProfile = `/profile/${nip19.npubEncode(event.pubkey)}`; + const profileImageSrc = userData?.image || "https://robohash.org/" + event.pubkey; const content = event.content; - console.log("event", event.content); - return ( -
+
- {/* */} diff --git a/components/ReelFeed.tsx b/components/ReelFeed.tsx index 8490427..3bd7ef4 100644 --- a/components/ReelFeed.tsx +++ b/components/ReelFeed.tsx @@ -1,36 +1,38 @@ import { useRef } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; +import { useNostrEvents } from "@/hooks/useNDK"; import NoteCard from './NoteCard'; const ReelFeed: React.FC = () => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const { events } = useNostrEvents({ filter: { - // since: dateToUnix(now.current), // all new events from now - // since: 0, - // limit: 100, kinds: [1063], + since: Math.floor(now.current.getTime() / 1000) - 24 * 60 * 60, // Last 24 hours }, }); - // const filteredEvents = events.filter((event) => event.content.includes(".jpg")); - // filter events with regex that checks for png, jpg, or gif + // Filter events with media content let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g)?.[0]); - // now filter all events with a tag[0] == t and tag[1] == nsfw - // filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "t" && tag[1] == "nsfw")); - filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 't' && tag[1] == 'nsfw'})); - // filter out all replies - filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + // Filter out NSFW content + filteredEvents = filteredEvents.filter((event) => { + return !event.tags.some(tag => tag[0] === 't' && tag[1] === 'nsfw'); + }); return ( <> -

Reel Feed

{filteredEvents.map((event) => ( - //

{event.pubkey} posted: {event.content}

- +
))} diff --git a/components/TagFeed.tsx b/components/TagFeed.tsx index 1b6744c..2b122c7 100644 --- a/components/TagFeed.tsx +++ b/components/TagFeed.tsx @@ -1,6 +1,5 @@ import { useRef } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; -import NoteCard from './NoteCard'; +import { useNostrEvents } from "@/hooks/useNDK"; import KIND20Card from "./KIND20Card"; import { getImageUrl } from "@/utils/utils"; @@ -9,15 +8,13 @@ interface TagFeedProps { } const TagFeed: React.FC = ({tag}) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered + const now = useRef(new Date()); const { events } = useNostrEvents({ filter: { - // since: dateToUnix(now.current), // all new events from now - // since: 0, - // limit: 100, kinds: [20], "#t": [tag], + since: Math.floor(now.current.getTime() / 1000) - 7 * 24 * 60 * 60, // Last week }, }); @@ -25,9 +22,17 @@ const TagFeed: React.FC = ({tag}) => { <>

Tag Feed for {tag}

{events.map((event) => ( - //

{event.pubkey} posted: {event.content}

- +
))} diff --git a/components/TrendingAccounts.tsx b/components/TrendingAccounts.tsx index bcc48a2..14b34bf 100644 --- a/components/TrendingAccounts.tsx +++ b/components/TrendingAccounts.tsx @@ -1,17 +1,25 @@ import React, { useState, useEffect } from 'react'; import TrendingAccount from '@/components/TrendingAccount'; +import { useNDK } from '@/hooks/useNDK'; export function TrendingAccounts() { + const ndk = useNDK(); const [profiles, setProfiles] = useState([]); useEffect(() => { fetch('https://api.nostr.band/v0/trending/profiles') .then(res => res.json()) - .then(data => setProfiles(data.profiles)) + .then(data => { + // Pre-fetch profiles to have them in NDK cache + data.profiles.forEach((profile: any) => { + ndk.getUser({ pubkey: profile.pubkey }).fetchProfile(); + }); + setProfiles(data.profiles); + }) .catch(error => { console.error('Error calling trending profiles:', error); }); - }, []); + }, [ndk]); return (
diff --git a/components/TrendingImage.tsx b/components/TrendingImage.tsx index b11b1f3..5862468 100644 --- a/components/TrendingImage.tsx +++ b/components/TrendingImage.tsx @@ -1,18 +1,15 @@ import React, { useMemo } from 'react'; -import { useNostr, useNostrEvents, useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; +import { nip19 } from "nostr-tools"; import { Card, CardHeader, CardTitle, SmallCardContent, } from "@/components/ui/card" -import Image from 'next/image'; import Link from 'next/link'; import { Avatar } from './ui/avatar'; import { AvatarImage } from '@radix-ui/react-avatar'; +import { useProfile, useNostrEvents } from '@/hooks/useNDK'; interface TrendingImageProps { eventId: string; @@ -20,9 +17,7 @@ interface TrendingImageProps { } const TrendingImage: React.FC = ({ eventId, pubkey }) => { - const { data: userData } = useProfile({ - pubkey, - }); + const { data: userData } = useProfile(pubkey); const { events } = useNostrEvents({ filter: { @@ -37,47 +32,45 @@ const TrendingImage: React.FC = ({ eventId, pubkey }) => { return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); }, [pubkey]); - let text = events && events.length > 0 ? events[0].content : ''; - const createdAt = events && events.length > 0 ? new Date(events[0].created_at * 1000) : new Date(); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - text = text.replaceAll('\n', ' '); - const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g, ''); + const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || npubShortened; const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const profileImageSrc = userData?.image || "https://robohash.org/" + pubkey; + + if (events.length === 0) return null; + + const event = events[0]; + const imageSrc = event.content.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g)?.[0]; + if (!imageSrc) return null; return ( - <> - - - + + + +
- + - {title} + {title}
-
-
- -
-
- {imageSrc && imageSrc.length > 0 && ( -
- - {text} - -
- // {text} - //
- // {text} - //
- )} -
-
-
-
- + + + + + + {`Trending + + + ); } diff --git a/components/TrendingImages.tsx b/components/TrendingImages.tsx index 10c09c7..ae1cfb9 100644 --- a/components/TrendingImages.tsx +++ b/components/TrendingImages.tsx @@ -1,25 +1,32 @@ import React, { useState, useEffect } from 'react'; import TrendingImage from './TrendingImage'; +import { useNDK } from '@/hooks/useNDK'; export function TrendingImages() { - const [profiles, setProfiles] = useState([]); + const ndk = useNDK(); + const [images, setImages] = useState([]); useEffect(() => { fetch('https://api.nostr.band/v0/trending/images') .then(res => res.json()) - .then(data => setProfiles(data.images)) + .then(data => { + // Pre-fetch events to have them in NDK cache + data.images.forEach((image: any) => { + ndk.getEvent(image.id); + }); + setImages(data.images); + }) .catch(error => { - console.error('Error calling trending profiles:', error); + console.error('Error calling trending images:', error); }); - }, []); + }, [ndk]); return (

Currently Trending

- {profiles && profiles.length > 0 && profiles.map((profile, index) => ( - //

{profile.id}

- + {images && images.length > 0 && images.map((image, index) => ( + ))}
diff --git a/components/UpdateProfileForm.tsx b/components/UpdateProfileForm.tsx index 57c5e56..aa49e5b 100644 --- a/components/UpdateProfileForm.tsx +++ b/components/UpdateProfileForm.tsx @@ -3,41 +3,28 @@ import React, { useState, useEffect } from 'react'; import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' import { nip19 } from "nostr-tools" import { Label } from "./ui/label" import { Textarea } from "@/components/ui/textarea" -import { finalizeEvent, verifyEvent } from 'nostr-tools/pure' -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' -import { useNostr, useProfile } from 'nostr-react'; +import { useProfile, useNDK } from '@/hooks/useNDK'; export function UpdateProfileForm() { - - const { publish } = useNostr(); + const ndk = useNDK(); let npub = ''; let pubkey = ''; - let nsec: Uint8Array; if (typeof window !== 'undefined') { pubkey = window.localStorage.getItem("pubkey") ?? ''; - const nsecHex = window.localStorage.getItem("nsec"); - if (pubkey && pubkey.length > 0) { npub = nip19.npubEncode(pubkey); } - - if (nsecHex && nsecHex.length > 0) { - nsec = hexToBytes(nsecHex); - } } - let { data: userData } = useProfile({ - pubkey, - }); + const { data: userData } = useProfile(pubkey); const [username, setUsername] = useState(userData?.name); - const [displayName, setDisplayName] = useState(userData?.display_name); + const [displayName, setDisplayName] = useState(userData?.displayName); const [bio, setBio] = useState(userData?.about); const handleUsernameChange = (event: React.ChangeEvent) => { @@ -51,27 +38,30 @@ export function UpdateProfileForm() { }; async function handleProfileUpdate() { - const username = (document.getElementById('username') as HTMLInputElement).value; - const bio = (document.getElementById('bio') as HTMLInputElement).value; - const displayname = (document.getElementById('displayname') as HTMLInputElement).value; + const ndkEvent = ndk.getEvent(); + ndkEvent.kind = 0; + ndkEvent.created_at = Math.floor(Date.now() / 1000); + ndkEvent.content = JSON.stringify({ + name: username, + displayName: displayName, + about: bio + }); - if (nsec) { - let event = finalizeEvent({ - kind: 0, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: `{"name": "${username}", "about": "${bio}"}`, - }, nsec); - - let isGood = verifyEvent(event); - - // console.log('isGood: ' + isGood); - // console.log(event); - - if (isGood) { - publish(event); - window.location.href = `/profile/${npub}`; + try { + const loginType = window.localStorage.getItem("loginType"); + if (loginType === "extension") { + const signedEvent = await window.nostr.signEvent(ndkEvent.rawEvent()); + Object.assign(ndkEvent, signedEvent); + } else if (loginType === "raw_nsec") { + const nsecStr = window.localStorage.getItem("nsec"); + if (!nsecStr) throw new Error("No nsec found"); + await ndkEvent.sign(); } + + await ndkEvent.publish(); + window.location.href = `/profile/${npub}`; + } catch (error) { + console.error("Failed to update profile:", error); } } @@ -92,7 +82,6 @@ export function UpdateProfileForm() {
- {/* */} -
- -
-
- {/* */} - Upload to - -
- {previewUrl && Preview} - {isLoading ? ( - - ) : ( - - )} - -
- - - - Upload Status - - {isNoteLoading ? ( -
- - Checking note status... -
- ) : events.length > 0 ? ( -
- Success! - Note found with ID: - - {`${events[0].id.slice(0, 5)}...${events[0].id.slice(-3)}`} - -
- ) : ( -

Note not found. It may take a moment to propagate.

- )} -
-
- - {events.length === 0 && ( -