Skip to content

Commit

Permalink
Merge pull request #32 from ostyjs/notifications
Browse files Browse the repository at this point in the history
Notifications Widget
  • Loading branch information
sepehr-safari authored Jan 21, 2025
2 parents 27484de + 50e63ef commit d44650a
Show file tree
Hide file tree
Showing 13 changed files with 454 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-osty",
"version": "0.7.4",
"version": "0.7.5",
"type": "module",
"license": "MIT",
"author": "Sepehr Safari",
Expand Down
2 changes: 1 addition & 1 deletion templates/nostribe/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nostribe",
"private": true,
"version": "0.0.5",
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk';
import { EventPointer, NDKEvent, NDKUser } from '@nostr-dev-kit/ndk';
import { useRealtimeProfile } from 'nostr-hooks';
import { neventEncode } from 'nostr-tools/nip19';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';

Expand All @@ -19,7 +20,6 @@ export const NoteContent = memo(
switch (chunk.type) {
case 'text':
case 'naddr':
case 'nevent':
return (
<span key={index} className="[overflow-wrap:anywhere]">
{chunk.content}
Expand Down Expand Up @@ -60,6 +60,25 @@ export const NoteContent = memo(
{chunk.content}
</a>
);
case 'nevent':
if (!inView) {
return null;
}

const parsedEvent = JSON.parse(chunk.content) as EventPointer;
if (parsedEvent.kind === 1) {
return (
<div className="p-4 bg-secondary/50">
<NoteByNoteId key={index} noteId={parsedEvent.id} />
</div>
);
} else {
return (
<span key={index} className="[overflow-wrap:anywhere]">
{`nostr:${neventEncode(parsedEvent)}`}
</span>
);
}
case 'note':
if (inView) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ export const parseChunks = (content: string): Chunk[] => {
const decoded = decode(match[0].substring(6));
switch (decoded.type) {
case 'naddr':
case 'nevent':
matches.push({
type: decoded.type,
content: match[0],
index: match.index,
fullLength: match[0].length,
});
break;
case 'nevent':
matches.push({
type: decoded.type,
content: JSON.stringify(decoded.data),
index: match.index,
fullLength: match[0].length,
});
break;
case 'npub':
case 'note':
matches.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk';
import { formatDistanceToNowStrict } from 'date-fns';
import { EllipsisIcon, FileJsonIcon, HeartIcon, LinkIcon, TagIcon, TextIcon } from 'lucide-react';
import {
EllipsisIcon,
FileJsonIcon,
HeartIcon,
LinkIcon,
SquareArrowOutUpRight,
TagIcon,
TextIcon,
} from 'lucide-react';

import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui/avatar';
import { Button } from '@/shared/components/ui/button';
Expand Down Expand Up @@ -59,6 +67,11 @@ export const NoteHeader = ({ event }: { event: NDKEvent }) => {
</DropdownMenuTrigger>

<DropdownMenuContent align="end" sideOffset={8}>
<DropdownMenuItem onClick={() => navigate(`/note/${nevent}`)}>
<SquareArrowOutUpRight className="w-4 h-4 mr-2" />
Open
</DropdownMenuItem>

<DropdownMenuItem
onClick={() => {
// TODO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './mentions-and-replies';
export * from './reactions';
export * from './reposts';
export * from './zaps';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';

import { NoteByNoteId } from '@/features/note-widget';

export const MentionsAndReplies = ({
mentionsAndReplies,
}: {
mentionsAndReplies: NDKEvent[] | undefined;
}) => {
return mentionsAndReplies?.map((event) => (
<>
<div key={event.id} className="px-2 py-1">
<div className="pt-2 rounded-md border flex flex-col gap-2">
<NoteByNoteId noteId={event.id} />
</div>
</div>
</>
));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ThumbsUpIcon } from 'lucide-react';
import { useRealtimeProfile } from 'nostr-hooks';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';

import { Avatar, AvatarImage } from '@/shared/components/ui/avatar';

import { Spinner } from '@/shared/components/spinner';

import { ellipsis } from '@/shared/utils';

import { NoteByNoteId } from '@/features/note-widget';

type CategorizedReactions = Map<string, NDKEvent[]>;

export const Reactions = memo(({ reactions }: { reactions: NDKEvent[] | undefined }) => {
const categorizedReactions: CategorizedReactions = useMemo(() => {
const categorizedReactions = new Map<string, NDKEvent[]>();

reactions?.forEach((reaction) => {
const eTags = reaction.getMatchingTags('e');
if (eTags.length > 0) {
const eTag = eTags[eTags.length - 1];
if (eTag.length > 0) {
const eventId = eTag[1];
const reactions = categorizedReactions.get(eventId) || [];
reactions.push(reaction);
categorizedReactions.set(eventId, reactions);
}
}
});

return categorizedReactions;
}, [reactions]);

if (reactions === undefined) {
return <Spinner />;
}

if (reactions.length === 0) {
return <div>No reactions yet</div>;
}

return (
<>
{Array.from(categorizedReactions.keys()).map((eventId) => (
<div key={eventId} className="px-2 py-1">
<div className="rounded-md border flex flex-col gap-2">
<div className="flex flex-wrap items-center gap-2 px-2 py-1 border-b">
<ThumbsUpIcon size={18} />

{categorizedReactions
.get(eventId)
?.map((reaction) => <Reaction key={reaction.id} reaction={reaction} />)}
</div>

<NoteByNoteId noteId={eventId} />
</div>
</div>
))}
</>
);
});

const Reaction = memo(({ reaction }: { reaction: NDKEvent }) => {
const { profile } = useRealtimeProfile(reaction.pubkey);

return (
<Link to={`/profile/${reaction.author.npub}`}>
<div className="flex gap-1 items-center">
<Avatar className="bg-secondary w-5 h-5">
<AvatarImage src={profile?.image} className="bg-secondary" />
</Avatar>

<p className="text-xs font-light">
<span>{ellipsis(profile?.name?.toString() || reaction.author.npub, 20)}</span>
<span> {reaction.content === '+' ? '👍' : reaction.content}</span>
</p>
</div>
</Link>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { Repeat2Icon } from 'lucide-react';
import { useRealtimeProfile } from 'nostr-hooks';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';

import { Avatar, AvatarImage } from '@/shared/components/ui/avatar';

import { Spinner } from '@/shared/components/spinner';

import { ellipsis } from '@/shared/utils';

import { NoteByNoteId } from '@/features/note-widget';

type CategorizedReposts = Map<string, NDKEvent[]>;

export const Reposts = memo(({ reposts }: { reposts: NDKEvent[] | undefined }) => {
const categorizedReposts: CategorizedReposts = useMemo(() => {
const categorizedReposts = new Map<string, NDKEvent[]>();

reposts?.forEach((repost) => {
const eTags = repost.getMatchingTags('e');
if (eTags.length > 0) {
const eTag = eTags[eTags.length - 1];
if (eTag.length > 0) {
const eventId = eTag[1];
const reposts = categorizedReposts.get(eventId) || [];
reposts.push(repost);
categorizedReposts.set(eventId, reposts);
}
}
});

return categorizedReposts;
}, [reposts]);

if (reposts === undefined) {
return <Spinner />;
}

if (reposts.length === 0) {
return <div>No reposts yet</div>;
}

return (
<>
{Array.from(categorizedReposts.keys()).map((eventId) => (
<div key={eventId} className="px-2 py-1">
<div className="rounded-md border flex flex-col gap-2">
<div className="flex flex-wrap items-center gap-2 px-2 py-1 border-b">
<Repeat2Icon size={18} />

{categorizedReposts
.get(eventId)
?.map((repost) => <Repost key={repost.id} repost={repost} />)}
</div>

<NoteByNoteId noteId={eventId} />
</div>
</div>
))}
</>
);
});

const Repost = memo(({ repost }: { repost: NDKEvent }) => {
const { profile } = useRealtimeProfile(repost.pubkey);

return (
<Link to={`/profile/${repost.author.npub}`}>
<div className="flex gap-1 items-center">
<Avatar className="bg-secondary w-5 h-5">
<AvatarImage src={profile?.image} className="bg-secondary" />
</Avatar>

<p className="text-xs font-light">
<span>{ellipsis(profile?.name?.toString() || repost.author.npub, 20)}</span>
</p>
</div>
</Link>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { NDKEvent, NDKUser, zapInvoiceFromEvent } from '@nostr-dev-kit/ndk';
import { ZapIcon } from 'lucide-react';
import { useRealtimeProfile } from 'nostr-hooks';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';

import { Avatar, AvatarImage } from '@/shared/components/ui/avatar';

import { Spinner } from '@/shared/components/spinner';

import { ellipsis } from '@/shared/utils';

import { NoteByNoteId } from '@/features/note-widget';

type CategorizedZaps = Map<string, NDKEvent[]>;

export const Zaps = memo(({ zaps }: { zaps: NDKEvent[] | undefined }) => {
const categorizedZaps: CategorizedZaps = useMemo(() => {
const categorizedZaps = new Map<string, NDKEvent[]>();

zaps?.forEach((zap) => {
const eTags = zap.getMatchingTags('e');
if (eTags.length > 0) {
const eTag = eTags[eTags.length - 1];
if (eTag.length > 0) {
const eventId = eTag[1];
const zaps = categorizedZaps.get(eventId) || [];
zaps.push(zap);
categorizedZaps.set(eventId, zaps);
}
}
});

return categorizedZaps;
}, [zaps]);

if (zaps === undefined) {
return <Spinner />;
}

if (zaps.length === 0) {
return <div>No zaps yet</div>;
}

return (
<>
{Array.from(categorizedZaps.keys()).map((eventId) => (
<div key={eventId} className="px-2 py-1">
<div className="rounded-md border flex flex-col gap-2">
<div className="flex flex-wrap items-center gap-2 px-2 py-1 border-b">
<ZapIcon size={18} />

{categorizedZaps.get(eventId)?.map((zap) => <Zap key={zap.id} zap={zap} />)}
</div>

<NoteByNoteId noteId={eventId} />
</div>
</div>
))}
</>
);
});

const Zap = memo(
({ zap }: { zap: NDKEvent }) => {
const invoice = zapInvoiceFromEvent(zap);
const { profile } = useRealtimeProfile(invoice?.zappee);
const npub = useMemo(
() => (invoice && invoice.zapper ? new NDKUser({ pubkey: invoice.zapper }).npub : ''),
[invoice?.zapper],
);

return (
<Link to={`/profile/${npub}`}>
<div className="flex gap-1 items-center">
<Avatar className="bg-secondary w-5 h-5">
<AvatarImage src={profile?.image} className="bg-secondary" />
</Avatar>

<p className="text-xs font-light">
<span className="font-bold"> {(invoice?.amount || 0) / 1000} sats</span>
<span> from {ellipsis(profile?.name ? profile.name.toString() : npub, 20)}</span>
</p>
</div>
</Link>
);
},
(prevProps, nextProps) => prevProps.zap.id === nextProps.zap.id,
);
Loading

0 comments on commit d44650a

Please sign in to comment.