SHA256
Обновить DM, сеть и оффлайн-бандл
This commit is contained in:
@@ -264,18 +264,30 @@ function openChatConfirmModal({
|
||||
});
|
||||
}
|
||||
|
||||
function openDeleteChatConfirmModal({ contactName = '', onConfirm }) {
|
||||
function openDeleteChatConfirmModal({ contactName = '', relationType = 'none', onConfirm }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
const relation = normalizeChatRelationType(relationType);
|
||||
const isCloseFriend = relation === 'close_friend';
|
||||
const isFriend = relation === 'friend';
|
||||
const isProtectedRelation = isCloseFriend || isFriend;
|
||||
const relationName = isCloseFriend ? 'близких друзей' : 'друзей';
|
||||
const safeName = String(contactName || '').trim() || 'этого пользователя';
|
||||
|
||||
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>
|
||||
${isProtectedRelation ? `
|
||||
<p class="meta-muted">Можно удалить содержимое переписки, но чат с ${isCloseFriend ? 'близким другом' : 'другом'} останется в списке.</p>
|
||||
<p class="meta-muted">Удалить ${safeName} из ${relationName} и удалить чат?</p>
|
||||
` : `
|
||||
<p class="meta-muted">Удалить пользователя ${safeName} из контактов?</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>
|
||||
@@ -290,10 +302,12 @@ function openDeleteChatConfirmModal({ contactName = '', onConfirm }) {
|
||||
|
||||
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);
|
||||
const deleteHistory = isProtectedRelation
|
||||
? true
|
||||
: Boolean(root.querySelector('#chat-delete-chat-history')?.checked);
|
||||
close();
|
||||
if (typeof onConfirm === 'function') {
|
||||
await onConfirm({ deleteHistory });
|
||||
await onConfirm({ deleteHistory, removeRelation: isProtectedRelation });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1173,24 +1187,40 @@ export function render({ navigate, route, chrome }) {
|
||||
});
|
||||
},
|
||||
onDeleteChat: async () => {
|
||||
const relationBeforeDelete = normalizeChatRelationType(peerRelationType);
|
||||
openDeleteChatConfirmModal({
|
||||
contactName: contact.name,
|
||||
relationType: relationBeforeDelete,
|
||||
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 relationKinds = relationBeforeDelete === 'close_friend' || relationBeforeDelete === 'friend'
|
||||
? ['close_friend', 'friend', 'contact']
|
||||
: ['contact'];
|
||||
for (const kind of relationKinds) {
|
||||
await authService.setUserRelation({
|
||||
login: state.session.login,
|
||||
toLogin: chatId,
|
||||
kind,
|
||||
enabled: false,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
});
|
||||
}
|
||||
|
||||
const contactsPayload = await authService.listContacts();
|
||||
setContacts(contactsPayload?.contacts || []);
|
||||
setContacts(
|
||||
contactsPayload?.contacts
|
||||
|| contactsPayload?.dialogs?.filter((x) => x?.relationFlag !== 'none').map((x) => x.peerLogin)
|
||||
|| [],
|
||||
);
|
||||
notifyUnreadStateUpdated();
|
||||
showToast('Чат удалён из контактов', { timeoutMs: 1200 });
|
||||
showToast('Чат удалён', { timeoutMs: 1200 });
|
||||
navigate('messages-list');
|
||||
} catch (error) {
|
||||
showToast(`Не удалось удалить чат: ${error?.message || 'unknown'}`, { kind: 'error', timeoutMs: 1600 });
|
||||
|
||||
@@ -448,6 +448,9 @@ function renderRow(item) {
|
||||
});
|
||||
|
||||
const rows = Array.from(byPeer.values())
|
||||
// Технический tombstone очистки истории сам по себе не создаёт видимый диалог.
|
||||
// Пустые друзья/контакты остаются, а пользователь без связи исчезает после очистки.
|
||||
.filter((item) => normalizeRelationFlag(item.relationFlag) !== 'none' || Boolean(item.hasDialog))
|
||||
.filter((item) => currentChatFilter === 'all' || normalizeRelationFlag(item.relationFlag) === currentChatFilter)
|
||||
.sort((a, b) => {
|
||||
const orderA = relationOrder(a.relationFlag);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { createDropdownMenu } from '../components/dropdown-menu.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import { makeProfileRoute } from '../services/shine-routes.js';
|
||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||
@@ -25,32 +27,6 @@ function createDebounced(fn, delayMs = 2000) {
|
||||
};
|
||||
}
|
||||
|
||||
function createHeaderSearchIcon() {
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(ns, 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
svg.setAttribute('class', 'header-icon-svg header-icon-svg--search');
|
||||
|
||||
const circle = document.createElementNS(ns, 'circle');
|
||||
circle.setAttribute('cx', '11');
|
||||
circle.setAttribute('cy', '11');
|
||||
circle.setAttribute('r', '6.5');
|
||||
circle.setAttribute('fill', 'none');
|
||||
circle.setAttribute('stroke', 'currentColor');
|
||||
circle.setAttribute('stroke-width', '2');
|
||||
|
||||
const handle = document.createElementNS(ns, 'path');
|
||||
handle.setAttribute('d', 'M16 16l4.5 4.5');
|
||||
handle.setAttribute('fill', 'none');
|
||||
handle.setAttribute('stroke', 'currentColor');
|
||||
handle.setAttribute('stroke-width', '2');
|
||||
handle.setAttribute('stroke-linecap', 'round');
|
||||
|
||||
svg.append(circle, handle);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function normKey(value) {
|
||||
return normalizeLogin(value).toLowerCase();
|
||||
}
|
||||
@@ -290,7 +266,7 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
<div class="modal" id="network-search-modal">
|
||||
<div class="modal-card stack">
|
||||
<button class="icon-btn" type="button" id="network-search-close" style="justify-self:end;">✕</button>
|
||||
<h3 class="modal-title">Найти человека</h3>
|
||||
<h3 class="modal-title">Найти пользователя</h3>
|
||||
<div class="row" style="gap:8px;">
|
||||
<input class="input" id="network-search-input" type="text" maxlength="30" placeholder="Введите логин" />
|
||||
<button class="primary-btn" type="button" id="network-search-run">Искать</button>
|
||||
@@ -462,17 +438,32 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
title: 'Связи',
|
||||
rightActions: [
|
||||
{
|
||||
iconNode: createHeaderSearchIcon(),
|
||||
title: 'Найти пользователя',
|
||||
ariaLabel: 'Найти пользователя',
|
||||
className: 'chat-header-icon-btn',
|
||||
onClick: openSearchModal,
|
||||
iconNode: createOverflowDots(),
|
||||
title: 'Меню связей',
|
||||
ariaLabel: 'Открыть меню связей',
|
||||
className: 'chat-header-icon-btn network-header-menu-btn',
|
||||
onClick: () => {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const networkMenuButton = header.querySelector('.network-header-menu-btn');
|
||||
const searchIconHtml = `
|
||||
<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>
|
||||
`;
|
||||
const networkMenu = createDropdownMenu({
|
||||
anchorEl: networkMenuButton,
|
||||
minWidth: 220,
|
||||
items: [
|
||||
{ label: 'Найти пользователя', iconHtml: searchIconHtml, action: openSearchModal },
|
||||
],
|
||||
});
|
||||
|
||||
// Ресайз и перерисовку рёбер движок обрабатывает сам (window resize + ResizeObserver внутри).
|
||||
screen.cleanup = () => {
|
||||
networkMenu.destroy();
|
||||
if (engine) engine.destroy();
|
||||
engine = null;
|
||||
appScreenEl?.classList.remove('network-scroll-lock');
|
||||
|
||||
Reference in New Issue
Block a user