diff --git a/Dev_Docs/Pending_Features/2026-07-07_1730_dm_v1_e2ee_и_удаление.md b/Dev_Docs/Pending_Features/2026-07-07_1730_dm_v1_e2ee_и_удаление.md
deleted file mode 100644
index e581d8d..0000000
--- a/Dev_Docs/Pending_Features/2026-07-07_1730_dm_v1_e2ee_и_удаление.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# DM v1: E2EE, tombstone и fan-out
-
-- краткое описание фичи:
- Новый протокол личных сообщений `SHiNE_DM` v1: реальные зашифрованные тела `X25519 + HKDF-SHA256 + AES-256-GCM`, read-receipt в том же контейнере, удаление одного сообщения, удаление истории переписки и межсерверная доставка по `access_servers`.
-
-- что именно проверять:
- 1. Отправка нового DM между двумя пользователями и чтение текста на стороне получателя.
- 2. Отправка/чтение своей исходящей копии у отправителя.
- 3. Редактирование сообщения с тем же `baseKey` и ростом `revisionTimeMs`.
- 4. Подтверждение прочтения `type=3/4`.
- 5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
- 6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
- 7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
- 8. Первый DM пользователю, которого ещё нет в локальной `solana_users`, с lazy-import из Solana PDA.
- 9. Две версии одного сообщения с одинаковым `revisionTimeMs`, но разным `reencryptedAtMs`: должна победить версия с большим `reencryptedAtMs`.
- 10. После уже известного `DeleteConversation` отправка старого сообщения с другого сервера должна приводить к повторной рассылке tombstone удаления переписки.
- 11. Попытка вызвать legacy `SendDirectMessage` должна не работать, а новый UI должен использовать только `SendMessagePair`.
-
-- ожидаемый результат:
- Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, lazy-import из PDA позволяет отправить первый DM без ручного прогрева БД, версия с большим `reencryptedAtMs` корректно замещает старую при равном `revisionTimeMs`, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
-
-- статус:
- pending
diff --git a/Dev_Docs/Pending_Features/2026-07-09_1415_turn_только_один_живой_узел.md b/Dev_Docs/Pending_Features/2026-07-09_1415_turn_только_один_живой_узел.md
deleted file mode 100644
index e5eec33..0000000
--- a/Dev_Docs/Pending_Features/2026-07-09_1415_turn_только_один_живой_узел.md
+++ /dev/null
@@ -1,16 +0,0 @@
-Lf dc` hfm# TURN: только один живой узел для звонков
-
-- краткое описание:
- - в серверном конфиге оставлен только один TURN-узел `185.229.109.118:3478` для ICE-конфига звонков;
- - мёртвый узел `93.170.12.154:3478` убран из локальной конфигурации и test-деплоя `t2`.
-- что проверять:
- - открыть два клиента на `t2.shineup.me`;
- - начать аудиозвонок между ними;
- - убедиться, что `GetCallIceConfig` отдаёт только TURN `185.229.109.118`;
- - убедиться, что звонок выходит из стадии `connecting` и появляется `call_connected` либо как минимум `relay`-кандидаты в клиентской диагностике.
-- ожидаемый результат:
- - клиентам больше не предлагается `93.170.12.154`;
- - звонок проходит стабильнее, чем до изменения;
- - в серверных логах нет новых `incoming_failed/outgoing_failed` только из-за нерабочего TURN.
-- статус:
- - pending
diff --git a/Dev_Docs/Personal_Messages/Протокол_DM_v1.md b/Dev_Docs/Personal_Messages/Протокол_DM_v1.md
index e7c4a9f..f8e42c9 100644
--- a/Dev_Docs/Personal_Messages/Протокол_DM_v1.md
+++ b/Dev_Docs/Personal_Messages/Протокол_DM_v1.md
@@ -446,6 +446,11 @@ Request:
- `5`, если удаление инициировал отправитель;
- `6`, если удаление инициировал получатель.
+UI-следствие для клиента:
+
+- пользователь может удалить как своё исходящее сообщение, так и входящее;
+- для удаления входящего UI должен отправлять вариант, где удаление инициировал получатель (`type=6`).
+
### 10.4. `DeleteConversation`
Назначение:
@@ -469,6 +474,13 @@ Request:
- `7`, если удаление инициировал отправитель;
- `8`, если удаление инициировал получатель.
+UI-следствие для клиента:
+
+- действие `Очистить историю` должно отправлять служебное сообщение удаления переписки;
+- для обычной клиентской кнопки очистки истории допускается вариант удаления, инициированный текущим получателем (`type=8`);
+- после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории;
+- отдельное UI-действие `Удалить чат` может дополнительно спросить, нужно ли вместе с удалением контакта также отправить `DeleteConversation`.
+
## 11. Межсерверная доставка
### 11.1. Клиентская сторона
diff --git a/Dev_Docs/Personal_Messages/Формат_DM_v1.md b/Dev_Docs/Personal_Messages/Формат_DM_v1.md
index 9f36e40..26c2856 100644
--- a/Dev_Docs/Personal_Messages/Формат_DM_v1.md
+++ b/Dev_Docs/Personal_Messages/Формат_DM_v1.md
@@ -296,6 +296,12 @@ ReadReceiptBody_v1_0
- `1`, `2`, `5`, `7` подписывает отправитель сообщения;
- `6`, `8` подписывает получатель сообщения.
+Практическое UI-следствие:
+
+- клиентский UI может инициировать удаление входящего сообщения через `type=6`;
+- клиентский UI может инициировать очистку истории переписки через `type=8`;
+- сами контейнеры и байтовый формат при этом не отличаются от уже описанных типов `5/6/7/8`.
+
## 12. Какие поля должны совпадать у пары `1/2`
При обычной парной отправке через `SendMessagePair` у двух копий должны совпадать:
diff --git a/VERSION.properties b/VERSION.properties
index 9d3ea52..48b2dad 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.304
-server.version=1.2.283
+client.version=1.2.305
+server.version=1.2.284
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js
index 0612131..2b23d73 100644
--- a/shine-UI/js/app.js
+++ b/shine-UI/js/app.js
@@ -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',
diff --git a/shine-UI/js/components/header.js b/shine-UI/js/components/header.js
index 7a5b68e..ead1a75 100644
--- a/shine-UI/js/components/header.js
+++ b/shine-UI/js/components/header.js
@@ -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);
});
diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index 1dcd307..524ec3e 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -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 = `
+