diff --git a/.gitignore b/.gitignore index 29bcddec..34986288 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,7 @@ ESP32/**/*.a # Полные серверные бэкапы (тяжёлые архивы, не коммитим) deploy/backup/archive/** !deploy/backup/archive/.gitkeep +SHiNE-bundle-*.zip # Локальная дев-обвязка AI-агентов (сессии, планы, настройки) — не коммитим .agents/ diff --git a/VERSION.properties b/VERSION.properties index d66506bb..6d57c3ce 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.12.8 +client.version=1.12.9 server.version=1.10.3 diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 26536cb3..c4693aed 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -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)); diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 08188241..5089a820 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -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; diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 7048bb4e..34f9382c 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -290,11 +290,11 @@ function renderRow(item) {

- ${item.unreadCount ? `${item.unreadCount > 99 ? '99+' : item.unreadCount}` : ''}
${item.lastMessageTimeMs ? '' : ''} ${SVG_CHEVRON}
+ ${item.unreadCount ? `${item.unreadCount > 99 ? '99+' : item.unreadCount}` : ''}
`; const titleEl = row.querySelector('.dm-row-title'); diff --git a/shine-UI/styles/features/channels-list.css b/shine-UI/styles/features/channels-list.css index da1cd6ab..1f09b302 100644 --- a/shine-UI/styles/features/channels-list.css +++ b/shine-UI/styles/features/channels-list.css @@ -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; +} diff --git a/shine-UI/styles/features/messages.css b/shine-UI/styles/features/messages.css index 545bbabb..775f01fb 100644 --- a/shine-UI/styles/features/messages.css +++ b/shine-UI/styles/features/messages.css @@ -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; +}