feat: Enhance notification display with date grouping (#123)

* feat: Enhance notification display with date grouping and time formatting

* fix: Update date parsing in Notifications component and improve notification key assignment

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
mroxso
2025-05-24 23:04:33 +02:00
committed by GitHub
parent 41b8ffe70e
commit 621d4bb752
4 changed files with 119 additions and 50 deletions

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { useNostrEvents, useProfile } from "nostr-react";
import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card';
import {
NostrEvent,
Event,
@@ -8,6 +7,7 @@ import {
} from "nostr-tools";
import { Avatar, AvatarImage } from './ui/avatar';
import Link from 'next/link';
import { format } from 'date-fns';
interface NotificationProps {
event: NostrEvent;
@@ -52,60 +52,69 @@ const Notification: React.FC<NotificationProps> = ({ event }) => {
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 formatTime = (date: Date) => {
return format(date, 'h:mm a');
};
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'>
<Avatar>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
const getNotificationContent = () => {
switch (event.kind) {
case 9735: // ZAP
return (
<div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'>
<div className='flex-shrink-0 w-10 text-center font-medium text-amber-500'>
{sats}
</div>
<div className='col-span-4'>
<p>{name} zapped you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
<Avatar className='flex-shrink-0'>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>zapped you</span></p>
<p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</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>
);
case 3: // FOLLOW
return (
<div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'>
<div className='flex-shrink-0 w-10 text-center font-medium text-blue-500'>
👋
</div>
<div className='col-span-4'>
<p>{name} started following you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
<Avatar className='flex-shrink-0'>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>started following you</span></p>
<p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</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>
);
case 7: // REACTION
return (
<Link href={"/note/" + reactedToId} className='block'>
<div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'>
<div className='flex-shrink-0 w-10 text-center text-lg'>
{event.content}
</div>
<div className='col-span-4'>
<p>{name} reacted to you</p>
<p>{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}</p>
<Avatar className='flex-shrink-0'>
<AvatarImage src={userData?.picture} alt={name} />
</Avatar>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>reacted to your post</span></p>
<p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</p>
</div>
</div>
</Link>
)}
</div>
<hr className='mt-6' />
</>
);
default:
return null;
}
};
return (
<div className='notification-item'>
{getNotificationContent()}
</div>
);
}

View File

@@ -9,6 +9,7 @@ import {
nip19,
} from "nostr-tools";
import Notification from './Notification';
import { format, isSameDay, parseISO } from 'date-fns';
interface NotificationsProps {
pubkey: string;
@@ -52,10 +53,46 @@ const Notifications: React.FC<NotificationsProps> = ({ pubkey }) => {
// }
// });
// Create a combined and properly sorted array of all notifications
// const allNotifications = [...(zaps || []), ...(reactions || [])].sort(
// (a, b) => (b.created_at || 0) - (a.created_at || 0)
// );
// Sort all notifications by date (newest first)
const sortedEvents = [...events].sort(
(a, b) => (b.created_at || 0) - (a.created_at || 0)
);
// Group notifications by date
const groupedNotifications = () => {
const groups: { [key: string]: typeof events } = {};
sortedEvents.forEach(event => {
const date = new Date(event.created_at * 1000);
const dateKey = format(date, 'yyyy-MM-dd');
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(event);
});
return groups;
};
// Get formatted date heading based on date
const getDateHeading = (dateStr: string) => {
const date = parseISO(dateStr);
const today = new Date();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (isSameDay(date, today)) {
return "Today";
} else if (isSameDay(date, yesterday)) {
return "Yesterday";
} else {
return format(date, 'EEEE, MMMM d, yyyy');
}
};
const notificationGroups = groupedNotifications();
return (
<>
@@ -67,8 +104,19 @@ const Notifications: React.FC<NotificationsProps> = ({ pubkey }) => {
</CardHeader>
<CardContent>
{events.length > 0 ? (
events.map((notification, index) => (
<Notification key={index} event={notification} />
Object.keys(notificationGroups).map(dateKey => (
<div key={dateKey} className="mb-6">
<div className="sticky top-0 bg-background/95 backdrop-blur-sm py-2 mb-2 border-b">
<h3 className="text-sm font-medium text-muted-foreground">
{getDateHeading(dateKey)}
</h3>
</div>
<div className="space-y-1">
{notificationGroups[dateKey].map((notification) => (
<Notification key={notification.id} event={notification} />
))}
</div>
</div>
))
) : (
<div className="text-center py-4 text-muted-foreground">No notifications yet</div>

11
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"bolt11": "^1.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.0.0-rc21",
"html5-qrcode": "^2.3.8",
"light-bolt11-decoder": "^3.1.1",
@@ -6033,6 +6034,16 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.4",
"license": "MIT",

View File

@@ -30,6 +30,7 @@
"bolt11": "^1.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.0.0-rc21",
"html5-qrcode": "^2.3.8",
"light-bolt11-decoder": "^3.1.1",