From ff53c735ddf877336f5f4315f4c402dc683dce5c Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 13 Sep 2026 15:55:33 +0300 Subject: [PATCH] =?UTF-8?q?chat:=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=B4=D0=B8=D0=B0=D1=81=D0=BE=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20(An?= =?UTF-8?q?droid=20+=20desktop)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jni_bridge/android_jni_bridge.c | 58 +++++++++++++++++-- tools/chatgui/db/db_manager.cpp | 56 ++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 79d95566..d5e08ec0 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -337,6 +337,53 @@ static void bridge_get_node_name(sqlite3* db, uint64_t node_id, const char* chan sqlite3_finalize(st); } +/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */ +static void bridge_format_preview_body(const char* ct, const char* d, char* out, size_t out_sz) { + int is_text = (!ct || !ct[0] || strcmp(ct, "text/plain") == 0 || strcmp(ct, "voice_transcription") == 0); + if (is_text) { + size_t tl = strlen(d); + if (tl > 60) tl = 60; + if (tl >= out_sz) tl = out_sz - 1; + memcpy(out, d, tl); out[tl] = '\0'; + return; + } + if (strcmp(ct, "audio/opus") == 0) { + int dur_ms = 0; + if (d[0] == '*') { + const char* pipe = strchr(d, '|'); + size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1; + uint8_t dec[256]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec)); + if (declen > 0) { + char raw[256]; if (declen >= sizeof(raw)) declen = sizeof(raw) - 1; + memcpy(raw, dec, declen); raw[declen] = '\0'; + const char* dur = strstr(raw, ";dur="); + if (dur) dur_ms = atoi(dur + 5); + } + } + if (dur_ms > 0) { + int s = dur_ms / 1000; + snprintf(out, out_sz, "Voice message (%d:%02d)", s / 60, s % 60); + } else { + snprintf(out, out_sz, "Voice message"); + } + return; + } + if (strncmp(ct, "video/", 6) == 0) { snprintf(out, out_sz, "Video"); return; } + if (strncmp(ct, "image/", 6) == 0) { snprintf(out, out_sz, "Photo"); return; } + /* файл и прочие нетекстовые: декодируем base64-префикс в имя файла */ + if (d[0] == '*') { + const char* pipe = strchr(d, '|'); + size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1; + uint8_t dec[512]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec)); + if (declen > 0) { + if (declen >= out_sz) declen = out_sz - 1; + memcpy(out, dec, declen); out[declen] = '\0'; + return; + } + } + snprintf(out, out_sz, "File"); +} + char* utun_bridge_get_channels_json(void) { sqlite3* db = chat_core_get_db(); if (!chat_core_is_initialized() || !db) { @@ -360,7 +407,7 @@ char* utun_bridge_get_channels_json(void) { int64_t last_msg_at = sqlite3_column_int64(st, 2); if (!ch_id) continue; - char tbl_msg[80], tbl_peers[80], last_msg[128]; + char tbl_msg[80], tbl_peers[80], last_msg[160]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg)); peers_table_name(ch_id, tbl_peers, sizeof(tbl_peers)); last_msg[0] = '\0'; @@ -373,17 +420,20 @@ char* utun_bridge_get_channels_json(void) { int64_t author = sqlite3_column_int64(lm, 0); const uint8_t* dptr = sqlite3_column_blob(lm, 1); int dlen = sqlite3_column_bytes(lm, 1); - char txt[128] = ""; + char body[160] = ""; if (dptr && dlen > 0) { const char* dp = (const char*)dptr; + char ct[32] = "text/plain"; + const char* cts = strstr(dp, "\"ct\":\""); + if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } } const char* ds = strstr(dp, "\"d\":\""); - if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 120) tl = 120; memcpy(txt, ds, tl); txt[tl] = '\0'; } } + if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 511) tl = 511; char d[512]; memcpy(d, ds, tl); d[tl] = '\0'; bridge_format_preview_body(ct, d, body, sizeof(body)); } } } char author_name[128]; bridge_get_node_name(db, (uint64_t)author, ch_id, author_name, sizeof(author_name)); if (!author_name[0]) snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author); - snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, txt); + snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, body); } sqlite3_finalize(lm); } diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index d1b1d56e..338e1154 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -144,6 +144,54 @@ QList DbManager::getChannels(ChannelSortMode sort) const { return list; } +/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */ +static QString mediaPreviewBody(const QString& ct, const QByteArray& d) { + bool isText = ct.isEmpty() || ct == QStringLiteral("text/plain") || + ct == QStringLiteral("voice_transcription"); + if (isText) { + QString t = QString::fromUtf8(d); + if (t.size() > 60) { t = t.left(60); t += QStringLiteral("..."); } + return t; + } + if (ct == QStringLiteral("audio/opus")) { + int durMs = 0; + if (d.startsWith('*')) { + int pipe = d.indexOf('|'); + if (pipe > 1) { + QByteArray b64 = d.mid(1, pipe - 1); + b64.replace('-', '+'); + b64.replace('_', '/'); + QString raw = QString::fromUtf8(QByteArray::fromBase64(b64)); + int durPos = raw.indexOf(QStringLiteral(";dur=")); + if (durPos >= 0) { + int end = raw.indexOf(';', durPos + 5); + QString ds = (end > 0) ? raw.mid(durPos + 5, end - durPos - 5) + : raw.mid(durPos + 5); + durMs = ds.toInt(); + } + } + } + if (durMs > 0) { + int s = durMs / 1000; + return QStringLiteral("Voice message (%1:%2)") + .arg(s / 60).arg(s % 60, 2, 10, QLatin1Char('0')); + } + return QStringLiteral("Voice message"); + } + if (ct.startsWith(QStringLiteral("video/"))) return QStringLiteral("Video"); + if (ct.startsWith(QStringLiteral("image/"))) return QStringLiteral("Photo"); + /* файл и прочие нетекстовые: base64-префикс — имя файла */ + if (d.startsWith('*')) { + int pipe = d.indexOf('|'); + QByteArray b64 = d.mid(1, (pipe > 1) ? pipe - 1 : d.size() - 1); + b64.replace('-', '+'); + b64.replace('_', '/'); + QString fname = QString::fromUtf8(QByteArray::fromBase64(b64)); + if (!fname.isEmpty()) return fname; + } + return QStringLiteral("File"); +} + QString DbManager::getChannelLastMessage(const QString& channelId) const { QString sql = QStringLiteral( "SELECT node_id, data FROM \"%1\" ORDER BY id DESC LIMIT 1") @@ -154,12 +202,10 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const { if (sqlite3_step(stmt) == SQLITE_ROW) { quint64 author = (quint64)sqlite3_column_int64(stmt, 0); QByteArray jdata = colBlob(stmt, 1); - QString text = QString::fromUtf8(parseJsonField(jdata, "d")); - if (text.isEmpty()) text = QString::fromUtf8(parseJsonField(jdata, "ct")); + QString ct = QString::fromUtf8(parseJsonField(jdata, "ct")); + QByteArray d = parseJsonField(jdata, "d"); QString nick = getDisplayName(author); - QString shortText = text.left(60); - if (text.size() > 60) shortText += QStringLiteral("..."); - result = nick + QStringLiteral(": ") + shortText; + result = nick + QStringLiteral(": ") + mediaPreviewBody(ct, d); } sqlite3_finalize(stmt); return result;