SHA256
Обновить UI чата и закрыть проверенные DM задачи
This commit is contained in:
@@ -82,6 +82,8 @@ import * as addPersonalPublicChatView from './pages/add-personal-public-chat-vie
|
||||
import * as networkView from './pages/network-view.js';
|
||||
import * as notificationsView from './pages/notifications-view.js';
|
||||
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
|
||||
const routes = {
|
||||
'start-view': startView,
|
||||
'entry-settings-view': entrySettingsView,
|
||||
@@ -1182,6 +1184,16 @@ async function init() {
|
||||
});
|
||||
} else if (messageType === 7 || messageType === 8) {
|
||||
const removed = deleteConversationMessagesBefore(chatId, Number(parsed.timeMs || 0));
|
||||
addSignedMessageToChat({
|
||||
chatId,
|
||||
messageKey,
|
||||
baseKey: parsed.baseKey,
|
||||
from: messageType === 8 ? 'out' : 'in',
|
||||
text: CONVERSATION_CLEAR_NOTICE_TEXT,
|
||||
messageType,
|
||||
unread: false,
|
||||
rawBlobB64: blobB64,
|
||||
});
|
||||
if (removed > 0) shouldRefreshToolbarUnread = true;
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
|
||||
@@ -26,8 +26,10 @@ export function renderHeader({ title, leftAction, leftLabel = '', rightActions =
|
||||
right.className = 'header-actions';
|
||||
rightActions.forEach((action) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'icon-btn';
|
||||
btn.className = `icon-btn${action.className ? ` ${action.className}` : ''}`;
|
||||
btn.textContent = action.label;
|
||||
if (action.title) btn.title = action.title;
|
||||
if (action.ariaLabel) btn.setAttribute('aria-label', action.ariaLabel);
|
||||
btn.addEventListener('click', action.onClick);
|
||||
right.append(btn);
|
||||
});
|
||||
|
||||
+249
-15
@@ -6,6 +6,7 @@ import {
|
||||
addSignedMessageToChat,
|
||||
addSystemChatMessage,
|
||||
addOutgoingPendingMessage,
|
||||
deleteConversationMessagesBefore,
|
||||
getChatMessages,
|
||||
markChatRead,
|
||||
markOutgoingSent,
|
||||
@@ -21,6 +22,7 @@ import { isSpeechToTextConfigured, isTextToSpeechConfigured, speakTextBySettings
|
||||
import { showToast } from '../services/channels-ux.js';
|
||||
|
||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
|
||||
function truncatePreviewText(value, maxLen = 72) {
|
||||
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
|
||||
@@ -56,6 +58,74 @@ function openDeleteMessageConfirmModal({ onConfirm }) {
|
||||
});
|
||||
}
|
||||
|
||||
function openChatConfirmModal({
|
||||
title = 'Подтвердить действие',
|
||||
text = '',
|
||||
confirmLabel = 'Да',
|
||||
confirmKind = 'primary',
|
||||
onConfirm,
|
||||
}) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
const confirmClass = confirmKind === 'danger' ? 'destructive-btn' : 'primary-btn';
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="chat-confirm-modal">
|
||||
<div class="modal-card stack dm-dialog-card">
|
||||
<h3 class="modal-title">${title}</h3>
|
||||
<p class="meta-muted">${text}</p>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" type="button" id="chat-confirm-no">Нет</button>
|
||||
<button class="${confirmClass}" type="button" id="chat-confirm-yes">${confirmLabel}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const close = () => {
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
root.querySelector('#chat-confirm-no')?.addEventListener('click', close);
|
||||
root.querySelector('#chat-confirm-yes')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onConfirm === 'function') await onConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
function openDeleteChatConfirmModal({ contactName = '', onConfirm }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="chat-delete-chat-modal">
|
||||
<div class="modal-card stack dm-dialog-card">
|
||||
<h3 class="modal-title">Удалить чат?</h3>
|
||||
<p class="meta-muted">Удалить пользователя ${contactName} из контактов?</p>
|
||||
<label class="dm-confirm-check">
|
||||
<input type="checkbox" id="chat-delete-chat-history" checked />
|
||||
<span>Также удалить всю историю переписки</span>
|
||||
</label>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" type="button" id="chat-delete-chat-no">Нет</button>
|
||||
<button class="destructive-btn" type="button" id="chat-delete-chat-yes">Да</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const close = () => {
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
root.querySelector('#chat-delete-chat-no')?.addEventListener('click', close);
|
||||
root.querySelector('#chat-delete-chat-yes')?.addEventListener('click', async () => {
|
||||
const deleteHistory = Boolean(root.querySelector('#chat-delete-chat-history')?.checked);
|
||||
close();
|
||||
if (typeof onConfirm === 'function') {
|
||||
await onConfirm({ deleteHistory });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openMessageActionsMenu({
|
||||
anchorX = 0,
|
||||
anchorY = 0,
|
||||
@@ -148,6 +218,78 @@ function openMessageActionsMenu({
|
||||
});
|
||||
}
|
||||
|
||||
function openChatActionsMenu({
|
||||
anchorX = 0,
|
||||
anchorY = 0,
|
||||
onCall,
|
||||
onClearHistory,
|
||||
onDeleteChat,
|
||||
}) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
|
||||
const menuId = `chat-header-actions-menu-${Date.now()}`;
|
||||
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-clear-history">Очистить историю</button>
|
||||
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">Удалить чат</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const menu = root.querySelector(`#${menuId}`);
|
||||
if (!menu) return;
|
||||
|
||||
const close = () => {
|
||||
document.removeEventListener('pointerdown', onDocumentPointerDown, true);
|
||||
window.removeEventListener('resize', close);
|
||||
window.removeEventListener('scroll', close, true);
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
const onDocumentPointerDown = (event) => {
|
||||
if (menu.contains(event.target)) return;
|
||||
close();
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', onDocumentPointerDown, true);
|
||||
window.addEventListener('resize', close);
|
||||
window.addEventListener('scroll', close, true);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
const viewportHeight = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
||||
const left = Math.min(
|
||||
Math.max(12, Number(anchorX || 0) - menuRect.width + 8),
|
||||
Math.max(12, viewportWidth - menuRect.width - 12)
|
||||
);
|
||||
const top = Math.min(
|
||||
Math.max(12, Number(anchorY || 0) + 10),
|
||||
Math.max(12, viewportHeight - menuRect.height - 12)
|
||||
);
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
menu.style.transformOrigin = `${Math.round(Number(anchorX || left) - left)}px top`;
|
||||
menu.classList.add('is-visible');
|
||||
});
|
||||
|
||||
root.querySelector('#chat-menu-call')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onCall === 'function') await onCall();
|
||||
});
|
||||
root.querySelector('#chat-menu-clear-history')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onClearHistory === 'function') await onClearHistory();
|
||||
});
|
||||
root.querySelector('#chat-menu-delete-chat')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onDeleteChat === 'function') await onDeleteChat();
|
||||
});
|
||||
}
|
||||
|
||||
function showTtsMissingConfigDialog(navigate) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
@@ -443,6 +585,46 @@ export function render({ navigate, route }) {
|
||||
await speakTextBySettings(String(msg?.text || ''), state.entrySettings);
|
||||
};
|
||||
|
||||
const handleStartCall = async () => {
|
||||
try {
|
||||
await startOutgoingCall(chatId);
|
||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
||||
} catch (e) {
|
||||
addSystemChatMessage(chatId, `[Звонок] Ошибка запуска: ${e.message || 'unknown'}`, {
|
||||
from: 'out',
|
||||
kind: 'call-tech',
|
||||
});
|
||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
||||
}
|
||||
};
|
||||
|
||||
const clearConversationHistory = async () => {
|
||||
const result = await authService.deleteConversation({
|
||||
login: state.session.login,
|
||||
toLogin: chatId,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
deleteByRecipient: true,
|
||||
});
|
||||
const parsed = authService.parseSignedMessageBlob(String(result?.localBlobB64 || '').trim());
|
||||
deleteConversationMessagesBefore(chatId, Number(parsed?.timeMs || 0));
|
||||
addSignedMessageToChat({
|
||||
chatId,
|
||||
messageKey: String(parsed?.messageKey || ''),
|
||||
baseKey: String(parsed?.baseKey || ''),
|
||||
from: 'out',
|
||||
text: CONVERSATION_CLEAR_NOTICE_TEXT,
|
||||
messageType: Number(parsed?.messageType || 8),
|
||||
unread: false,
|
||||
rawBlobB64: String(result?.localBlobB64 || ''),
|
||||
});
|
||||
renderLog(log, chatId, {
|
||||
onOpenActions: handleOpenActions,
|
||||
markAsRead: false,
|
||||
scrollMode: 'latest',
|
||||
});
|
||||
notifyUnreadStateUpdated();
|
||||
};
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'chat-wrap dm-chat-wrap';
|
||||
|
||||
@@ -453,21 +635,70 @@ export function render({ navigate, route }) {
|
||||
renderHeader({
|
||||
title: `Чат с ${contact.name}`,
|
||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||
rightActions: [{
|
||||
label: 'Позвонить',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await startOutgoingCall(chatId);
|
||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
||||
} catch (e) {
|
||||
addSystemChatMessage(chatId, `[Звонок] Ошибка запуска: ${e.message || 'unknown'}`, {
|
||||
from: 'out',
|
||||
kind: 'call-tech',
|
||||
});
|
||||
renderLog(log, chatId, { onOpenActions: handleOpenActions });
|
||||
}
|
||||
rightActions: [
|
||||
{
|
||||
label: '☎',
|
||||
title: 'Позвонить',
|
||||
ariaLabel: 'Позвонить',
|
||||
className: 'chat-header-icon-btn chat-header-call-btn',
|
||||
onClick: handleStartCall,
|
||||
},
|
||||
}],
|
||||
{
|
||||
label: '⋯',
|
||||
title: 'Действия чата',
|
||||
ariaLabel: 'Открыть меню действий чата',
|
||||
className: 'chat-header-icon-btn chat-header-menu-btn',
|
||||
onClick: (event) => {
|
||||
openChatActionsMenu({
|
||||
anchorX: Number(event?.currentTarget?.getBoundingClientRect?.().right || event?.clientX || 0),
|
||||
anchorY: Number(event?.currentTarget?.getBoundingClientRect?.().bottom || event?.clientY || 0),
|
||||
onCall: handleStartCall,
|
||||
onClearHistory: async () => {
|
||||
openChatConfirmModal({
|
||||
title: 'Очистить историю?',
|
||||
text: `Добавить техническое сообщение очистки истории переписки с ${contact.name}?`,
|
||||
confirmLabel: 'Очистить',
|
||||
confirmKind: 'danger',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await clearConversationHistory();
|
||||
showToast('История чата очищена', { timeoutMs: 1200 });
|
||||
} catch (error) {
|
||||
showToast(`Не удалось очистить историю: ${error?.message || 'unknown'}`, { kind: 'error', timeoutMs: 1600 });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
onDeleteChat: async () => {
|
||||
openDeleteChatConfirmModal({
|
||||
contactName: contact.name,
|
||||
onConfirm: async ({ deleteHistory }) => {
|
||||
try {
|
||||
if (deleteHistory) {
|
||||
await clearConversationHistory();
|
||||
}
|
||||
await authService.setUserRelation({
|
||||
login: state.session.login,
|
||||
toLogin: chatId,
|
||||
kind: 'contact',
|
||||
enabled: false,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
});
|
||||
const contactsPayload = await authService.listContacts();
|
||||
setContacts(contactsPayload?.contacts || []);
|
||||
notifyUnreadStateUpdated();
|
||||
showToast('Чат удалён из контактов', { timeoutMs: 1200 });
|
||||
navigate('messages-list');
|
||||
} catch (error) {
|
||||
showToast(`Не удалось удалить чат: ${error?.message || 'unknown'}`, { kind: 'error', timeoutMs: 1600 });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
@@ -751,7 +982,10 @@ export function render({ navigate, route }) {
|
||||
messageText: msg?.text || '',
|
||||
showReadAloud: isTextToSpeechReady,
|
||||
canEdit: msg?.from === 'out' && Number(msg?.messageType || 0) === 2,
|
||||
canDelete: msg?.from === 'out' && Number(msg?.messageType || 0) === 2,
|
||||
canDelete: (
|
||||
(msg?.from === 'out' && Number(msg?.messageType || 0) === 2)
|
||||
|| (msg?.from === 'in' && Number(msg?.messageType || 0) === 1)
|
||||
),
|
||||
onReadAloud: async () => handleReadAloud(msg),
|
||||
onEdit: async () => {
|
||||
startEditMode(msg);
|
||||
|
||||
@@ -44,6 +44,26 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-header-icon-btn {
|
||||
min-width: 42px;
|
||||
width: 42px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-header-call-btn {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.chat-header-menu-btn {
|
||||
border-radius: 12px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.icon-btn,
|
||||
.text-btn,
|
||||
.primary-btn,
|
||||
@@ -4062,6 +4082,22 @@ html, body { overflow-x: hidden; }
|
||||
background: rgba(112, 28, 28, 0.22);
|
||||
}
|
||||
|
||||
.dm-confirm-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(17, 26, 46, 0.62);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #d9e6ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dm-confirm-check input {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.speech-actions-top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
Reference in New Issue
Block a user