mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-07 22:16:46 +02:00
Feature: Implement NIP-68 (Picture-first feeds) (#26)
* update global feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * update profile quick view feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * refactor: move getImageUrl function to utils for better modularity * show kind 20 notes on note subpage * also show comments on kind 20 events * update follower feed and follower quick view feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * update profile feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * update search feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * update tag feed to use kind 20 and kind 20 cards (NIP-68 Picture-first feeds) * add relay.damus.io * update KIND20Card and QuickViewKind20NoteCard to use Next.js Image component and extract dimensions from event metadata
This commit is contained in:
@@ -18,6 +18,7 @@ const DashboardPage: React.FC= ({ }) => {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function FeedPage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ export default function Home() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function NotePage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ const NotificationsPage: React.FC= ({ }) => {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function OnboardingCreateProfile() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ export default function OnboardingHome() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,8 @@ import { NostrProvider } from "nostr-react";
|
||||
export default function Home() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
// "wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function ProfilePage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function ProfileSettingsPage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NostrProvider } from "nostr-react";
|
||||
export default function ReelPage() {
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function SearchPage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function SearchMainPage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function Home() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function UploadPage() {
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.damus.io",
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@ const CommentsCompontent: React.FC<CommentsCompontentProps> = ({ pubkey, event }
|
||||
|
||||
const { events } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [1],
|
||||
// kinds: [1],
|
||||
'#e': [event.id],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useRef } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import NoteCard from './NoteCard';
|
||||
import KIND20Card from "./KIND20Card";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface FollowerFeedProps {
|
||||
pubkey: string;
|
||||
@@ -25,26 +27,17 @@ const FollowerFeed: React.FC<FollowerFeedProps> = ({ pubkey }) => {
|
||||
// since: dateToUnix(now.current), // all new events from now
|
||||
// since: 0,
|
||||
limit: 1000,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
authors: followingPubkeys,
|
||||
},
|
||||
});
|
||||
|
||||
// const filteredEvents = events.filter((event) => event.content.includes(".jpg"));
|
||||
// filter events with regex that checks for png, jpg, or gif
|
||||
let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|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"));
|
||||
// filter out all replies
|
||||
filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' }));
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredEvents.map((event) => (
|
||||
{events.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} />
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useRef } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import QuickViewNoteCard from "./QuickViewNoteCard";
|
||||
import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface FollowerQuickViewFeedProps {
|
||||
pubkey: string;
|
||||
@@ -26,19 +28,15 @@ const FollowerQuickViewFeed: React.FC<FollowerQuickViewFeedProps> = ({ pubkey })
|
||||
// since: dateToUnix(now.current), // all new events from now
|
||||
// since: 0,
|
||||
limit: 1000,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
authors: followingPubkeys,
|
||||
},
|
||||
});
|
||||
|
||||
let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g)?.[0]);
|
||||
// filter out all replies (tag[0] == e)
|
||||
filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{filteredEvents.length === 0 ? (
|
||||
{events.length === 0 ? (
|
||||
<>
|
||||
<div>
|
||||
<Skeleton className="h-[125px] rounded-xl" />
|
||||
@@ -62,8 +60,8 @@ const FollowerQuickViewFeed: React.FC<FollowerQuickViewFeedProps> = ({ pubkey })
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (filteredEvents.map((event) => (
|
||||
<QuickViewNoteCard key={event.id} pubkey={event.pubkey} text={event.content} event={event} tags={event.tags} eventId={event.id} linkToNote={true} />
|
||||
) : (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>
|
||||
</>
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
import { useRef } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import NoteCard from './NoteCard';
|
||||
import { useNostrEvents } from "nostr-react";
|
||||
import KIND20Card from "./KIND20Card";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
const GlobalFeed: React.FC = () => {
|
||||
const now = useRef(new Date()); // Make sure current time isn't re-rendered
|
||||
|
||||
const { events } = useNostrEvents({
|
||||
filter: {
|
||||
// since: dateToUnix(now.current), // all new events from now
|
||||
// since: 0,
|
||||
limit: 100,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
},
|
||||
});
|
||||
|
||||
// const filteredEvents = events.filter((event) => event.content.includes(".jpg"));
|
||||
// filter events with regex that checks for png, jpg, or gif
|
||||
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' }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Global 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} />
|
||||
</div>
|
||||
))}
|
||||
{events.map((event) => {
|
||||
const imageUrl = getImageUrl(event.tags);
|
||||
return (
|
||||
<div key={event.id} className="py-6">
|
||||
<KIND20Card
|
||||
key={event.id}
|
||||
pubkey={event.pubkey}
|
||||
text={event.content}
|
||||
image={imageUrl}
|
||||
eventId={event.id}
|
||||
tags={event.tags}
|
||||
event={event}
|
||||
showViewNoteCardButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalFeed;
|
||||
export default GlobalFeed;
|
||||
|
||||
|
||||
124
lumina/components/KIND20Card.tsx
Normal file
124
lumina/components/KIND20Card.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { useProfile } from "nostr-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"
|
||||
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 ViewCopyButton from './ViewCopyButton';
|
||||
import { Event as NostrEvent } from "nostr-tools";
|
||||
import ZapButton from './ZapButton';
|
||||
import Image from 'next/image';
|
||||
import { extractDimensions } from '@/utils/utils';
|
||||
|
||||
interface KIND20CardProps {
|
||||
pubkey: string;
|
||||
text: string;
|
||||
image: string;
|
||||
eventId: string;
|
||||
tags: string[][];
|
||||
event: NostrEvent;
|
||||
showViewNoteCardButton: boolean;
|
||||
}
|
||||
|
||||
const KIND20Card: React.FC<KIND20CardProps> = ({ pubkey, text, image, eventId, tags, event, showViewNoteCardButton }) => {
|
||||
const { data: userData } = useProfile({
|
||||
pubkey,
|
||||
});
|
||||
|
||||
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 { width, height } = extractDimensions(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Link href={hrefProfile} style={{ textDecoration: 'none' }}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar>
|
||||
<AvatarImage src={profileImageSrc} />
|
||||
</Avatar>
|
||||
<span className='break-all' style={{ marginLeft: '10px' }}>{title}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Link>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='py-4'>
|
||||
<div className='w-full h-full px-10'>
|
||||
{image && (
|
||||
<Image
|
||||
src={image || "/placeholder.svg"}
|
||||
alt={text}
|
||||
width={width}
|
||||
height={height}
|
||||
className='rounded lg:rounded-lg'
|
||||
style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<div className='break-word overflow-hidden'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='py-4 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'>
|
||||
<ViewCopyButton event={event} />
|
||||
<ViewRawButton event={event} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<small className="text-muted">{createdAt.toLocaleString()}</small>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default KIND20Card;
|
||||
@@ -2,6 +2,8 @@ import { useRef } from "react";
|
||||
import { useNostrEvents } from "nostr-react";
|
||||
import NoteCard from '@/components/NoteCard';
|
||||
import CommentsCompontent from "@/components/CommentsCompontent";
|
||||
import KIND20Card from "./KIND20Card";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface NotePageComponentProps {
|
||||
id: string;
|
||||
@@ -12,23 +14,45 @@ const NotePageComponent: React.FC<NotePageComponentProps> = ({ id }) => {
|
||||
|
||||
const { events } = useNostrEvents({
|
||||
filter: {
|
||||
// since: dateToUnix(now.current), // all new events from now
|
||||
// since: 0,
|
||||
ids: [id],
|
||||
limit: 1,
|
||||
kinds: [1],
|
||||
},
|
||||
});
|
||||
|
||||
// filter out all events that also have another e tag with another id
|
||||
const filteredEvents = events.filter((event) => { return event.tags.filter((tag) => { return tag[0] === '#e' && tag[1] !== id }).length === 0 });
|
||||
const filteredEvents = events.filter((event) => {
|
||||
return event.tags.filter((tag) => {
|
||||
return tag[0] === '#e' && tag[1] !== id;
|
||||
}).length === 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{events.map((event) => (
|
||||
// <p key={event.id}>{event.pubkey} posted: {event.content}</p>
|
||||
{filteredEvents.map((event) => (
|
||||
<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={false} />
|
||||
{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>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useRef } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import NoteCard from '@/components/NoteCard';
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import KIND20Card from "./KIND20Card";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface ProfileFeedProps {
|
||||
pubkey: string;
|
||||
@@ -16,19 +18,15 @@ const ProfileFeed: React.FC<ProfileFeedProps> = ({ pubkey }) => {
|
||||
authors: [pubkey],
|
||||
// since: 0,
|
||||
// limit: 10,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
},
|
||||
});
|
||||
|
||||
let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov|jpeg)/g)?.[0]);
|
||||
// filter out all replies (tag[0] == e)
|
||||
filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' }));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <h2>Profile Feed</h2> */}
|
||||
|
||||
{filteredEvents.length === 0 ? (
|
||||
{events.length === 0 ? (
|
||||
<div className="flex flex-col space-y-3">
|
||||
<Skeleton className="h-[125px] rounded-xl" />
|
||||
<div className="space-y-2">
|
||||
@@ -36,11 +34,11 @@ const ProfileFeed: React.FC<ProfileFeedProps> = ({ pubkey }) => {
|
||||
<Skeleton className="h-4 w-[200px]" />
|
||||
</div>
|
||||
</div>
|
||||
) : (filteredEvents.map((event) => (
|
||||
) : (events.map((event) => (
|
||||
// <p key={event.id}>{event.pubkey} posted: {event.content}</p>
|
||||
// <ProfileNoteCard key={event.id} pubkey={event.pubkey} text={event.content} event={event} tags={event.tags} />
|
||||
<div key={event.id} className="py-6">
|
||||
<NoteCard key={event.id} pubkey={event.pubkey} text={event.content} event={event} tags={event.tags} eventId={event.id} showViewNoteCardButton={true}/>
|
||||
<KIND20Card key={event.id} pubkey={event.pubkey} text={event.content} image={getImageUrl(event.tags)} event={event} tags={event.tags} eventId={event.id} showViewNoteCardButton={true}/>
|
||||
</div>
|
||||
)))}
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import { useNostrEvents } from "nostr-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import QuickViewNoteCard from "./QuickViewNoteCard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface ProfileQuickViewFeedProps {
|
||||
pubkey: string;
|
||||
@@ -12,18 +13,14 @@ const ProfileQuickViewFeed: React.FC<ProfileQuickViewFeedProps> = ({ pubkey }) =
|
||||
const now = useRef(new Date()); // Make sure current time isn't re-rendered
|
||||
const [limit, setLimit] = useState(100);
|
||||
|
||||
const { isLoading ,events } = useNostrEvents({
|
||||
const { isLoading, events } = useNostrEvents({
|
||||
filter: {
|
||||
authors: [pubkey],
|
||||
limit: limit,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
},
|
||||
});
|
||||
|
||||
let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov|jpeg)/g)?.[0]);
|
||||
// filter out all replies (tag[0] == e)
|
||||
filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' }));
|
||||
|
||||
const loadMore = () => {
|
||||
setLimit(limit => limit + 50);
|
||||
}
|
||||
@@ -31,7 +28,7 @@ const ProfileQuickViewFeed: React.FC<ProfileQuickViewFeedProps> = ({ pubkey }) =
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{filteredEvents.length === 0 && isLoading ? (
|
||||
{events.length === 0 && isLoading ? (
|
||||
<>
|
||||
<div>
|
||||
<Skeleton className="h-[125px] rounded-xl" />
|
||||
@@ -45,9 +42,12 @@ const ProfileQuickViewFeed: React.FC<ProfileQuickViewFeedProps> = ({ pubkey }) =
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{filteredEvents.map((event) => (
|
||||
<QuickViewNoteCard key={event.id} pubkey={event.pubkey} text={event.content} event={event} tags={event.tags} eventId={event.id} linkToNote={true} />
|
||||
))}
|
||||
{events.map((event) => {
|
||||
const imageUrl = getImageUrl(event.tags);
|
||||
return (
|
||||
<QuickViewKind20NoteCard key={event.id} pubkey={event.pubkey} text={event.content} image={imageUrl} event={event} tags={event.tags} eventId={event.id} linkToNote={true} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
65
lumina/components/QuickViewKind20NoteCard.tsx
Normal file
65
lumina/components/QuickViewKind20NoteCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { useProfile } from "nostr-react";
|
||||
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 {
|
||||
pubkey: string;
|
||||
text: string;
|
||||
image: string;
|
||||
eventId: string;
|
||||
tags: string[][];
|
||||
event: any;
|
||||
linkToNote: boolean;
|
||||
}
|
||||
|
||||
const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubkey, text, image, eventId, tags, event, linkToNote }) => {
|
||||
|
||||
text = text.replaceAll('\n', ' ');
|
||||
const encodedNoteId = nip19.noteEncode(event.id)
|
||||
|
||||
const { width, height } = extractDimensions(event);
|
||||
|
||||
const card = (
|
||||
<Card>
|
||||
<SmallCardContent>
|
||||
<div>
|
||||
<div className='d-flex justify-content-center align-items-center'>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Image
|
||||
src={image || "/placeholder.svg"}
|
||||
alt={text}
|
||||
width={width}
|
||||
height={height}
|
||||
className='rounded lg:rounded-lg'
|
||||
style={{ maxWidth: '100%', maxHeight: '75vh', objectFit: 'contain', margin: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmallCardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{linkToNote ? (
|
||||
<Link href={`/note/${encodedNoteId}`}>
|
||||
{card}
|
||||
</Link>
|
||||
) : (
|
||||
card
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuickViewKind20NoteCard;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useRef } from "react";
|
||||
import { useNostrEvents, dateToUnix } from "nostr-react";
|
||||
import NoteCard from './NoteCard';
|
||||
import KIND20Card from "./KIND20Card";
|
||||
import { getImageUrl } from "@/utils/utils";
|
||||
|
||||
interface TagFeedProps {
|
||||
tag: string;
|
||||
@@ -14,27 +16,18 @@ const TagFeed: React.FC<TagFeedProps> = ({tag}) => {
|
||||
// since: dateToUnix(now.current), // all new events from now
|
||||
// since: 0,
|
||||
// limit: 100,
|
||||
kinds: [1],
|
||||
kinds: [20],
|
||||
"#t": [tag],
|
||||
},
|
||||
});
|
||||
|
||||
// const filteredEvents = events.filter((event) => event.content.includes(".jpg"));
|
||||
// filter events with regex that checks for png, jpg, or gif
|
||||
let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|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"));
|
||||
// filter out all replies
|
||||
filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Tag Feed for {tag}</h2>
|
||||
{filteredEvents.map((event) => (
|
||||
{events.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} />
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -30,6 +30,8 @@ import Link from 'next/link';
|
||||
import { Event as NostrEvent } from "nostr-tools";
|
||||
import ProfileInfoCard from '../ProfileInfoCard';
|
||||
import NoteCard from '../NoteCard';
|
||||
import KIND20Card from '../KIND20Card';
|
||||
import { getImageUrl } from '@/utils/utils';
|
||||
|
||||
interface SearchNotesBoxProps {
|
||||
searchTag: string;
|
||||
@@ -38,7 +40,7 @@ interface SearchNotesBoxProps {
|
||||
const SearchNotesBox: React.FC<SearchNotesBoxProps> = ({ searchTag }) => {
|
||||
const { events: notes } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [1],
|
||||
kinds: [1, 20],
|
||||
search: searchTag,
|
||||
limit: 10,
|
||||
},
|
||||
@@ -53,7 +55,11 @@ const SearchNotesBox: React.FC<SearchNotesBoxProps> = ({ searchTag }) => {
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{notes.map((event: NostrEvent) => (
|
||||
<NoteCard event={event} eventId={event.id} pubkey={event.pubkey} showViewNoteCardButton={true} tags={event.tags} text={event.content} key={event.id} />
|
||||
event.kind === 1 ? (
|
||||
<NoteCard event={event} eventId={event.id} pubkey={event.pubkey} showViewNoteCardButton={true} tags={event.tags} text={event.content} key={event.id} />
|
||||
) : event.kind === 20 ? (
|
||||
<KIND20Card key={event.id} pubkey={event.pubkey} text={event.content} image={getImageUrl(event.tags)} event={event} tags={event.tags} eventId={event.id} showViewNoteCardButton={true}/>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
24
lumina/utils/utils.ts
Normal file
24
lumina/utils/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Event as NostrEvent } from "nostr-tools";
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
return { width: 500, height: 300 }; // Default dimensions if not found
|
||||
}
|
||||
Reference in New Issue
Block a user