notifications big attention

This commit is contained in:
alma 2026-01-11 23:01:31 +01:00
parent 0fc5c75217
commit 313c2a874f
9 changed files with 3 additions and 860 deletions

View File

@ -1,79 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from "@/app/api/auth/options";
import { NotificationService } from '@/lib/services/notifications/notification-service';
// POST /api/notifications/{id}/read
export async function POST(
request: Request,
context: { params: Promise<{ id: string }> }
) {
const startTime = Date.now();
try {
console.log('[NOTIFICATION_API] Mark as read endpoint called');
// Authenticate user
const session = await getServerSession(authOptions);
if (!session || !session.user?.id) {
console.log('[NOTIFICATION_API] Mark as read - Authentication failed');
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
// Await params as per Next.js requirements
const params = await context.params;
const id = params?.id;
if (!id) {
console.log('[NOTIFICATION_API] Mark as read - Missing notification ID');
return NextResponse.json(
{ error: "Missing notification ID" },
{ status: 400 }
);
}
const userId = session.user.id;
console.log('[NOTIFICATION_API] Mark as read - Processing', {
userId,
notificationId: id,
timestamp: new Date().toISOString()
});
const notificationService = NotificationService.getInstance();
const success = await notificationService.markAsRead(userId, id);
const duration = Date.now() - startTime;
if (!success) {
console.log('[NOTIFICATION_API] Mark as read - Failed', {
userId,
notificationId: id,
duration: `${duration}ms`
});
return NextResponse.json(
{ error: "Failed to mark notification as read" },
{ status: 400 }
);
}
console.log('[NOTIFICATION_API] Mark as read - Success', {
userId,
notificationId: id,
duration: `${duration}ms`
});
return NextResponse.json({ success: true });
} catch (error: any) {
const duration = Date.now() - startTime;
console.error('[NOTIFICATION_API] Mark as read - Error', {
error: error.message,
stack: error.stack,
duration: `${duration}ms`
});
return NextResponse.json(
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}
}

View File

@ -1,62 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from "@/app/api/auth/options";
import { NotificationService } from '@/lib/services/notifications/notification-service';
// POST /api/notifications/read-all
export async function POST(request: Request) {
const startTime = Date.now();
try {
console.log('[NOTIFICATION_API] Mark all as read endpoint called');
// Authenticate user
const session = await getServerSession(authOptions);
if (!session || !session.user?.id) {
console.log('[NOTIFICATION_API] Mark all as read - Authentication failed');
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
const userId = session.user.id;
console.log('[NOTIFICATION_API] Mark all as read - Processing', {
userId,
timestamp: new Date().toISOString()
});
const notificationService = NotificationService.getInstance();
const success = await notificationService.markAllAsRead(userId);
const duration = Date.now() - startTime;
if (!success) {
console.log('[NOTIFICATION_API] Mark all as read - Failed', {
userId,
duration: `${duration}ms`
});
return NextResponse.json(
{ error: "Failed to mark all notifications as read" },
{ status: 400 }
);
}
console.log('[NOTIFICATION_API] Mark all as read - Success', {
userId,
duration: `${duration}ms`
});
return NextResponse.json({ success: true });
} catch (error: any) {
const duration = Date.now() - startTime;
console.error('[NOTIFICATION_API] Mark all as read - Error', {
error: error.message,
stack: error.stack,
duration: `${duration}ms`
});
return NextResponse.json(
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}
}

View File

@ -1,6 +1,6 @@
import React, { memo, useState, useEffect } from 'react';
import Link from 'next/link';
import { Bell, Check, ExternalLink, AlertCircle, LogIn, Kanban, MessageSquare, Mail } from 'lucide-react';
import { Bell, ExternalLink, AlertCircle, LogIn, Kanban, MessageSquare, Mail } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { useNotifications } from '@/hooks/use-notifications';
import { Button } from '@/components/ui/button';
@ -22,7 +22,7 @@ interface NotificationBadgeProps {
// Use React.memo to prevent unnecessary re-renders
export const NotificationBadge = memo(function NotificationBadge({ className }: NotificationBadgeProps) {
const { data: session, status } = useSession();
const { notifications, notificationCount, markAsRead, markAllAsRead, fetchNotifications, loading, error, markingProgress } = useNotifications();
const { notifications, notificationCount, fetchNotifications, loading, error } = useNotifications();
const hasUnread = notificationCount.unread > 0;
const [isOpen, setIsOpen] = useState(false);
const [manualFetchAttempted, setManualFetchAttempted] = useState(false);
@ -69,19 +69,6 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
}
}, [isOpen, status]);
const handleMarkAsRead = async (notificationId: string) => {
await markAsRead(notificationId);
};
const handleMarkAllAsRead = async () => {
// Don't close dropdown immediately - show progress
await markAllAsRead();
// Close dropdown after a short delay to show completion
setTimeout(() => {
setIsOpen(false);
}, 500);
};
// Force fetch when component mounts
useEffect(() => {
if (status === 'authenticated') {
@ -126,40 +113,10 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
<DropdownMenuContent align="end" className="w-80 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between p-4">
<h3 className="font-medium">Notifications</h3>
{notificationCount.unread > 0 && !markingProgress && (
<Button variant="ghost" size="sm" onClick={handleMarkAllAsRead}>
<Check className="h-4 w-4 mr-2" />
Mark all read
</Button>
)}
{markingProgress && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900"></div>
<span>Marking {markingProgress.current} of {markingProgress.total}...</span>
</div>
)}
</div>
<DropdownMenuSeparator />
{markingProgress ? (
<div className="py-8 px-4 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
<p className="text-sm text-muted-foreground mb-2">
Marking notifications as read...
</p>
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{
width: `${(markingProgress.current / markingProgress.total) * 100}%`
}}
></div>
</div>
<p className="text-xs text-muted-foreground">
{markingProgress.current} of {markingProgress.total} completed
</p>
</div>
) : loading ? (
{loading ? (
<div className="py-8 px-4 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
<p className="text-sm text-muted-foreground">Loading notifications...</p>
@ -229,17 +186,6 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
</div>
</div>
<div className="flex space-x-1 ml-2">
{!notification.isRead && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => handleMarkAsRead(notification.id)}
>
<Check className="h-3.5 w-3.5" />
<span className="sr-only">Mark as read</span>
</Button>
)}
{notification.link && (
<Link href={notification.link}>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">

View File

@ -18,7 +18,6 @@ export function useNotifications() {
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [markingProgress, setMarkingProgress] = useState<{ current: number; total: number } | null>(null);
const isMountedRef = useRef<boolean>(false);
// Fetch notification count with request deduplication
@ -119,137 +118,6 @@ export function useNotifications() {
}
}, [session?.user]);
// Mark notification as read
const markAsRead = useCallback(async (notificationId: string) => {
if (!session?.user) return false;
try {
console.log('[useNotifications] Marking notification as read:', notificationId);
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // Ensure cookies are sent with the request
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to mark notification as read:', {
status: response.status,
body: errorText
});
return false;
}
// Update local state optimistically (only if we're confident it will succeed)
setNotifications(prev =>
prev.map(notification =>
notification.id === notificationId
? { ...notification, isRead: true }
: notification
)
);
// Update count optimistically (decrement unread count)
setNotificationCount(prev => ({
...prev,
unread: Math.max(0, prev.unread - 1), // Ensure it doesn't go below 0
total: prev.total, // Keep total the same
}));
// Refresh notification count after a delay to ensure cache is invalidated
// Poll until count matches expected value or timeout
let pollCount = 0;
const maxPolls = 5;
const pollInterval = 500; // 500ms between polls
const pollForCount = async () => {
if (pollCount >= maxPolls) return;
pollCount++;
await new Promise(resolve => setTimeout(resolve, pollInterval));
await fetchNotificationCount(true);
// Check if count matches expected (unread should be prev - 1)
// If not, poll again
if (pollCount < maxPolls) {
setTimeout(pollForCount, pollInterval);
}
};
// Start polling after initial delay
setTimeout(pollForCount, 300);
return true;
} catch (err) {
console.error('Error marking notification as read:', err);
return false;
}
}, [session?.user, fetchNotificationCount]);
// Mark all notifications as read with progress tracking
const markAllAsRead = useCallback(async () => {
if (!session?.user) return false;
try {
console.log('[useNotifications] Marking all notifications as read');
// Show loading state instead of optimistic update
setMarkingProgress({ current: 0, total: notificationCount.unread });
const response = await fetch('/api/notifications/read-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to mark all notifications as read:', {
status: response.status,
body: errorText
});
setMarkingProgress(null);
return false;
}
// Update local state optimistically after successful response
setNotifications(prev =>
prev.map(notification => ({ ...notification, isRead: true }))
);
// Update count optimistically
setNotificationCount(prev => ({
...prev,
unread: 0,
total: prev.total,
sources: Object.fromEntries(
Object.entries(prev.sources).map(([key, value]) => [
key,
{ ...value, unread: 0 }
])
),
}));
// Clear progress
setMarkingProgress(null);
// Refresh notification count after a delay to ensure cache is invalidated
setTimeout(() => {
fetchNotificationCount(true);
}, 500);
return true;
} catch (err) {
console.error('Error marking all notifications as read:', err);
setMarkingProgress(null);
return false;
}
}, [session?.user, fetchNotificationCount, notificationCount.unread]);
// Use unified refresh system for notification count
const { refresh: refreshCount } = useUnifiedRefresh({
resource: 'notifications-count',
@ -298,10 +166,7 @@ export function useNotifications() {
notificationCount,
loading,
error,
markingProgress, // Progress for mark all as read
fetchNotifications,
fetchNotificationCount: () => fetchNotificationCount(true),
markAsRead,
markAllAsRead
};
}

View File

@ -344,23 +344,4 @@ export class EmailAdapter implements NotificationAdapter {
}
}
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
// Email read status is handled by the email service when emails are viewed
// We return true to acknowledge the UI action, but the actual read status
// will be updated when the user views the email in the email service
logger.debug('[EMAIL_ADAPTER] markAsRead called (read status handled by email service)', {
userId,
notificationId,
});
return true;
}
async markAllAsRead(userId: string): Promise<boolean> {
// Email read status is handled by the email service when emails are viewed
// We return true to acknowledge the UI action
logger.debug('[EMAIL_ADAPTER] markAllAsRead called (read status handled by email service)', {
userId,
});
return true;
}
}

View File

@ -283,365 +283,6 @@ export class LeantimeAdapter implements NotificationAdapter {
}
}
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
logger.debug('[LEANTIME_ADAPTER] markAsRead called', {
userId,
notificationId,
});
try {
// Extract the source ID from our compound ID
const sourceId = notificationId.replace(`${this.sourceName}-`, '');
// Get user email and ID
const email = await this.getUserEmail();
if (!email) {
logger.error('[LEANTIME_ADAPTER] Could not get user email from session');
return false;
}
const leantimeUserId = await this.getLeantimeUserId(email);
if (!leantimeUserId) {
logger.error('[LEANTIME_ADAPTER] User not found in Leantime', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
return false;
}
// Make request to Leantime API to mark notification as read
// According to Leantime docs: method is markNotificationRead, params are id and userId
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.Notifications.Notifications.markNotificationRead',
params: {
id: parseInt(sourceId),
userId: leantimeUserId
},
id: 1
};
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify(jsonRpcBody)
});
logger.debug('[LEANTIME_ADAPTER] markAsRead response status', {
status: response.status,
});
if (!response.ok) {
const errorText = await response.text();
logger.error('[LEANTIME_ADAPTER] markAsRead HTTP error', {
status: response.status,
bodyPreview: errorText.substring(0, 200),
});
return false;
}
const responseText = await response.text();
let data;
try {
data = JSON.parse(responseText);
} catch (parseError) {
logger.error('[LEANTIME_ADAPTER] markAsRead failed to parse response', {
error: parseError instanceof Error ? parseError.message : String(parseError),
});
return false;
}
if (data.error) {
logger.error('[LEANTIME_ADAPTER] markAsRead API error', {
error: data.error,
});
return false;
}
const success = data.result === true || data.result === "true" || !!data.result;
logger.debug('[LEANTIME_ADAPTER] markAsRead success', { success });
return success;
} catch (error) {
logger.error('[LEANTIME_ADAPTER] Error marking notification as read', {
error: error instanceof Error ? error.message : String(error),
});
return false;
}
}
async markAllAsRead(userId: string): Promise<boolean> {
// CRITICAL: This should ALWAYS appear if method is called
// Using multiple logging methods to ensure visibility
logger.info('[LEANTIME_ADAPTER] markAllAsRead START', {
userId,
hasApiUrl: !!this.apiUrl,
hasApiToken: !!this.apiToken,
});
try {
// Get user email and ID
const email = await this.getUserEmail();
if (!email) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead could not get user email from session');
return false;
}
const leantimeUserId = await this.getLeantimeUserId(email);
if (!leantimeUserId) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead user not found in Leantime', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
return false;
}
// Leantime doesn't have a "mark all as read" method, so we need to:
// 1. Fetch all unread notifications directly from API (bypassing any cache)
// 2. Mark each one individually using markNotificationRead
// Fetch all notifications directly from API (up to 1000) to get fresh data (not cached)
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.Notifications.Notifications.getAllNotifications',
params: {
userId: leantimeUserId,
showNewOnly: 0, // Get all, not just unread
limitStart: 0,
limitEnd: 1000,
filterOptions: []
},
id: 1
};
const fetchResponse = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify(jsonRpcBody)
});
if (!fetchResponse.ok) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead failed to fetch notifications', {
status: fetchResponse.status,
});
return false;
}
const fetchData = await fetchResponse.json();
if (fetchData.error) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead error fetching notifications', {
error: fetchData.error,
});
return false;
}
// Transform the raw Leantime notifications to our format
const rawNotifications = Array.isArray(fetchData.result) ? fetchData.result : [];
const unreadNotifications = rawNotifications
.filter((n: any) => n.read === 0 || n.read === false || n.read === '0')
.map((n: any) => ({ id: n.id, sourceId: String(n.id) }));
logger.info('[LEANTIME_ADAPTER] markAllAsRead unread notifications', {
unreadCount: unreadNotifications.length,
total: rawNotifications.length,
});
if (unreadNotifications.length === 0) {
logger.info('[LEANTIME_ADAPTER] markAllAsRead no unread notifications');
return true;
}
// Mark notifications in batches to prevent API overload and connection resets
const BATCH_SIZE = 15; // Process 15 notifications at a time
const BATCH_DELAY = 200; // 200ms delay between batches
const MAX_RETRIES = 2; // Retry failed notifications up to 2 times
let successCount = 0;
let failureCount = 0;
const failedNotifications: number[] = [];
// Helper function to mark a single notification
const markSingleNotification = async (notificationId: number, retryCount = 0): Promise<boolean> => {
try {
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.Notifications.Notifications.markNotificationRead',
params: {
id: notificationId,
userId: leantimeUserId
},
id: 1
};
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify(jsonRpcBody)
});
if (!response.ok) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead failed to mark notification', {
notificationId,
status: response.status,
});
// Retry on server errors (5xx) or rate limiting (429)
if ((response.status >= 500 || response.status === 429) && retryCount < MAX_RETRIES) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 2000); // Exponential backoff, max 2s
logger.debug('[LEANTIME_ADAPTER] Retrying notification after HTTP error', {
notificationId,
delay,
attempt: retryCount + 1,
maxRetries: MAX_RETRIES,
});
await new Promise(resolve => setTimeout(resolve, delay));
return markSingleNotification(notificationId, retryCount + 1);
}
return false;
}
const data = await response.json();
if (data.error) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead JSON-RPC error marking notification', {
notificationId,
error: data.error,
});
// Retry on certain JSON-RPC errors
if (retryCount < MAX_RETRIES && (data.error.code === -32603 || data.error.code === -32000)) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 2000);
logger.debug('[LEANTIME_ADAPTER] Retrying notification after JSON-RPC error', {
notificationId,
delay,
attempt: retryCount + 1,
maxRetries: MAX_RETRIES,
});
await new Promise(resolve => setTimeout(resolve, delay));
return markSingleNotification(notificationId, retryCount + 1);
}
return false;
}
return data.result === true || data.result === "true" || !!data.result;
} catch (error) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead exception marking notification', {
notificationId,
error: error instanceof Error ? error.message : String(error),
});
// Retry on network errors
if (retryCount < MAX_RETRIES && error instanceof Error) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 2000);
logger.debug('[LEANTIME_ADAPTER] Retrying notification after network error', {
notificationId,
delay,
attempt: retryCount + 1,
maxRetries: MAX_RETRIES,
});
await new Promise(resolve => setTimeout(resolve, delay));
return markSingleNotification(notificationId, retryCount + 1);
}
return false;
}
};
// Process notifications in batches
const notificationIds: number[] = unreadNotifications
.map((n: { id: number | string; sourceId: string }): number | null => {
const id = typeof n.id === 'number' ? n.id : parseInt(String(n.id || n.sourceId));
return isNaN(id) ? null : id;
})
.filter((id: number | null): id is number => id !== null);
logger.info('[LEANTIME_ADAPTER] markAllAsRead processing notifications', {
count: notificationIds.length,
batchSize: BATCH_SIZE,
});
// Split into batches
for (let i = 0; i < notificationIds.length; i += BATCH_SIZE) {
const batch = notificationIds.slice(i, i + BATCH_SIZE);
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
const totalBatches = Math.ceil(notificationIds.length / BATCH_SIZE);
logger.debug('[LEANTIME_ADAPTER] markAllAsRead processing batch', {
batchNumber,
totalBatches,
batchSize: batch.length,
});
// Process batch in parallel
const batchResults = await Promise.all(
batch.map(async (notificationId) => {
const result = await markSingleNotification(notificationId);
if (result) {
successCount++;
} else {
failureCount++;
failedNotifications.push(notificationId);
}
return result;
})
);
// Add delay between batches (except for the last batch)
if (i + BATCH_SIZE < notificationIds.length) {
await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
}
}
// Retry failed notifications once more
if (failedNotifications.length > 0 && failedNotifications.length < notificationIds.length) {
logger.info('[LEANTIME_ADAPTER] markAllAsRead retrying failed notifications', {
failedCount: failedNotifications.length,
});
const retryResults = await Promise.all(
failedNotifications.map(async (notificationId) => {
const result = await markSingleNotification(notificationId, 0);
if (result) {
successCount++;
failureCount--;
}
return result;
})
);
}
logger.info('[LEANTIME_ADAPTER] markAllAsRead final results', {
successCount,
failureCount,
total: notificationIds.length,
});
// Consider it successful if majority were marked (at least 80% success rate)
const successRate = notificationIds.length > 0 ? successCount / notificationIds.length : 0;
const success = successRate >= 0.8;
logger.info('[LEANTIME_ADAPTER] markAllAsRead END', {
successRate,
success,
});
return success;
} catch (error) {
logger.error('[LEANTIME_ADAPTER] markAllAsRead exception', {
error: error instanceof Error ? error.message : String(error),
});
return false;
}
}
async isConfigured(): Promise<boolean> {
return !!(this.apiUrl && this.apiToken);
}

View File

@ -22,21 +22,6 @@ export interface NotificationAdapter {
*/
getNotificationCount(userId: string): Promise<NotificationCount>;
/**
* Mark a specific notification as read
* @param userId The user ID
* @param notificationId The notification ID
* @returns Promise with success status
*/
markAsRead(userId: string, notificationId: string): Promise<boolean>;
/**
* Mark all notifications as read
* @param userId The user ID
* @returns Promise with success status
*/
markAllAsRead(userId: string): Promise<boolean>;
/**
* Check if this adapter is configured and ready to use
* @returns Promise with boolean indicating if adapter is ready

View File

@ -314,121 +314,6 @@ export class NotificationService {
return aggregatedCount;
}
/**
* Mark a notification as read
*/
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
// Extract the source from the notification ID (format: "source-id")
const [source, ...idParts] = notificationId.split('-');
const sourceId = idParts.join('-'); // Reconstruct the ID in case it has hyphens
if (!source || !this.adapters.has(source)) {
logger.warn('[NOTIFICATION_SERVICE] markAsRead invalid source or adapter not found', {
source,
});
// Still invalidate cache to ensure fresh data
await this.invalidateCache(userId);
return false;
}
const adapter = this.adapters.get(source)!;
let success = false;
try {
success = await adapter.markAsRead(userId, notificationId);
logger.debug('[NOTIFICATION_SERVICE] markAsRead result', {
userId,
notificationId,
success,
});
} catch (error) {
logger.error('[NOTIFICATION_SERVICE] markAsRead error', {
error: error instanceof Error ? error.message : String(error),
});
success = false;
}
// Always invalidate cache after marking attempt (even on failure)
// This ensures fresh data on next fetch, even if the operation partially failed
logger.debug('[NOTIFICATION_SERVICE] markAsRead invalidating cache', {
userId,
success,
});
await this.invalidateCache(userId);
return success;
}
/**
* Mark all notifications from all sources as read
*/
async markAllAsRead(userId: string): Promise<boolean> {
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead called', {
userId,
adapters: Array.from(this.adapters.keys()),
});
const promises = Array.from(this.adapters.values())
.map(async (adapter) => {
const adapterName = adapter.sourceName;
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead processing adapter', {
adapter: adapterName,
});
try {
const configured = await adapter.isConfigured();
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead adapter configuration', {
adapter: adapterName,
configured,
});
if (!configured) {
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead skipping adapter (not configured)', {
adapter: adapterName,
});
return true; // Not configured, so nothing to mark (treat as success)
}
logger.debug('[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter', {
adapter: adapterName,
});
const result = await adapter.markAllAsRead(userId);
logger.debug('[NOTIFICATION_SERVICE] Adapter markAllAsRead result', {
adapter: adapterName,
result,
});
return result;
} catch (error) {
logger.error('[NOTIFICATION_SERVICE] Error marking all notifications as read for adapter', {
adapter: adapterName,
error: error instanceof Error ? error.message : String(error),
});
return false;
}
});
const results = await Promise.all(promises);
const success = results.every(result => result);
const anySuccess = results.some(result => result);
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead results', {
results,
success,
anySuccess,
});
// Always invalidate cache after marking attempt (even on failure)
// This ensures fresh data on next fetch, even if the operation failed
// The user might have marked some notifications manually, or the operation might have partially succeeded
logger.debug('[NOTIFICATION_SERVICE] markAllAsRead invalidating caches', {
userId,
anySuccess,
success,
});
await this.invalidateCache(userId);
return success;
}
/**
* Invalidate notification caches for a user
* Made public so it can be called from API routes for force refresh

View File

@ -526,23 +526,4 @@ export class RocketChatAdapter implements NotificationAdapter {
}
}
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
// RocketChat handles read status automatically when messages are viewed
// We return true to acknowledge the UI action, but the actual read status
// will be updated when the user views the message in RocketChat
logger.debug('[ROCKETCHAT_ADAPTER] markAsRead called (read status handled by RocketChat)', {
userId,
notificationId,
});
return true;
}
async markAllAsRead(userId: string): Promise<boolean> {
// RocketChat handles read status automatically when messages are viewed
// We return true to acknowledge the UI action
logger.debug('[ROCKETCHAT_ADAPTER] markAllAsRead called (read status handled by RocketChat)', {
userId,
});
return true;
}
}