mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-06-04 17:51:16 +02:00
AI made all of this lol
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'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't have an Account? <Link href="/onboarding">Create Account</Link></p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
))
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
})}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
72
hooks/useNDK.ts
Normal 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);
|
||||
}
|
||||
106
utils/utils.ts
106
utils/utils.ts
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user