diff --git a/src/App.tsx b/src/App.tsx index bb1e97d..f17cd10 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { Sidebar } from "./components/sidebar/Sidebar"; import { Feed } from "./components/feed/Feed"; import { RelaysView } from "./components/shared/RelaysView"; import { SettingsView } from "./components/shared/SettingsView"; +import { ProfileView } from "./components/profile/ProfileView"; import { useUIStore } from "./stores/ui"; function App() { @@ -14,6 +15,7 @@ function App() { {currentView === "feed" && } {currentView === "relays" && } {currentView === "settings" && } + {currentView === "profile" && } ); diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index 8767e36..883c4fe 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -2,6 +2,7 @@ import { useState, useRef } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useProfile } from "../../hooks/useProfile"; import { useUserStore } from "../../stores/user"; +import { useUIStore } from "../../stores/ui"; import { timeAgo, shortenPubkey } from "../../lib/utils"; import { publishReaction, publishReply } from "../../lib/nostr"; import { NoteContent } from "./NoteContent"; @@ -18,6 +19,7 @@ export function NoteCard({ event }: NoteCardProps) { const time = event.created_at ? timeAgo(event.created_at) : ""; const { loggedIn } = useUserStore(); + const { openProfile } = useUIStore(); const [liked, setLiked] = useState(false); const [liking, setLiking] = useState(false); const [showReply, setShowReply] = useState(false); @@ -68,19 +70,19 @@ export function NoteCard({ event }: NoteCardProps) {
{/* Avatar */} -
+
openProfile(event.pubkey)}> {avatar ? ( { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : ( -
+
{name.charAt(0).toUpperCase()}
)} @@ -89,7 +91,10 @@ export function NoteCard({ event }: NoteCardProps) { {/* Content */}
- {name} + openProfile(event.pubkey)} + >{name} {nip05 && ( {nip05} )} diff --git a/src/components/profile/ProfileView.tsx b/src/components/profile/ProfileView.tsx new file mode 100644 index 0000000..e89d81d --- /dev/null +++ b/src/components/profile/ProfileView.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { useUIStore } from "../../stores/ui"; +import { useProfile } from "../../hooks/useProfile"; +import { fetchUserNotes } from "../../lib/nostr"; +import { shortenPubkey } from "../../lib/utils"; +import { NoteCard } from "../feed/NoteCard"; + +export function ProfileView() { + const { selectedPubkey, setView } = useUIStore(); + const pubkey = selectedPubkey!; + const profile = useProfile(pubkey); + + const [notes, setNotes] = useState([]); + const [loading, setLoading] = useState(true); + + const name = profile?.displayName || profile?.name || shortenPubkey(pubkey); + const avatar = profile?.picture; + const about = profile?.about; + const nip05 = profile?.nip05; + const website = profile?.website; + + useEffect(() => { + setLoading(true); + fetchUserNotes(pubkey).then((events) => { + setNotes(events); + setLoading(false); + }).catch(() => setLoading(false)); + }, [pubkey]); + + return ( +
+ {/* Header */} +
+ +

Profile

+
+ +
+ {/* Profile card */} +
+
+ {avatar ? ( + { (e.target as HTMLImageElement).style.display = "none"; }} + /> + ) : ( +
+ {name.charAt(0).toUpperCase()} +
+ )} + +
+
{name}
+ {nip05 && ( +
{nip05}
+ )} + {website && ( + + {website.replace(/^https?:\/\//, "")} + + )} + {about && ( +

+ {about} +

+ )} +
+ {shortenPubkey(pubkey)} +
+
+
+
+ + {/* Notes */} + {loading && ( +
+ Loading notes… +
+ )} + + {!loading && notes.length === 0 && ( +
+ No notes found. +
+ )} + + {notes.map((event) => ( + + ))} +
+
+ ); +} diff --git a/src/lib/nostr/client.ts b/src/lib/nostr/client.ts index a85b49a..1b4f581 100644 --- a/src/lib/nostr/client.ts +++ b/src/lib/nostr/client.ts @@ -99,6 +99,19 @@ export async function publishNote(content: string): Promise { await event.publish(); } +export async function fetchUserNotes(pubkey: string, limit = 30): Promise { + const instance = getNDK(); + const filter: NDKFilter = { + kinds: [NDKKind.Text], + authors: [pubkey], + limit, + }; + const events = await instance.fetchEvents(filter, { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + }); + return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)); +} + export async function fetchProfile(pubkey: string) { const instance = getNDK(); const user = instance.getUser({ pubkey }); diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index cf11870..07bfd2d 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1 +1 @@ -export { getNDK, connectToRelays, fetchGlobalFeed, publishNote, publishReaction, publishReply, fetchProfile } from "./client"; +export { getNDK, connectToRelays, fetchGlobalFeed, publishNote, publishReaction, publishReply, fetchUserNotes, fetchProfile } from "./client"; diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 29869fc..28a9dfe 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,17 +1,21 @@ import { create } from "zustand"; -type View = "feed" | "relays" | "settings"; +type View = "feed" | "relays" | "settings" | "profile"; interface UIState { currentView: View; sidebarCollapsed: boolean; + selectedPubkey: string | null; setView: (view: View) => void; + openProfile: (pubkey: string) => void; toggleSidebar: () => void; } export const useUIStore = create((set) => ({ currentView: "feed", sidebarCollapsed: false, + selectedPubkey: null, setView: (currentView) => set({ currentView }), + openProfile: (pubkey) => set({ currentView: "profile", selectedPubkey: pubkey }), toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), }));