SHA256
Finalize DM docs and queue routing
This commit is contained in:
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.340
|
client.version=1.2.341
|
||||||
server.version=1.2.312
|
server.version=1.2.313
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||||
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
||||||
| `DeleteConversation` | `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` | подтверждение доставки в сессию |
|
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
|
||||||
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
||||||
| `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию |
|
| `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию |
|
||||||
|
|||||||
@@ -509,6 +509,35 @@ UI-следствие для клиента:
|
|||||||
- после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории;
|
- после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории;
|
||||||
- отдельное UI-действие `Удалить чат` может дополнительно спросить, нужно ли вместе с удалением контакта также отправить `DeleteConversation`.
|
- отдельное 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. Межсерверная доставка
|
||||||
|
|
||||||
### 11.1. Клиентская сторона
|
### 11.1. Клиентская сторона
|
||||||
@@ -547,9 +576,12 @@ UI-следствие для клиента:
|
|||||||
В ней должны сохраняться:
|
В ней должны сохраняться:
|
||||||
|
|
||||||
- обычные контентные DM;
|
- обычные контентные DM;
|
||||||
|
- read-receipt DM;
|
||||||
- tombstone одного сообщения;
|
- tombstone одного сообщения;
|
||||||
- tombstone удаления переписки.
|
- tombstone удаления переписки.
|
||||||
|
|
||||||
|
Для контентных сообщений в БД дополнительно должно поддерживаться серверное поле `read_at_ms`, если для этого сообщения уже было принято read-receipt событие.
|
||||||
|
|
||||||
Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы:
|
Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы:
|
||||||
|
|
||||||
- защищать от повторного приёма старых версий;
|
- защищать от повторного приёма старых версий;
|
||||||
|
|||||||
@@ -313,6 +313,16 @@ function latestVersionText(versions) {
|
|||||||
return '';
|
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) {
|
function resolveNodeText(node) {
|
||||||
return firstNonEmptyText(
|
return firstNonEmptyText(
|
||||||
node?.text,
|
node?.text,
|
||||||
@@ -388,6 +398,8 @@ function openReplyModal({ onSubmit, navigate }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => root.querySelector('#thread-reply-submit')?.click());
|
||||||
|
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,6 +489,8 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => submitEl?.click());
|
||||||
|
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +576,7 @@ function openEditMessageModal({ initialText = '', onSave, onDelete }) {
|
|||||||
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => root.querySelector('#thread-edit-save')?.click());
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }) {
|
function openReplyModal({ onSubmit, navigate }) {
|
||||||
const root = document.getElementById('modal-root');
|
const root = document.getElementById('modal-root');
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
@@ -363,6 +373,8 @@ function openReplyModal({ onSubmit, navigate }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => submitEl?.click());
|
||||||
|
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +460,7 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
|
|||||||
errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.');
|
errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => submitEl?.click());
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +519,8 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => submitEl?.click());
|
||||||
|
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +607,7 @@ function openEditMessageModal({ initialText = '', onSave, onDelete }) {
|
|||||||
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
bindSubmitOnPlainEnter(textEl, () => root.querySelector('#edit-message-save')?.click());
|
||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1392,22 +1392,10 @@ export function render({ navigate, route }) {
|
|||||||
if (!emojiPickerOpen || form.contains(event.target)) return;
|
if (!emojiPickerOpen || form.contains(event.target)) return;
|
||||||
closeEmojiPicker();
|
closeEmojiPicker();
|
||||||
});
|
});
|
||||||
// Enter — отправить; Ctrl+Enter — перенос строки.
|
// Enter — отправить; Shift+Enter и Ctrl+Enter — перенос строки.
|
||||||
input?.addEventListener('keydown', async (event) => {
|
input?.addEventListener('keydown', async (event) => {
|
||||||
if (event.key !== 'Enter') return;
|
if (event.key !== 'Enter') return;
|
||||||
if (event.ctrlKey) {
|
if (event.shiftKey || 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ export function render({ navigate }) {
|
|||||||
void renderSupportBuyIntro();
|
void renderSupportBuyIntro();
|
||||||
});
|
});
|
||||||
actions.querySelector('#support-queue')?.addEventListener('click', () => {
|
actions.querySelector('#support-queue')?.addEventListener('click', () => {
|
||||||
void renderSupportQueue();
|
navigate('quene');
|
||||||
});
|
});
|
||||||
actions.querySelector('#support-keygen')?.addEventListener('click', () => {
|
actions.querySelector('#support-keygen')?.addEventListener('click', () => {
|
||||||
void renderSupportKeygen();
|
void renderSupportKeygen();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const PRETTY_PATHS = new Map([
|
|||||||
['solana-users-init-view', 'settings/solana-users-init'],
|
['solana-users-init-view', 'settings/solana-users-init'],
|
||||||
['solana-rpc-check-view', 'settings/solana-rpc-check'],
|
['solana-rpc-check-view', 'settings/solana-rpc-check'],
|
||||||
['wallet-view', 'wallet'],
|
['wallet-view', 'wallet'],
|
||||||
|
['queue', 'quene'],
|
||||||
['device-view', 'devices'],
|
['device-view', 'devices'],
|
||||||
['device-session-view', 'devices'],
|
['device-session-view', 'devices'],
|
||||||
['connect-device-view', 'devices/connect'],
|
['connect-device-view', 'devices/connect'],
|
||||||
@@ -274,6 +275,10 @@ export function getRoute() {
|
|||||||
return { pageId: 'wallet-view', params: {} };
|
return { pageId: 'wallet-view', params: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageId === 'quene' || pageId === 'queue') {
|
||||||
|
return { pageId: 'queue', params: {} };
|
||||||
|
}
|
||||||
|
|
||||||
if (pageId === 'devices') {
|
if (pageId === 'devices') {
|
||||||
const sub = decodePart(segments[1] || '').toLowerCase();
|
const sub = decodePart(segments[1] || '').toLowerCase();
|
||||||
if (sub === 'connect') return { pageId: 'connect-device-view', params: {} };
|
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]) : '' } };
|
return { pageId, params: { mode: segments[1] ? decodePart(segments[1]) : '' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageId === 'queue') {
|
|
||||||
return { pageId, params: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { pageId, params: {} };
|
return { pageId, params: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user