diff --git a/VERSION.properties b/VERSION.properties index 3e890aa2..2aadb317 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.340 -server.version=1.2.312 +client.version=1.2.341 +server.version=1.2.313 diff --git a/docs/API/09_Operations_Index.md b/docs/API/09_Operations_Index.md index 2fad3d51..8eb08bfd 100644 --- a/docs/API/09_Operations_Index.md +++ b/docs/API/09_Operations_Index.md @@ -62,6 +62,7 @@ | `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока | | `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон | | `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки | +| `GetDirectMessages` | `12_Direct_Messages_Push_Calls_API.md` | постраничная загрузка истории личного диалога | | `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию | | `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку | | `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию | diff --git a/docs/Personal_Messages/Протокол_DM_v1.md b/docs/Personal_Messages/Протокол_DM_v1.md index 5effcafd..84328880 100644 --- a/docs/Personal_Messages/Протокол_DM_v1.md +++ b/docs/Personal_Messages/Протокол_DM_v1.md @@ -509,6 +509,35 @@ UI-следствие для клиента: - после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории; - отдельное UI-действие `Удалить чат` может дополнительно спросить, нужно ли вместе с удалением контакта также отправить `DeleteConversation`. +### 10.5. `GetDirectMessages` + +Назначение: + +- постранично отдавать историю одного личного диалога; +- не вываливать весь старый backlog автоматически в момент логина; +- отдавать клиенту только полезные chat-сообщения, без технических receipt/tombstone блоков. + +Правила: + +- начиная с 22 июля 2026 года старая история DM больше не должна автоматически пушиться клиенту сразу после входа; +- клиент сам запрашивает первую страницу истории у нужного собеседника; +- последующие страницы клиент запрашивает по курсору `beforeTimeMs` + `beforeMessageKey`; +- realtime-новые сообщения по-прежнему приходят отдельно через `SignedMessageArrived`. + +Содержимое страницы: + +- сервер возвращает только контентные DM типов `1/2`; +- read-receipt (`3/4`) и tombstone удаления (`5/6/7/8`) в историю страницы не включаются; +- сообщения идут страницами от новых к старым; +- лимит страницы считается по самим chat-сообщениям диалога. + +Поле прочтения: + +- для каждого контентного сообщения сервер может вернуть `readAtMs`; +- `readAtMs` означает точное время прочтения сообщения, если оно уже известно серверу; +- если точное время для старого сообщения неизвестно, но по более новым данным видно, что сообщение уже точно прочитано, UI может показывать его как прочитанное без точного времени; +- read-receipt при этом остаётся отдельным DM-событием синхронизации, но в обычную историю страницы не подмешивается. + ## 11. Межсерверная доставка ### 11.1. Клиентская сторона @@ -547,9 +576,12 @@ UI-следствие для клиента: В ней должны сохраняться: - обычные контентные DM; +- read-receipt DM; - tombstone одного сообщения; - tombstone удаления переписки. +Для контентных сообщений в БД дополнительно должно поддерживаться серверное поле `read_at_ms`, если для этого сообщения уже было принято read-receipt событие. + Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы: - защищать от повторного приёма старых версий; diff --git a/shine-UI/js/pages/channel-thread-view.js b/shine-UI/js/pages/channel-thread-view.js index 61cce350..02565b51 100644 --- a/shine-UI/js/pages/channel-thread-view.js +++ b/shine-UI/js/pages/channel-thread-view.js @@ -313,6 +313,16 @@ function latestVersionText(versions) { return ''; } +function bindSubmitOnPlainEnter(textarea, submit) { + if (!(textarea instanceof HTMLTextAreaElement) || typeof submit !== 'function') return; + textarea.addEventListener('keydown', (event) => { + if (event.key !== 'Enter') return; + if (event.shiftKey || event.ctrlKey) return; + event.preventDefault(); + submit(); + }); +} + function resolveNodeText(node) { return firstNonEmptyText( node?.text, @@ -388,6 +398,8 @@ function openReplyModal({ onSubmit, navigate }) { } }); + bindSubmitOnPlainEnter(textEl, () => root.querySelector('#thread-reply-submit')?.click()); + if (textEl) textEl.focus(); } @@ -477,6 +489,8 @@ function openRepostModal({ navigate, channels = [], onSubmit }) { } }); + bindSubmitOnPlainEnter(textEl, () => submitEl?.click()); + if (textEl) textEl.focus(); } @@ -562,6 +576,7 @@ function openEditMessageModal({ initialText = '', onSave, onDelete }) { errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.'); } }); + bindSubmitOnPlainEnter(textEl, () => root.querySelector('#thread-edit-save')?.click()); if (textEl) textEl.focus(); } diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 9780c031..1743c137 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -297,6 +297,16 @@ function openAboutChannelModal(channel) { }); } +function bindSubmitOnPlainEnter(textarea, submit) { + if (!(textarea instanceof HTMLTextAreaElement) || typeof submit !== 'function') return; + textarea.addEventListener('keydown', (event) => { + if (event.key !== 'Enter') return; + if (event.shiftKey || event.ctrlKey) return; + event.preventDefault(); + submit(); + }); +} + function openReplyModal({ onSubmit, navigate }) { const root = document.getElementById('modal-root'); root.innerHTML = ` @@ -363,6 +373,8 @@ function openReplyModal({ onSubmit, navigate }) { } }); + bindSubmitOnPlainEnter(textEl, () => submitEl?.click()); + if (textEl) textEl.focus(); } @@ -448,6 +460,7 @@ function openRepostModal({ navigate, channels = [], onSubmit }) { errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.'); } }); + bindSubmitOnPlainEnter(textEl, () => submitEl?.click()); if (textEl) textEl.focus(); } @@ -506,6 +519,8 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) { } }); + bindSubmitOnPlainEnter(textEl, () => submitEl?.click()); + if (textEl) textEl.focus(); } @@ -592,6 +607,7 @@ function openEditMessageModal({ initialText = '', onSave, onDelete }) { errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.'); } }); + bindSubmitOnPlainEnter(textEl, () => root.querySelector('#edit-message-save')?.click()); if (textEl) textEl.focus(); } diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index ce2e08b7..3b4c718f 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -1392,22 +1392,10 @@ export function render({ navigate, route }) { if (!emojiPickerOpen || form.contains(event.target)) return; closeEmojiPicker(); }); - // Enter — отправить; Ctrl+Enter — перенос строки. + // Enter — отправить; Shift+Enter и Ctrl+Enter — перенос строки. input?.addEventListener('keydown', async (event) => { if (event.key !== 'Enter') return; - if (event.ctrlKey) { - event.preventDefault(); - const start = Number(input.selectionStart ?? input.value.length); - const end = Number(input.selectionEnd ?? input.value.length); - const value = String(input.value || ''); - input.value = `${value.slice(0, start)}\n${value.slice(end)}`; - const nextPos = start + 1; - try { - input.setSelectionRange(nextPos, nextPos); - } catch { - // ignore - } - autoResizeComposer(input); + if (event.shiftKey || event.ctrlKey) { return; } event.preventDefault(); diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index 04941eb2..10826498 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -489,7 +489,7 @@ export function render({ navigate }) { void renderSupportBuyIntro(); }); actions.querySelector('#support-queue')?.addEventListener('click', () => { - void renderSupportQueue(); + navigate('quene'); }); actions.querySelector('#support-keygen')?.addEventListener('click', () => { void renderSupportKeygen(); diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index a1b675fb..d07f52ad 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -39,6 +39,7 @@ const PRETTY_PATHS = new Map([ ['solana-users-init-view', 'settings/solana-users-init'], ['solana-rpc-check-view', 'settings/solana-rpc-check'], ['wallet-view', 'wallet'], + ['queue', 'quene'], ['device-view', 'devices'], ['device-session-view', 'devices'], ['connect-device-view', 'devices/connect'], @@ -274,6 +275,10 @@ export function getRoute() { return { pageId: 'wallet-view', params: {} }; } + if (pageId === 'quene' || pageId === 'queue') { + return { pageId: 'queue', params: {} }; + } + if (pageId === 'devices') { const sub = decodePart(segments[1] || '').toLowerCase(); if (sub === 'connect') return { pageId: 'connect-device-view', params: {} }; @@ -332,10 +337,6 @@ export function getRoute() { return { pageId, params: { mode: segments[1] ? decodePart(segments[1]) : '' } }; } - if (pageId === 'queue') { - return { pageId, params: {} }; - } - return { pageId, params: {} }; }