Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eceb289
add lastSeenTimestamp state to useNotifications
Mosas2000 Mar 16, 2026
955fb89
sync lastSeenTimestamp state in markAllRead
Mosas2000 Mar 16, 2026
c6d9936
expose lastSeenTimestamp in useNotifications return value
Mosas2000 Mar 16, 2026
8d56db5
destructure lastSeenTimestamp from useNotifications in App
Mosas2000 Mar 16, 2026
db1db72
pass lastSeenTimestamp prop to Header component
Mosas2000 Mar 16, 2026
40e0905
accept lastSeenTimestamp prop in Header component
Mosas2000 Mar 16, 2026
75bf95b
forward lastSeenTimestamp to NotificationBell in Header
Mosas2000 Mar 16, 2026
017177e
accept lastSeenTimestamp prop in NotificationBell
Mosas2000 Mar 16, 2026
1d983b8
add isUnread helper to determine per-item read state
Mosas2000 Mar 16, 2026
4f389fd
conditionally render green dot only for unread notifications
Mosas2000 Mar 16, 2026
9e21a1f
add subtle background highlight for unread notification items
Mosas2000 Mar 16, 2026
24c7d74
add useNotifications test scaffold with basic assertions
Mosas2000 Mar 16, 2026
966caa5
test lastSeenTimestamp is exposed from localStorage value
Mosas2000 Mar 16, 2026
b21fbf3
test lastSeenTimestamp defaults to 0 when localStorage is empty
Mosas2000 Mar 16, 2026
0656d58
test markAllRead advances lastSeenTimestamp
Mosas2000 Mar 16, 2026
b514122
test markAllRead persists timestamp to localStorage
Mosas2000 Mar 16, 2026
8765930
test markAllRead resets unreadCount to zero
Mosas2000 Mar 16, 2026
5ce6125
test loading passthrough and no-op refetch function
Mosas2000 Mar 16, 2026
96fba6c
test synthetic timestamp fallback and enriched timestamp preservation
Mosas2000 Mar 16, 2026
76b47f8
test deduplication behavior and empty events graceful handling
Mosas2000 Mar 16, 2026
e0e966b
test dynamic unread count updates and address change to null
Mosas2000 Mar 16, 2026
e1cb83d
test sender exclusion from notification list
Mosas2000 Mar 16, 2026
84c6f9e
test new events after markAllRead appear as unread
Mosas2000 Mar 16, 2026
6fb046f
test unread count with mixed-order event timestamps
Mosas2000 Mar 16, 2026
b8946a4
add NotificationBell test scaffold with rendering tests
Mosas2000 Mar 16, 2026
7b59ea8
test badge count edge cases at 9 and 10
Mosas2000 Mar 16, 2026
9fb3c1d
test dropdown toggle, onMarkRead, and close behavior
Mosas2000 Mar 16, 2026
7768f38
test read/unread visual distinction with green dot and highlight
Mosas2000 Mar 16, 2026
60707c5
test timestamp boundaries, empty states, accessibility, and click out…
Mosas2000 Mar 16, 2026
208d190
test notification items, dot shape, multi-highlight, and mark all read
Mosas2000 Mar 16, 2026
f898e0b
include lastSeenTimestamp in routing test useNotifications mock
Mosas2000 Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function App() {
const { healthy, error: healthError, checking: healthChecking, retry: retryHealth } = useContractHealth();

const userAddress = getMainnetAddress(userData);
const { notifications, unreadCount, markAllRead, loading: notificationsLoading } = useNotifications(userAddress);
const { notifications, unreadCount, lastSeenTimestamp, markAllRead, loading: notificationsLoading } = useNotifications(userAddress);
const { isOwner } = useAdmin(userAddress);

usePageTitle();
Expand Down Expand Up @@ -137,6 +137,7 @@ function App() {
authLoading={authLoading}
notifications={notifications}
unreadCount={unreadCount}
lastSeenTimestamp={lastSeenTimestamp}
onMarkNotificationsRead={markAllRead}
notificationsLoading={notificationsLoading}
apiReachable={healthy}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Sun, Moon } from 'lucide-react';
* @param {Function} props.onMarkNotificationsRead - Callback to mark all read.
* @param {boolean} props.notificationsLoading - Whether notifications are loading.
*/
export default function Header({ userData, onAuth, authLoading, notifications, unreadCount, onMarkNotificationsRead, notificationsLoading, apiReachable = null }) {
export default function Header({ userData, onAuth, authLoading, notifications, unreadCount, lastSeenTimestamp, onMarkNotificationsRead, notificationsLoading, apiReachable = null }) {
const { theme, toggleTheme } = useTheme();
const isOnline = useOnlineStatus();

Expand Down Expand Up @@ -85,6 +85,7 @@ export default function Header({ userData, onAuth, authLoading, notifications, u
unreadCount={unreadCount}
onMarkRead={onMarkNotificationsRead}
loading={notificationsLoading}
lastSeenTimestamp={lastSeenTimestamp}
/>
)}

Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/NotificationBell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { formatSTX, formatAddress } from '../lib/utils';
import { Bell } from 'lucide-react';

export default function NotificationBell({ notifications, unreadCount, onMarkRead, loading }) {
export default function NotificationBell({ notifications, unreadCount, onMarkRead, loading, lastSeenTimestamp }) {
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);

Expand All @@ -25,6 +25,11 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea

const truncateAddr = (addr) => formatAddress(addr, 6, 4);

const isUnread = (tip) => {
if (lastSeenTimestamp == null) return false;
return tip.timestamp > lastSeenTimestamp;
};

return (
<div className="relative" ref={dropdownRef}>
<button
Expand Down Expand Up @@ -75,7 +80,7 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea
notifications.slice(0, 20).map((tip, i) => (
<div
key={tip.txId || i}
className="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors border-b border-gray-50 dark:border-gray-800/50 last:border-0"
className={`px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors border-b border-gray-50 dark:border-gray-800/50 last:border-0${isUnread(tip) ? ' bg-blue-50 dark:bg-blue-900/20' : ''}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
Expand All @@ -94,7 +99,9 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea
</p>
)}
</div>
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0 mt-1.5" />
{isUnread(tip) && (
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0 mt-1.5" />
)}
</div>
</div>
))
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/hooks/useNotifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ const STORAGE_KEY = 'tipstream_last_seen_tip_ts';
export function useNotifications(userAddress) {
const { events, eventsLoading } = useTipContext();
const [unreadCount, setUnreadCount] = useState(0);
const lastSeenRef = useRef(
parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10)
);
const initialLastSeen = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10);
const lastSeenRef = useRef(initialLastSeen);
const [lastSeenTimestamp, setLastSeenTimestamp] = useState(initialLastSeen);

/** Derive received tips from the shared event cache. */
const notifications = useMemo(() => {
Expand All @@ -41,9 +41,10 @@ export function useNotifications(userAddress) {
const markAllRead = useCallback(() => {
const now = Math.floor(Date.now() / 1000);
lastSeenRef.current = now;
setLastSeenTimestamp(now);
localStorage.setItem(STORAGE_KEY, String(now));
setUnreadCount(0);
}, []);

return { notifications, unreadCount, loading: eventsLoading, markAllRead, refetch: () => {} };
return { notifications, unreadCount, lastSeenTimestamp, loading: eventsLoading, markAllRead, refetch: () => {} };
}
Loading
Loading