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 = {}) { ...@@ -18,7 +18,15 @@ export async function api(endpoint, options = {}) {
} }
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE}/${endpoint}`; 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(); const text = await res.text();
let data; let data;
try { try {
...@@ -65,7 +73,14 @@ export async function del(endpoint, body) { ...@@ -65,7 +73,14 @@ export async function del(endpoint, body) {
return api(endpoint, { method: 'DELETE', body }); return api(endpoint, { method: 'DELETE', body });
} }
let refreshPromise = null;
async function refreshToken() { 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'); const refreshToken = store.get('auth.refreshToken');
if (!refreshToken) return false; if (!refreshToken) return false;
try { try {
......
...@@ -3,12 +3,12 @@ import { mountTable } from './scenes/table.js'; ...@@ -3,12 +3,12 @@ import { mountTable } from './scenes/table.js';
import { mountBotSelect } from './scenes/bot-select.js'; import { mountBotSelect } from './scenes/bot-select.js';
import { mountTimeSelect } from './scenes/time-select.js'; import { mountTimeSelect } from './scenes/time-select.js';
import { mountQueue, unmountQueue } from './scenes/queue.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'; import { mountChallenge } from './scenes/challenge.js';
scene.register('play-table', mountTable); scene.register('play-table', mountTable);
scene.register('play-bot-select', mountBotSelect); scene.register('play-bot-select', mountBotSelect);
scene.register('play-time-select', mountTimeSelect); scene.register('play-time-select', mountTimeSelect);
scene.register('play-queue', mountQueue, unmountQueue); scene.register('play-queue', mountQueue, unmountQueue);
scene.register('game-lobby', mountLobby); scene.register('game-lobby', mountLobby, unmountLobby);
scene.register('challenge-friend', mountChallenge); scene.register('challenge-friend', mountChallenge);
import * as scene from '../../core/scene.js'; 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 { mountNotifications } from './scenes/notifications.js';
import { mountActivity } from './scenes/activity.js'; import { mountActivity } from './scenes/activity.js';
import { mountChat } from './scenes/chat.js'; import { mountChat } from './scenes/chat.js';
...@@ -8,7 +8,7 @@ import { mountGroupCreate } from './scenes/group-create.js'; ...@@ -8,7 +8,7 @@ import { mountGroupCreate } from './scenes/group-create.js';
import { mountGroupChat } from './scenes/group-chat.js'; import { mountGroupChat } from './scenes/group-chat.js';
import { mountGroupMembers } from './scenes/group-members.js'; import { mountGroupMembers } from './scenes/group-members.js';
scene.register('friends', mountFriends); scene.register('friends', mountFriends, unmountFriends);
scene.register('notifications', mountNotifications); scene.register('notifications', mountNotifications);
scene.register('activity-feed', mountActivity); scene.register('activity-feed', mountActivity);
scene.register('friend-chat', mountChat); scene.register('friend-chat', mountChat);
......
...@@ -130,7 +130,7 @@ async function pollNewMessages(el) { ...@@ -130,7 +130,7 @@ async function pollNewMessages(el) {
if (isLoading || !friendId) return; if (isLoading || !friendId) return;
try { try {
const lastTime = messages.length > 0 ? messages[messages.length - 1].created_at : null; 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 || []; const newMsgs = data.messages || [];
if (newMsgs.length > messages.length) { if (newMsgs.length > messages.length) {
......
...@@ -667,3 +667,8 @@ function timeAgo(dateStr) { ...@@ -667,3 +667,8 @@ function timeAgo(dateStr) {
if (days < 7) return `منذ ${days} يوم`; if (days < 7) return `منذ ${days} يوم`;
return new Date(dateStr).toLocaleDateString('ar'); 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) { ...@@ -17,7 +17,6 @@ export async function mountNotifications(el) {
const data = await net.get('notifications.php'); const data = await net.get('notifications.php');
const notifications = data.notifications || data || []; const notifications = data.notifications || data || [];
renderNotifications(el, notifications); renderNotifications(el, notifications);
store.set('notifications.unreadCount', 0);
} catch (e) { } catch (e) {
el.querySelector('#notif-list').innerHTML = `<p style="color:var(--text-secondary);text-align:center;">${t('common.empty')}</p>`; 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) { ...@@ -30,13 +29,22 @@ function renderNotifications(el, notifications) {
return; return;
} }
list.innerHTML = notifications.map(n => ` const hasUnread = notifications.some(n => !n.is_read);
<div class="card" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};"> 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: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: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 style="font-size:10px;color:var(--text-muted);margin-top:4px;">${timeAgo(n.created_at)}</div>
</div> </div>
`).join(''); `).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) { 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