наверное работает

This commit is contained in:
AidarKC 2026-04-21 01:04:05 +03:00
parent 2d48ae7a16
commit 185ba5b1d3
12 changed files with 186 additions and 34 deletions

View File

@ -16,6 +16,7 @@ import {
state, state,
terminateCurrentSession, terminateCurrentSession,
addSignedMessageToChat, addSignedMessageToChat,
markIncomingReadByBaseKey,
markOutgoingReadByBaseKey, markOutgoingReadByBaseKey,
setContacts, setContacts,
} from './state.js'; } from './state.js';
@ -398,16 +399,20 @@ async function init() {
} catch {} } catch {}
} }
} else if (messageType === 3 || messageType === 4) { } else if (messageType === 3 || messageType === 4) {
const refBaseKey = String(payload.receiptRefBaseKey || '').trim(); let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
if (refBaseKey) { if (!refBaseKey) {
markOutgoingReadByBaseKey(refBaseKey);
} else {
try { try {
const ref = authService.parseReadReceiptPayload(parsed.payloadBytes); const ref = authService.parseReadReceiptPayload(parsed.payloadBytes);
const fallbackRefBase = `${ref.refFromLogin}|${ref.refToLogin}|${ref.refTimeMs}|${ref.refNonce}`; refBaseKey = `${ref.refFromLogin}|${ref.refToLogin}|${ref.refTimeMs}|${ref.refNonce}`;
markOutgoingReadByBaseKey(fallbackRefBase);
} catch {} } catch {}
} }
if (refBaseKey) {
if (messageType === 3) {
markOutgoingReadByBaseKey(refBaseKey);
} else {
markIncomingReadByBaseKey(refBaseKey);
}
}
addAppLogEntry({ addAppLogEntry({
level: 'info', level: 'info',
source: 'signed-dm', source: 'signed-dm',

View File

@ -1,4 +1,4 @@
export function renderHeader({ title, leftAction, rightActions = [] }) { export function renderHeader({ title, leftAction, leftLabel = '', rightActions = [] }) {
const wrap = document.createElement('header'); const wrap = document.createElement('header');
wrap.className = 'page-header'; wrap.className = 'page-header';
@ -11,6 +11,12 @@ export function renderHeader({ title, leftAction, rightActions = [] }) {
btn.addEventListener('click', leftAction.onClick); btn.addEventListener('click', leftAction.onClick);
left.append(btn); left.append(btn);
} }
if (leftLabel) {
const label = document.createElement('span');
label.className = 'header-left-label';
label.textContent = leftLabel;
left.append(label);
}
const h1 = document.createElement('h1'); const h1 = document.createElement('h1');
h1.className = 'page-title'; h1.className = 'page-title';

View File

@ -508,17 +508,13 @@ function toListModel(groups) {
function renderEmptyState(activeTab, navigate) { function renderEmptyState(activeTab, navigate) {
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = 'channels-empty-state'; wrap.className = 'channels-empty-state channels-empty-state--compact';
const icon = document.createElement('div');
icon.className = 'channels-empty-icon';
icon.textContent = '◌';
const text = document.createElement('div'); const text = document.createElement('div');
text.className = 'meta-muted'; text.className = 'meta-muted';
text.textContent = 'В этом разделе пока нет каналов'; text.textContent = 'В этом разделе нет сообщений';
wrap.append(icon, text); wrap.append(text);
if (activeTab === 'my') { if (activeTab === 'my') {
const cta = document.createElement('button'); const cta = document.createElement('button');

View File

@ -7,6 +7,7 @@ import {
getChatMessages, getChatMessages,
markChatRead, markChatRead,
markOutgoingSent, markOutgoingSent,
markReadReceiptSentByBaseKey,
authService, authService,
state, state,
} from '../state.js'; } from '../state.js';
@ -33,10 +34,10 @@ function renderLog(list, chatId) {
messages.forEach((msg) => { messages.forEach((msg) => {
if (!unreadSeparatorInserted && msg?.from === 'in' && msg?.unread) { if (!unreadSeparatorInserted && msg?.from === 'in' && msg?.unread) {
const sep = document.createElement('div'); const sep = document.createElement('div');
sep.className = 'meta-muted'; sep.className = 'chat-unread-separator';
sep.style.textAlign = 'center'; const label = document.createElement('span');
sep.style.margin = '8px 0'; label.textContent = 'Новые сообщения';
sep.textContent = 'Новые сообщения'; sep.append(label);
list.append(sep); list.append(sep);
unreadSeparatorInserted = true; unreadSeparatorInserted = true;
} }
@ -216,7 +217,11 @@ async function sendReadReceiptsForVisible(chatId) {
refNonce: ref.nonce, refNonce: ref.nonce,
refType: 1, refType: 1,
}); });
row.readReceiptSent = true; if (row.baseKey) {
markReadReceiptSentByBaseKey(row.baseKey);
} else {
row.readReceiptSent = true;
}
} catch (e) { } catch (e) {
addAppLogEntry({ addAppLogEntry({
level: 'warn', level: 'warn',

View File

@ -12,6 +12,7 @@ export function render({ navigate }) {
screen.append( screen.append(
renderHeader({ renderHeader({
title: 'Личные сообщения', title: 'Личные сообщения',
leftLabel: String(state.session.login || '').trim(),
rightActions: [{ label: '+', onClick: () => navigate('contact-search-view') }], rightActions: [{ label: '+', onClick: () => navigate('contact-search-view') }],
}), }),
); );

View File

@ -348,6 +348,37 @@ export function markOutgoingReadByBaseKey(baseKey) {
}); });
} }
export function markIncomingReadByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'in') return;
if (row.baseKey === baseKey) {
row.unread = false;
row.readReceiptSent = true;
persistMessageRecord(chatId, row);
}
});
});
}
export function markReadReceiptSentByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'in') return;
if (row.baseKey === baseKey) {
row.readReceiptSent = true;
persistMessageRecord(chatId, row);
}
});
});
}
export function addSignedMessageToChat({ export function addSignedMessageToChat({
chatId, chatId,
messageKey, messageKey,

View File

@ -24,6 +24,16 @@
justify-content: flex-start; justify-content: flex-start;
} }
.header-left-label {
display: inline-block;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: #a8bcdf;
}
.header-actions { .header-actions {
justify-content: flex-end; justify-content: flex-end;
} }
@ -669,6 +679,27 @@
align-content: start; align-content: start;
} }
.chat-unread-separator {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
color: #9ab0de;
font-size: 12px;
}
.chat-unread-separator::before,
.chat-unread-separator::after {
content: '';
flex: 1 1 auto;
height: 1px;
background: linear-gradient(90deg, rgba(126, 150, 199, 0.12), rgba(126, 150, 199, 0.45), rgba(126, 150, 199, 0.12));
}
.chat-unread-separator > span {
white-space: nowrap;
}
.bubble { .bubble {
max-width: 76%; max-width: 76%;
padding: 10px 12px; padding: 10px 12px;
@ -992,15 +1023,15 @@ textarea.input {
.node.is-shine .node-dot::before { .node.is-shine .node-dot::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: -7px; inset: -14px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle, rgba(130, 235, 255, 0.45) 0%, rgba(130, 235, 255, 0.18) 45%, rgba(130, 235, 255, 0) 72%); background: radial-gradient(circle, rgba(130, 235, 255, 0.62) 0%, rgba(130, 235, 255, 0.28) 42%, rgba(130, 235, 255, 0) 76%);
filter: blur(1px); filter: blur(2px);
z-index: -1; z-index: -1;
} }
.node.is-shine .node-dot { .node.is-shine .node-dot {
box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.38), 0 0 16px rgba(102, 220, 255, 0.38), 0 8px 16px rgba(4, 8, 15, 0.35); box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.5), 0 0 28px rgba(102, 220, 255, 0.58), 0 8px 16px rgba(4, 8, 15, 0.35);
} }
.node.is-friend .node-dot { .node.is-friend .node-dot {
@ -1375,6 +1406,7 @@ textarea.input {
.channels-groups { .channels-groups {
gap: 11px; gap: 11px;
justify-items: start;
} }
.channels-section { .channels-section {
@ -1407,6 +1439,7 @@ textarea.input {
grid-template-columns: 46px minmax(0, 1fr) 72px; grid-template-columns: 46px minmax(0, 1fr) 72px;
gap: 12px; gap: 12px;
padding: 14px 13px; padding: 14px 13px;
width: min(100%, 340px);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(193, 157, 82, 0.28); border: 1px solid rgba(193, 157, 82, 0.28);
background: background:
@ -1865,6 +1898,14 @@ textarea.input {
background: rgba(10, 18, 34, 0.52); background: rgba(10, 18, 34, 0.52);
} }
.channels-empty-state--compact {
border: 0;
background: transparent;
border-radius: 0;
padding: 4px 2px;
gap: 6px;
}
.channels-empty-icon { .channels-empty-icon {
font-size: 20px; font-size: 20px;
color: #d9b56d; color: #d9b56d;
@ -2076,7 +2117,7 @@ textarea.input {
.channels-list-content { .channels-list-content {
display: grid; display: grid;
gap: 10px; gap: 10px;
min-height: 42vh; min-height: 0;
} }
.channels-list-body-fade { .channels-list-body-fade {

View File

@ -528,6 +528,18 @@ public final class DatabaseInitializer {
ON signed_messages_v2 (base_key, message_type); ON signed_messages_v2 (base_key, message_type);
"""); """);
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
ON signed_messages_v2 (target_login, receipt_ref_base_key)
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
""");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
ON signed_messages_v2 (target_login, receipt_ref_base_key)
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
""");
// 14) signed_message_session_delivery (доставка по сессиям) // 14) signed_message_session_delivery (доставка по сессиям)
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS signed_message_session_delivery ( CREATE TABLE IF NOT EXISTS signed_message_session_delivery (

View File

@ -90,6 +90,7 @@ public final class SqliteDbController {
ensureConnectionsIndexes(st); ensureConnectionsIndexes(st);
ensureReactionsIndexes(st); ensureReactionsIndexes(st);
ensureChannelNamesIndexes(st); ensureChannelNamesIndexes(st);
ensureSignedMessageReceiptUniq(c, st);
DatabaseTriggersInstaller.createAllTriggers(st); DatabaseTriggersInstaller.createAllTriggers(st);
@ -223,6 +224,53 @@ public final class SqliteDbController {
"""); """);
} }
private static void ensureSignedMessageReceiptUniq(Connection c, Statement st) throws SQLException {
if (!tableExists(c, "signed_messages_v2")) return;
if (tableExists(c, "signed_message_session_delivery")) {
st.executeUpdate("""
DELETE FROM signed_message_session_delivery
WHERE message_key IN (
SELECT message_key
FROM signed_messages_v2
WHERE message_type IN (3, 4)
AND receipt_ref_base_key IS NOT NULL
AND rowid NOT IN (
SELECT MIN(rowid)
FROM signed_messages_v2
WHERE message_type IN (3, 4)
AND receipt_ref_base_key IS NOT NULL
GROUP BY target_login, message_type, receipt_ref_base_key
)
);
""");
}
st.executeUpdate("""
DELETE FROM signed_messages_v2
WHERE message_type IN (3, 4)
AND receipt_ref_base_key IS NOT NULL
AND rowid NOT IN (
SELECT MIN(rowid)
FROM signed_messages_v2
WHERE message_type IN (3, 4)
AND receipt_ref_base_key IS NOT NULL
GROUP BY target_login, message_type, receipt_ref_base_key
);
""");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
ON signed_messages_v2 (target_login, receipt_ref_base_key)
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
""");
st.executeUpdate("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
ON signed_messages_v2 (target_login, receipt_ref_base_key)
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
""");
}
private static void rebuildConnectionsStateTable(Statement st) throws SQLException { private static void rebuildConnectionsStateTable(Statement st) throws SQLException {
st.executeUpdate("DROP TABLE IF EXISTS connections_state_v2"); st.executeUpdate("DROP TABLE IF EXISTS connections_state_v2");
st.executeUpdate(""" st.executeUpdate("""

View File

@ -43,8 +43,11 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения"); return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
} }
SignedMessagesCore.saveIfAbsent(entry); boolean inserted = SignedMessagesCore.saveIfAbsent(entry);
SignedMessagesRealtime.DeliveryCounters counters = SignedMessagesRealtime.deliverToTargetSessions(entry, null); SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
if (inserted) {
counters = SignedMessagesRealtime.deliverToTargetSessions(entry, null);
}
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response(); Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());

View File

@ -53,15 +53,19 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения"); return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
} }
SignedMessagesCore.saveIfAbsent(incomingEntry); boolean incomingInserted = SignedMessagesCore.saveIfAbsent(incomingEntry);
SignedMessagesCore.saveIfAbsent(outgoingEntry); boolean outgoingInserted = SignedMessagesCore.saveIfAbsent(outgoingEntry);
SignedMessagesRealtime.DeliveryCounters inCounters = SignedMessagesRealtime.DeliveryCounters inCounters = new SignedMessagesRealtime.DeliveryCounters();
SignedMessagesRealtime.deliverToTargetSessions(incomingEntry, null); if (incomingInserted) {
inCounters = SignedMessagesRealtime.deliverToTargetSessions(incomingEntry, null);
}
String excludeSessionId = outgoingEntry.getTargetLogin().equalsIgnoreCase(ctx.getLogin()) ? ctx.getSessionId() : null; String excludeSessionId = outgoingEntry.getTargetLogin().equalsIgnoreCase(ctx.getLogin()) ? ctx.getSessionId() : null;
SignedMessagesRealtime.DeliveryCounters outCounters = SignedMessagesRealtime.DeliveryCounters outCounters = new SignedMessagesRealtime.DeliveryCounters();
SignedMessagesRealtime.deliverToTargetSessions(outgoingEntry, excludeSessionId); if (outgoingInserted) {
outCounters = SignedMessagesRealtime.deliverToTargetSessions(outgoingEntry, excludeSessionId);
}
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response(); Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());

View File

@ -83,7 +83,7 @@ final class SignedMessagesCore {
return entry; return entry;
} }
static void saveIfAbsent(SignedMessageV2Entry entry) throws Exception { static boolean saveIfAbsent(SignedMessageV2Entry entry) throws Exception {
SignedMessagesV2DAO.getInstance().insertIfAbsent(entry); return SignedMessagesV2DAO.getInstance().insertIfAbsent(entry);
} }
} }