SHA256
Исправить обновление непрочитанных в UI
This commit is contained in:
@@ -105,6 +105,7 @@ ESP32/**/*.a
|
||||
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||
deploy/backup/archive/**
|
||||
!deploy/backup/archive/.gitkeep
|
||||
SHiNE-bundle-*.zip
|
||||
|
||||
# Локальная дев-обвязка AI-агентов (сессии, планы, настройки) — не коммитим
|
||||
.agents/
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
client.version=1.12.8
|
||||
client.version=1.12.9
|
||||
server.version=1.10.3
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,7 +1210,9 @@ export function render({ navigate, route, chrome }) {
|
||||
loadFeedAndRender({ screen, listState, contentEl, navigate });
|
||||
|
||||
screen.cleanup = () => {
|
||||
channelsFilterMenu.destroy();
|
||||
if (countersRefreshTimer) window.clearTimeout(countersRefreshTimer);
|
||||
unsubscribeCountersChanged();
|
||||
channelsFilterMenu.destroy();
|
||||
};
|
||||
|
||||
return screen;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user