diff --git a/res/lang/de.json b/res/lang/de.json index a08a415..b1a7cc7 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -167,6 +167,7 @@ "chat_export_done": "Unterhaltung exportiert", "chat_export_failed": "Exportdatei konnte nicht geschrieben werden.", "chat_export_warn": "Speichert die entschlüsselten Nachrichten als Klartext. Bewahre die Datei sicher auf.", + "chat_filter": "Chat", "chat_hidden_toast": "Unterhaltung ausgeblendet – eine neue Nachricht holt sie zurück", "chat_hide": "Ausblenden", "chat_hide_hidden": "Ausgeblendete verbergen", @@ -1526,6 +1527,7 @@ "tt_website": "Die DragonX-Website öffnen", "tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)", "tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet", + "tx_chat_badge": "Nachricht", "tx_confirmations": "%d Bestätigungen", "tx_details_title": "Transaktionsdetails", "tx_from_address": "Von Adresse:", diff --git a/res/lang/es.json b/res/lang/es.json index 48ab26f..c86fa5e 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -167,6 +167,7 @@ "chat_export_done": "Conversación exportada", "chat_export_failed": "No se pudo escribir el archivo de exportación.", "chat_export_warn": "Guarda los mensajes descifrados como texto sin cifrar. Guarda el archivo de forma segura.", + "chat_filter": "Chat", "chat_hidden_toast": "Conversación oculta: un mensaje nuevo la recupera", "chat_hide": "Ocultar", "chat_hide_hidden": "Ocultar ocultos", @@ -1526,6 +1527,7 @@ "tt_website": "Abrir el sitio web de DragonX", "tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)", "tt_wizard": "Volver a ejecutar el asistente de configuración inicial\\nEl daemon será reiniciado", + "tx_chat_badge": "Mensaje", "tx_confirmations": "%d confirmaciones", "tx_details_title": "Detalles de Transacción", "tx_from_address": "Dirección Origen:", diff --git a/res/lang/fr.json b/res/lang/fr.json index 25ba5e2..d9fd349 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -167,6 +167,7 @@ "chat_export_done": "Conversation exportée", "chat_export_failed": "Impossible d'écrire le fichier d'exportation.", "chat_export_warn": "Enregistre les messages déchiffrés en texte clair. Conservez le fichier en lieu sûr.", + "chat_filter": "Chat", "chat_hidden_toast": "Conversation masquée — un nouveau message la fait réapparaître", "chat_hide": "Masquer", "chat_hide_hidden": "Masquer masqués", @@ -1526,6 +1527,7 @@ "tt_website": "Ouvrir le site web DragonX", "tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)", "tt_wizard": "Relancer l'assistant de configuration initiale\\nLe daemon sera redémarré", + "tx_chat_badge": "Message", "tx_confirmations": "%d confirmations", "tx_details_title": "Détails de la transaction", "tx_from_address": "Adresse d'origine :", diff --git a/res/lang/ja.json b/res/lang/ja.json index e13d9bc..569d7b1 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -167,6 +167,7 @@ "chat_export_done": "会話をエクスポートしました", "chat_export_failed": "エクスポートファイルを書き込めませんでした。", "chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。", + "chat_filter": "チャット", "chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます", "chat_hide": "非表示", "chat_hide_hidden": "非表示を隠す", @@ -1526,6 +1527,7 @@ "tt_website": "DragonX ウェブサイトを開く", "tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)", "tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます", + "tx_chat_badge": "メッセージ", "tx_confirmations": "%d 確認", "tx_details_title": "取引の詳細", "tx_from_address": "送信元アドレス:", diff --git a/res/lang/ko.json b/res/lang/ko.json index 5b2a585..6d61a47 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -167,6 +167,7 @@ "chat_export_done": "대화를 내보냈습니다", "chat_export_failed": "내보내기 파일을 쓸 수 없습니다.", "chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.", + "chat_filter": "채팅", "chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다", "chat_hide": "숨기기", "chat_hide_hidden": "숨김 접기", @@ -1526,6 +1527,7 @@ "tt_website": "DragonX 웹사이트 열기", "tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)", "tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다", + "tx_chat_badge": "메시지", "tx_confirmations": "%d 확인", "tx_details_title": "거래 상세", "tx_from_address": "보낸 주소:", diff --git a/res/lang/pt.json b/res/lang/pt.json index 8f42661..373306a 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -167,6 +167,7 @@ "chat_export_done": "Conversa exportada", "chat_export_failed": "Não foi possível gravar o arquivo de exportação.", "chat_export_warn": "Salva as mensagens descriptografadas como texto simples. Guarde o arquivo com segurança.", + "chat_filter": "Chat", "chat_hidden_toast": "Conversa ocultada — uma nova mensagem a traz de volta", "chat_hide": "Ocultar", "chat_hide_hidden": "Ocultar ocultas", @@ -1526,6 +1527,7 @@ "tt_website": "Abrir o site do DragonX", "tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)", "tt_wizard": "Executar novamente o assistente de configuração inicial\\nO daemon será reiniciado", + "tx_chat_badge": "Mensagem", "tx_confirmations": "%d confirmações", "tx_details_title": "Detalhes da Transação", "tx_from_address": "Endereço de Origem:", diff --git a/res/lang/ru.json b/res/lang/ru.json index b28ef24..b28de01 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -167,6 +167,7 @@ "chat_export_done": "Разговор экспортирован", "chat_export_failed": "Не удалось записать файл экспорта.", "chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.", + "chat_filter": "Чат", "chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его", "chat_hide": "Скрыть", "chat_hide_hidden": "Скрыть скрытые", @@ -1526,6 +1527,7 @@ "tt_website": "Открыть сайт DragonX", "tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)", "tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен", + "tx_chat_badge": "Сообщение", "tx_confirmations": "%d подтверждений", "tx_details_title": "Детали транзакции", "tx_from_address": "Адрес отправителя:", diff --git a/res/lang/zh.json b/res/lang/zh.json index fa16303..ccf1fc3 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -167,6 +167,7 @@ "chat_export_done": "会话已导出", "chat_export_failed": "无法写入导出文件。", "chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。", + "chat_filter": "聊天", "chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示", "chat_hide": "隐藏", "chat_hide_hidden": "收起已隐藏", @@ -1526,6 +1527,7 @@ "tt_website": "打开 DragonX 网站", "tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)", "tt_wizard": "重新运行初始设置向导\\n守护进程将被重启", + "tx_chat_badge": "消息", "tx_confirmations": "%d 次确认", "tx_details_title": "交易详情", "tx_from_address": "发送地址:", diff --git a/src/chat/chat_store.h b/src/chat/chat_store.h index 60559a0..3c923e8 100644 --- a/src/chat/chat_store.h +++ b/src/chat/chat_store.h @@ -28,6 +28,16 @@ public: // Distinct conversation ids, in first-seen order. std::vector conversationIds() const; + // The set of on-chain txids that carried a chat message (sent or received, messages + contact + // requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies. + std::unordered_set chatTxids() const { + std::unordered_set out; + out.reserve(messages_.size()); + for (const auto& m : messages_) + if (!m.txid.empty()) out.insert(m.txid); + return out; + } + std::size_t size() const { return messages_.size(); } bool empty() const { return messages_.empty(); } void clear(); diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index c70b83a..6f57f83 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include namespace dragonx { @@ -141,6 +142,12 @@ void RenderTransactionsTab(App* app) const auto addrLabel = S.label("tabs.transactions", "address-label"); const auto& state = app->state(); + // Txids that carried a chat message (sent or received) — drives the "Message" badge and the Chat + // filter below. Rebuilt each frame from the in-memory chat store (cheap: O(chat messages), which + // is small); empty when chat is disabled or the wallet has no chat identity. Both variants populate + // the chat store before this tab renders, so the same lookup serves full-node and lite. + const std::unordered_set chatTxids = app->chatService().store().chatTxids(); + // Responsive scale factors (recomputed every frame) ImVec2 contentAvail = ImGui::GetContentRegionAvail(); const float hs = Layout::hScale(contentAvail.x); @@ -250,8 +257,9 @@ void RenderTransactionsTab(App* app) innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper", minedCount, minedTotal, "+", 3, type_filter); - // Selected card accent - if (type_filter > 0) { + // Selected card accent (only the three summary cards map to a card position — the Chat filter + // (4) has no card, so guard the idx_map lookup to 1..3). + if (type_filter > 0 && type_filter <= 3) { int idx_map[] = {-1, 1, 0, 2}; int idx = idx_map[type_filter]; float xOff = idx * (cardW + cardGap); @@ -281,7 +289,8 @@ void RenderTransactionsTab(App* app) ImGui::SameLine(0, filterGap); float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs); ImGui::SetNextItemWidth(comboW); - const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") }; + const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), + TR("mined_filter"), TR("chat_filter") }; ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types)); // Sort selector @@ -476,6 +485,7 @@ void RenderTransactionsTab(App* app) if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue; if (type_filter == 2 && dtx.display_type != "receive") continue; if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue; + if (type_filter == 4 && chatTxids.count(dtx.txid) == 0) continue; // chat-only } if (!search_str.empty()) { if (!containsIgnoreCase(dtx.address, search_str) && @@ -657,6 +667,7 @@ void RenderTransactionsTab(App* app) // Determine type info bool shieldedDisplay = tx.display_type == "shield"; + const bool isChatTx = chatTxids.count(tx.txid) > 0; // carried a chat message ImU32 iconCol; const char* typeStr; if (shieldedDisplay) { @@ -749,12 +760,17 @@ void RenderTransactionsTab(App* app) } // Position status badge in the middle-right area ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr); - const char* shieldedStr = TR("shielded_type"); - ImVec2 shieldSz = shieldedDisplay - ? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr) + // Optional "top" badge stacked above the status pill: "Message" for a chat tx or + // "Shielded" for an autoshield. Chat txs are send/receive (not the "shield" type), + // so the two are mutually exclusive; chat takes precedence. + const bool showTopBadge = isChatTx || shieldedDisplay; + const char* topBadgeStr = isChatTx ? TR("tx_chat_badge") : TR("shielded_type"); + const ImU32 topBadgeCol = isChatTx ? Secondary() : Primary(); + ImVec2 topSz = showTopBadge + ? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, topBadgeStr) : ImVec2(0, 0); - float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f; - float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x; + float topPillW = topSz.x + Layout::spacingSm() * 2.0f; + float stackW = showTopBadge ? std::max(sSz.x, topPillW) : sSz.x; float statusX = amtX - stackW - Layout::spacingXxl(); float minStatusX = cx + innerW * 0.25f; // don't overlap address if (statusX < minStatusX) statusX = minStatusX; @@ -763,18 +779,17 @@ void RenderTransactionsTab(App* app) // row background on every skin (faint alpha-30 fills used to vanish on dark-red / // near-white / gradient skins). Fill 48, border 90 of the state color's RGB. const float pillRound = schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size; - if (shieldedDisplay) { - float shieldX = statusX + (stackW - shieldSz.x) * 0.5f; - ImU32 shieldCol = Primary(); - ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f); - ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(), - shieldPillMin.y + capFont->LegacySize + Layout::spacingXs()); - dl->AddRectFilled(shieldPillMin, shieldPillMax, - (shieldCol & 0x00FFFFFFu) | (static_cast(48) << 24), pillRound); - dl->AddRect(shieldPillMin, shieldPillMax, - (shieldCol & 0x00FFFFFFu) | (static_cast(90) << 24), pillRound, 0, 1.0f); + if (showTopBadge) { + float topX = statusX + (stackW - topSz.x) * 0.5f; + ImVec2 topPillMin(topX - Layout::spacingSm(), cy - 1.0f); + ImVec2 topPillMax(topX + topSz.x + Layout::spacingSm(), + topPillMin.y + capFont->LegacySize + Layout::spacingXs()); + dl->AddRectFilled(topPillMin, topPillMax, + (topBadgeCol & 0x00FFFFFFu) | (static_cast(48) << 24), pillRound); + dl->AddRect(topPillMin, topPillMax, + (topBadgeCol & 0x00FFFFFFu) | (static_cast(90) << 24), pillRound, 0, 1.0f); dl->AddText(capFont, capFont->LegacySize, - ImVec2(shieldX, cy), shieldCol, shieldedStr); + ImVec2(topX, cy), topBadgeCol, topBadgeStr); } // Background pill ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 0ac0c87..52de3cd 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1011,6 +1011,7 @@ void I18n::loadBuiltinEnglish() strings_["sent_filter"] = "Sent"; strings_["received_filter"] = "Received"; strings_["mined_filter"] = "Mined"; + strings_["chat_filter"] = "Chat"; strings_["sort_date_newest"] = "Newest first"; strings_["sort_date_oldest"] = "Oldest first"; strings_["sort_amount_high"] = "Largest amount"; @@ -1026,6 +1027,7 @@ void I18n::loadBuiltinEnglish() strings_["address_upper"] = "ADDRESS"; strings_["memo_upper"] = "MEMO"; strings_["shielded_type"] = "Shielded"; + strings_["tx_chat_badge"] = "Message"; strings_["recv_type"] = "Recv"; strings_["sent_type"] = "Sent"; strings_["immature_type"] = "Immature";