Исправить обновление непрочитанных в UI

This commit is contained in:
AidarKC
2026-09-10 12:18:39 +03:00
parent 3a466b4c38
commit 107f85b818
7 changed files with 112 additions and 13 deletions
+1
View File
@@ -105,6 +105,7 @@ ESP32/**/*.a
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
deploy/backup/archive/**
!deploy/backup/archive/.gitkeep
SHiNE-bundle-*.zip
# Локальная дев-обвязка AI-агентов (сессии, планы, настройки) — не коммитим
.agents/
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.12.8
client.version=1.12.9
server.version=1.10.3
+6 -1
View File
@@ -360,12 +360,17 @@ function createChannelReadTracker({
const root = getChannelScrollRoot();
const viewportHeight = root?.clientHeight || window.innerHeight || document.documentElement.clientHeight || 0;
const thresholdTop = Math.max(0, Math.round(viewportHeight * unreadAnchorFraction));
const visibleBottom = Math.max(thresholdTop, viewportHeight - 24);
let seen = safeInitialSeenCount;
for (const card of cards) {
const localNumber = Number(card.dataset.localNumber || 0);
if (!Number.isFinite(localNumber) || localNumber <= 0) continue;
const rect = card.getBoundingClientRect();
if (rect.top > thresholdTop + 1) break;
if (rect.top > visibleBottom) break;
if (rect.bottom < 0) {
seen = Math.max(seen, localNumber);
continue;
}
seen = Math.max(seen, localNumber);
}
return Math.max(safeInitialSeenCount, Math.min(seen, safeMessagesCount));
+31 -9
View File
@@ -729,7 +729,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
const channelTypeVersion = Number(summary?.channel?.channelTypeVersion ?? 1);
const isOwn = bucketKey === 'own';
const title = displayTitle || channelName;
const technicalLabel = `${ownerLogin} / ${channelName}`;
const technicalLabel = `@${ownerLogin}/${channelName}`;
const lastMessage = resolveChannelLastMessage(summary);
return {
@@ -948,13 +948,21 @@ function renderChannelMain(channel) {
const main = document.createElement('div');
main.className = 'channel-row-main';
const titleLine = document.createElement('div');
titleLine.className = 'channel-row-title-line';
const title = document.createElement('strong');
title.className = 'channel-row-title';
title.textContent = channel.title;
const time = document.createElement('span');
time.className = 'channel-row-time';
time.textContent = channel.lastMessageAt ? formatRelativeTime(channel.lastMessageAt) : '';
titleLine.append(title, time);
const technical = document.createElement('p');
technical.className = 'channel-row-technical';
technical.textContent = channel.technicalLabel || `${channel.ownerName || ''} / ${channel.channelName || ''}`;
technical.textContent = channel.technicalLabel || `@${channel.ownerName || ''}/${channel.channelName || ''}`;
if (channel.channelDescription) {
const desc = document.createElement('p');
@@ -970,16 +978,13 @@ function renderChannelMain(channel) {
preview.className = 'channel-row-message';
preview.textContent = channel.messagePreview || 'Ждем ваших начинаний';
const time = document.createElement('span');
time.className = 'channel-row-time';
time.textContent = channel.lastMessageAt ? formatRelativeTime(channel.lastMessageAt) : '';
previewLine.append(preview, time);
previewLine.append(preview);
const meta = document.createElement('p');
meta.className = 'channel-row-owner channel-counter-meta';
meta.textContent = `Сообщений: ${channel.messagesCount || 0}`;
main.prepend(title, technical);
main.prepend(titleLine, technical);
main.append(previewLine, meta);
return main;
}
@@ -1041,8 +1046,8 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
container.append(list);
}
async function loadFeedAndRender({ screen, listState, contentEl, navigate }) {
renderSkeletonList(contentEl, 5);
async function loadFeedAndRender({ screen, listState, contentEl, navigate, silent = false }) {
if (!silent) renderSkeletonList(contentEl, 5);
if (!state.session.isAuthorized) {
setChannelsFeed(null, {});
@@ -1092,6 +1097,7 @@ async function loadFeedAndRender({ screen, listState, contentEl, navigate }) {
refreshFeed: async () => loadFeedAndRender({ screen, listState, contentEl, navigate }),
});
} catch (error) {
if (silent) return;
setChannelsFeed(null, {});
contentEl.innerHTML = '';
@@ -1161,6 +1167,20 @@ export function render({ navigate, route, chrome }) {
const reloadFeed = async () => loadFeedAndRender({ screen, listState, contentEl, navigate });
let countersRefreshTimer = null;
let lastChannelsUnreadCount = Math.max(0, Number(state.userCounters?.channelsUnreadCount || 0) || 0);
const unsubscribeCountersChanged = authService.onEvent('UserCountersChanged', (event) => {
const nextChannelsUnreadCount = Math.max(0, Number(event?.payload?.channelsUnreadCount || 0) || 0);
if (nextChannelsUnreadCount === lastChannelsUnreadCount) return;
lastChannelsUnreadCount = nextChannelsUnreadCount;
if (countersRefreshTimer) window.clearTimeout(countersRefreshTimer);
countersRefreshTimer = window.setTimeout(() => {
countersRefreshTimer = null;
void loadFeedAndRender({ screen, listState, contentEl, navigate, silent: true });
}, 120);
});
const rerenderList = () => {
listState.viewMode = normalizeChannelsViewMode({ params: route?.params || {} });
@@ -1190,6 +1210,8 @@ export function render({ navigate, route, chrome }) {
loadFeedAndRender({ screen, listState, contentEl, navigate });
screen.cleanup = () => {
if (countersRefreshTimer) window.clearTimeout(countersRefreshTimer);
unsubscribeCountersChanged();
channelsFilterMenu.destroy();
};
+1 -1
View File
@@ -290,11 +290,11 @@ function renderRow(item) {
<p class="dm-row-last-message"></p>
</div>
<div class="dm-row-meta-col">
${item.unreadCount ? `<span class="dm-unread-badge">${item.unreadCount > 99 ? '99+' : item.unreadCount}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
<div class="dm-row-meta-line">
${item.lastMessageTimeMs ? '<span class="dm-row-time"></span>' : '<span class="dm-row-time dm-row-time--empty"></span>'}
<span class="dm-chevron">${SVG_CHEVRON}</span>
</div>
${item.unreadCount ? `<span class="dm-unread-badge">${item.unreadCount > 99 ? '99+' : item.unreadCount}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
</div>
`;
const titleEl = row.querySelector('.dm-row-title');
@@ -1517,3 +1517,61 @@
flex: 1 1 0;
min-width: 0;
}
/* ===== Channel row realtime layout: time top-right, unread bottom-right ===== */
.channels-screen--list .channel-row-main {
padding-right: 0;
}
.channels-screen--list .channel-row-title-line {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.channels-screen--list .channel-row-title-line .channel-row-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.channels-screen--list .channel-row-title-line .channel-row-time {
position: static;
transform: none;
width: auto;
max-width: max-content;
flex: 0 0 auto;
margin: 0;
padding: 0;
font-size: 11px;
line-height: 1.12;
white-space: nowrap;
}
.channels-screen--list .channel-row-preview-line {
display: block;
padding-right: 42px;
box-sizing: border-box;
}
.channels-screen--list .channel-row-controls {
position: absolute;
right: 7px;
bottom: 6px;
top: auto;
width: auto;
min-width: 0;
height: auto;
display: flex;
align-items: flex-end;
pointer-events: none;
}
.channels-screen--list .channel-row .channel-row-controls > .channel-row-count {
position: static;
margin: 0;
align-self: flex-end;
}
+13
View File
@@ -722,3 +722,16 @@ button.dm-via-node:hover { border-color: rgba(25, 229, 138, 0.5); }
opacity: 0.72;
transform: translateY(-1px);
}
/* ===== Personal chat row layout: time top-right, unread bottom-right ===== */
.dm-list-screen .dm-row-time {
grid-row: 1;
align-self: center;
}
.dm-list-screen .dm-unread-badge,
.dm-list-screen .dm-row-meta-spacer {
grid-row: 2;
align-self: center;
}