SHA256
UI: исправить автообновление и хронологию диалогов; обновить деплой-цели
This commit is contained in:
+45
-8
@@ -105,6 +105,7 @@ const appShellEl = document.querySelector('.app-shell');
|
||||
|
||||
const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000;
|
||||
const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim();
|
||||
const UI_BUILD_HASH_PATTERN = /window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/;
|
||||
|
||||
let currentCleanup = null;
|
||||
let pingIntervalId = null;
|
||||
@@ -119,6 +120,7 @@ let connectionCheckInFlight = false;
|
||||
let wsSessionRestoreInFlight = null;
|
||||
let uiUpdateReloadScheduled = false;
|
||||
let pwaUpdateCheckAttempted = false;
|
||||
let uiVersionCheckInFlight = false;
|
||||
|
||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||
initPwaInstallPromptHandling();
|
||||
@@ -235,16 +237,51 @@ async function triggerImmediateConnectionRetry() {
|
||||
await checkConnectionHealth();
|
||||
}
|
||||
|
||||
function checkAndReloadIfUiUpdated(remoteHashRaw) {
|
||||
function extractBuildHashFromHtml(htmlText) {
|
||||
const html = String(htmlText || '');
|
||||
if (!html) return '';
|
||||
const match = html.match(UI_BUILD_HASH_PATTERN);
|
||||
return String(match?.[1] || '').trim();
|
||||
}
|
||||
|
||||
async function fetchCurrentHostUiBuildHash() {
|
||||
try {
|
||||
const url = `./index.html?build_probe=${Date.now()}`;
|
||||
const response = await fetch(url, { cache: 'no-store' });
|
||||
if (!response?.ok) return '';
|
||||
const html = await response.text();
|
||||
return extractBuildHashFromHtml(html);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndReloadIfUiUpdated(remoteHashRaw) {
|
||||
const remoteHash = String(remoteHashRaw || '').trim();
|
||||
if (!remoteHash || !CURRENT_BUILD_HASH || remoteHash === CURRENT_BUILD_HASH) return;
|
||||
if (uiVersionCheckInFlight) return;
|
||||
|
||||
scheduleUiReload({
|
||||
source: 'version-check',
|
||||
message: `Обнаружена новая версия UI (через Ping): ${CURRENT_BUILD_HASH} -> ${remoteHash}`,
|
||||
delayMs: 600,
|
||||
activateWaitingWorker: true,
|
||||
});
|
||||
uiVersionCheckInFlight = true;
|
||||
try {
|
||||
const latestHostHash = await fetchCurrentHostUiBuildHash();
|
||||
if (!latestHostHash || latestHostHash === CURRENT_BUILD_HASH) {
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'version-check',
|
||||
message: `Ping сообщил другую версию UI (${remoteHash}), но текущий хост всё ещё отдаёт ${CURRENT_BUILD_HASH}. Reload пропущен, чтобы избежать цикла.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleUiReload({
|
||||
source: 'version-check',
|
||||
message: `Обнаружена новая версия UI: ${CURRENT_BUILD_HASH} -> ${latestHostHash} (ping: ${remoteHash})`,
|
||||
delayMs: 600,
|
||||
activateWaitingWorker: true,
|
||||
});
|
||||
} finally {
|
||||
uiVersionCheckInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshServiceWorkers({ activateWaitingWorker = false } = {}) {
|
||||
@@ -313,7 +350,7 @@ async function checkConnectionHealth() {
|
||||
}
|
||||
const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
||||
const remoteUiBuildHash = pingResp?.payload?.uiBuildHash || pingResp?.uiBuildHash || '';
|
||||
checkAndReloadIfUiUpdated(remoteUiBuildHash);
|
||||
void checkAndReloadIfUiUpdated(remoteUiBuildHash);
|
||||
await tryUpdatePwaOnFirstConnectedPing();
|
||||
setConnectionStatus('connected');
|
||||
} catch {
|
||||
|
||||
@@ -68,6 +68,17 @@ function resolveDeliveryStatus(msg) {
|
||||
return '…';
|
||||
}
|
||||
|
||||
function scrollToLatestMessage(list) {
|
||||
if (!list) return;
|
||||
const apply = () => {
|
||||
list.scrollTop = list.scrollHeight;
|
||||
};
|
||||
apply();
|
||||
window.requestAnimationFrame(apply);
|
||||
window.setTimeout(apply, 0);
|
||||
window.setTimeout(apply, 120);
|
||||
}
|
||||
|
||||
function renderLog(list, chatId) {
|
||||
list.innerHTML = '';
|
||||
const messages = getChatMessages(chatId);
|
||||
@@ -110,7 +121,7 @@ function renderLog(list, chatId) {
|
||||
bubble.append(textNode, metaNode);
|
||||
list.append(bubble);
|
||||
});
|
||||
list.scrollTop = list.scrollHeight;
|
||||
scrollToLatestMessage(list);
|
||||
markChatRead(chatId);
|
||||
}
|
||||
|
||||
@@ -240,10 +251,11 @@ export function render({ navigate, route }) {
|
||||
}
|
||||
});
|
||||
|
||||
renderLog(log, chatId);
|
||||
void sendReadReceiptsForVisible(chatId);
|
||||
wrap.append(log, form);
|
||||
screen.append(wrap);
|
||||
renderLog(log, chatId);
|
||||
window.requestAnimationFrame(() => scrollToLatestMessage(log));
|
||||
void sendReadReceiptsForVisible(chatId);
|
||||
return screen;
|
||||
}
|
||||
async function sendReadReceiptsForVisible(chatId) {
|
||||
|
||||
+89
-3
@@ -263,8 +263,69 @@ export const authService = new AuthService(state.entrySettings.shineServer);
|
||||
let onSessionReset = null;
|
||||
let onSessionAuthorized = null;
|
||||
|
||||
function parseMessageTimeFromKey(rawKey) {
|
||||
const value = String(rawKey || '').trim();
|
||||
if (!value) return 0;
|
||||
const parts = value.split('|');
|
||||
if (parts.length < 4) return 0;
|
||||
const timeMs = Number(parts[2] || 0);
|
||||
if (!Number.isFinite(timeMs) || timeMs <= 0) return 0;
|
||||
return Math.trunc(timeMs);
|
||||
}
|
||||
|
||||
function resolveChatMessageTimeMs(row) {
|
||||
const fromBaseKey = parseMessageTimeFromKey(row?.baseKey);
|
||||
if (fromBaseKey > 0) return fromBaseKey;
|
||||
|
||||
const fromMessageKey = parseMessageTimeFromKey(row?.messageKey);
|
||||
if (fromMessageKey > 0) return fromMessageKey;
|
||||
|
||||
const fromCreatedAt = Number(row?.createdAtMs || row?.ts || 0);
|
||||
if (Number.isFinite(fromCreatedAt) && fromCreatedAt > 0) return Math.trunc(fromCreatedAt);
|
||||
|
||||
const tempId = String(row?.tempId || '').trim();
|
||||
if (tempId.startsWith('tmp-')) {
|
||||
const parts = tempId.split('-');
|
||||
const ts = Number(parts[1] || 0);
|
||||
if (Number.isFinite(ts) && ts > 0) return Math.trunc(ts);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function stableMessageOrderKey(row) {
|
||||
const key = String(row?.messageKey || '').trim();
|
||||
if (key) return `mk:${key}`;
|
||||
const tmp = String(row?.tempId || '').trim();
|
||||
if (tmp) return `tmp:${tmp}`;
|
||||
const base = String(row?.baseKey || '').trim();
|
||||
if (base) return `bk:${base}`;
|
||||
const text = String(row?.text || '').trim();
|
||||
return `txt:${text}`;
|
||||
}
|
||||
|
||||
function sortChatMessagesInPlace(chatId) {
|
||||
const list = getChatMessages(chatId);
|
||||
list.sort((a, b) => {
|
||||
const ta = resolveChatMessageTimeMs(a);
|
||||
const tb = resolveChatMessageTimeMs(b);
|
||||
if (ta !== tb) return ta - tb;
|
||||
|
||||
const ka = stableMessageOrderKey(a);
|
||||
const kb = stableMessageOrderKey(b);
|
||||
const byKey = ka.localeCompare(kb, 'ru');
|
||||
if (byKey !== 0) return byKey;
|
||||
|
||||
if ((a?.from || '') !== (b?.from || '')) {
|
||||
return (a?.from === 'in') ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function persistMessageRecord(chatId, row) {
|
||||
if (!chatId || !row?.messageKey) return;
|
||||
const resolvedTs = resolveChatMessageTimeMs(row);
|
||||
void putStoredMessage({
|
||||
messageKey: row.messageKey,
|
||||
chatId,
|
||||
@@ -278,13 +339,14 @@ function persistMessageRecord(chatId, row) {
|
||||
secondTick: Boolean(row.secondTick),
|
||||
readReceiptSent: Boolean(row.readReceiptSent),
|
||||
refBaseKey: String(row.refBaseKey || ''),
|
||||
ts: Date.now(),
|
||||
ts: resolvedTs > 0 ? resolvedTs : Date.now(),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
export async function hydrateMessagesFromStore() {
|
||||
try {
|
||||
const rows = await listStoredMessages();
|
||||
const touchedChats = new Set();
|
||||
rows
|
||||
.sort((a, b) => Number(a?.ts || 0) - Number(b?.ts || 0))
|
||||
.forEach((row) => {
|
||||
@@ -293,6 +355,7 @@ export async function hydrateMessagesFromStore() {
|
||||
if (!chatId || !messageKey) return;
|
||||
if (state.knownMessageKeys[messageKey]) return;
|
||||
state.knownMessageKeys[messageKey] = true;
|
||||
touchedChats.add(chatId);
|
||||
getChatMessages(chatId).push({
|
||||
from: row.from === 'out' ? 'out' : 'in',
|
||||
text: String(row.text || ''),
|
||||
@@ -305,8 +368,10 @@ export async function hydrateMessagesFromStore() {
|
||||
secondTick: Boolean(row.secondTick),
|
||||
readReceiptSent: Boolean(row.readReceiptSent),
|
||||
refBaseKey: String(row.refBaseKey || ''),
|
||||
createdAtMs: Number(row.ts || 0),
|
||||
});
|
||||
});
|
||||
touchedChats.forEach((chatId) => sortChatMessagesInPlace(chatId));
|
||||
} catch {
|
||||
// ignore broken storage
|
||||
}
|
||||
@@ -322,7 +387,15 @@ export function getChatMessages(chatId) {
|
||||
export function addChatMessage(chatId, text) {
|
||||
const message = text.trim();
|
||||
if (!message) return;
|
||||
getChatMessages(chatId).push({ from: 'out', text: message, firstTick: false, secondTick: false, unread: false });
|
||||
getChatMessages(chatId).push({
|
||||
from: 'out',
|
||||
text: message,
|
||||
firstTick: false,
|
||||
secondTick: false,
|
||||
unread: false,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
sortChatMessagesInPlace(chatId);
|
||||
}
|
||||
|
||||
export function addSystemChatMessage(chatId, text, { from = 'out', kind = 'system' } = {}) {
|
||||
@@ -335,7 +408,9 @@ export function addSystemChatMessage(chatId, text, { from = 'out', kind = 'syste
|
||||
unread: from === 'in',
|
||||
firstTick: from !== 'in',
|
||||
secondTick: false,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
sortChatMessagesInPlace(chatId);
|
||||
}
|
||||
|
||||
|
||||
@@ -344,7 +419,14 @@ export function addIncomingMessage(chatId, text, messageId = '') {
|
||||
if (!msg) return false;
|
||||
if (messageId && state.incomingDedup[messageId]) return false;
|
||||
if (messageId) state.incomingDedup[messageId] = true;
|
||||
getChatMessages(chatId).push({ from: 'in', text: msg, messageId, unread: true });
|
||||
getChatMessages(chatId).push({
|
||||
from: 'in',
|
||||
text: msg,
|
||||
messageId,
|
||||
unread: true,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
sortChatMessagesInPlace(chatId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -359,7 +441,9 @@ export function addOutgoingPendingMessage(chatId, text) {
|
||||
firstTick: false,
|
||||
secondTick: false,
|
||||
unread: false,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
sortChatMessagesInPlace(chatId);
|
||||
return tempId;
|
||||
}
|
||||
|
||||
@@ -377,6 +461,7 @@ export function markOutgoingSent(tempId, { messageKey = '', baseKey = '' } = {})
|
||||
state.knownMessageKeys[messageKey] = true;
|
||||
persistMessageRecord(chatId, row);
|
||||
}
|
||||
sortChatMessagesInPlace(chatId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -455,6 +540,7 @@ export function addSignedMessageToChat({
|
||||
secondTick: false,
|
||||
};
|
||||
getChatMessages(chatId).push(row);
|
||||
sortChatMessagesInPlace(chatId);
|
||||
persistMessageRecord(chatId, row);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user