Browse Source

chat: превью медиасообщений в списке каналов (Android + desktop)

v2
evgeny 3 weeks ago
parent
commit
ff53c735dd
  1. 58
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  2. 56
      tools/chatgui/db/db_manager.cpp

58
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);
}

56
tools/chatgui/db/db_manager.cpp

@ -144,6 +144,54 @@ QList<ChannelRow> 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;

Loading…
Cancel
Save