Require explicit persisted acknowledgement of notifications

This commit is contained in:
2026-09-06 12:52:14 +03:00
parent c7bb67b101
commit 458f638b81
7 changed files with 89 additions and 23 deletions
+38 -17
View File
@@ -1,3 +1,4 @@
import {mergeServerNotifications,acknowledgeNotifications} from '../services/notifications';
import { searchTender } from '../services/searchTender';
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
@@ -159,6 +160,7 @@ export const AppProvider = ({ children }) => {
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
useEffect(() => {
notificationVersion.current++;notificationMutation.current=null;seenServerNotifications.current.clear();setNotificationError(null);setNotificationBusy(false);
aiRunRef.current=null;setAiAnalyzing(false);setAiJobStatus('');
setVeriSenkron(false); setPlan(null); setSaveError(null);
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
@@ -564,11 +566,17 @@ export const AppProvider = ({ children }) => {
};
const [notifications, setNotifications] = useState([]);
const [notificationError,setNotificationError]=useState(null);
const [notificationBusy,setNotificationBusy]=useState(false);
const notificationVersion=useRef(0),notificationMutation=useRef(null),seenServerNotifications=useRef(new Set());
// Gerçek bildirim: listeye ekler; sessiz saat dışında ve push açıksa tarayıcı
// bildirimi (Notification API) da gösterir.
const notify = (title, text) => {
setNotifications((prev) => [{ id: 'n-' + Date.now(), title, text, time: 'az önce', read: false }, ...prev].slice(0, 30));
pushNotification(title,text);
};
const pushNotification=(title,text)=>{
if (isQuietNow() || !reminderSettings.pushEnabled) return;
try {
if ('Notification' in window && Notification.permission === 'granted') {
@@ -581,7 +589,31 @@ export const AppProvider = ({ children }) => {
if (Notification.permission === 'granted') return 'granted';
try { return await Notification.requestPermission(); } catch { return 'denied'; }
};
const markNotificationsRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
const refreshNotifications=async()=>{
const session=getToken(),version=++notificationVersion.current;
setNotificationError(null);
try {
const rows=await bildirimleriGetir();
if(getToken()!==session||version!==notificationVersion.current)return;
const checked=mergeServerNotifications([],rows);
const fresh=checked.filter(row=>!row.read&&!seenServerNotifications.current.has(row.id));
checked.forEach(row=>seenServerNotifications.current.add(row.id));
fresh.slice(0,5).forEach(row=>pushNotification(row.title,row.text));
setNotifications(current=>[...checked,...current.filter(row=>!row.serverId)].slice(0,80));
}catch(error){if(getToken()===session&&version===notificationVersion.current)setNotificationError(error.message);}
};
const markNotificationsRead=async()=>{
if(notificationMutation.current)return;
const operation={session:getToken()};notificationMutation.current=operation;
notificationVersion.current++;setNotificationBusy(true);setNotificationError(null);
try {
const ids=await acknowledgeNotifications(notifications,bildirimOkundu);
if(getToken()!==operation.session||notificationMutation.current!==operation)return;
notificationVersion.current++;
setNotifications(current=>current.map(row=>ids.has(row.id)?{...row,read:true}:row));
}catch(error){if(getToken()===operation.session&&notificationMutation.current===operation)setNotificationError(error.message);}
finally {if(notificationMutation.current===operation){notificationMutation.current=null;setNotificationBusy(false);}}
};
// Persist state to localstorage
// Havuz on binlerce kayda çıkabiliyor; tamamı localStorage kotasını (≈5 MB)
@@ -645,22 +677,10 @@ export const AppProvider = ({ children }) => {
// Sunucu bildirimleri (zeyilname / sonuç): 5 dk'da bir çek, yenileri göster
useEffect(() => {
if (!veriSenkron) return;
let iptal = false;
const session = getToken();
const cek = async () => {
try {
const liste = await bildirimleriGetir();
const yeni = liste.filter((b) => !b.okundu);
if (iptal || getToken() !== session || !yeni.length) return;
for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin);
await bildirimOkundu(yeni.map((b) => b.id));
} catch { /* sessiz */ }
};
cek();
const z = setInterval(cek, 5 * 60 * 1000);
return () => { iptal = true; clearInterval(z); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [veriSenkron]);
void refreshNotifications();
const timer=setInterval(refreshNotifications,5*60*1000);
return()=>{notificationVersion.current++;clearInterval(timer);};
}, [veriSenkron,authUser?.email]);
// Şirket belgesi süre uyarısı: süresi dolmuş / 30 gün içinde dolacak
// belgeler için günde bir kez bildirim (kimse teklif hazırlanana kadar
@@ -807,6 +827,7 @@ export const AppProvider = ({ children }) => {
notify,
requestPushPermission,
markNotificationsRead,
refreshNotifications,notificationError,notificationBusy,
isQuietNow,
favoriteKurumlar,
toggleFavoriteKurum,