SHA256
Зафиксировали текущие изменения UI и документации
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="32" r="10.2" stroke="#F2F5FB" stroke-width="2.8"/>
|
||||
<path d="M32 16.8L34.8 20.4L39.3 19.2L40.2 23.8L44.8 24.7L43.6 29.2L47.2 32L43.6 34.8L44.8 39.3L40.2 40.2L39.3 44.8L34.8 43.6L32 47.2L29.2 43.6L24.7 44.8L23.8 40.2L19.2 39.3L20.4 34.8L16.8 32L20.4 29.2L19.2 24.7L23.8 23.8L24.7 19.2L29.2 20.4L32 16.8Z" stroke="#F2F5FB" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="32" r="3.5" fill="#F2F5FB"/>
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15.25A3.25 3.25 0 1 0 12 8.75a3.25 3.25 0 0 0 0 6.5Z" stroke="#F2F5FB" stroke-width="1.65"/>
|
||||
<path d="M19.15 13.15c.04-.38.04-.77 0-1.15l1.55-1.2-1.75-3.02-1.82.74a7.4 7.4 0 0 0-1-.58L15.86 6h-3.5l-.27 1.94c-.35.16-.68.35-1 .58l-1.82-.74-1.75 3.02L9.07 12a6 6 0 0 0 0 1.15l-1.55 1.2 1.75 3.03 1.82-.75c.32.23.65.42 1 .58l.27 1.94h3.5l.27-1.94c.35-.16.68-.35 1-.58l1.82.75 1.75-3.03-1.55-1.2Z" stroke="#F2F5FB" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 599 B |
+1
-1
@@ -12,7 +12,7 @@
|
||||
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
||||
<title>СИЯНИЕ</title>
|
||||
<script>
|
||||
window.__SHINE_BUILD_HASH__ = '20260822140000';
|
||||
window.__SHINE_BUILD_HASH__ = '20260826145335';
|
||||
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
||||
</script>
|
||||
<script>
|
||||
|
||||
+6
-2
@@ -200,14 +200,17 @@ let orientationLockInFlight = false;
|
||||
let currentChromeCleanup = null;
|
||||
const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1';
|
||||
const SCROLL_TO_BOTTOM_PAGE_IDS = new Set([
|
||||
'messages-list',
|
||||
'chat-view',
|
||||
'channels-list',
|
||||
'channel-view',
|
||||
'channel-thread-view',
|
||||
'notifications-view',
|
||||
]);
|
||||
|
||||
const FILLED_ACTION_BUTTON_PAGE_IDS = new Set([
|
||||
...PRE_AUTH_PAGES.filter((pageId) => pageId !== 'entry-settings-view'),
|
||||
'settings-view',
|
||||
]);
|
||||
|
||||
const GUEST_ALLOWED_PAGES = new Set([
|
||||
'start-view',
|
||||
'entry-settings-view',
|
||||
@@ -1204,6 +1207,7 @@ function renderApp() {
|
||||
|
||||
screenEl.classList.toggle('no-app-chrome', !showAppChrome);
|
||||
screenEl.classList.toggle('preauth-flow', PRE_AUTH_PAGES.includes(pageId));
|
||||
screenEl.classList.toggle('filled-action-buttons', FILLED_ACTION_BUTTON_PAGE_IDS.has(pageId));
|
||||
|
||||
toolbarEl.innerHTML = '';
|
||||
if (showAppChrome) {
|
||||
|
||||
@@ -31,6 +31,19 @@ const CHANNELS_VIEW_ALL = 'all';
|
||||
const CHANNELS_VIEW_OWNED = 'owned';
|
||||
const CHANNELS_VIEW_FOLLOWING = 'following';
|
||||
|
||||
function channelMenuIcon(name) {
|
||||
const paths = {
|
||||
search: '<circle cx="11" cy="11" r="6.5"/><path d="m16 16 4 4"/>',
|
||||
add: '<path d="M12 5v14M5 12h14"/>',
|
||||
all: '<rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><rect x="14" y="14" width="6" height="6" rx="1"/>',
|
||||
mine: '<circle cx="12" cy="8" r="4"/><path d="M5 21a7 7 0 0 1 14 0"/>',
|
||||
following: '<path d="M12 21s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 11c0 5.6-7 10-7 10Z"/>',
|
||||
subscribe: '<path d="M12 3a6 6 0 0 0-6 6c0 7-3 7-3 9h18c0-2-3-2-3-9a6 6 0 0 0-6-6Z"/><path d="M10 21h4"/>',
|
||||
notifications: '<path d="M12 3a6 6 0 0 0-6 6c0 7-3 7-3 9h18c0-2-3-2-3-9a6 6 0 0 0-6-6Z"/><path d="M10 21h4"/>',
|
||||
};
|
||||
return `<svg class="channel-menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[name] || ''}</svg>`;
|
||||
}
|
||||
|
||||
function cleanChannelMessagePreview(text) {
|
||||
const parsed = parseMessageAttachments(text);
|
||||
const dmParsed = parseDmTechBlocks(String(parsed.text || ''));
|
||||
@@ -977,12 +990,12 @@ function openTopChannelsMenu({
|
||||
menu.style.width = `${Math.round(menuWidth)}px`;
|
||||
|
||||
const items = [
|
||||
{ label: 'Поиск', action: () => onFindChannel?.() },
|
||||
{ label: 'Новый канал', action: () => navigate('add-channel-view') },
|
||||
{ label: 'Найти канал', icon: 'search', action: () => onFindChannel?.() },
|
||||
{ label: 'Новый канал', icon: 'add', action: () => navigate('add-channel-view') },
|
||||
{ divider: true },
|
||||
{ label: 'Все каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_ALL)) },
|
||||
{ label: 'Мои каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_OWNED)) },
|
||||
{ label: 'Подписки', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_FOLLOWING)) },
|
||||
{ label: 'Все каналы', icon: 'all', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_ALL)) },
|
||||
{ label: 'Мои каналы', icon: 'mine', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_OWNED)) },
|
||||
{ label: 'Подписки', icon: 'following', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_FOLLOWING)) },
|
||||
];
|
||||
|
||||
items.forEach((item) => {
|
||||
@@ -998,7 +1011,7 @@ function openTopChannelsMenu({
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'channel-menu-item';
|
||||
btn.textContent = item.label;
|
||||
btn.innerHTML = `${channelMenuIcon(item.icon)}<span>${item.label}</span>`;
|
||||
btn.addEventListener('click', () => {
|
||||
closeTopChannelsMenu(listState);
|
||||
item.action?.();
|
||||
@@ -1054,8 +1067,11 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi
|
||||
actionBtn.type = 'button';
|
||||
actionBtn.className = `channel-menu-item ${channel.isSubscribed ? 'destructive' : ''}`.trim();
|
||||
|
||||
const actionLabel = document.createElement('span');
|
||||
actionBtn.append(document.createRange().createContextualFragment(channelMenuIcon('subscribe')), actionLabel);
|
||||
|
||||
if (canToggleSubscription) {
|
||||
actionBtn.textContent = channel.pending
|
||||
actionLabel.textContent = channel.pending
|
||||
? 'Выполняется...'
|
||||
: channel.isSubscribed
|
||||
? 'Отписаться'
|
||||
@@ -1075,7 +1091,7 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi
|
||||
|
||||
channel.pending = true;
|
||||
actionBtn.disabled = true;
|
||||
actionBtn.textContent = 'Выполняется...';
|
||||
actionLabel.textContent = 'Выполняется...';
|
||||
|
||||
const nextSubscribed = !channel.isSubscribed;
|
||||
try {
|
||||
@@ -1097,13 +1113,13 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi
|
||||
} catch (error) {
|
||||
channel.pending = false;
|
||||
actionBtn.disabled = false;
|
||||
actionBtn.textContent = channel.isSubscribed ? 'Отписаться' : 'Подписаться';
|
||||
actionLabel.textContent = channel.isSubscribed ? 'Отписаться' : 'Подписаться';
|
||||
showToast(toUserMessage(error, 'Не удалось изменить подписку.'), { kind: 'error' });
|
||||
rerenderList();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionBtn.textContent = 'Собственный канал';
|
||||
actionLabel.textContent = 'Собственный канал';
|
||||
actionBtn.disabled = true;
|
||||
}
|
||||
|
||||
@@ -1111,7 +1127,8 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi
|
||||
toggleWrap.className = 'channel-menu-toggle';
|
||||
|
||||
const toggleLabel = document.createElement('span');
|
||||
toggleLabel.textContent = 'Уведомления';
|
||||
toggleLabel.className = 'channel-menu-toggle-label';
|
||||
toggleLabel.innerHTML = `${channelMenuIcon('notifications')}<span>Уведомления</span>`;
|
||||
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.type = 'button';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { createShineConnectionsLogo } from '../components/shine-logo.js';
|
||||
import { directMessages } from '../mock-data.js';
|
||||
import {
|
||||
addAppLogEntry,
|
||||
@@ -36,6 +35,72 @@ import { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-route
|
||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
|
||||
function menuIconSvg(name) {
|
||||
const paths = {
|
||||
reply: '<path d="M9 7 4 12l5 5"/><path d="M5 12h8a6 6 0 0 1 6 6"/>',
|
||||
copy: '<rect x="8" y="8" width="10" height="10" rx="2"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/>',
|
||||
read: '<path d="M5 9v6h4l5 4V5L9 9H5Z"/><path d="M17 9a4 4 0 0 1 0 6"/>',
|
||||
edit: '<path d="m4 20 4.4-1 9.9-9.9a2.1 2.1 0 0 0-3-3L5.4 16 4 20Z"/><path d="m13.8 7.2 3 3"/>',
|
||||
delete: '<path d="M4 7h16"/><path d="M9 7V4h6v3"/><path d="m7 7 1 13h8l1-13"/>',
|
||||
call: '<path d="M7.6 10.8a15 15 0 0 0 5.6 5.6l1.9-1.9a1 1 0 0 1 1-.2c1.1.4 2.3.6 3.5.6a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.6 21 3 13.4 3 4a1 1 0 0 1 1-1h3.4a1 1 0 0 1 1 1c0 1.2.2 2.4.6 3.5a1 1 0 0 1-.3 1l-1.1 1.3Z"/>',
|
||||
video: '<rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 10 5-3v10l-5-3"/>',
|
||||
clear: '<path d="M4 7h16"/><path d="M9 7V4h6v3"/><path d="m7 7 1 13h8l1-13"/>',
|
||||
};
|
||||
return `<svg class="dm-menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[name] || ''}</svg>`;
|
||||
}
|
||||
|
||||
function openUserIdentityMenu({ anchorEl, login, navigate }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root || !anchorEl) return;
|
||||
const cleanLogin = String(login || '').trim();
|
||||
root.innerHTML = `
|
||||
<div class="dm-floating-menu-layer dm-user-menu-layer" id="chat-user-menu-layer">
|
||||
<div class="dm-head-menu dm-head-menu--portal dm-user-identity-menu" role="menu">
|
||||
<button type="button" class="dm-head-menu-item" role="menuitem" data-user-action="connections">
|
||||
<img class="dm-menu-image-icon" src="/assets/SHiNE_connections_blue.svg" alt="" aria-hidden="true" />
|
||||
<span>Связи</span>
|
||||
</button>
|
||||
<button type="button" class="dm-head-menu-item" role="menuitem" data-user-action="profile">
|
||||
<img class="dm-menu-image-icon" src="/assets/profile-icon-profile.svg" alt="" aria-hidden="true" />
|
||||
<span>Профиль</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const layer = root.querySelector('#chat-user-menu-layer');
|
||||
const menu = root.querySelector('.dm-user-identity-menu');
|
||||
const close = () => {
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
root.innerHTML = '';
|
||||
};
|
||||
const onKeydown = (event) => {
|
||||
if (event.key === 'Escape') close();
|
||||
};
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!menu) return;
|
||||
const width = menu.offsetWidth || 190;
|
||||
const left = Math.max(10, Math.min(window.innerWidth - width - 10, rect.left));
|
||||
menu.style.left = `${Math.round(left)}px`;
|
||||
menu.style.top = `${Math.round(rect.bottom + 7)}px`;
|
||||
});
|
||||
|
||||
layer?.addEventListener('pointerdown', (event) => {
|
||||
if (event.target === layer) close();
|
||||
});
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
root.querySelector('[data-user-action="connections"]')?.addEventListener('click', () => {
|
||||
close();
|
||||
navigate(makeProfileLinksRoute(cleanLogin));
|
||||
});
|
||||
root.querySelector('[data-user-action="profile"]')?.addEventListener('click', () => {
|
||||
close();
|
||||
navigate(makeProfileRoute(cleanLogin));
|
||||
});
|
||||
}
|
||||
|
||||
function createChatHeaderParts(login, navigate) {
|
||||
const cleanLogin = String(login || '').trim() || 'unknown';
|
||||
|
||||
@@ -52,12 +117,13 @@ function createChatHeaderParts(login, navigate) {
|
||||
const avatarButton = document.createElement('button');
|
||||
avatarButton.type = 'button';
|
||||
avatarButton.className = 'chat-header-avatar-btn';
|
||||
avatarButton.title = `Профиль ${cleanLogin}`;
|
||||
avatarButton.setAttribute('aria-label', `Открыть профиль ${cleanLogin}`);
|
||||
avatarButton.title = `Меню ${cleanLogin}`;
|
||||
avatarButton.setAttribute('aria-label', `Открыть меню пользователя ${cleanLogin}`);
|
||||
avatarButton.append(avatarSlot);
|
||||
|
||||
const loginEl = document.createElement('span');
|
||||
loginEl.className = 'chat-header-login';
|
||||
const loginEl = document.createElement('button');
|
||||
loginEl.type = 'button';
|
||||
loginEl.className = 'chat-header-login chat-header-login-btn';
|
||||
loginEl.setAttribute('role', 'heading');
|
||||
loginEl.setAttribute('aria-level', '1');
|
||||
loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`);
|
||||
@@ -84,23 +150,14 @@ function createChatHeaderParts(login, navigate) {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const connectionsButton = document.createElement('button');
|
||||
connectionsButton.type = 'button';
|
||||
connectionsButton.className = 'icon-btn chat-header-icon-btn chat-header-connections-btn';
|
||||
connectionsButton.title = `Связи ${cleanLogin}`;
|
||||
connectionsButton.setAttribute('aria-label', `Открыть связи ${cleanLogin}`);
|
||||
connectionsButton.append(createShineConnectionsLogo({ className: 'chat-header-connections-logo' }));
|
||||
connectionsButton.addEventListener('click', () => {
|
||||
const openMenu = (event) => {
|
||||
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||
navigate(makeProfileLinksRoute(cleanLogin));
|
||||
});
|
||||
openUserIdentityMenu({ anchorEl: event.currentTarget, login: cleanLogin, navigate });
|
||||
};
|
||||
avatarButton.addEventListener('click', openMenu);
|
||||
loginEl.addEventListener('click', openMenu);
|
||||
|
||||
avatarButton.addEventListener('click', () => {
|
||||
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||
navigate(makeProfileRoute(cleanLogin));
|
||||
});
|
||||
|
||||
return { centerNode: loginEl, avatarButton, connectionsButton };
|
||||
return { centerNode: loginEl, avatarButton };
|
||||
}
|
||||
|
||||
function truncatePreviewText(value, maxLen = 72) {
|
||||
@@ -232,11 +289,11 @@ function openMessageActionsMenu({
|
||||
root.innerHTML = `
|
||||
<div class="dm-floating-menu-layer" id="chat-message-actions-layer">
|
||||
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
||||
${canReply ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-reply">Ответить</button>' : ''}
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-copy">Скопировать как текст</button>
|
||||
${showReadAloud ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-read">Прочесть</button>' : ''}
|
||||
${canEdit ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-edit">Изменить</button>' : ''}
|
||||
${canDelete ? '<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="msg-action-delete">Удалить</button>' : ''}
|
||||
${canReply ? `<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-reply">${menuIconSvg('reply')}<span>Ответить</span></button>` : ''}
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-copy">${menuIconSvg('copy')}<span>Скопировать как текст</span></button>
|
||||
${showReadAloud ? `<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-read">${menuIconSvg('read')}<span>Прочесть</span></button>` : ''}
|
||||
${canEdit ? `<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-edit">${menuIconSvg('edit')}<span>Изменить</span></button>` : ''}
|
||||
${canDelete ? `<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="msg-action-delete">${menuIconSvg('delete')}<span>Удалить</span></button>` : ''}
|
||||
${String(infoText || '').trim() ? `<div class="meta-muted">${String(infoText || '').trim()}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,10 +385,10 @@ function openChatActionsMenu({
|
||||
root.innerHTML = `
|
||||
<div class="dm-floating-menu-layer" id="chat-header-actions-layer">
|
||||
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-call">Звонок</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">Видеозвонок</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-clear-history">Очистить историю</button>
|
||||
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">Удалить чат</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-call">${menuIconSvg('call')}<span>Звонок</span></button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">${menuIconSvg('video')}<span>Видеозвонок</span></button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-clear-history">${menuIconSvg('clear')}<span>Очистить историю</span></button>
|
||||
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">${menuIconSvg('delete')}<span>Удалить чат</span></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1083,7 +1140,7 @@ export function render({ navigate, route, chrome }) {
|
||||
],
|
||||
});
|
||||
const chatHeaderLeft = chatHeader.querySelector('.header-left');
|
||||
chatHeaderLeft?.append(chatHeaderParts.connectionsButton, chatHeaderParts.avatarButton);
|
||||
chatHeaderLeft?.append(chatHeaderParts.avatarButton);
|
||||
chrome?.setTopbar(chatHeader);
|
||||
|
||||
if (!isKnownContact) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
authService,
|
||||
getChatMessages,
|
||||
isSessionInvalidError,
|
||||
normalizeDmChatId,
|
||||
setContacts,
|
||||
@@ -106,6 +107,8 @@ function clipPreviewText(text, maxLen = PREVIEW_MAX_LEN) {
|
||||
}
|
||||
|
||||
async function resolveDialogPreview(dialog) {
|
||||
const localText = clipPreviewText(String(dialog?.lastMessageText || '').trim());
|
||||
if (localText) return localText;
|
||||
const blobB64 = String(dialog?.lastMessageBlobB64 || '').trim();
|
||||
if (!blobB64) return 'Диалог пока пуст.';
|
||||
|
||||
@@ -145,6 +148,29 @@ async function resolveDialogPreview(dialog) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
function resolveLocalMessageTimeMs(message) {
|
||||
const keys = [message?.baseKey, message?.messageKey];
|
||||
for (const key of keys) {
|
||||
const parts = String(key || '').split('|');
|
||||
const timeMs = Number(parts[2] || 0);
|
||||
if (parts.length >= 4 && Number.isFinite(timeMs) && timeMs > 0) return timeMs;
|
||||
}
|
||||
const tempParts = String(message?.tempId || '').split('-');
|
||||
const tempTimeMs = Number(tempParts[1] || 0);
|
||||
if (tempParts[0] === 'tmp' && Number.isFinite(tempTimeMs) && tempTimeMs > 0) return tempTimeMs;
|
||||
const createdAtMs = Number(message?.createdAtMs || message?.ts || 0);
|
||||
return Number.isFinite(createdAtMs) && createdAtMs > 0 ? createdAtMs : 0;
|
||||
}
|
||||
|
||||
function latestLocalDialogMessage(peerLogin) {
|
||||
const messages = getChatMessages(peerLogin);
|
||||
const latest = [...messages].sort((a, b) => resolveLocalMessageTimeMs(b) - resolveLocalMessageTimeMs(a))[0];
|
||||
if (!latest) return null;
|
||||
const parsed = parseDmTechBlocks(String(latest?.text || ''));
|
||||
const text = clipPreviewText(String(parsed.displayText || parsed.visibleText || '').trim()) || 'Сообщение';
|
||||
return { text, timeMs: resolveLocalMessageTimeMs(latest) };
|
||||
}
|
||||
|
||||
function formatChatRowTime(ts) {
|
||||
const value = Number(ts || 0);
|
||||
if (!Number.isFinite(value) || value <= 0) return '';
|
||||
@@ -350,16 +376,23 @@ function renderRow(item) {
|
||||
unreadCount: Number(dialog?.unreadCount || 0),
|
||||
hasDialog: Boolean(dialog?.hasDialog),
|
||||
};
|
||||
const localLatest = latestLocalDialogMessage(peerLogin);
|
||||
if (localLatest && localLatest.timeMs >= next.lastMessageTimeMs) {
|
||||
next.lastMessageText = localLatest.text;
|
||||
next.lastMessageTimeMs = localLatest.timeMs;
|
||||
}
|
||||
const current = byPeer.get(key);
|
||||
if (!current) {
|
||||
byPeer.set(key, next);
|
||||
return;
|
||||
}
|
||||
const currentRank = relationOrder(current.relationFlag);
|
||||
const nextRank = relationOrder(relationFlag);
|
||||
if (nextRank < currentRank || (nextRank === currentRank && next.lastMessageTimeMs > current.lastMessageTimeMs)) {
|
||||
byPeer.set(key, next);
|
||||
}
|
||||
const newest = next.lastMessageTimeMs > current.lastMessageTimeMs ? next : current;
|
||||
newest.relationFlag = relationOrder(relationFlag) < relationOrder(current.relationFlag)
|
||||
? relationFlag
|
||||
: current.relationFlag;
|
||||
newest.unreadCount = Math.max(Number(current.unreadCount || 0), Number(next.unreadCount || 0));
|
||||
newest.hasDialog = Boolean(current.hasDialog || next.hasDialog);
|
||||
byPeer.set(key, newest);
|
||||
});
|
||||
|
||||
const rows = Array.from(byPeer.values()).sort((a, b) => {
|
||||
|
||||
@@ -221,3 +221,98 @@
|
||||
0 0 12px rgba(72, 145, 255, 0.34) !important;
|
||||
filter: drop-shadow(0 0 4px rgba(92, 190, 255, 0.46)) !important;
|
||||
}
|
||||
|
||||
/* Второй общий тип кнопок. Цвет меняется одной переменной --shine-action-blue
|
||||
* в main.css. Экран настройки сервера намеренно не получает этот класс. */
|
||||
:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn),
|
||||
:root .screen-content.filled-action-buttons a.primary-btn,
|
||||
:root .screen-content.filled-action-buttons a.secondary-btn,
|
||||
:root .screen-content.filled-action-buttons a.ghost-btn,
|
||||
:root .screen-content.filled-action-buttons a.text-btn {
|
||||
color: #ffffff !important;
|
||||
background: var(--shine-action-blue) !important;
|
||||
background-image: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0)) !important;
|
||||
border: 0 !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.24),
|
||||
0 8px 22px rgba(var(--shine-action-blue-rgb), .24) !important;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,.18) !important;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):hover,
|
||||
:root .screen-content.filled-action-buttons a.primary-btn:hover,
|
||||
:root .screen-content.filled-action-buttons a.secondary-btn:hover,
|
||||
:root .screen-content.filled-action-buttons a.ghost-btn:hover,
|
||||
:root .screen-content.filled-action-buttons a.text-btn:hover {
|
||||
background: var(--shine-action-blue-hover) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.28),
|
||||
0 10px 26px rgba(var(--shine-action-blue-rgb), .32) !important;
|
||||
}
|
||||
|
||||
:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):active,
|
||||
:root .screen-content.filled-action-buttons a.primary-btn:active,
|
||||
:root .screen-content.filled-action-buttons a.secondary-btn:active,
|
||||
:root .screen-content.filled-action-buttons a.ghost-btn:active,
|
||||
:root .screen-content.filled-action-buttons a.text-btn:active {
|
||||
background: var(--shine-action-blue-pressed) !important;
|
||||
box-shadow: inset 0 3px 8px rgba(0,0,0,.26) !important;
|
||||
}
|
||||
|
||||
:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):disabled {
|
||||
color: rgba(255,255,255,.58) !important;
|
||||
background: rgba(var(--shine-action-blue-rgb), .42) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Специальная системная кнопка должна сохранять синюю круглую подложку,
|
||||
* несмотря на глобальный borderless-reset выше. */
|
||||
:root button.scroll-to-bottom-btn,
|
||||
:root button.scroll-to-bottom-btn:hover,
|
||||
:root button.scroll-to-bottom-btn:focus,
|
||||
:root button.scroll-to-bottom-btn:focus-visible {
|
||||
color: #ffffff !important;
|
||||
background: var(--shine-action-blue) !important;
|
||||
border: 0 !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.25),
|
||||
0 10px 26px rgba(var(--shine-action-blue-rgb), .34) !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
:root button.scroll-to-bottom-btn:hover {
|
||||
background: var(--shine-action-blue-hover) !important;
|
||||
}
|
||||
|
||||
:root button.scroll-to-bottom-btn:active {
|
||||
background: var(--shine-action-blue-pressed) !important;
|
||||
box-shadow: inset 0 3px 9px rgba(0,0,0,.28) !important;
|
||||
}
|
||||
|
||||
/* Пункты унифицированных выпадающих меню остаются без постоянной заливки,
|
||||
* но получают общую синюю реакцию при наведении/фокусе. */
|
||||
:root button.dm-head-menu-item,
|
||||
:root button.channel-menu-item,
|
||||
:root button.dm-message-action-btn {
|
||||
justify-content: flex-start !important;
|
||||
color: #f4f8ff !important;
|
||||
background: transparent !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
:root button.dm-head-menu-item:hover,
|
||||
:root button.dm-head-menu-item:focus-visible,
|
||||
:root button.channel-menu-item:hover,
|
||||
:root button.channel-menu-item:focus-visible,
|
||||
:root button.dm-message-action-btn:hover,
|
||||
:root button.dm-message-action-btn:focus-visible {
|
||||
background: rgba(var(--shine-action-blue-rgb), .12) !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:root button.channel-menu-item.destructive,
|
||||
:root button.dm-message-action-btn--danger {
|
||||
color: #ffb7c5 !important;
|
||||
}
|
||||
|
||||
@@ -9695,3 +9695,243 @@ body.chat-topbar-overlay .composer-slot > * {
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* ===== Единая полировка меню, DM и управляющих кнопок (2026-08-26) ===== */
|
||||
|
||||
/* Центральная вкладка остаётся на прежней оси иконок, но имеет большую
|
||||
* интерактивную область; подпись опущена ниже и больше не заходит на логотип. */
|
||||
.toolbar-btn-network {
|
||||
width: calc(100% + 6px);
|
||||
min-height: 58px;
|
||||
margin-inline: -3px;
|
||||
}
|
||||
|
||||
.toolbar-btn-network > span:last-child {
|
||||
display: block;
|
||||
position: relative;
|
||||
transform: translateY(7px);
|
||||
}
|
||||
|
||||
/* Общая круглая кнопка прокрутки. Её цвет задаётся --shine-action-blue. */
|
||||
.scroll-to-bottom-btn {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
right: 16px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.scroll-to-bottom-btn__icon {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Один визуальный язык для меню профиля, чатов и каналов. */
|
||||
.dm-head-menu,
|
||||
.channel-menu-wrap,
|
||||
.dm-message-actions-menu {
|
||||
min-width: 196px;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
border: 1px solid rgba(92, 190, 255, 0.22);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(155deg, rgba(19, 27, 42, 0.97), rgba(7, 12, 22, 0.98));
|
||||
box-shadow:
|
||||
0 18px 42px rgba(0, 0, 0, 0.48),
|
||||
0 0 24px rgba(39, 141, 255, 0.10),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(20px) saturate(125%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(125%);
|
||||
}
|
||||
|
||||
/* У меню больше нет «облачного» треугольного хвостика. */
|
||||
.dm-head-menu::before {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.dm-head-menu-item,
|
||||
.channel-menu-item,
|
||||
.dm-message-action-btn {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #f4f8ff;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dm-head-menu-item:hover,
|
||||
.dm-head-menu-item:focus-visible,
|
||||
.channel-menu-item:hover,
|
||||
.channel-menu-item:focus-visible,
|
||||
.dm-message-action-btn:hover,
|
||||
.dm-message-action-btn:focus-visible {
|
||||
outline: none;
|
||||
background: rgba(39, 141, 255, 0.12);
|
||||
}
|
||||
|
||||
.dm-head-menu-item svg,
|
||||
.dm-head-menu-item img,
|
||||
.channel-menu-icon,
|
||||
.dm-menu-icon,
|
||||
.dm-menu-image-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
flex: 0 0 18px;
|
||||
object-fit: contain;
|
||||
color: #65b7ff;
|
||||
filter: drop-shadow(0 0 5px rgba(39, 141, 255, 0.28));
|
||||
}
|
||||
|
||||
.profile-head-menu .profile-head-menu-item img {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
min-width: 17px;
|
||||
flex-basis: 17px;
|
||||
}
|
||||
|
||||
.channel-menu-wrap--portal {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.channel-menu-divider {
|
||||
margin: 4px 8px !important;
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.channel-menu-item.destructive,
|
||||
.dm-message-action-btn--danger {
|
||||
color: #ffb7c5;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.channel-menu-toggle {
|
||||
min-height: 42px;
|
||||
padding: 0 11px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.channel-menu-toggle-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dm-message-actions-menu {
|
||||
width: min(78vw, 240px);
|
||||
}
|
||||
|
||||
.dm-user-identity-menu {
|
||||
width: 196px;
|
||||
}
|
||||
|
||||
.dm-user-menu-layer {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.chat-header-login-btn {
|
||||
padding: 4px 7px;
|
||||
min-height: 34px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Поле сообщения занимает почти всю ширину; действия прижаты к правому краю. */
|
||||
body.chat-topbar-overlay .composer-slot {
|
||||
padding-left: 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.dm-chat-input {
|
||||
gap: 4px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.dm-actions-col {
|
||||
grid-template-columns: repeat(2, 43px);
|
||||
grid-template-rows: 46px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dm-chat-input--multiline .dm-actions-col {
|
||||
grid-template-columns: 43px;
|
||||
}
|
||||
|
||||
.dm-emoji-btn,
|
||||
.dm-send-icon-btn {
|
||||
min-width: 43px;
|
||||
width: 43px;
|
||||
}
|
||||
|
||||
/* Loader не имеет права участвовать в растяжении пустого flex-контейнера. */
|
||||
.dm-history-loader {
|
||||
flex: 0 0 34px;
|
||||
align-self: center;
|
||||
width: max-content;
|
||||
max-width: calc(100% - 24px);
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
max-height: 34px;
|
||||
top: calc(var(--topbar-height, 64px) + 8px);
|
||||
margin: 0 auto -4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dm-history-loader__pill {
|
||||
flex: 0 0 auto;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* «Личные»: широкие, низкие и более плотные плитки. */
|
||||
.dm-list-screen .dm-list {
|
||||
width: calc(100% + 16px);
|
||||
margin-inline: -8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dm-list-screen .dm-dialog-card {
|
||||
grid-template-columns: 50px minmax(0, 1fr) auto;
|
||||
min-height: 54px;
|
||||
margin: 0;
|
||||
padding: 5px 6px 5px 4px;
|
||||
gap: 8px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.dm-list-screen .dm-av,
|
||||
.dm-list-screen .dm-av .avatar,
|
||||
.dm-list-screen .list-item .avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.dm-list-screen .dm-row-main,
|
||||
.dm-list-screen .dm-row-meta-col {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.dm-list-screen .dm-dialog-card {
|
||||
grid-template-columns: 50px minmax(0, 1fr) auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
--accent-soft: rgba(217, 181, 111, 0.18);
|
||||
--danger: #ff718f;
|
||||
--ok: #84f4a1;
|
||||
/* Единый цвет основных действий: стартовые/регистрационные кнопки и scroll-down. */
|
||||
--shine-action-blue: #278dff;
|
||||
--shine-action-blue-hover: #43a2ff;
|
||||
--shine-action-blue-pressed: #1973db;
|
||||
--shine-action-blue-rgb: 39, 141, 255;
|
||||
--radius-lg: 18px;
|
||||
--radius-md: 12px;
|
||||
--radius-sm: 9px;
|
||||
|
||||
Reference in New Issue
Block a user