SHA256
Merge remote-tracking branch 'origin/main' into расширение-функционала-каналов
This commit is contained in:
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-visual"
|
||||||
/>
|
/>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="manifest" href="./manifest.webmanifest" />
|
<link rel="manifest" href="./manifest.webmanifest" />
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
||||||
<title>СИЯНИЕ</title>
|
<title>СИЯНИЕ</title>
|
||||||
<script>
|
<script>
|
||||||
window.__SHINE_BUILD_HASH__ = '20260806223040';
|
window.__SHINE_BUILD_HASH__ = '20260819190000';
|
||||||
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
+106
-1
@@ -82,7 +82,7 @@ import * as solanaUsersInitView from './pages/solana-users-init-view.js';
|
|||||||
import * as solanaRpcCheckView from './pages/solana-rpc-check-view.js';
|
import * as solanaRpcCheckView from './pages/solana-rpc-check-view.js';
|
||||||
import * as messagesList from './pages/messages-list.js';
|
import * as messagesList from './pages/messages-list.js';
|
||||||
import * as contactSearchView from './pages/contact-search-view.js';
|
import * as contactSearchView from './pages/contact-search-view.js';
|
||||||
import * as chatView from './pages/chat-view.js?v=202607152145';
|
import * as chatView from './pages/chat-view.js?v=202608191738';
|
||||||
import * as userProfileView from './pages/user-profile-view.js';
|
import * as userProfileView from './pages/user-profile-view.js';
|
||||||
import * as channelsList from './pages/channels-list.js';
|
import * as channelsList from './pages/channels-list.js';
|
||||||
import * as channelView from './pages/channel-view.js';
|
import * as channelView from './pages/channel-view.js';
|
||||||
@@ -221,6 +221,62 @@ function setKeyboardOffsetPx(valuePx = 0) {
|
|||||||
setShellMetricVar('--keyboard-offset', valuePx);
|
setShellMetricVar('--keyboard-offset', valuePx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stableViewportHeightPx = Math.max(
|
||||||
|
1,
|
||||||
|
Math.round(window.innerHeight || document.documentElement?.clientHeight || window.visualViewport?.height || 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
function isTextEntryFocused() {
|
||||||
|
const active = document.activeElement;
|
||||||
|
return active instanceof HTMLTextAreaElement
|
||||||
|
|| (active instanceof HTMLInputElement && !['button', 'checkbox', 'radio', 'range', 'file', 'submit', 'reset'].includes(active.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncViewportMetrics() {
|
||||||
|
if (!appShellEl) return;
|
||||||
|
const viewport = window.visualViewport || null;
|
||||||
|
const currentLayoutHeightPx = Math.max(
|
||||||
|
1,
|
||||||
|
Math.round(window.innerHeight || document.documentElement?.clientHeight || viewport?.height || 0),
|
||||||
|
);
|
||||||
|
const widthPx = Math.max(1, Math.round(window.innerWidth || viewport?.width || 0));
|
||||||
|
const offsetLeftPx = Math.max(0, Math.round(viewport?.offsetLeft || 0));
|
||||||
|
const textEntryFocused = isTextEntryFocused();
|
||||||
|
|
||||||
|
// Android Chrome/Firefox могут уменьшать и visualViewport, и innerHeight.
|
||||||
|
// Поэтому высоту экрана до открытия клавиатуры запоминаем отдельно и во
|
||||||
|
// время ввода НЕ переписываем ею app-shell. Иначе toolbar тоже поднимется.
|
||||||
|
if (!textEntryFocused) {
|
||||||
|
stableViewportHeightPx = Math.max(stableViewportHeightPx, currentLayoutHeightPx);
|
||||||
|
// После поворота/реального resize разрешаем уменьшить базу, но только
|
||||||
|
// когда никакое текстовое поле не держит экранную клавиатуру.
|
||||||
|
if (Math.abs(stableViewportHeightPx - currentLayoutHeightPx) > 220) {
|
||||||
|
stableViewportHeightPx = currentLayoutHeightPx;
|
||||||
|
}
|
||||||
|
} else if (currentLayoutHeightPx > stableViewportHeightPx) {
|
||||||
|
stableViewportHeightPx = currentLayoutHeightPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visualBottomPx = viewport
|
||||||
|
? Math.round((viewport.offsetTop || 0) + viewport.height)
|
||||||
|
: currentLayoutHeightPx;
|
||||||
|
const rawKeyboardOffsetPx = Math.max(
|
||||||
|
0,
|
||||||
|
stableViewportHeightPx - Math.min(currentLayoutHeightPx, visualBottomPx),
|
||||||
|
);
|
||||||
|
// Address bar Android обычно даёт небольшую дельту; клавиатура — заметно больше.
|
||||||
|
const keyboardOffsetPx = textEntryFocused && rawKeyboardOffsetPx >= 100
|
||||||
|
? rawKeyboardOffsetPx
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
setShellMetricVar('--app-viewport-width', widthPx);
|
||||||
|
setShellMetricVar('--app-viewport-height', keyboardOffsetPx > 0 ? stableViewportHeightPx : currentLayoutHeightPx);
|
||||||
|
setShellMetricVar('--app-viewport-offset-top', 0);
|
||||||
|
setShellMetricVar('--app-viewport-offset-left', offsetLeftPx);
|
||||||
|
setKeyboardOffsetPx(keyboardOffsetPx);
|
||||||
|
appShellEl.classList.toggle('keyboard-open', keyboardOffsetPx > 0);
|
||||||
|
}
|
||||||
|
|
||||||
function attachSlotHeightObserver(slotEl, cssVarName) {
|
function attachSlotHeightObserver(slotEl, cssVarName) {
|
||||||
if (!slotEl || typeof ResizeObserver !== 'function') return null;
|
if (!slotEl || typeof ResizeObserver !== 'function') return null;
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
@@ -237,6 +293,55 @@ const topbarHeightObserver = attachSlotHeightObserver(topbarEl, '--topbar-height
|
|||||||
const composerHeightObserver = attachSlotHeightObserver(composerEl, '--composer-height');
|
const composerHeightObserver = attachSlotHeightObserver(composerEl, '--composer-height');
|
||||||
const toolbarHeightObserver = attachSlotHeightObserver(toolbarEl, '--toolbar-height');
|
const toolbarHeightObserver = attachSlotHeightObserver(toolbarEl, '--toolbar-height');
|
||||||
|
|
||||||
|
syncViewportMetrics();
|
||||||
|
window.visualViewport?.addEventListener('resize', syncViewportMetrics);
|
||||||
|
window.visualViewport?.addEventListener('scroll', syncViewportMetrics);
|
||||||
|
window.addEventListener('resize', syncViewportMetrics);
|
||||||
|
document.addEventListener('focusin', () => {
|
||||||
|
requestAnimationFrame(syncViewportMetrics);
|
||||||
|
// Samsung One UI / Firefox can finish the OSK viewport transition several
|
||||||
|
// frames after focus. Re-sample through the animation instead of trusting
|
||||||
|
// the first resize event.
|
||||||
|
window.setTimeout(syncViewportMetrics, 80);
|
||||||
|
window.setTimeout(syncViewportMetrics, 180);
|
||||||
|
window.setTimeout(syncViewportMetrics, 320);
|
||||||
|
});
|
||||||
|
document.addEventListener('focusout', () => {
|
||||||
|
window.setTimeout(syncViewportMetrics, 80);
|
||||||
|
window.setTimeout(syncViewportMetrics, 220);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional on-device viewport diagnostics: append ?keyboard-debug=1 to the URL.
|
||||||
|
if (new URLSearchParams(window.location.search).get('keyboard-debug') === '1') {
|
||||||
|
const debugEl = document.createElement('pre');
|
||||||
|
debugEl.id = 'keyboard-viewport-debug';
|
||||||
|
Object.assign(debugEl.style, {
|
||||||
|
position: 'fixed', top: '4px', right: '4px', zIndex: '999999', margin: '0',
|
||||||
|
maxWidth: '94vw', padding: '6px 8px', fontSize: '10px', lineHeight: '1.25',
|
||||||
|
color: '#fff', background: 'rgba(0,0,0,.82)', pointerEvents: 'none',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
});
|
||||||
|
document.body.append(debugEl);
|
||||||
|
const syncDebug = () => {
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
debugEl.textContent = [
|
||||||
|
`innerHeight=${window.innerHeight}`,
|
||||||
|
`clientHeight=${document.documentElement.clientHeight}`,
|
||||||
|
`vv.height=${vv ? Math.round(vv.height) : 'n/a'}`,
|
||||||
|
`vv.offsetTop=${vv ? Math.round(vv.offsetTop) : 'n/a'}`,
|
||||||
|
`stable=${stableViewportHeightPx}`,
|
||||||
|
`keyboard=${getComputedStyle(appShellEl).getPropertyValue('--keyboard-offset').trim()}`,
|
||||||
|
`focused=${isTextEntryFocused()}`,
|
||||||
|
].join(' | ');
|
||||||
|
};
|
||||||
|
window.visualViewport?.addEventListener('resize', syncDebug);
|
||||||
|
window.visualViewport?.addEventListener('scroll', syncDebug);
|
||||||
|
window.addEventListener('resize', syncDebug);
|
||||||
|
document.addEventListener('focusin', () => window.setTimeout(syncDebug, 330));
|
||||||
|
document.addEventListener('focusout', () => window.setTimeout(syncDebug, 230));
|
||||||
|
syncDebug();
|
||||||
|
}
|
||||||
|
|
||||||
function clearSlot(slotEl, cssVarName) {
|
function clearSlot(slotEl, cssVarName) {
|
||||||
if (!slotEl) return;
|
if (!slotEl) return;
|
||||||
slotEl.innerHTML = '';
|
slotEl.innerHTML = '';
|
||||||
|
|||||||
@@ -572,12 +572,27 @@ function scrollToUnreadSeparator(list) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode = 'latest' } = {}) {
|
function renderLog(
|
||||||
|
list,
|
||||||
|
chatId,
|
||||||
|
{
|
||||||
|
onOpenActions,
|
||||||
|
markAsRead = true,
|
||||||
|
scrollMode = 'latest',
|
||||||
|
showUnreadSeparator = true,
|
||||||
|
unreadSeparatorMessageKey = '',
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
const messages = getChatMessages(chatId);
|
const messages = getChatMessages(chatId);
|
||||||
let unreadSeparatorInserted = false;
|
let unreadSeparatorInserted = false;
|
||||||
|
const separatorMessageKey = String(unreadSeparatorMessageKey || '').trim();
|
||||||
messages.forEach((msg) => {
|
messages.forEach((msg) => {
|
||||||
if (!unreadSeparatorInserted && msg?.from === 'in' && msg?.unread) {
|
const isUnreadBoundary = showUnreadSeparator
|
||||||
|
&& !unreadSeparatorInserted
|
||||||
|
&& separatorMessageKey
|
||||||
|
&& String(msg?.messageKey || '').trim() === separatorMessageKey;
|
||||||
|
if (isUnreadBoundary) {
|
||||||
const sep = document.createElement('div');
|
const sep = document.createElement('div');
|
||||||
sep.className = 'chat-unread-separator';
|
sep.className = 'chat-unread-separator';
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
@@ -781,12 +796,8 @@ async function mergeDirectMessagesPage(chatId, payloadMessages) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setChatKeyboardOpen(isOpen) {
|
|
||||||
document.body.classList.toggle('chat-keyboard-open', !!isOpen);
|
|
||||||
document.body.classList.toggle('chat-toolbar-persistent', !!isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function render({ navigate, route, chrome }) {
|
export function render({ navigate, route, chrome }) {
|
||||||
|
document.body.classList.add('chat-topbar-overlay');
|
||||||
const routeChatId = route.params.chatId || 'u1';
|
const routeChatId = route.params.chatId || 'u1';
|
||||||
const chatId = normalizeDmChatId(routeChatId) || 'u1';
|
const chatId = normalizeDmChatId(routeChatId) || 'u1';
|
||||||
const contact = directMessages.find((d) => normalizeDmChatId(d.id) === chatId) || {
|
const contact = directMessages.find((d) => normalizeDmChatId(d.id) === chatId) || {
|
||||||
@@ -800,12 +811,53 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
|
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
|
||||||
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
|
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
|
||||||
const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread);
|
const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread);
|
||||||
|
const UNREAD_SEPARATOR_AUTO_HIDE_MS = 5000;
|
||||||
let historyHasMore = true;
|
let historyHasMore = true;
|
||||||
let historyLoading = false;
|
let historyLoading = false;
|
||||||
let historyNextBeforeTimeMs = 0;
|
let historyNextBeforeTimeMs = 0;
|
||||||
let historyNextBeforeMessageKey = '';
|
let historyNextBeforeMessageKey = '';
|
||||||
let historyBootstrapped = false;
|
let historyBootstrapped = false;
|
||||||
let boundScrollContainer = null;
|
let boundScrollContainer = null;
|
||||||
|
let unreadSeparatorVisible = hasUnreadIncoming;
|
||||||
|
let unreadSeparatorHideTimer = null;
|
||||||
|
let unreadSeparatorAnchorMessageKey = getChatMessages(chatId).find((msg) => msg?.from === 'in' && msg?.unread)?.messageKey || '';
|
||||||
|
|
||||||
|
const clearUnreadSeparatorHideTimer = () => {
|
||||||
|
if (unreadSeparatorHideTimer) {
|
||||||
|
window.clearTimeout(unreadSeparatorHideTimer);
|
||||||
|
unreadSeparatorHideTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderChatLog = ({ scrollMode = 'latest', markAsRead = true } = {}) => {
|
||||||
|
renderLog(log, chatId, {
|
||||||
|
onOpenActions: handleOpenActions,
|
||||||
|
markAsRead,
|
||||||
|
scrollMode,
|
||||||
|
showUnreadSeparator: unreadSeparatorVisible,
|
||||||
|
unreadSeparatorMessageKey: unreadSeparatorAnchorMessageKey,
|
||||||
|
});
|
||||||
|
if (unreadSeparatorVisible) {
|
||||||
|
scheduleUnreadSeparatorAutoHide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideUnreadSeparator = ({ rerender = true } = {}) => {
|
||||||
|
clearUnreadSeparatorHideTimer();
|
||||||
|
if (!unreadSeparatorVisible) return;
|
||||||
|
unreadSeparatorVisible = false;
|
||||||
|
if (rerender) {
|
||||||
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleUnreadSeparatorAutoHide = () => {
|
||||||
|
clearUnreadSeparatorHideTimer();
|
||||||
|
if (!unreadSeparatorVisible) return;
|
||||||
|
unreadSeparatorHideTimer = window.setTimeout(() => {
|
||||||
|
hideUnreadSeparator({ rerender: true });
|
||||||
|
}, UNREAD_SEPARATOR_AUTO_HIDE_MS);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReadAloud = async (msg) => {
|
const handleReadAloud = async (msg) => {
|
||||||
if (!isTextToSpeechConfigured(state.entrySettings)) {
|
if (!isTextToSpeechConfigured(state.entrySettings)) {
|
||||||
@@ -819,13 +871,13 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const handleStartCall = async (mode = 'audio') => {
|
const handleStartCall = async (mode = 'audio') => {
|
||||||
try {
|
try {
|
||||||
await startOutgoingCall(chatId, { mode });
|
await startOutgoingCall(chatId, { mode });
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addSystemChatMessage(chatId, `[Звонок] Ошибка запуска: ${e.message || 'unknown'}`, {
|
addSystemChatMessage(chatId, `[Звонок] Ошибка запуска: ${e.message || 'unknown'}`, {
|
||||||
from: 'out',
|
from: 'out',
|
||||||
kind: 'call-tech',
|
kind: 'call-tech',
|
||||||
});
|
});
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -848,11 +900,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
unread: false,
|
unread: false,
|
||||||
rawBlobB64: String(result?.localBlobB64 || ''),
|
rawBlobB64: String(result?.localBlobB64 || ''),
|
||||||
});
|
});
|
||||||
renderLog(log, chatId, {
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
onOpenActions: handleOpenActions,
|
|
||||||
markAsRead: false,
|
|
||||||
scrollMode: 'latest',
|
|
||||||
});
|
|
||||||
notifyUnreadStateUpdated();
|
notifyUnreadStateUpdated();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1010,29 +1058,6 @@ export function render({ navigate, route, chrome }) {
|
|||||||
let inputFocused = false;
|
let inputFocused = false;
|
||||||
let emojiPickerOpen = false;
|
let emojiPickerOpen = false;
|
||||||
let emojiSelection = null;
|
let emojiSelection = null;
|
||||||
const baseViewportHeight = Math.max(window.visualViewport?.height || 0, window.innerHeight || 0);
|
|
||||||
const appShell = document.querySelector('.app-shell');
|
|
||||||
|
|
||||||
const setKeyboardInset = (valuePx = 0) => {
|
|
||||||
appShell?.style.setProperty('--keyboard-offset', `${Math.max(0, Math.ceil(Number(valuePx || 0)))}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncKeyboardUi = () => {
|
|
||||||
const viewport = window.visualViewport || null;
|
|
||||||
const viewportHeight = Math.max(viewport?.height || 0, window.innerHeight || 0);
|
|
||||||
const viewportShrunk = baseViewportHeight - viewportHeight > 120;
|
|
||||||
const keyboardInset = viewportShrunk
|
|
||||||
? Math.max(0, baseViewportHeight - viewportHeight)
|
|
||||||
: 0;
|
|
||||||
setKeyboardInset(keyboardInset);
|
|
||||||
setChatKeyboardOpen(inputFocused && viewportShrunk);
|
|
||||||
if (viewportShrunk && window.scrollY !== 0) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
if (inputFocused) {
|
|
||||||
window.requestAnimationFrame(() => scrollToLatestMessage(log));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHistoryLoadingState = (isLoading) => {
|
const setHistoryLoadingState = (isLoading) => {
|
||||||
historyLoader.hidden = !isLoading;
|
historyLoader.hidden = !isLoading;
|
||||||
@@ -1235,20 +1260,21 @@ export function render({ navigate, route, chrome }) {
|
|||||||
if (activeEdit?.messageKey && activeEdit.messageKey === String(msg?.messageKey || '')) {
|
if (activeEdit?.messageKey && activeEdit.messageKey === String(msg?.messageKey || '')) {
|
||||||
cancelEditMode({ restoreDraft: true });
|
cancelEditMode({ restoreDraft: true });
|
||||||
}
|
}
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendTextMessage = async (rawText) => {
|
const sendTextMessage = async (rawText) => {
|
||||||
const safeText = sanitizeUserDmTextForSend(String(rawText || ''));
|
const safeText = sanitizeUserDmTextForSend(String(rawText || ''));
|
||||||
const text = safeText.trim();
|
const text = safeText.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
hideUnreadSeparator({ rerender: false });
|
||||||
const editing = activeEdit;
|
const editing = activeEdit;
|
||||||
const replying = !editing ? activeReply : null;
|
const replying = !editing ? activeReply : null;
|
||||||
const finalText = editing
|
const finalText = editing
|
||||||
? `${String(editing?.prefixText || '')}${text}`
|
? `${String(editing?.prefixText || '')}${text}`
|
||||||
: `${replying ? buildDmReplyTechBlock({ baseKey: replying.baseKey }) : ''}${text}`;
|
: `${replying ? buildDmReplyTechBlock({ baseKey: replying.baseKey }) : ''}${text}`;
|
||||||
const tempId = editing ? '' : addOutgoingPendingMessage(chatId, text);
|
const tempId = editing ? '' : addOutgoingPendingMessage(chatId, text);
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
scrollToLatestMessageSmart(log, { smoothIfNearBottom: true });
|
scrollToLatestMessageSmart(log, { smoothIfNearBottom: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1288,7 +1314,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
cancelReplyMode({ restoreDraft: false });
|
cancelReplyMode({ restoreDraft: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
if (localRevisionApplied) {
|
if (localRevisionApplied) {
|
||||||
notifyUnreadStateUpdated();
|
notifyUnreadStateUpdated();
|
||||||
}
|
}
|
||||||
@@ -1331,7 +1357,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
error: e?.message || 'unknown',
|
error: e?.message || 'unknown',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
renderChatLog({ scrollMode: 'latest', markAsRead: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1393,7 +1419,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
historyNextBeforeTimeMs = Number(payload?.nextBeforeTimeMs || 0);
|
historyNextBeforeTimeMs = Number(payload?.nextBeforeTimeMs || 0);
|
||||||
historyNextBeforeMessageKey = String(payload?.nextBeforeMessageKey || '').trim();
|
historyNextBeforeMessageKey = String(payload?.nextBeforeMessageKey || '').trim();
|
||||||
historyBootstrapped = true;
|
historyBootstrapped = true;
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions, markAsRead: false, scrollMode: preserveScroll ? 'preserve' : 'latest' });
|
renderChatLog({ markAsRead: false, scrollMode: preserveScroll ? 'preserve' : 'latest' });
|
||||||
if (preserveScroll) {
|
if (preserveScroll) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const nextHeight = Number(scrollContainer?.scrollHeight || 0);
|
const nextHeight = Number(scrollContainer?.scrollHeight || 0);
|
||||||
@@ -1445,12 +1471,12 @@ export function render({ navigate, route, chrome }) {
|
|||||||
input?.addEventListener('focus', () => {
|
input?.addEventListener('focus', () => {
|
||||||
rememberEmojiSelection();
|
rememberEmojiSelection();
|
||||||
inputFocused = true;
|
inputFocused = true;
|
||||||
syncKeyboardUi();
|
window.requestAnimationFrame(() => {
|
||||||
scrollToLatestMessage(log);
|
if (inputFocused) scrollToLatestMessage(log);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
input?.addEventListener('blur', () => {
|
input?.addEventListener('blur', () => {
|
||||||
inputFocused = false;
|
inputFocused = false;
|
||||||
setChatKeyboardOpen(false);
|
|
||||||
});
|
});
|
||||||
emojiToggle?.setAttribute('aria-expanded', 'false');
|
emojiToggle?.setAttribute('aria-expanded', 'false');
|
||||||
emojiToggle?.addEventListener('pointerdown', (event) => {
|
emojiToggle?.addEventListener('pointerdown', (event) => {
|
||||||
@@ -1493,25 +1519,34 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const handleIncomingChatRefresh = async (event) => {
|
const handleIncomingChatRefresh = async (event) => {
|
||||||
const updatedChatId = normalizeDmChatId(event?.detail?.chatId);
|
const updatedChatId = normalizeDmChatId(event?.detail?.chatId);
|
||||||
if (updatedChatId !== chatId) return;
|
if (updatedChatId !== chatId) return;
|
||||||
|
if (Number(event?.detail?.messageType || 0) === 1) {
|
||||||
|
if (!unreadSeparatorVisible) {
|
||||||
|
unreadSeparatorAnchorMessageKey = getChatMessages(chatId).find((msg) => msg?.from === 'in' && msg?.unread)?.messageKey || unreadSeparatorAnchorMessageKey;
|
||||||
|
unreadSeparatorVisible = Boolean(unreadSeparatorAnchorMessageKey);
|
||||||
|
}
|
||||||
|
scheduleUnreadSeparatorAutoHide();
|
||||||
|
}
|
||||||
preserveComposerSelection(input, () => {
|
preserveComposerSelection(input, () => {
|
||||||
renderLog(log, chatId, { onOpenActions: handleOpenActions, scrollMode: 'latest' });
|
renderChatLog({ scrollMode: 'latest' });
|
||||||
});
|
});
|
||||||
window.requestAnimationFrame(() => scrollToLatestMessage(log));
|
window.requestAnimationFrame(() => scrollToLatestMessage(log));
|
||||||
void sendReadReceiptsForVisible(chatId);
|
void sendReadReceiptsForVisible(chatId);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
|
window.addEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
|
||||||
window.visualViewport?.addEventListener('resize', syncKeyboardUi);
|
|
||||||
window.addEventListener('resize', syncKeyboardUi);
|
|
||||||
|
|
||||||
chrome?.setComposer(form);
|
|
||||||
wrap.append(historyLoader, log);
|
wrap.append(historyLoader, log);
|
||||||
screen.append(wrap);
|
screen.append(wrap);
|
||||||
|
chrome?.setComposer(form);
|
||||||
renderLog(log, chatId, {
|
renderLog(log, chatId, {
|
||||||
onOpenActions: handleOpenActions,
|
onOpenActions: handleOpenActions,
|
||||||
markAsRead: false,
|
markAsRead: false,
|
||||||
scrollMode: hasUnreadIncoming ? 'unread' : 'latest',
|
scrollMode: hasUnreadIncoming ? 'unread' : 'latest',
|
||||||
|
showUnreadSeparator: unreadSeparatorVisible,
|
||||||
});
|
});
|
||||||
|
if (unreadSeparatorVisible) {
|
||||||
|
scheduleUnreadSeparatorAutoHide();
|
||||||
|
}
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (markChatRead(chatId) > 0) {
|
if (markChatRead(chatId) > 0) {
|
||||||
notifyUnreadStateUpdated();
|
notifyUnreadStateUpdated();
|
||||||
@@ -1519,18 +1554,18 @@ export function render({ navigate, route, chrome }) {
|
|||||||
}, 220);
|
}, 220);
|
||||||
void sendReadReceiptsForVisible(chatId);
|
void sendReadReceiptsForVisible(chatId);
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
boundScrollContainer = log.closest('.screen-content') || wrap.parentElement || wrap;
|
boundScrollContainer = wrap;
|
||||||
boundScrollContainer?.addEventListener('scroll', handleHistoryScroll, { passive: true });
|
boundScrollContainer?.addEventListener('scroll', handleHistoryScroll, { passive: true });
|
||||||
void loadHistoryPage({ preserveScroll: true });
|
void loadHistoryPage({ preserveScroll: true });
|
||||||
});
|
});
|
||||||
screen.cleanup = () => {
|
screen.cleanup = () => {
|
||||||
setChatKeyboardOpen(false);
|
hideUnreadSeparator({ rerender: false });
|
||||||
setKeyboardInset(0);
|
|
||||||
stopAllTwemojiAnimations();
|
stopAllTwemojiAnimations();
|
||||||
window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
|
window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
|
||||||
window.visualViewport?.removeEventListener('resize', syncKeyboardUi);
|
|
||||||
window.removeEventListener('resize', syncKeyboardUi);
|
|
||||||
boundScrollContainer?.removeEventListener('scroll', handleHistoryScroll);
|
boundScrollContainer?.removeEventListener('scroll', handleHistoryScroll);
|
||||||
|
clearUnreadSeparatorHideTimer();
|
||||||
|
document.body.classList.remove('chat-topbar-overlay');
|
||||||
|
chrome?.setComposer(null);
|
||||||
};
|
};
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ function formatChatRowTime(ts) {
|
|||||||
}).format(new Date(value));
|
}).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareChatRows(a, b) {
|
||||||
|
const timeA = Number(a?.lastTimeMs || 0);
|
||||||
|
const timeB = Number(b?.lastTimeMs || 0);
|
||||||
|
if (timeA !== timeB) return timeB - timeA;
|
||||||
|
const nameA = String(a?.name || '').toLowerCase();
|
||||||
|
const nameB = String(b?.name || '').toLowerCase();
|
||||||
|
return nameA.localeCompare(nameB, 'ru');
|
||||||
|
}
|
||||||
|
|
||||||
const SVG_CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>';
|
const SVG_CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>';
|
||||||
|
|
||||||
export function render({ navigate, chrome }) {
|
export function render({ navigate, chrome }) {
|
||||||
@@ -99,11 +108,103 @@ export function render({ navigate, chrome }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="dm-head-title">Контакты</h1>
|
<h1 class="dm-head-title">Контакты</h1>
|
||||||
<button type="button" class="dm-head-plus" aria-label="Новый диалог">+</button>
|
<div class="dm-head-menu-wrap">
|
||||||
|
<button type="button" class="dm-head-menu-btn" aria-label="Меню контактов" aria-haspopup="menu" aria-expanded="false">
|
||||||
|
<span class="dm-head-menu-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||||
|
</button>
|
||||||
|
<div class="dm-head-menu" role="menu" hidden>
|
||||||
|
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="6.5"></circle>
|
||||||
|
<path d="M16 16l4 4"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Поиск контактов</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
const headName = head.querySelector('.dm-head-name');
|
const headName = head.querySelector('.dm-head-name');
|
||||||
if (headName) headName.textContent = login;
|
if (headName) headName.textContent = login;
|
||||||
head.querySelector('.dm-head-plus')?.addEventListener('click', () => navigate('contact-search-view'));
|
|
||||||
|
const menuWrap = head.querySelector('.dm-head-menu-wrap');
|
||||||
|
const menuButton = head.querySelector('.dm-head-menu-btn');
|
||||||
|
const menuTemplate = head.querySelector('.dm-head-menu');
|
||||||
|
// The header itself is mounted into chrome/topbar, whose hit-test area ends at the
|
||||||
|
// header bounds. A dropdown overflowing below it can look visible but clicks may
|
||||||
|
// land on the content layer underneath. Render the open menu as a body portal.
|
||||||
|
menuTemplate?.remove();
|
||||||
|
|
||||||
|
let menuPortal = null;
|
||||||
|
|
||||||
|
const closeHeadMenu = () => {
|
||||||
|
menuPortal?.remove();
|
||||||
|
menuPortal = null;
|
||||||
|
menuButton?.setAttribute('aria-expanded', 'false');
|
||||||
|
menuWrap?.classList.remove('is-open');
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionHeadMenu = () => {
|
||||||
|
if (!menuPortal || !menuButton) return;
|
||||||
|
const rect = menuButton.getBoundingClientRect();
|
||||||
|
const margin = 10;
|
||||||
|
const menuWidth = menuPortal.offsetWidth || 206;
|
||||||
|
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
||||||
|
menuPortal.style.left = `${Math.round(left)}px`;
|
||||||
|
menuPortal.style.top = `${Math.round(rect.bottom + 7)}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openHeadMenu = () => {
|
||||||
|
if (!menuButton || menuPortal) return;
|
||||||
|
const portal = document.createElement('div');
|
||||||
|
portal.className = 'dm-head-menu dm-head-menu--portal';
|
||||||
|
portal.setAttribute('role', 'menu');
|
||||||
|
portal.innerHTML = `
|
||||||
|
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="6.5"></circle>
|
||||||
|
<path d="M16 16l4 4"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Поиск контактов</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
portal.querySelector('[data-action="search-contacts"]')?.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeHeadMenu();
|
||||||
|
navigate('contact-search-view');
|
||||||
|
});
|
||||||
|
portal.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
|
||||||
|
document.body.append(portal);
|
||||||
|
menuPortal = portal;
|
||||||
|
menuButton.setAttribute('aria-expanded', 'true');
|
||||||
|
menuWrap?.classList.add('is-open');
|
||||||
|
positionHeadMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
menuButton?.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (menuPortal) closeHeadMenu();
|
||||||
|
else openHeadMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onOutsideClick = (event) => {
|
||||||
|
if (!menuPortal) return;
|
||||||
|
if (menuPortal.contains(event.target) || menuButton?.contains(event.target)) return;
|
||||||
|
closeHeadMenu();
|
||||||
|
};
|
||||||
|
const onMenuKeydown = (event) => {
|
||||||
|
if (event.key !== 'Escape' || !menuPortal) return;
|
||||||
|
closeHeadMenu();
|
||||||
|
menuButton?.focus();
|
||||||
|
};
|
||||||
|
const onMenuViewportChange = () => positionHeadMenu();
|
||||||
|
document.addEventListener('click', onOutsideClick);
|
||||||
|
document.addEventListener('keydown', onMenuKeydown);
|
||||||
|
window.addEventListener('resize', onMenuViewportChange, { passive: true });
|
||||||
|
window.addEventListener('scroll', onMenuViewportChange, { passive: true, capture: true });
|
||||||
|
|
||||||
const divider = document.createElement('div');
|
const divider = document.createElement('div');
|
||||||
divider.className = 'dm-divider';
|
divider.className = 'dm-divider';
|
||||||
@@ -175,6 +276,7 @@ export function render({ navigate, chrome }) {
|
|||||||
time: formatChatRowTime(lastTimeMs),
|
time: formatChatRowTime(lastTimeMs),
|
||||||
unread,
|
unread,
|
||||||
notInContacts: false,
|
notInContacts: false,
|
||||||
|
lastTimeMs,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,10 +299,11 @@ export function render({ navigate, chrome }) {
|
|||||||
time: formatChatRowTime(lastTimeMs),
|
time: formatChatRowTime(lastTimeMs),
|
||||||
unread,
|
unread,
|
||||||
notInContacts: true,
|
notInContacts: true,
|
||||||
|
lastTimeMs,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = [...contactRows, ...extraRows];
|
const rows = [...contactRows, ...extraRows].sort(compareChatRows);
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'card meta-muted';
|
empty.className = 'card meta-muted';
|
||||||
@@ -251,5 +354,14 @@ export function render({ navigate, chrome }) {
|
|||||||
chrome?.setTopbar(head);
|
chrome?.setTopbar(head);
|
||||||
screen.append(divider, list);
|
screen.append(divider, list);
|
||||||
loadList();
|
loadList();
|
||||||
|
|
||||||
|
screen.cleanup = () => {
|
||||||
|
closeHeadMenu();
|
||||||
|
document.removeEventListener('click', onOutsideClick);
|
||||||
|
document.removeEventListener('keydown', onMenuKeydown);
|
||||||
|
window.removeEventListener('resize', onMenuViewportChange);
|
||||||
|
window.removeEventListener('scroll', onMenuViewportChange, true);
|
||||||
|
};
|
||||||
|
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-22
@@ -6268,6 +6268,37 @@ html, body { overflow-x: hidden; }
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dm-chat-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-chat-screen > .dm-chat-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-chat-composer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 12;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-content:has(> .dm-chat-screen) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-content:has(> .dm-chat-screen) > .dm-chat-screen {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.screen-content:has(> .dm-screen) {
|
.screen-content:has(> .dm-screen) {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
@@ -6282,27 +6313,14 @@ html, body { overflow-x: hidden; }
|
|||||||
.screen-content:has(> .dm-chat-screen) {
|
.screen-content:has(> .dm-chat-screen) {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: none;
|
||||||
scrollbar-color: rgba(212, 175, 55, 0.65) rgba(255, 255, 255, 0.06);
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen-content:has(> .dm-chat-screen)::-webkit-scrollbar {
|
.screen-content:has(> .dm-chat-screen)::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 0;
|
||||||
height: 4px;
|
height: 0;
|
||||||
display: block;
|
display: none;
|
||||||
}
|
|
||||||
|
|
||||||
.screen-content:has(> .dm-chat-screen)::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-content:has(> .dm-chat-screen)::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(212, 175, 55, 0.7);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-content:has(> .dm-chat-screen)::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(240, 198, 76, 0.9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-messages-log {
|
.dm-messages-log {
|
||||||
@@ -6401,11 +6419,10 @@ html, body { overflow-x: hidden; }
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
position: sticky;
|
position: relative;
|
||||||
bottom: 0;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
margin-inline: -14px;
|
margin-inline: 0;
|
||||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
padding: 10px 0;
|
||||||
border-top: 1px solid rgba(212, 175, 55, 0.22);
|
border-top: 1px solid rgba(212, 175, 55, 0.22);
|
||||||
background: rgba(8, 12, 20, 0.9);
|
background: rgba(8, 12, 20, 0.9);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
@@ -7900,3 +7917,128 @@ html, body { overflow-x: hidden; }
|
|||||||
color: #D4AF37;
|
color: #D4AF37;
|
||||||
text-shadow: 0 0 10px rgba(212, 175, 55, 0.6);
|
text-shadow: 0 0 10px rgba(212, 175, 55, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Contacts header overflow menu — vertical ellipsis + glass dropdown. */
|
||||||
|
.dm-head-menu-wrap {
|
||||||
|
position: relative;
|
||||||
|
justify-self: end;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
.dm-head-menu-btn {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #FFD98A;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 160ms ease, box-shadow 160ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
.dm-head-menu-btn:active { transform: scale(0.94); }
|
||||||
|
.dm-head-menu-wrap.is-open .dm-head-menu-btn,
|
||||||
|
.dm-head-menu-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(240, 184, 46, 0.08);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(240, 184, 46, 0.24), 0 0 18px rgba(240, 184, 46, 0.12);
|
||||||
|
}
|
||||||
|
.dm-head-menu-dots {
|
||||||
|
width: 6px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3.5px;
|
||||||
|
}
|
||||||
|
.dm-head-menu-dots i {
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 0 0 7px rgba(240, 184, 46, 0.45);
|
||||||
|
}
|
||||||
|
.dm-head-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 7px);
|
||||||
|
right: 0;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 190px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid rgba(240, 184, 46, 0.26);
|
||||||
|
border-radius: 15px;
|
||||||
|
background: linear-gradient(155deg, rgba(22, 24, 31, 0.97), rgba(10, 12, 18, 0.97));
|
||||||
|
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.42), 0 0 20px rgba(240, 184, 46, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
transform-origin: top right;
|
||||||
|
animation: dm-head-menu-in 140ms ease-out both;
|
||||||
|
}
|
||||||
|
.dm-head-menu[hidden] { display: none; }
|
||||||
|
.dm-head-menu::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: 18px;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-left: 1px solid rgba(240, 184, 46, 0.22);
|
||||||
|
border-top: 1px solid rgba(240, 184, 46, 0.22);
|
||||||
|
background: rgba(19, 21, 28, 0.98);
|
||||||
|
}
|
||||||
|
.dm-head-menu-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #FCEAC0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dm-head-menu-item svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #E7B83D;
|
||||||
|
filter: drop-shadow(0 0 5px rgba(240, 184, 46, 0.22));
|
||||||
|
}
|
||||||
|
.dm-head-menu-item:hover,
|
||||||
|
.dm-head-menu-item:focus-visible,
|
||||||
|
.dm-head-menu-item:active {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(240, 184, 46, 0.09);
|
||||||
|
}
|
||||||
|
@keyframes dm-head-menu-in {
|
||||||
|
from { opacity: 0; transform: translateY(-5px) scale(0.97); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.dm-head-menu { animation: none; }
|
||||||
|
.dm-head-menu-btn { transition: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contacts overflow menu body portal: avoids topbar overflow hit-testing on desktop/Android. */
|
||||||
|
.dm-head-menu--portal {
|
||||||
|
position: fixed;
|
||||||
|
right: auto;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: auto;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|||||||
+53
-22
@@ -2,7 +2,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #05070A;
|
background: #05070A;
|
||||||
min-height: 100vh;
|
min-height: 100dvh;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,11 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
width: min(100vw, 430px);
|
width: min(var(--app-viewport-width, 100vw), 430px);
|
||||||
height: 100vh;
|
height: var(--app-viewport-height, 100vh);
|
||||||
height: 100svh;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: var(--app-viewport-offset-top, 0px);
|
||||||
left: 50%;
|
left: calc(50% + var(--app-viewport-offset-left, 0px) / 2);
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
--call-minimized-bar-height: 0px;
|
--call-minimized-bar-height: 0px;
|
||||||
--topbar-height: 0px;
|
--topbar-height: 0px;
|
||||||
@@ -94,10 +93,11 @@ body::before {
|
|||||||
|
|
||||||
.composer-slot {
|
.composer-slot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 50%;
|
||||||
right: 0;
|
width: min(var(--app-viewport-width, 100vw), 430px);
|
||||||
|
transform: translateX(-50%);
|
||||||
bottom: calc(var(--toolbar-height, 78px) + env(safe-area-inset-bottom));
|
bottom: calc(var(--toolbar-height, 78px) + env(safe-area-inset-bottom));
|
||||||
padding: 0 12px 8px;
|
padding: 0 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,20 +115,42 @@ body::before {
|
|||||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chat-keyboard-open .screen-content {
|
body.chat-topbar-overlay .topbar-slot {
|
||||||
bottom: calc(var(--composer-height, 0px) + max(var(--toolbar-height, 78px), var(--keyboard-offset, 0px)));
|
position: fixed;
|
||||||
padding-bottom: calc(14px + env(safe-area-inset-bottom));
|
top: var(--call-minimized-bar-height, 0px);
|
||||||
}
|
left: 0;
|
||||||
|
right: 0;
|
||||||
body.chat-keyboard-open .composer-slot {
|
width: min(100vw, 430px);
|
||||||
bottom: max(var(--toolbar-height, 78px), var(--keyboard-offset, 0px));
|
margin: 0 auto;
|
||||||
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
body.chat-keyboard-open .toolbar-slot {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.chat-topbar-overlay .screen-content {
|
||||||
|
top: calc(var(--call-minimized-bar-height, 0px) + var(--topbar-height, 0px));
|
||||||
|
bottom: calc(var(--toolbar-height, 78px) + var(--composer-height, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.chat-topbar-overlay .composer-slot {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: min(var(--app-viewport-width, 100vw), 430px);
|
||||||
|
bottom: calc(var(--toolbar-height, 78px) + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* При открытой клавиатуре toolbar остаётся на физическом дне экрана и
|
||||||
|
перекрывается клавиатурой. Composer прижимается ровно к верхней границе
|
||||||
|
visualViewport, а область сообщений заканчивается прямо над composer. */
|
||||||
|
.app-shell.keyboard-open .toolbar-slot {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.chat-topbar-overlay .app-shell.keyboard-open .composer-slot {
|
||||||
|
bottom: var(--keyboard-offset, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.chat-topbar-overlay .app-shell.keyboard-open .screen-content {
|
||||||
|
bottom: calc(var(--keyboard-offset, 0px) + var(--composer-height, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-retry-banner {
|
.connection-retry-banner {
|
||||||
@@ -177,3 +199,12 @@ body.chat-keyboard-open .toolbar-slot {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Android keyboard: composer touches the keyboard edge; only the composer moves. */
|
||||||
|
body.chat-topbar-overlay .app-shell.keyboard-open .composer-slot {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.chat-topbar-overlay .app-shell.keyboard-open .composer-slot .dm-chat-input {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user