Commit 13f912f1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: timer leaks, token refresh race, notifications, chat incremental fetch

- friends.js: add unmountFriends() to clear refresh/invite timers
- play/mod.js: register unmountLobby for game-lobby scene
- social/mod.js: register unmountFriends for friends scene
- net.js: add 10s request timeout via AbortController
- net.js: add mutex for token refresh (prevents duplicate refresh calls)
- notifications.js: don't auto-mark-read on mount; add explicit button
- chat.js: pass lastTime as 'after' param for incremental fetch

Fixes WTF #38, #63, #66, #69, #91, #175
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 43bfe37d
......@@ -18,7 +18,15 @@ export async function api(endpoint, options = {}) {
}
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE}/${endpoint}`;
const res = await fetch(url, config);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeout || 10000);
config.signal = controller.signal;
let res;
try {
res = await fetch(url, config);
} finally {
clearTimeout(timeout);
}
const text = await res.text();
let data;
try {
......@@ -65,7 +73,14 @@ export async function del(endpoint, body) {
return api(endpoint, { method: 'DELETE', body });
}
let refreshPromise = null;
async function refreshToken() {
if (refreshPromise) return refreshPromise;
refreshPromise = doRefresh();
try { return await refreshPromise; } finally { refreshPromise = null; }
}
async function doRefresh() {
const refreshToken = store.get('auth.refreshToken');
if (!refreshToken) return false;
try {
......
......@@ -3,12 +3,12 @@ import { mountTable } from './scenes/table.js';
import { mountBotSelect } from './scenes/bot-select.js';
import { mountTimeSelect } from './scenes/time-select.js';
import { mountQueue, unmountQueue } from './scenes/queue.js';
import { mountLobby } from './scenes/lobby.js';
import { mountLobby, unmountLobby } from './scenes/lobby.js';
import { mountChallenge } from './scenes/challenge.js';
scene.register('play-table', mountTable);
scene.register('play-bot-select', mountBotSelect);
scene.register('play-time-select', mountTimeSelect);
scene.register('play-queue', mountQueue, unmountQueue);
scene.register('game-lobby', mountLobby);
scene.register('game-lobby', mountLobby, unmountLobby);
scene.register('challenge-friend', mountChallenge);
import * as scene from '../../core/scene.js';
import { mountFriends } from './scenes/friends.js';
import { mountFriends, unmountFriends } from './scenes/friends.js';
import { mountNotifications } from './scenes/notifications.js';
import { mountActivity } from './scenes/activity.js';
import { mountChat } from './scenes/chat.js';
......@@ -8,7 +8,7 @@ import { mountGroupCreate } from './scenes/group-create.js';
import { mountGroupChat } from './scenes/group-chat.js';
import { mountGroupMembers } from './scenes/group-members.js';
scene.register('friends', mountFriends);
scene.register('friends', mountFriends, unmountFriends);
scene.register('notifications', mountNotifications);
scene.register('activity-feed', mountActivity);
scene.register('friend-chat', mountChat);
......
......@@ -130,7 +130,7 @@ async function pollNewMessages(el) {
if (isLoading || !friendId) return;
try {
const lastTime = messages.length > 0 ? messages[messages.length - 1].created_at : null;
const data = await net.get('chat.php', { action: 'history', friend_id: friendId, limit: 20 });
const data = await net.get('chat.php', { action: 'history', friend_id: friendId, limit: 20, after: lastTime });
const newMsgs = data.messages || [];
if (newMsgs.length > messages.length) {
......
......@@ -667,3 +667,8 @@ function timeAgo(dateStr) {
if (days < 7) return `منذ ${days} يوم`;
return new Date(dateStr).toLocaleDateString('ar');
}
export function unmountFriends() {
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
if (inviteCheckTimer) { clearInterval(inviteCheckTimer); inviteCheckTimer = null; }
}
......@@ -17,7 +17,6 @@ export async function mountNotifications(el) {
const data = await net.get('notifications.php');
const notifications = data.notifications || data || [];
renderNotifications(el, notifications);
store.set('notifications.unreadCount', 0);
} catch (e) {
el.querySelector('#notif-list').innerHTML = `<p style="color:var(--text-secondary);text-align:center;">${t('common.empty')}</p>`;
}
......@@ -30,13 +29,22 @@ function renderNotifications(el, notifications) {
return;
}
list.innerHTML = notifications.map(n => `
<div class="card" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};">
const hasUnread = notifications.some(n => !n.is_read);
list.innerHTML = (hasUnread ? `<button id="mark-all-read" style="margin-bottom:8px;background:none;border:1px solid rgba(255,255,255,0.1);color:#94a3b8;font-size:12px;padding:6px 14px;border-radius:8px;cursor:pointer;font-family:inherit;">تعليم الكل كمقروء</button>` : '') +
notifications.map(n => `
<div class="card notif-item" data-id="${n.id}" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};cursor:pointer;">
<div style="font-size:14px;font-weight:${n.is_read ? '400' : '600'};">${n.title_ar || n.title || ''}</div>
<div style="font-size:12px;color:var(--text-secondary);margin-top:2px;">${n.body_ar || n.body || ''}</div>
<div style="font-size:10px;color:var(--text-muted);margin-top:4px;">${timeAgo(n.created_at)}</div>
</div>
`).join('');
list.querySelector('#mark-all-read')?.addEventListener('click', async () => {
await net.post('notifications.php', { action: 'mark-all-read' }).catch(() => {});
store.set('notifications.unreadCount', 0);
notifications.forEach(n => n.is_read = true);
renderNotifications(el, notifications);
});
}
function timeAgo(date) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment