feat(chat): личный чат тьютор ↔ админ с вложениями
Диалоги 1:1 (один тред на тьютора), отдельный SSE-канал whatido:chat вместо общей ленты notifications, вложения с белым списком расширений и вставкой из буфера (Ctrl+V), очистка по chat_retention_days. Таблицы в бэкап, chat_attachments добавлен в sweepOrphanedUploads.
This commit is contained in:
@@ -52,6 +52,7 @@ function buildSidebar(active) {
|
||||
{ page: 'journal', label: 'Журнал', icon: 'book-open' },
|
||||
{ page: 'lessons', label: 'Отчёты о занятиях', icon: 'notebook-pen' },
|
||||
{ page: 'notifications', label: 'Уведомления', icon: 'bell', badge: true },
|
||||
{ page: 'chat', label: 'Чаты', icon: 'messages-square', chatBadge: true },
|
||||
{ page: 'students', label: 'Ученики', icon: 'graduation-cap' },
|
||||
{ page: 'groups', label: 'Группы', icon: 'users' },
|
||||
{ page: 'photos', label: 'Фото', icon: 'Image' },
|
||||
@@ -74,10 +75,12 @@ function buildSidebar(active) {
|
||||
const renderItem = n => {
|
||||
const isAct = n.page === active;
|
||||
const badgeHtml = n.badge ? `<span class="sidebar-badge" id="sidebarNotifyBadge" ${notifyUnread > 0 ? '' : 'hidden'}>${notifyUnread > 99 ? '99+' : notifyUnread}</span>` : '';
|
||||
const chatBadgeHtml = n.chatBadge ? `<span class="sidebar-badge" id="sidebarChatBadge" ${chatUnread > 0 ? '' : 'hidden'}>${chatUnread > 99 ? '99+' : chatUnread}</span>` : '';
|
||||
return `<a href="${n.page}.html" data-nav="${n.page}" class="${isAct ? 'active' : ''}" title="${n.label}">
|
||||
<i data-lucide="${n.icon}"></i>
|
||||
<span class="nav-text">${n.label}</span>
|
||||
${badgeHtml}
|
||||
${chatBadgeHtml}
|
||||
</a>`;
|
||||
};
|
||||
el.innerHTML = `
|
||||
@@ -133,6 +136,8 @@ function buildSidebar(active) {
|
||||
loadVersion();
|
||||
loadBrand();
|
||||
initNotifications();
|
||||
loadChatUnread();
|
||||
connectChatStream();
|
||||
}
|
||||
|
||||
async function loadBrand() {
|
||||
@@ -218,6 +223,57 @@ function setNotifyUnread(n) {
|
||||
}
|
||||
}
|
||||
|
||||
let chatUnread = 0;
|
||||
let chatStream = null;
|
||||
let chatReconnectTimer = null;
|
||||
|
||||
function setChatUnread(n) {
|
||||
chatUnread = Number(n) || 0;
|
||||
const badge = document.getElementById('sidebarChatBadge');
|
||||
if (badge) {
|
||||
badge.textContent = chatUnread > 99 ? '99+' : String(chatUnread);
|
||||
badge.hidden = chatUnread <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChatUnread() {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/chat/unread`, { headers: hdr() });
|
||||
if (!res.ok) return;
|
||||
setChatUnread((await res.json()).unread);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function connectChatStream() {
|
||||
if (chatStream) { chatStream.close(); chatStream = null; }
|
||||
const connect = () => {
|
||||
const t = sessionStorage.getItem('authToken');
|
||||
if (!t) return;
|
||||
chatStream = new EventSource(`${API}/api/chat/stream?token=${encodeURIComponent(t)}`);
|
||||
chatStream.addEventListener('ready', ev => {
|
||||
try { setChatUnread(JSON.parse(ev.data).unread); } catch {}
|
||||
});
|
||||
chatStream.addEventListener('message', ev => {
|
||||
let p = null;
|
||||
try { p = JSON.parse(ev.data); } catch { return; }
|
||||
if (!p || !p.message) return;
|
||||
const m = p.message;
|
||||
if (m.sender_id !== currentUser?.id) {
|
||||
setChatUnread(chatUnread + 1);
|
||||
showToast(`Новое сообщение в чате: ${(m.sender_name || '').trim() || 'диалог'}`);
|
||||
}
|
||||
if (typeof onChatMessage === 'function') onChatMessage(p);
|
||||
});
|
||||
chatStream.onerror = () => {
|
||||
if (chatStream) { chatStream.close(); chatStream = null; }
|
||||
clearTimeout(chatReconnectTimer);
|
||||
chatReconnectTimer = setTimeout(connect, 5000);
|
||||
};
|
||||
};
|
||||
connect();
|
||||
window.addEventListener('beforeunload', () => { if (chatStream) chatStream.close(); });
|
||||
}
|
||||
|
||||
function notifyItemHtml(n, expanded) {
|
||||
const level = NOTIFY_LEVELS[n.level] ? ` level-${n.level}` : '';
|
||||
return `<a href="#" class="notify-item${n.read ? '' : ' unread'}${level}" data-notify-id="${n.id}" data-notify-link="${attrEsc(n.link || '')}">
|
||||
|
||||
Reference in New Issue
Block a user