AI made all of this lol

This commit is contained in:
highperfocused
2025-02-13 10:57:18 +01:00
parent 0d1606bd31
commit 0bf2f7c307
39 changed files with 1179 additions and 1358 deletions

View File

@@ -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<NDK | null>(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 (
<html lang="en">
@@ -39,15 +48,15 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<TopNavigation />
<Toaster />
<Umami />
<div className="main-content pb-14">
<NostrProvider relayUrls={relayUrls} debug={false}>
<NDKContext.Provider value={ndk}>
<TopNavigation />
<Toaster />
<Umami />
<div className="main-content pb-14">
{children}
</NostrProvider>
</div>
<BottomBar />
</div>
<BottomBar />
</NDKContext.Provider>
</ThemeProvider>
</body>
</html>

View File

@@ -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<CommentCardProps> = ({ pubkey, text, eventId, tags, event }) => {
const { data: userData } = useProfile({
pubkey,
});
const CommentCard: React.FC<CommentCardProps> = ({ 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<CommentCardProps> = ({ pubkey, text, eventId, tags, eve
</CardTitle>
</CardHeader>
<CardContent>
<div className='py-4'>
{
<div className='w-full h-full px-10'>
{imageSrc && imageSrc.length > 1 ? (
<Carousel>
<CarouselContent>
{imageSrc.map((src, index) => (
<CarouselItem key={index}>
<img
key={index}
src={src}
className='rounded lg:rounded-lg'
style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'contain', margin: 'auto' }}
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
) : (
imageSrc ? <img src={imageSrc[0]} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'contain', margin: 'auto' }} /> : ""
)}
</div>
}
<br />
<div className='break-word overflow-hidden'>
{textWithoutImage}
</div>
<div className="space-y-2">
<p className='break-words whitespace-pre-wrap'>{textWithoutImage}</p>
{imageSrc && imageSrc.length > 0 && (
<Carousel>
<CarouselContent>
{imageSrc.map((image, index) => (
<CarouselItem key={index}>
<img src={image} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '75vh', objectFit: 'contain', margin: 'auto' }} alt={`Image ${index + 1}`} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)}
</div>
<hr />
<div className='py-4 space-x-4 flex justify-between items-start'>
<div className='flex space-x-4'>
<ReactionButton event={event} />
<hr className="my-4" />
<div className="flex justify-between items-start">
<ReactionButton event={event} />
<div>
<ViewRawButton event={event} />
</div>
<ViewRawButton event={event} />
</div>
</CardContent>
<CardFooter>
<small className="text-muted">{createdAt.toLocaleString()}</small>
<small className="text-muted-foreground">
{createdAt.toLocaleString()}
</small>
</CardFooter>
</Card>
</>
);
}
export default NoteCard;
export default CommentCard;

View File

@@ -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<CommentsCompontentProps> = ({ pubkey, event }) => {
const { events } = useNostrEvents({
filter: {
kinds: [1],
@@ -24,7 +20,14 @@ const CommentsCompontent: React.FC<CommentsCompontentProps> = ({ pubkey, event }
<h1 className='text-xl'>Comments</h1>
{events.map((event) => (
<div key={event.id} className="py-6">
<CommentCard key={event.id} pubkey={event.pubkey} text={event.content} eventId={event.id} tags={event.tags} event={event} />
<CommentCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
eventId={event.id}
tags={event.tags}
event={event.rawEvent()}
/>
</div>
))}
</>

View File

@@ -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<FollowButtonProps> = ({ 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<FollowButtonProps> = ({ 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 (
<Button className='w-full' onClick={handleFollow} disabled>
{isFollowing ? 'Unfollow' : 'Follow'}
<Button
variant={isFollowing ? "default" : "outline"}
className="w-full"
onClick={handleFollow}
>
{isFollowing ? "Following" : "Follow"}
</Button>
);
};
}
export default FollowButton;

View File

@@ -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<FollowerFeedProps> = ({ 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<FollowerFeedProps> = ({ pubkey }) => {
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 px-2 md:px-4">
{events.map((event) => (
<div key={event.id} className="mb-4 md:mb-6">
<KIND20Card
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={getImageUrl(event.tags)}
eventId={event.id}
tags={event.tags}
event={event}
showViewNoteCardButton={true}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{events.length === 0 && isLoading ? (
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
))}
) : events.some(event => getImageUrl(event.tags)) ? (
<>
{events.map((event) => {
const imageUrl = getImageUrl(event.tags);
return imageUrl ? (
<KIND20Card
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={imageUrl}
event={event.rawEvent()}
tags={event.tags}
eventId={event.id}
showViewNoteCardButton={true}
/>
) : null;
})}
</>
) : (
<div className="flex flex-col items-center justify-center py-10 text-gray-500"></div>
<p className="text-lg">No posts found :(</p>
</div>
)}
</div>
{!isLoading && (
<div className="flex justify-center p-4">
<Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button>
{!isLoading && events.some(event => getImageUrl(event.tags)) ? (
<div className="flex justify-center p-4"></div>
<Button className="w-full" onClick={loadMore}>Load More</Button>
</div>
)}
) : null}
</>
);
}

View File

@@ -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<FollowerQuickViewFeedProps> = ({ 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<FollowerQuickViewFeedProps> = ({ 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<FollowerQuickViewFeedProps> = ({ pubkey })
{events.length === 0 && isLoading ? (
<>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
<Skeleton className="h-[125px] rounded-xl" />
</div>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
<Skeleton className="h-[125px] rounded-xl" />
</div>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
</div>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
</div>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
</div>
<div>
<Skeleton className="h-[33vh] rounded-xl" />
<Skeleton className="h-[125px] rounded-xl" />
</div>
</>
) : events.some(event => getImageUrl(event.tags)) ? (
<>
{events.map((event) => {
const imageUrl = getImageUrl(event.tags);
return imageUrl ? (
<QuickViewKind20NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={imageUrl}
event={event.rawEvent()}
tags={event.tags}
eventId={event.id}
linkToNote={true}
/>
) : null;
})}
</>
) : (
events.map((event) => (
<QuickViewKind20NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={getImageUrl(event.tags)}
event={event}
tags={event.tags}
eventId={event.id}
linkToNote={true}
/>
))
<div className="col-span-3 flex flex-col items-center justify-center py-10 text-gray-500">
<p className="text-lg">No posts found :(</p>
</div>
)}
</div>
{!isLoading && (
{!isLoading && events.some(event => getImageUrl(event.tags)) ? (
<div className="flex justify-center p-4">
<Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button>
<Button className="w-full" onClick={loadMore}>Load More</Button>
</div>
)}
) : null}
</>
);
}

View File

@@ -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}
/>
</div>

View File

@@ -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<KIND20CardProps> = ({
@@ -34,24 +33,20 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
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 (
<>
<div key={event.id} className="py-6">
<Card>
<CardHeader>
@@ -64,9 +59,7 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
<Avatar>
<AvatarImage src={profileImageSrc} />
</Avatar>
<span className="break-all" style={{ marginLeft: "10px" }}>
{title}
</span>
<span className="break-all" style={{ marginLeft: "10px" }}>{title}</span>
</div>
</TooltipTrigger>
<TooltipContent>
@@ -77,22 +70,29 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
</Link>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="px-2 sm:px-4">
<div className="w-full">
<div className="relative w-full" style={{ paddingBottom: "100%" }}>
<Image
src={image}
alt={text}
fill
className="rounded-lg object-contain"
onError={() => setImageError(true)}
/>
<CardContent>
<div>
<div className="space-y-2">
{text && <p className="break-words whitespace-pre-wrap">{text}</p>}
<div>
<div className="d-flex justify-content-center align-items-center">
<div style={{ position: "relative" }}>
<img
src={image}
className="rounded lg:rounded-lg"
style={{
maxWidth: "100%",
maxHeight: "75vh",
objectFit: "contain",
margin: "auto",
}}
alt={text}
onError={() => setImageError(true)}
/>
</div>
</div>
</div>
</div>
</div>
<div className="p-4">
<div className="break-word overflow-hidden">{text}</div>
<hr className="my-4" />
<div className="space-x-4 flex justify-between items-start">
<div className="flex space-x-4">

View File

@@ -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<HTMLInputElement>(null);
let npubInput = useRef<HTMLInputElement>(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 (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle className="text-2xl">Login to Lumina</CardTitle>
<CardDescription>
Login to your account either with a nostr extension or with your nsec.
</CardDescription>
<CardTitle>Login</CardTitle>
<CardDescription>Login with your preferred method.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid grid-cols-8 gap-2">
<Button className="w-full col-span-7" onClick={handleExtensionLogin}>Sign in with Extension (NIP-07)</Button>
<Link target="_blank" href="https://www.getflamingo.org/">
<Button variant={"outline"}><InfoIcon /></Button>
</Link>
</div>
<div className="grid grid-cols-8 gap-2">
<Button className="w-full col-span-7" onClick={handleAmber}>Sign in with Amber</Button>
<Link target="_blank" href="https://github.com/greenart7c3/Amber">
<Button variant={"outline"}><InfoIcon /></Button>
</Link>
</div>
<hr />
or
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Login with npub (read-only)</AccordionTrigger>
<AccordionContent>
<div className="grid gap-2">
<Label htmlFor="npub">npub</Label>
<Input placeholder="npub1..." id="npub" ref={npubInput} type="text" />
<Button className="w-full" onClick={handleNpubLogin}>Sign in</Button>
<CardContent>
<div className='grid gap-4'>
<div>
<Button className='w-full' onClick={handleExtension}></Button>
Login with Extension
</Button>
</div>
<div>
<Button className='w-full' onClick={handleAmber}>
Login with Amber
</Button>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>
<div className='flex flex-row items-center'>
Login with Private Key (nsec)
<InfoIcon className='ml-2 h-4 w-4' />
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
or
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Login with nsec (not recommended)</AccordionTrigger>
<AccordionContent>
<div className="grid gap-2">
<Label htmlFor="nsec">nsec</Label>
<Input placeholder="nsecabcdefghijklmnopqrstuvwxyz" id="nsec" ref={nsecInput} type="password" />
<Button className="w-full" onClick={handleNsecLogin}>Sign in</Button>
</AccordionTrigger>
<AccordionContent>
<div className='py-4'>
<p className="text-sm text-muted-foreground">Warning: This is not recommended for security reasons.</p>
<div className='flex flex-row space-x-2 py-4'>
<Input type="text" placeholder="Enter nsec.." ref={nsecInput} />
<Button onClick={handleNsecLogin}>Login</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>
<div className='flex flex-row items-center'>
Login with Public Key (npub)
<InfoIcon className='ml-2 h-4 w-4' />
</div>
</AccordionTrigger>
<AccordionContent>
<div className='py-4'>
<p className="text-sm text-muted-foreground">Read-only mode - you won&apos;t be able to post.</p>
<div className='flex flex-row space-x-2 py-4'>
<Input type="text" placeholder="Enter npub.." ref={npubInput} />
<Button onClick={handleNpubLogin}>Login</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</CardContent>
<CardFooter>
<div className='w-full text-center'>
<p className="text-sm text-muted-foreground">Don&apos;t have an Account? <Link href="/onboarding">Create Account</Link></p>
</div>
</CardFooter>
</Card>
)

View File

@@ -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<NoteCardProps> = ({ 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', '<br />');
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<NoteCardProps> = ({ pubkey, text, eventId, tags, event,
</CardTitle>
</CardHeader>
<CardContent>
<div className='py-4'>
{
<div>
<div className='w-full h-full px-10'>
{imageSrc && imageSrc.length > 1 ? (
<Carousel>
<CarouselContent>
{imageSrc.map((src, index) => (
<CarouselItem key={index}>
<img
key={index}
src={src}
className='rounded lg:rounded-lg'
style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }}
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
) : (
imageSrc ? <img src={imageSrc[0]} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }} /> : ""
)}
</div>
<div className='w-full h-full px-10'>
{videoSrc && videoSrc.length > 1 ? (
<Carousel>
<CarouselContent>
{videoSrc.map((src, index) => (
<CarouselItem key={index}>
<video
key={index}
src={src}
controls
className='rounded lg:rounded-lg'
style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }}
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
) : (
videoSrc ? <video src={videoSrc[0]} controls className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }} /> : ""
)}
</div>
</div>
}
<br />
<div className='break-word overflow-hidden'>
{textWithoutImage}
</div>
<div className="space-y-2">
{textWithoutImage && <p className='break-words whitespace-pre-wrap'>{textWithoutImage}</p>}
{imageSrc && imageSrc.length > 0 && (
<Carousel>
<CarouselContent>
{imageSrc.map((image, index) => (
<CarouselItem key={index}>
<img src={image} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '75vh', objectFit: 'contain', margin: 'auto' }} alt={`Image ${index + 1}`} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)}
{videoSrc && videoSrc.length > 0 && (
<Carousel>
<CarouselContent>
{videoSrc.map((video, index) => (
<CarouselItem key={index}>
<video className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '75vh', margin: 'auto' }} controls>
<source src={video} />
</video>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)}
</div>
<hr />
<div className='py-4 space-x-4 flex justify-between items-start'>
<div className='flex space-x-4'>
<hr className="my-4" />
<div className="space-x-4 flex justify-between items-start">
<div className="flex space-x-4">
<ReactionButton event={event} />
<ZapButton event={event} />
{showViewNoteCardButton && <ViewNoteButton event={event} />}
</div>
<div className='flex space-x-2'>
<div className="flex space-x-2">
<ViewCopyButton event={event} />
<ViewRawButton event={event} />
</div>
</div>
</CardContent>
<CardFooter>
<small className="text-muted">{createdAt.toLocaleString()}</small>
<div className="grid grid-cols-1">
<small className="text-muted">{createdAt.toLocaleString()}</small>
{uploadedVia && <small className="text-muted">Uploaded via {uploadedVia}</small>}
</div>
</CardFooter>
</Card>
</>

View File

@@ -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<NotePageComponentProps> = ({ 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<NotePageComponentProps> = ({ 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) => (
<div key={event.id} className="py-6">
{event.kind === 1 && (
<NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
eventId={event.id}
tags={event.tags}
event={event}
showViewNoteCardButton={false}
/>
)}
{event.kind === 20 && (
<KIND20Card
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={getImageUrl(event.tags)}
eventId={event.id}
tags={event.tags}
event={event}
showViewNoteCardButton={false}
/>
)}
<div className="py-6 px-6">
<CommentsCompontent pubkey={event.pubkey} event={event} />
<div key={event.id}>
{event.kind === 20 ? (
<KIND20Card
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={getImageUrl(event.tags)}
eventId={event.id}
tags={event.tags}
event={event.rawEvent()}
showViewNoteCardButton={false}
/>
) : (
<NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
eventId={event.id}
tags={event.tags}
event={event.rawEvent()}
showViewNoteCardButton={false}
/>
)}
<div className="py-6">
<CommentsCompontent pubkey={event.pubkey} event={event.rawEvent()} />
</div>
</div>
))}

View File

@@ -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<NotificationProps> = ({ event }) => {
@@ -18,9 +14,7 @@ const Notification: React.FC<NotificationProps> = ({ 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<NotificationProps> = ({ 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 (
<>
<div className='pt-6 px-6'>
{/* ZAP */}
{event.kind === 9735 && (
<div className='grid grid-cols-6 justify-center items-center'>
<p className='col-span-1'>{sats} sats </p>
<div className='col-span-1'>
<Card>
<CardHeader>
<CardTitle>
<Link href={`/profile/${nip19.npubEncode(sender)}`} style={{ textDecoration: 'none' }}></Link>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar>
<AvatarImage src={userData?.picture} alt={name} />
<AvatarImage src={profileImageSrc} />
</Avatar>
</div>
<div className='col-span-4'>
<p>{name} zapped you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
</div>
</div>
)}
{/* FOLLOW */}
{event.kind === 3 && (
<div className='grid grid-cols-6 justify-center items-center'>
<p className='col-span-1'>{event.content}</p>
<div className='col-span-1'>
<Avatar>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
</div>
<div className='col-span-4'>
<p>{name} started following you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
</div>
</div>
)}
{/* REACTION */}
{event.kind === 7 && (
<Link href={"/note/" + reactedToId}>
<div className='grid grid-cols-6 justify-center items-center'>
<p className='col-span-1'>{event.content}</p>
<div className='col-span-1'>
<Avatar>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
</div>
<div className='col-span-4'>
<p>{name} reacted to you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
</div>
<span className='break-all' style={{ marginLeft: '10px' }}>{name}</span>
</div>
</Link>
</CardTitle>
</CardHeader>
<CardContent>
{event.kind === 9735 && (
<div>
<p>{name} zapped you with {sats} sats</p>
<p>{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}</p>
</div>
)}
</div>
<hr className='mt-6' />
</>
{event.kind === 3 && (
<div>
<p>{name} started following you</p>
<p>{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}</p>
</div>
)}
{event.kind === 7 && (
<Link href={`/note/${reactedToId}`} style={{ textDecoration: 'none' }}>
<p>{name} reacted to your note</p>
<p>{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}</p>
</Link>
)}
</CardContent>
</Card>
);
}

View File

@@ -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<NotificationsProps> = ({ 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<NotificationsProps> = ({ 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<NotificationsProps> = ({ 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 (
<>
<div className='pt-6 px-6'>
{/* <ProfileInfoCard pubkey={pubkey.toString()} /> */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-normal">Notifications</CardTitle>
</CardHeader>
<CardContent>
{allNotifications.map((notification, index) => (
<Notification key={index} event={notification} />
<Notification key={index} event={notification.rawEvent()} />
))}
</CardContent>
</Card>

View File

@@ -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<ProfileFeedProps> = ({ pubkey }) => {
filter: {
authors: [pubkey],
kinds: [20],
limit: limit,
limit,
},
});
@@ -46,7 +45,7 @@ const ProfileFeed: React.FC<ProfileFeedProps> = ({ pubkey }) => {
pubkey={event.pubkey}
text={event.content}
image={imageUrl}
event={event}
event={event.rawEvent()}
tags={event.tags}
eventId={event.id}
showViewNoteCardButton={true}

View File

@@ -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<ProfileGalleryViewFeedProps> = ({ 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<ProfileGalleryViewFeedProps> = ({ pubkey
return (
<>
<div className="grid grid-cols-3 gap-2">
{imagesAndIds.length === 0 && isLoading ? (
{isLoading ? (
<>
<div>
<Skeleton className="h-[125px] rounded-xl" />
@@ -41,15 +41,9 @@ const ProfileGalleryViewFeed: React.FC<ProfileGalleryViewFeedProps> = ({ pubkey
</div>
</>
) : (
imagesAndIds.map((galleryEntry) => (
galleryEntry.images.map((imageUrl, index) => (
<GalleryCard
pubkey={pubkey}
key={`${galleryEntry.id[index]}-${index}`}
eventId={galleryEntry.id[index]}
imageUrl={imageUrl}
linkToNote={true}
/>
imagesAndIds.map(({ id, images }) => (
images.map((image, index) => (
<GalleryCard key={index} pubkey={pubkey} eventId={id[index]} imageUrl={image} linkToNote={true} />
))
))
)}

View File

@@ -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<ProfileInfoCardProps> = React.memo(({ pubkey }) => {
let userPubkey = '';
let host = '';
if (typeof window !== 'undefined') {
@@ -36,7 +35,7 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = 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<ProfileInfoCardProps> = 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, '<br>');
const nip05 = userData?.nip05;
@@ -88,7 +87,7 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = React.memo(({ pubkey })
<Link href={`/profile/${nip19.npubEncode(pubkey)}`}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar className='mr-2'>
<AvatarImage src={userData?.picture} alt={title} />
<AvatarImage src={userData?.image} alt={title} />
</Avatar>
{title}
</div>

View File

@@ -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<ProfileQuickViewFeedProps> = ({ 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<ProfileQuickViewFeedProps> = ({ pubkey }) =
{events.map((event) => {
const imageUrl = getImageUrl(event.tags);
return imageUrl ? (
<QuickViewKind20NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={imageUrl}
event={event}
tags={event.tags}
eventId={event.id}
linkToNote={true}
<QuickViewKind20NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={imageUrl}
event={event.rawEvent()}
tags={event.tags}
eventId={event.id}
linkToNote={true}
/>
) : null;
})}

View File

@@ -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<ProfileTextFeedProps> = ({ 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<ProfileTextFeedProps> = ({ pubkey }) => {
key={event.id}
pubkey={event.pubkey}
text={event.content}
event={event}
event={event.rawEvent()}
tags={event.tags}
eventId={event.id}
showViewNoteCardButton={true}

View File

@@ -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<QuickViewKind20NoteCardProps> = ({ pubkey, text, image, eventId, tags, event, linkToNote }) => {
const {data, isLoading} = useProfile({
pubkey,
});
const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({
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 = (
<Card className="aspect-square">
<SmallCardContent className="h-full p-0">
<div className="h-full w-full">
<div className='relative w-full h-full'>
<Image
src={image || "/placeholder.svg"}
alt={text}
fill
sizes="(max-width: 768px) 100vw, 300px"
className='rounded lg:rounded-lg object-cover'
priority
onError={() => setImageError(true)}
/>
</div>
</div>
<img
src={image}
className='rounded lg:rounded-lg'
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
alt={text}
onError={() => setImageError(true)}
/>
</SmallCardContent>
</Card>
);
return (
<>
{linkToNote ? (
<Link href={`/note/${encodedNoteId}`} className="block w-full aspect-square">
{card}
</Link>
) : (
card
)}
</>
);
if (linkToNote) {
return (
<Link href={'/note/' + encodedNoteId}>
{card}
</Link>
);
}
return card;
}
export default QuickViewKind20NoteCard;

View File

@@ -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)
}
}

View File

@@ -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 (
<ScrollArea className="px-4 h-[50vh]">
{filteredEvents.map((event: any) => (
<ReactionButtonReactionListItem key={event.id} event={event} />
))}
</ScrollArea>
<div className="px-4">
<div className="space-y-4">
{filteredEvents.map((event) => (
<ReactionButtonReactionListItem key={event.id} event={event.rawEvent()} />
))}
</div>
</div>
);
}

View File

@@ -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 (
<Link href={hrefProfile}>
<div key={event.id} className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 p-1">
{/* <img src={profileImageSrc} className="w-8 h-8 rounded-full" /> */}
<Avatar>
<AvatarImage src={profileImageSrc} alt={title} />
</Avatar>

View File

@@ -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 (
<>
<h2>Reel Feed</h2>
{filteredEvents.map((event) => (
// <p key={event.id}>{event.pubkey} posted: {event.content}</p>
<div key={event.id} className="py-6">
<NoteCard key={event.id} pubkey={event.pubkey} text={event.content} eventId={event.id} tags={event.tags} event={event} showViewNoteCardButton={true} />
<NoteCard
key={event.id}
pubkey={event.pubkey}
text={event.content}
eventId={event.id}
tags={event.tags}
event={event.rawEvent()}
showViewNoteCardButton={true}
/>
</div>
))}
</>

View File

@@ -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<TagFeedProps> = ({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<TagFeedProps> = ({tag}) => {
<>
<h2>Tag Feed for {tag}</h2>
{events.map((event) => (
// <p key={event.id}>{event.pubkey} posted: {event.content}</p>
<div key={event.id} className="py-6">
<KIND20Card key={event.id} pubkey={event.pubkey} text={event.content} image={getImageUrl(event.tags)} eventId={event.id} tags={event.tags} event={event} showViewNoteCardButton={true} />
<KIND20Card
key={event.id}
pubkey={event.pubkey}
text={event.content}
image={getImageUrl(event.tags)}
eventId={event.id}
tags={event.tags}
event={event.rawEvent()}
showViewNoteCardButton={true}
/>
</div>
))}
</>

View File

@@ -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<any[]>([]);
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 (
<div className="flex flex-col items-center py-6 px-6">

View File

@@ -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<TrendingImageProps> = ({ eventId, pubkey }) => {
const { data: userData } = useProfile({
pubkey,
});
const { data: userData } = useProfile(pubkey);
const { events } = useNostrEvents({
filter: {
@@ -37,47 +32,45 @@ const TrendingImage: React.FC<TrendingImageProps> = ({ 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 (
<>
<Card>
<CardHeader>
<CardTitle>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<Link href={hrefProfile} style={{ textDecoration: 'none' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar>
<AvatarImage src={profileImageSrc} />
<AvatarImage src={profileImageSrc} alt={title} />
</Avatar>
<span className='break-all' style={{ marginLeft: '10px' }}>{title}</span>
<span style={{ marginLeft: '10px' }}>{title}</span>
</div>
</CardTitle>
</CardHeader>
<SmallCardContent>
<div className='p-2'>
<div className='d-flex justify-content-center align-items-center'>
{imageSrc && imageSrc.length > 0 && (
<div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
<Link href={hrefProfile}>
<img src={imageSrc[0]} className='rounded lg:rounded-lg' style={{ width: '100%', height: '100%', objectFit: 'cover' }} alt={text} />
</Link>
</div>
// <img src={imageSrc[0]} style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'cover', margin: 'auto' }} alt={text} />
// <div style={{ position: 'relative', width: '100%', maxHeight: '100vh' }}>
// <Image src={imageSrc[0]} alt={text} layout='fill' objectFit='contain' />
// </div>
)}
</div>
</div>
</SmallCardContent>
</Card>
</>
</Link>
</CardTitle>
</CardHeader>
<SmallCardContent>
<Link href={`/note/${nip19.noteEncode(eventId)}`}>
<img
src={imageSrc}
className='rounded lg:rounded-lg'
style={{
width: '100%',
height: '200px',
objectFit: 'cover'
}}
alt={`Trending post by ${title}`}
/>
</Link>
</SmallCardContent>
</Card>
);
}

View File

@@ -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<any[]>([]);
const ndk = useNDK();
const [images, setImages] = useState<any[]>([]);
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 (
<div className="flex flex-col items-center py-6 px-6">
<h1 className="text-3xl font-bold">Currently Trending</h1>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6">
{profiles && profiles.length > 0 && profiles.map((profile, index) => (
// <h1 key={index}>{profile.id}</h1>
<TrendingImage key={index} eventId={profile.id} pubkey={profile.pubkey} />
{images && images.length > 0 && images.map((image, index) => (
<TrendingImage key={index} eventId={image.id} pubkey={image.pubkey} />
))}
</div>
</div>

View File

@@ -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<HTMLInputElement>) => {
@@ -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() {
</div>
<div className='py-4'>
<Label>Your Bio</Label>
{/* <Input type="text" id="bio" placeholder="Type something about you.." /> */}
<Textarea id="bio" placeholder="Type something about you.." rows={10} value={bio} onChange={handleBioChange} />
</div>
<Button variant={'default'} type="submit" className='w-full' onClick={handleProfileUpdate}>Submit</Button>

View File

@@ -1,5 +1,3 @@
import { useNostr, useNostrEvents } from "nostr-react"
import { nip19, type NostrEvent } from "nostr-tools"
import type React from "react"
import { type ChangeEvent, type FormEvent, useState, useEffect, useCallback } from "react"
import { Button } from "./ui/button"
@@ -16,7 +14,6 @@ import {
DrawerFooter,
} from "@/components/ui/drawer"
import { Spinner } from "@/components/spinner"
import { signEvent } from "@/utils/utils"
import {
Select,
SelectContent,
@@ -24,24 +21,25 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useNDK } from "@/hooks/useNDK"
import { createHash } from "crypto"
async function calculateBlurhash(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const img = new Image()
img.onload = () => {
const canvas = document.createElement("canvas")
canvas.width = 32
canvas.height = 32
ctx?.drawImage(img, 0, 0, 32, 32)
const imageData = ctx?.getImageData(0, 0, 32, 32)
if (imageData) {
const blurhash = encode(imageData.data, imageData.width, imageData.height, 4, 4)
resolve(blurhash)
} else {
reject(new Error("Failed to get image data"))
const ctx = canvas.getContext("2d")
if (!ctx) {
reject(new Error("Could not get canvas context"))
return
}
ctx.drawImage(img, 0, 0, 32, 32)
const imageData = ctx.getImageData(0, 0, 32, 32)
const blurhash = encode(imageData.data, imageData.width, imageData.height, 4, 4)
resolve(blurhash)
}
img.onerror = reject
img.src = URL.createObjectURL(file)
@@ -49,318 +47,195 @@ async function calculateBlurhash(file: File): Promise<string> {
}
const UploadComponent: React.FC = () => {
const { publish } = useNostr()
const { createHash } = require("crypto")
const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null
const [previewUrl, setPreviewUrl] = useState("")
const [isLoading, setIsLoading] = useState(false)
const ndk = useNDK()
const [file, setFile] = useState<File | null>(null)
const [desc, setDesc] = useState("")
const [isUploading, setIsUploading] = useState(false)
const [uploadUrl, setUploadUrl] = useState("")
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [uploadedNoteId, setUploadedNoteId] = useState("")
const [retryCount, setRetryCount] = useState(0)
const [shouldFetch, setShouldFetch] = useState(false)
const [serverChoice, setServerChoice] = useState("nostr.download")
const [visibility, setVisibility] = useState("public")
const [imageData, setImageData] = useState<{
width: number;
height: number;
hash: string;
blurhash: string;
} | null>(null)
const { events, isLoading: isNoteLoading } = useNostrEvents({
filter: shouldFetch
? {
ids: uploadedNoteId ? [uploadedNoteId] : [],
kinds: [20],
limit: 1,
}
: { ids: [], kinds: [20], limit: 1 },
enabled: shouldFetch,
})
useEffect(() => {
if (uploadedNoteId) {
setShouldFetch(true)
}
}, [uploadedNoteId])
useEffect(() => {
let timeoutId: NodeJS.Timeout
if (shouldFetch && events.length === 0 && !isNoteLoading) {
timeoutId = setTimeout(() => {
setRetryCount((prevCount) => prevCount + 1)
setShouldFetch(false)
setShouldFetch(true)
}, 5000) // Retry every 5 seconds
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [shouldFetch, events, isNoteLoading])
const handleRetry = useCallback(() => {
setRetryCount((prevCount) => prevCount + 1)
setShouldFetch(false)
setShouldFetch(true)
}, [])
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const url = URL.createObjectURL(file)
if (url.startsWith("blob:")) {
setPreviewUrl(url)
}
// Optional: Bereinigung alter URLs
return () => URL.revokeObjectURL(url)
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0])
}
}
const handleServerChange = (value: string) => {
setServerChoice(value)
const handleDescChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setDesc(e.target.value)
}
async function onSubmit(event: FormEvent<HTMLFormElement>) {
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setIsLoading(true)
if (!file) return
const formData = new FormData(event.currentTarget)
const desc = formData.get("description") as string
const file = formData.get("file") as File
let sha256 = ""
let finalNoteContent = desc
let finalFileUrl = ""
console.log("File:", file)
setIsUploading(true)
let sha256: string
if (!desc && !file.size) {
alert("Please enter a description and/or upload a file")
setIsLoading(false)
return
const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = () => reject(reader.error)
reader.readAsArrayBuffer(file)
})
}
// get every hashtag in desc and cut off the # symbol
let hashtags: string[] = desc.match(/#[a-zA-Z0-9]+/g) || []
if (hashtags) {
hashtags = hashtags.map((hashtag) => hashtag.slice(1))
}
try {
const arrayBuffer = await readFileAsArrayBuffer(file)
const hashBuffer = createHash("sha256").update(Buffer.from(arrayBuffer)).digest()
sha256 = hashBuffer.toString("hex")
// If file is present, upload it to the media server
if (file) {
const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = () => reject(reader.error)
reader.readAsArrayBuffer(file)
})
}
const unixNow = () => Math.floor(Date.now() / 1000)
const newExpirationValue = () => (unixNow() + 60 * 5).toString()
try {
const arrayBuffer = await readFileAsArrayBuffer(file)
const hashBuffer = createHash("sha256").update(Buffer.from(arrayBuffer)).digest()
sha256 = hashBuffer.toString("hex")
// Create auth event for blossom auth via nostr
const authEvent = ndk.getEvent()
authEvent.kind = 24242
authEvent.content = desc
authEvent.created_at = Math.floor(Date.now() / 1000)
authEvent.tags = [
["t", "upload"],
["x", sha256],
["url", "https://nostr.build"],
["method", "POST"],
["exp", newExpirationValue()],
]
const unixNow = () => Math.floor(Date.now() / 1000)
const newExpirationValue = () => (unixNow() + 60 * 5).toString()
const pubkey = window.localStorage.getItem("pubkey")
const createdAt = Math.floor(Date.now() / 1000)
// Create auth event for blossom auth via nostr
const authEvent: NostrEvent = {
kind: 24242,
content: desc,
created_at: createdAt,
tags: [
["t", "upload"],
["x", sha256],
["expiration", newExpirationValue()],
],
pubkey: "", // Add a placeholder for pubkey
id: "", // Add a placeholder for id
sig: "", // Add a placeholder for sig
// Sign the event
if (typeof window !== "undefined") {
const loginType = window.localStorage.getItem("loginType")
if (loginType === "extension") {
const signedEvent = await window.nostr.signEvent(authEvent.rawEvent())
Object.assign(authEvent, signedEvent)
} else if (loginType === "raw_nsec") {
const nsecStr = window.localStorage.getItem("nsec")
if (!nsecStr) throw new Error("No nsec found")
await authEvent.sign()
}
console.log(authEvent)
// Sign auth event
const authEventSigned = (await signEvent(loginType, authEvent)) as NostrEvent
// authEventSigned as base64 encoded string
let authString = Buffer.from(JSON.stringify(authEventSigned)).toString('base64')
const blossomServer = "https://" + serverChoice
await fetch(blossomServer + "/upload", {
method: "PUT",
body: file,
headers: { authorization: "Nostr " + authString },
}).then(async (res) => {
if (res.ok) {
const responseText = await res.text()
const responseJson = JSON.parse(responseText)
finalFileUrl = responseJson.url
const noteTags = hashtags.map((tag) => ["t", tag])
let blurhash = ""
if (file && file.type.startsWith("image/")) {
try {
blurhash = await calculateBlurhash(file)
} catch (error) {
console.error("Error calculating blurhash:", error)
}
}
if (finalFileUrl) {
const image = new Image()
image.src = URL.createObjectURL(file)
await new Promise((resolve) => {
image.onload = resolve
})
finalNoteContent = desc
noteTags.push([
"imeta",
"url " + finalFileUrl,
"m " + file.type,
"x " + sha256,
"ox " + sha256,
"blurhash " + blurhash,
`dim ${image.width}x${image.height}`,
])
}
const createdAt = Math.floor(Date.now() / 1000)
// NIP-89
// ["client","lumina","31990:ff363e4afc398b7dd8ceb0b2e73e96fe9621ababc22ab150ffbb1aa0f34df8b2:1731850618505"]
noteTags.push(["client", "lumina", "31990:" + "ff363e4afc398b7dd8ceb0b2e73e96fe9621ababc22ab150ffbb1aa0f34df8b2" + ":" + createdAt])
// Create the actual note
const noteEvent: NostrEvent = {
kind: 20,
content: finalNoteContent,
created_at: createdAt,
tags: noteTags,
pubkey: "", // Add a placeholder for pubkey
id: "", // Add a placeholder for id
sig: "", // Add a placeholder for sig
}
let signedEvent: NostrEvent | null = null
// Sign the actual note
signedEvent = (await signEvent(loginType, noteEvent)) as NostrEvent
// If we got a signed event, publish it to nostr
if (signedEvent) {
console.log("final Event: ")
console.log(signedEvent)
publish(signedEvent)
}
setIsLoading(false)
if (signedEvent != null) {
setUploadedNoteId(signedEvent.id)
setIsDrawerOpen(true)
setShouldFetch(true)
setRetryCount(0)
}
} else {
// alert(await res.text())
throw new Error("Failed to upload file: " + await res.text())
}
})
} catch (error) {
alert(error)
console.error("Error reading file:", error)
setIsLoading(false)
}
// Create form data and upload
const formData = new FormData()
formData.append("file", file)
formData.append("auth", JSON.stringify(authEvent.rawEvent()))
const response = await fetch("https://nostr.build/api/v2/upload/files", {
method: "POST",
body: formData,
})
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`)
}
const result = await response.json()
setUploadUrl(result.data[0].url)
// Create kind 20 nostr event
const ndkEvent = ndk.getEvent()
ndkEvent.kind = 20
ndkEvent.content = desc
ndkEvent.created_at = Math.floor(Date.now() / 1000)
// Add image dimensions and blurhash if available
if (imageData) {
ndkEvent.tags.push(["dim", `${imageData.width}x${imageData.height}`])
ndkEvent.tags.push(["blurhash", imageData.blurhash])
}
ndkEvent.tags.push(["r", result.data[0].url])
ndkEvent.tags.push(["client", "lumina"])
if (visibility !== "public") {
ndkEvent.tags.push(["sensitive", "true"])
}
// Sign and publish
if (typeof window !== "undefined") {
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()
setIsDrawerOpen(false)
window.location.href = "/"
} catch (error) {
console.error("Upload failed:", error)
} finally {
setIsUploading(false)
}
}
useEffect(() => {
if (file) {
// Get image dimensions and calculate blurhash
const img = new Image()
img.onload = async () => {
const blurhash = await calculateBlurhash(file)
setImageData({
width: img.width,
height: img.height,
hash: "",
blurhash,
})
}
img.src = URL.createObjectURL(file)
}
}, [file])
return (
<>
<div>
<form className="space-y-4" onSubmit={onSubmit}>
<Textarea
name="description"
rows={6}
placeholder="Your description"
id="description"
className="w-full"
></Textarea>
<div className="grid w-full max-w-sm items-center gap-1.5">
<Input id="file" name="file" type="file" accept="image/*" onChange={handleFileChange} />
</div>
<div className="grid grid-cols-2 w-full max-w-sm items-center gap-1.5">
{/* <select value={serverChoice} onChange={handleServerChange} className="w-full">
<option value="nostr.download">nostr.download</option>
<option value="blossom.primal.net">blossom.primal.net</option>
</select> */}
Upload to
<Select onValueChange={handleServerChange} value={serverChoice}>
<SelectTrigger className="w-full">
<SelectValue placeholder={serverChoice} />
</SelectTrigger>
<SelectContent>
<SelectItem value="nostr.download">nostr.download</SelectItem>
<SelectItem value="blossom.primal.net">blossom.primal.net</SelectItem>
</SelectContent>
</Select>
</div>
{previewUrl && <img src={previewUrl || "/placeholder.svg"} alt="Preview" className="w-full pt-4" />}
{isLoading ? (
<Button className="w-full" disabled>
Uploading.. <ReloadIcon className="m-2 h-4 w-4 animate-spin" />
</Button>
) : (
<Button className="w-full">Upload</Button>
)}
</form>
</div>
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Upload Status</DrawerTitle>
<DrawerDescription>
{isNoteLoading ? (
<div className="flex items-center space-x-2">
<Spinner />
<span>Checking note status...</span>
</div>
) : events.length > 0 ? (
<div
className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"
role="alert"
>
<strong className="font-bold">Success!</strong>
<span className="block sm:inline"> Note found with ID: </span>
<span className="block sm:inline font-mono">
{`${events[0].id.slice(0, 5)}...${events[0].id.slice(-3)}`}
</span>
</div>
) : (
<p>Note not found. It may take a moment to propagate.</p>
)}
</DrawerDescription>
</DrawerHeader>
<DrawerFooter className="flex flex-col space-y-2">
{events.length === 0 && (
<Button onClick={handleRetry} variant="outline" className="w-full">
Retry Now
<div className="w-full max-w-full">
{isUploading ? (
<div className="flex justify-center items-center">
<Spinner />
</div>
) : (
<form onSubmit={onSubmit}>
<div className="space-y-4">
<div>
<Input type="file" accept="image/*" onChange={handleFileChange} />
</div>
<div>
<Textarea
placeholder="Write a description..."
value={desc}
onChange={handleDescChange}
rows={4}
/>
</div>
<div>
<Select value={visibility} onValueChange={setVisibility}>
<SelectTrigger>
<SelectValue placeholder="Select visibility" />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public</SelectItem>
<SelectItem value="sensitive">Sensitive Content</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Button type="submit" className="w-full">
Upload
</Button>
)}
<Button asChild className="w-full">
<a href={`/note/${nip19.noteEncode(uploadedNoteId)}`}>View Note</a>
</Button>
<Button variant="outline" onClick={() => setIsDrawerOpen(false)} className="w-full">
Close
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
</div>
</div>
</form>
)}
</div>
)
}

View File

@@ -1,108 +1,76 @@
import { useNostr, dateToUnix, useNostrEvents } from "nostr-react";
import { useNostrEvents } from "@/hooks/useNDK"
import { Button } from "@/components/ui/button"
import {
type Event as NostrEvent,
getEventHash,
getPublicKey,
finalizeEvent,
} from "nostr-tools";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import { ReloadIcon } from "@radix-ui/react-icons";
import ZapButtonList from "./ZapButtonList";
import { Input } from "./ui/input";
import { ReloadIcon } from "@radix-ui/react-icons"
import ZapButtonList from "./ZapButtonList"
import { Input } from "./ui/input"
export default function ZapButton({ event }: { event: any }) {
const { events, isLoading } = useNostrEvents({
filter: {
// since: dateToUnix(now.current), // all new events from now
// since: 0,
// limit: 100,
'#e': [event.id],
kinds: [9735],
},
});
// filter out all events that also have another e tag with another id
// this will filter out likes that are made on comments and not on the note itself
// const filteredEvents = events.filter((event) => { return event.tags.filter((tag) => { return tag[0] === '#e' && tag[1] !== event.id }).length === 0 });
let sats = 0;
var lightningPayReq = require('bolt11');
events.forEach((event) => {
// console.log(event);
event.tags.forEach((tag) => {
if (tag[0] === 'bolt11') {
let decoded = lightningPayReq.decode(tag[1]);
// console.log(decoded.satoshis);
sats = sats + decoded.satoshis;
}
});
});
// const { publish } = useNostr();
// const onPost = async () => {
// const privKey = prompt("Paste your private key:");
// if (!privKey) {
// alert("no private key provided");
// return;
// }
// const message = prompt("Enter the message you want to send:");
// if (!message) {
// alert("no message provided");
// return;
// }
// const event: NostrEvent = {
// content: message,
// kind: 1,
// tags: [],
// created_at: dateToUnix(),
// pubkey: getPublicKey(privKey),
// id: "",
// sig: ""
// };
// event.id = getEventHash(event);
// event.sig = getSignature(event, privKey);
// publish(event);
// };
const handleZap = async (amount: number) => {
// TODO: Implement zap functionality with NDK
// This will require integration with a Lightning wallet
alert('Zap functionality coming soon!');
};
return (
<Drawer>
<DrawerTrigger asChild>
{isLoading ? (
<Button variant="outline"><ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> </Button>
) : (
<Button variant="outline">{sats} sats </Button>
)}
<Button variant="outline">
{isLoading ? (
<>
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
</>
) : (
<>
{sats}
</>
)}
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Zaps</DrawerTitle>
{/* <DrawerDescription>Sorry, but this feature is not implemented yet.</DrawerDescription> */}
<DrawerDescription>Send some sats!</DrawerDescription>
</DrawerHeader>
<div className="px-4 grid grid-cols-3">
<Button variant={"outline"} className="mx-1" disabled>1 sat</Button>
<Button variant={"outline"} className="mx-1" disabled>21 sats</Button>
<div className="flex">
<Input className="mx-1" placeholder="1000 sats" />
<Button variant={"outline"} className="mx-1" disabled>send</Button>
<div className="px-4">
<div className="grid grid-cols-3 gap-4 mb-4">
<Button onClick={() => handleZap(50)}>50 sats</Button>
<Button onClick={() => handleZap(100)}>100 sats</Button>
<Button onClick={() => handleZap(1000)}>1000 sats</Button>
<Button onClick={() => handleZap(5000)}>5000 sats</Button>
<Button onClick={() => handleZap(10000)}>10000 sats</Button>
<Button onClick={() => handleZap(50000)}>50000 sats</Button>
</div>
<div className="flex items-center mb-4">
<Input type="number" placeholder="Enter custom amount" className="mr-2" />
<Button onClick={() => handleZap(0)}>Zap</Button>
</div>
</div>
<hr className="my-4" />
@@ -110,13 +78,11 @@ export default function ZapButton({ event }: { event: any }) {
<DrawerFooter>
<DrawerClose asChild>
<div>
<Button variant={"outline"}>Close</Button>
<Button variant="outline">Close</Button>
</div>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
// <Button variant="default" onClick={onPost}>{events.length} Reactions</Button>
);
)
}

View File

@@ -1,12 +1,14 @@
import { ScrollArea } from "@/components/ui/scroll-area"
import ZapButtonListItem from "./ZapButtonListItem";
import { NDKEvent } from '@nostr-dev-kit/ndk';
export default function ZapButtonList({ events }: { events: any }) {
return (
<ScrollArea className="px-4 h-[50vh]">
{events.map((event: any) => (
<ZapButtonListItem key={event.id} event={event} />
))}
</ScrollArea>
);
export default function ZapButtonList({ events }: { events: NDKEvent[] }) {
return (
<div className="px-4">
<div className="space-y-4">
{events.map((event) => (
<ZapButtonListItem key={event.id} event={event.rawEvent()} />
))}
</div>
</div>
);
}

View File

@@ -1,48 +1,39 @@
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 ZapButtonListItem({ event }: { event: NostrEvent }) {
// Find the actual zapper's pubkey from the P tag
let pubkey = event.pubkey;
for(let i = 0; i < event.tags.length; i++) {
if(event.tags[i][0] === 'P') {
pubkey = event.tags[i][1];
for(let tag of event.tags) {
if(tag[0] === 'P') {
pubkey = tag[1];
break;
}
}
const { data: userData } = useProfile({
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 title = userData?.displayName || userData?.name || userData?.nip05 ||
nip19.npubEncode(pubkey).slice(0, 8) + ':' + nip19.npubEncode(pubkey).slice(-3);
const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`;
const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey;
const profileImageSrc = userData?.image || "https://robohash.org/" + pubkey;
// Calculate zap amount
let sats = 0;
var lightningPayReq = require('bolt11');
const lightningPayReq = require('bolt11');
event.tags.forEach((tag) => {
if (tag[0] === 'bolt11') {
let decoded = lightningPayReq.decode(tag[1]);
// console.log(decoded.satoshis);
sats = decoded.satoshis;
const decoded = lightningPayReq.decode(tag[1]);
sats = decoded.satoshis;
}
});
return (
<Link href={hrefProfile}>
<div key={event.id} className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 p-1">
{/* <img src={profileImageSrc} className="w-8 h-8 rounded-full" /> */}
<Avatar>
<AvatarImage src={profileImageSrc} alt={title} />
</Avatar>

View File

@@ -1,22 +1,16 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { useProfile } from "nostr-react";
import {
nip19,
} from "nostr-tools";
import { useProfile } from "@/hooks/useNDK";
import { nip19 } from "nostr-tools";
import Link from "next/link";
export function RecentFollower({ follower }: { follower: any }) {
const { data: userData } = useProfile(follower.pubkey);
const { data: userData, isLoading: userDataLoading } = useProfile({
pubkey: follower.pubkey,
});
let encoded = nip19.npubEncode(follower.pubkey);
let parts = encoded.split('npub');
let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3);
let title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened;
const profileImageSrc = userData?.picture || "https://robohash.org/" + follower.pubkey;
const encoded = nip19.npubEncode(follower.pubkey);
const npubShortened = `npub${encoded.split('npub')[1].slice(0, 4)}:${encoded.split('npub')[1].slice(-3)}`;
const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || npubShortened;
const profileImageSrc = userData?.image || "https://robohash.org/" + follower.pubkey;
return (
<div className="flex items-center" key={follower.id}>
<Link href={`/profile/${encoded}`}>
@@ -28,9 +22,9 @@ export function RecentFollower({ follower }: { follower: any }) {
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
<p className="text-sm text-muted-foreground">
{new Date(follower.created_at * 1000).toLocaleDateString()} {new Date(follower.created_at * 1000).toLocaleTimeString()} </p>
{new Date(follower.created_at * 1000).toLocaleDateString()} {new Date(follower.created_at * 1000).toLocaleTimeString()}
</p>
</div>
{/* <div className="ml-auto font-medium">{follower.amount}</div> */}
</div>
)
}

View File

@@ -1,23 +1,45 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { RecentFollower } from "./RecentFollower";
import React from 'react';
import { useProfile } from "@/hooks/useNDK";
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import Link from 'next/link';
import { nip19 } from "nostr-tools";
export function RecentFollowerCard({ followers }: { followers: Array<any> }) {
const lastFiveFollowers = followers.slice(-5).reverse();
return (
<Card className="col-span-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-normal">Recent Follower 🫂</CardTitle>
</CardHeader>
<CardContent>
<div className='pt-4'>
<div className="space-y-8">
{lastFiveFollowers.map((follower) => (
<RecentFollower follower={follower} key={follower.id}/>
))}
</div>
</div>
</CardContent>
</Card>
)
interface RecentFollowerCardProps {
followers: any[];
}
export function RecentFollowerCard({ followers }: RecentFollowerCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Followers</CardTitle>
</CardHeader>
<CardContent>
{followers.map((follower) => {
const { data: userData } = useProfile(follower.pubkey);
const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(follower.pubkey);
const profileImageSrc = userData?.image || "https://robohash.org/" + follower.pubkey;
const createdAt = new Date(follower.created_at * 1000);
const hrefProfile = `/profile/${nip19.npubEncode(follower.pubkey)}`;
return (
<div key={follower.id} className="flex items-center space-x-2 py-2">
<Link href={hrefProfile} style={{ textDecoration: 'none' }}>
<Avatar>
<AvatarImage src={profileImageSrc} alt={title} />
</Avatar>
</Link>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
<p className="text-sm text-muted-foreground">
{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}
</p>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}

View File

@@ -1,31 +1,33 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { useProfile } from "nostr-react";
import {
nip19,
} from "nostr-tools";
import { useProfile } from "@/hooks/useNDK";
import { nip19 } from "nostr-tools";
import Link from "next/link";
export function RecentZap({ zap }: { zap: any }) {
let zapperPubkey = zap.pubkey;
for(let tag of zap.tags){
for(let tag of zap.tags) {
if(tag[0] === 'P') {
zapperPubkey = tag[1];
}
}
const { data: userData, isLoading: userDataLoading } = useProfile({
pubkey: zapperPubkey,
});
const { data: userData } = useProfile(zapperPubkey);
console.log('zap', zap)
const encoded = nip19.npubEncode(zapperPubkey);
const npubShortened = `npub${encoded.split('npub')[1].slice(0, 4)}:${encoded.split('npub')[1].slice(-3)}`;
const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || npubShortened;
const profileImageSrc = userData?.image || "https://robohash.org/" + zapperPubkey;
let sats = 0;
for(let tag of zap.tags) {
if(tag[0] === 'bolt11') {
const lightningPayReq = require('bolt11');
const decoded = lightningPayReq.decode(tag[1]);
sats = decoded.satoshis;
break;
}
}
let encoded = nip19.npubEncode(zapperPubkey);
let parts = encoded.split('npub');
let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3);
let title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened;
const profileImageSrc = userData?.picture || "https://robohash.org/" + zap.pubkey;
return (
<div className="flex items-center" key={zap.id}>
<Link href={`/profile/${encoded}`}>
@@ -37,9 +39,10 @@ export function RecentZap({ zap }: { zap: any }) {
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
<p className="text-sm text-muted-foreground">
{new Date(zap.created_at * 1000).toLocaleDateString()} {new Date(zap.created_at * 1000).toLocaleTimeString()} </p>
{new Date(zap.created_at * 1000).toLocaleDateString()} {new Date(zap.created_at * 1000).toLocaleTimeString()}
</p>
<p className="text-sm text-muted-foreground">{sats} sats</p>
</div>
{/* <div className="ml-auto font-medium">{zap.amount}</div> */}
</div>
)
}

View File

@@ -1,23 +1,46 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { RecentZap } from "./RecentZap";
import React from 'react';
import { useProfile } from "@/hooks/useNDK";
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import Link from 'next/link';
import { nip19 } from "nostr-tools";
export function RecentZapsCard({ zaps }: { zaps: Array<any> }) {
const lastFiveZaps = zaps.slice(-5).reverse();
return (
<Card className="col-span-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-normal">Recent Zaps </CardTitle>
</CardHeader>
<CardContent>
<div className='pt-4'>
<div className="space-y-8">
{lastFiveZaps.map((zap) => (
<RecentZap zap={zap} key={zap.id}/>
))}
</div>
</div>
</CardContent>
</Card>
)
interface RecentZapsCardProps {
zaps: any[];
}
export function RecentZapsCard({ zaps }: RecentZapsCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Zaps</CardTitle>
</CardHeader>
<CardContent>
{zaps.map((zap) => {
const { data: userData } = useProfile(zap.pubkey);
const title = userData?.displayName || userData?.name || userData?.nip05 || userData?.npub || nip19.npubEncode(zap.pubkey);
const profileImageSrc = userData?.image || "https://robohash.org/" + zap.pubkey;
const createdAt = new Date(zap.created_at * 1000);
const hrefProfile = `/profile/${nip19.npubEncode(zap.pubkey)}`;
return (
<div key={zap.id} className="flex items-center space-x-2 py-2"></div>
<Link href={hrefProfile} style={{ textDecoration: 'none' }}>
<Avatar>
<AvatarImage src={profileImageSrc} alt={title} />
</Avatar>
</Link>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{title}</p>
<p className="text-sm text-muted-foreground">
{createdAt.toLocaleDateString()} {createdAt.toLocaleTimeString()}
</p>
<p className="text-sm text-muted-foreground">{zap.sats} sats</p>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useNostrEvents, useProfile } from "nostr-react";
import { useNostrEvents, useProfile } from "@/hooks/useNDK";
import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { AvatarImage } from '@radix-ui/react-avatar';
@@ -16,18 +16,16 @@ interface ProfileInfoCardProps {
}
const ProfileInfoCard: React.FC<ProfileInfoCardProps> = ({ pubkey }) => {
const { data: userData, isLoading: userDataLoading } = useProfile({
pubkey,
});
const { data: userData } = useProfile(pubkey);
const { events: followers, isLoading: followersLoading } = useNostrEvents({
const { events: followers } = useNostrEvents({
filter: {
kinds: [3],
'#p': [pubkey],
},
});
const { events: zaps, isLoading: zapsLoading } = useNostrEvents({
const { events: zaps } = useNostrEvents({
filter: {
kinds: [9735],
'#p': [pubkey],
@@ -35,7 +33,7 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = ({ pubkey }) => {
},
});
const { events: following, isLoading: followingLoading } = useNostrEvents({
const { events: following } = useNostrEvents({
filter: {
kinds: [3],
authors: [pubkey],
@@ -43,71 +41,33 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = ({ pubkey }) => {
},
});
// filter for only new followings (latest in a users followers list)
// 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;
}
return lastPTag?.[0] === "p" && lastPTag[1] === pubkey.toString();
});
let encoded = nip19.npubEncode(pubkey);
let parts = encoded.split('npub');
let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3);
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, '<br>');
const nip05 = userData?.nip05
let profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey;
const nip05 = userData?.nip05;
let profileImageSrc = userData?.image || "https://robohash.org/" + pubkey;
return (
<>
<div className='pt-6 px-6'>
{/* <ProfileInfoCard pubkey={pubkey.toString()} /> */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
{/* <CardTitle className="text-base font-normal">Profile</CardTitle> */}
</CardHeader>
<CardContent>
<div className="flex flex-row items-center space-x-4">
<Avatar>
<AvatarImage
src={profileImageSrc}
alt="Avatar"
className="rounded-full"
/>
</Avatar>
<div>
<h1 className="text-2xl font-bold">{title}</h1>
<NIP05 nip05={nip05?.toString() ?? ''} pubkey={pubkey} />
</div>
</div>
</CardContent>
</Card>
</div>
<div className='grid gap-4 grid-cols-2 p-6'>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-normal">Total Followers</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{followers.length}</div>
{/* <p className="text-xs text-muted-foreground">
+20.1% from last month
</p> */}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-normal">Total Following</CardTitle>
<CardTitle className="text-base font-normal">
Following
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{followingLoading ? "Loading.." : (following.length > 0 ? following[0]?.tags.length : "-")}
{following[0]?.tags.length || "-"}
</div>
{/* <p className="text-xs text-muted-foreground">
+20.1% from last month
</p> */}
</CardContent>
</Card>
<RecentFollowerCard followers={filteredFollowers.reverse()} />

72
hooks/useNDK.ts Normal file
View File

@@ -0,0 +1,72 @@
import { useContext, useEffect, useState } from 'react';
import { NDKContext } from '@/app/layout';
import { NDKFilter, NDKSubscription, NDKUser, NDKEvent } from '@nostr-dev-kit/ndk';
export function useNDK() {
const ndk = useContext(NDKContext);
if (!ndk) throw new Error('NDK context not found');
return ndk;
}
export function useProfile(pubkey: string) {
const ndk = useNDK();
const [profile, setProfile] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!pubkey) {
setLoading(false);
return;
}
const user = ndk.getUser({ pubkey });
user.fetchProfile().then((profile) => {
setProfile(profile);
setLoading(false);
}).catch(() => {
setLoading(false);
});
return () => {
// Cleanup if needed
};
}, [ndk, pubkey]);
return { data: profile, isLoading: loading };
}
export function useNostrEvents({ filter }: { filter: NDKFilter }) {
const ndk = useNDK();
const [events, setEvents] = useState<NDKEvent[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!filter) {
setLoading(false);
return;
}
const sub = ndk.subscribe(filter, { closeOnEose: false });
const eventList: NDKEvent[] = [];
sub.on('event', (event: NDKEvent) => {
eventList.push(event);
setEvents([...eventList].sort((a, b) => b.created_at - a.created_at));
});
sub.on('eose', () => {
setLoading(false);
});
return () => {
sub.stop();
};
}, [ndk, JSON.stringify(filter)]);
return { events, isLoading: loading };
}
// Helper function to publish events
export async function publishEvent(ndk: NDKEvent['ndk'], event: Partial<NDKEvent>) {
return await ndk.publish(event);
}

View File

@@ -1,47 +1,79 @@
import { Event as NostrEvent, finalizeEvent} from "nostr-tools";
import { hexToBytes } from "@noble/hashes/utils"
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { hexToBytes } from "@noble/hashes/utils";
export function getImageUrl(tags: string[][]): string {
const imetaTag = tags.find(tag => tag[0] === 'imeta');
if (imetaTag) {
const urlItem = imetaTag.find(item => item.startsWith('url '));
if (urlItem) {
return urlItem.split(' ')[1];
}
}
return '';
const rTag = tags.find(tag => tag[0] === 'r');
return rTag ? rTag[1] : '';
}
export function extractDimensions(event: NostrEvent): { width: number; height: number } {
const imetaTag = event.tags.find(tag => tag[0] === 'imeta');
if (imetaTag) {
const dimInfo = imetaTag.find(item => item.startsWith('dim '));
if (dimInfo) {
const [width, height] = dimInfo.split(' ')[1].split('x').map(Number);
return { width, height };
}
export function extractDimensions(event: NDKEvent | any): { width: number; height: number } {
let width = 0;
let height = 0;
// Try to get dimensions from dim tag
const dimTag = event.tags.find((tag: string[]) => tag[0] === 'dim');
if (dimTag) {
const [w, h] = dimTag[1].split('x').map(Number);
width = w;
height = h;
}
return { width: 500, height: 300 }; // Default dimensions if not found
return { width, height };
}
export async function signEvent(loginType: string | null, event: NostrEvent): Promise<NostrEvent | null> {
// Sign event
let eventSigned: NostrEvent = { ...event, sig: '' };
if (loginType === 'extension') {
eventSigned = await window.nostr.signEvent(event);
} else if (loginType === 'amber') {
// TODO: Sign event with amber
alert('Signing with Amber is not implemented yet, sorry!');
export async function signEvent(loginType: string | null, event: NDKEvent): Promise<NDKEvent | null> {
try {
if (loginType === 'extension') {
const signedRawEvent = await window.nostr.signEvent(event.rawEvent());
Object.assign(event, signedRawEvent);
return event;
} else if (loginType === 'amber') {
alert('Signing with Amber is not implemented yet, sorry!');
return null;
} else if (loginType === 'raw_nsec') {
const nsecStr = window.localStorage.getItem('nsec');
if (!nsecStr) throw new Error('No nsec found');
await event.sign();
return event;
}
} catch (error) {
console.error('Failed to sign event:', error);
return null;
}
return null;
}
export async function publishSignedEvent(event: NDKEvent): Promise<boolean> {
try {
await event.publish();
return true;
} catch (error) {
console.error('Failed to publish event:', error);
return false;
}
}
export function getEventRelays(event: NDKEvent): string[] {
return Array.from(event.relay?.url ? [event.relay.url] : []);
}
export function validateEventSignature(event: NDKEvent): boolean {
try {
return event.verify();
} catch (error) {
console.error('Event signature validation failed:', error);
return false;
}
}
export function parseNpub(input: string): string | null {
try {
if (input.startsWith('npub1')) {
return nip19.decode(input).data.toString();
}
return input;
} catch (error) {
console.error('Invalid npub format:', error);
return null;
} else if (loginType === 'raw_nsec') {
if (typeof window !== 'undefined') {
let nsecStr = null;
nsecStr = window.localStorage.getItem('nsec');
if (nsecStr != null) {
eventSigned = finalizeEvent(event, hexToBytes(nsecStr));
}
}
}
console.log(eventSigned);
return eventSigned;
}