diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx
new file mode 100644
index 0000000..d851a44
--- /dev/null
+++ b/src/components/feed/NoteCard.tsx
@@ -0,0 +1,106 @@
+import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
+import { useAuthor } from '@/hooks/useAuthor';
+import { genUserName } from '@/lib/genUserName';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { useNavigate } from 'react-router-dom';
+import { nip19 } from 'nostr-tools';
+import { ReactionButton } from '@/components/ReactionButton';
+import { ZapButton } from '@/components/ZapButton';
+import { NoteContent } from '@/components/NoteContent';
+
+interface NoteCardProps {
+ event: NostrEvent;
+}
+
+export function NoteCard({ event }: NoteCardProps) {
+ const author = useAuthor(event.pubkey);
+ const metadata: NostrMetadata | undefined = author.data?.metadata;
+ const navigate = useNavigate();
+
+ const displayName = metadata?.name ?? genUserName(event.pubkey);
+ const profileImage = metadata?.picture;
+ const npub = nip19.npubEncode(event.pubkey);
+ const nevent = nip19.neventEncode({ id: event.id, author: event.pubkey });
+
+ const formatTimestamp = (timestamp: number) => {
+ const date = new Date(timestamp * 1000);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+
+ return date.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
+ });
+ };
+
+ const handleCardClick = (e: React.MouseEvent) => {
+ // Don't navigate if clicking on interactive elements
+ const target = e.target as HTMLElement;
+ if (
+ target.closest('button') ||
+ target.closest('a') ||
+ target.tagName === 'BUTTON' ||
+ target.tagName === 'A'
+ ) {
+ return;
+ }
+ navigate(`/${nevent}`);
+ };
+
+ return (
+
+
+
+
+
{
+ e.stopPropagation();
+ navigate(`/${npub}`);
+ }}
+ >
+
+ {displayName[0]?.toUpperCase()}
+
+
+
+
+ {formatTimestamp(event.created_at)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/hooks/useUserNotes.ts b/src/hooks/useUserNotes.ts
new file mode 100644
index 0000000..982f73c
--- /dev/null
+++ b/src/hooks/useUserNotes.ts
@@ -0,0 +1,32 @@
+import { useNostr } from '@nostrify/react';
+import { useInfiniteQuery } from '@tanstack/react-query';
+import type { NostrEvent } from '@nostrify/nostrify';
+
+/**
+ * Hook for fetching a specific user's kind 1 (NIP-10) text notes with infinite scroll
+ */
+export function useUserNotes(pubkey: string) {
+ const { nostr } = useNostr();
+
+ return useInfiniteQuery({
+ queryKey: ['user-notes', pubkey],
+ queryFn: async ({ pageParam, signal }) => {
+ const filter = pageParam
+ ? { kinds: [1], authors: [pubkey], limit: 20, until: pageParam as number }
+ : { kinds: [1], authors: [pubkey], limit: 20 };
+
+ const events = await nostr.query([filter], {
+ signal: AbortSignal.any([signal, AbortSignal.timeout(1500)])
+ });
+
+ return events as NostrEvent[];
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.length === 0) return undefined;
+ // Subtract 1 since 'until' is inclusive
+ return lastPage[lastPage.length - 1].created_at - 1;
+ },
+ initialPageParam: undefined as number | undefined,
+ enabled: !!pubkey,
+ });
+}
diff --git a/src/pages/ProfileView.tsx b/src/pages/ProfileView.tsx
index 526eb4e..e79d9c0 100644
--- a/src/pages/ProfileView.tsx
+++ b/src/pages/ProfileView.tsx
@@ -1,23 +1,28 @@
import { useAuthor } from '@/hooks/useAuthor';
import { useUserPictures } from '@/hooks/useUserPictures';
+import { useUserNotes } from '@/hooks/useUserNotes';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
-import { Loader2 } from 'lucide-react';
+import { Loader2, Grid3x3, LayoutGrid, FileText } from 'lucide-react';
import { ProfileHeader } from '@/components/profile/ProfileHeader';
import { MinimalPictureCard } from '@/components/feed/MinimalPictureCard';
+import { PictureCard } from '@/components/feed/PictureCard';
+import { NoteCard } from '@/components/feed/NoteCard';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useState } from 'react';
export function ProfileView({ pubkey }: { pubkey: string }) {
+ const [activeTab, setActiveTab] = useState('minimal');
const author = useAuthor(pubkey);
- const {
- data,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
- isLoading,
- } = useUserPictures(pubkey);
+
+ const minimalPictures = useUserPictures(pubkey);
+ const fullPictures = useUserPictures(pubkey);
+ const notes = useUserNotes(pubkey);
- const pictures = data?.pages.flat() || [];
+ const minimalPicturesData = minimalPictures.data?.pages.flat() || [];
+ const fullPicturesData = fullPictures.data?.pages.flat() || [];
+ const notesData = notes.data?.pages.flat() || [];
return (
@@ -29,54 +34,182 @@ export function ProfileView({ pubkey }: { pubkey: string }) {
isLoading={author.isLoading}
/>
- {/* Pictures Section */}
-
-
Pictures
-
- {isLoading ? (
-
- {[...Array(8)].map((_, i) => (
-
- ))}
-
- ) : pictures.length === 0 ? (
-
-
-
- No pictures found for this user.
-
-
-
- ) : (
- <>
+ {/* Tabbed Content */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Minimal Pictures Tab */}
+
+ {minimalPictures.isLoading ? (
- {pictures.map((event) => (
-
+ {[...Array(8)].map((_, i) => (
+
))}
-
- {hasNextPage && (
-
-
+ ) : minimalPicturesData.length === 0 ? (
+
+
+
+ No pictures found for this user.
+
+
+
+ ) : (
+ <>
+
+ {minimalPicturesData.map((event) => (
+
+ ))}
- )}
- >
- )}
-
+
+ {minimalPictures.hasNextPage && (
+
+
+
+ )}
+ >
+ )}
+
+
+ {/* Full Pictures Tab */}
+
+ {fullPictures.isLoading ? (
+
+ {[...Array(6)].map((_, i) => (
+
+
+
+
+
+
+
+ ))}
+
+ ) : fullPicturesData.length === 0 ? (
+
+
+
+ No pictures found for this user.
+
+
+
+ ) : (
+ <>
+
+ {fullPicturesData.map((event) => (
+
+ ))}
+
+
+ {fullPictures.hasNextPage && (
+
+
+
+ )}
+ >
+ )}
+
+
+ {/* Notes Tab */}
+
+ {notes.isLoading ? (
+
+ {[...Array(5)].map((_, i) => (
+
+
+
+ ))}
+
+ ) : notesData.length === 0 ? (
+
+
+
+ No notes found for this user.
+
+
+
+ ) : (
+ <>
+
+ {notesData.map((event) => (
+
+ ))}
+
+
+ {notes.hasNextPage && (
+
+
+
+ )}
+ >
+ )}
+
+
);