From 055685a4c4eff764b1b1e2f55b2ccdcecc9bcfa9 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 16 Jul 2026 15:52:12 -0500 Subject: [PATCH] feat(chat): copy, add-to-contacts, and new-message toast (Q2/Q3/Q4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Q3: copy the peer z-address from the thread header (SmallButton), and right-click any message to copy its body. - Q2: an "Add contact" action in the header when the peer isn't already known — one click saves them to the address book (rename later in Contacts). - Q4: capture ChatService::ingest's new-message count (previously discarded) and fire an in-app toast when new encrypted chat arrives while the user isn't on the Chat tab (main-thread MainCb sites only). i18n (EN + 8 languages, additive; no new CJK glyphs). Co-Authored-By: Claude Opus 4.8 (1M context) --- res/lang/de.json | 3 +++ res/lang/es.json | 3 +++ res/lang/fr.json | 3 +++ res/lang/ja.json | 3 +++ res/lang/ko.json | 3 +++ res/lang/pt.json | 3 +++ res/lang/ru.json | 3 +++ res/lang/zh.json | 3 +++ src/app_network.cpp | 8 ++++++-- src/ui/windows/chat_tab.cpp | 24 ++++++++++++++++++++++-- src/util/i18n.cpp | 3 +++ 11 files changed, 55 insertions(+), 4 deletions(-) diff --git a/res/lang/de.json b/res/lang/de.json index bb6e95e..c4f9707 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -135,12 +135,15 @@ "change_pass_title": "Passphrase ändern", "characters": "Zeichen", "chat": "Chat", + "chat_add_contact": "Kontakt hinzufügen", "chat_cancel": "Abbrechen", + "chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um", "chat_contact_request": "kontaktanfrage", "chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.", "chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.", "chat_new_button": "Neue Unterhaltung", "chat_new_message": "Nachricht", + "chat_new_message_toast": "Neue verschlüsselte Chat-Nachricht", "chat_new_send": "Anfrage senden", "chat_new_title": "Neue Unterhaltung", "chat_new_zaddr": "z-Adresse des Empfängers", diff --git a/res/lang/es.json b/res/lang/es.json index 8a8762b..c4d6961 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -135,12 +135,15 @@ "change_pass_title": "Cambiar frase de contraseña", "characters": "caracteres", "chat": "Chat", + "chat_add_contact": "Añadir contacto", "chat_cancel": "Cancelar", + "chat_contact_added": "Contacto añadido: renómbralo en Contactos", "chat_contact_request": "solicitud de contacto", "chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.", "chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.", "chat_new_button": "Nueva conversación", "chat_new_message": "Mensaje", + "chat_new_message_toast": "Nuevo mensaje de chat cifrado", "chat_new_send": "Enviar solicitud", "chat_new_title": "Nueva conversación", "chat_new_zaddr": "Dirección z del destinatario", diff --git a/res/lang/fr.json b/res/lang/fr.json index 3097e50..d40e727 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -135,12 +135,15 @@ "change_pass_title": "Changer la phrase secrète", "characters": "caractères", "chat": "Discussion", + "chat_add_contact": "Ajouter un contact", "chat_cancel": "Annuler", + "chat_contact_added": "Contact ajouté — renommez-le dans Contacts", "chat_contact_request": "demande de contact", "chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.", "chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.", "chat_new_button": "Nouvelle conversation", "chat_new_message": "Message", + "chat_new_message_toast": "Nouveau message chiffré", "chat_new_send": "Envoyer la demande", "chat_new_title": "Nouvelle conversation", "chat_new_zaddr": "Adresse Z du destinataire", diff --git a/res/lang/ja.json b/res/lang/ja.json index cd6f9ba..3a0fb56 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -135,12 +135,15 @@ "change_pass_title": "パスフレーズを変更", "characters": "文字", "chat": "チャット", + "chat_add_contact": "連絡先に追加", "chat_cancel": "キャンセル", + "chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます", "chat_contact_request": "連絡リクエスト", "chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。", "chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。", "chat_new_button": "新しい会話", "chat_new_message": "メッセージ", + "chat_new_message_toast": "新しい暗号化チャットメッセージ", "chat_new_send": "リクエストを送信", "chat_new_title": "新しい会話", "chat_new_zaddr": "宛先Zアドレス", diff --git a/res/lang/ko.json b/res/lang/ko.json index fb3f94f..e209cc0 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -135,12 +135,15 @@ "change_pass_title": "암호 변경", "characters": "문자", "chat": "채팅", + "chat_add_contact": "연락처 추가", "chat_cancel": "취소", + "chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요", "chat_contact_request": "연락 요청", "chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.", "chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.", "chat_new_button": "새 대화", "chat_new_message": "메시지", + "chat_new_message_toast": "새 암호화 채팅 메시지", "chat_new_send": "요청 보내기", "chat_new_title": "새 대화", "chat_new_zaddr": "받는 사람 z-주소", diff --git a/res/lang/pt.json b/res/lang/pt.json index ed13eb3..afee38f 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -135,12 +135,15 @@ "change_pass_title": "Alterar senha", "characters": "caracteres", "chat": "Chat", + "chat_add_contact": "Adicionar contato", "chat_cancel": "Cancelar", + "chat_contact_added": "Contato adicionado — renomeie em Contatos", "chat_contact_request": "solicitação de contato", "chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.", "chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.", "chat_new_button": "Nova conversa", "chat_new_message": "Mensagem", + "chat_new_message_toast": "Nova mensagem de chat criptografada", "chat_new_send": "Enviar solicitação", "chat_new_title": "Nova conversa", "chat_new_zaddr": "Endereço-z do destinatário", diff --git a/res/lang/ru.json b/res/lang/ru.json index 62d0fc7..488643d 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -135,12 +135,15 @@ "change_pass_title": "Сменить пароль", "characters": "символов", "chat": "Чат", + "chat_add_contact": "Добавить контакт", "chat_cancel": "Отмена", + "chat_contact_added": "Контакт добавлен — переименуйте его в Контактах", "chat_contact_request": "запрос контакта", "chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.", "chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.", "chat_new_button": "Новая переписка", "chat_new_message": "Сообщение", + "chat_new_message_toast": "Новое зашифрованное сообщение", "chat_new_send": "Отправить запрос", "chat_new_title": "Новая переписка", "chat_new_zaddr": "Z-адрес получателя", diff --git a/res/lang/zh.json b/res/lang/zh.json index b28ec38..f384b34 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -135,12 +135,15 @@ "change_pass_title": "更改密码短语", "characters": "字符", "chat": "聊天", + "chat_add_contact": "添加联系人", "chat_cancel": "取消", + "chat_contact_added": "已添加联系人——可在联系人中重命名", "chat_contact_request": "联系人请求", "chat_empty_hint": "暂无对话。您收到的消息将显示在此处。", "chat_locked_hint": "解锁钱包以加载您的聊天记录。", "chat_new_button": "新建对话", "chat_new_message": "消息", + "chat_new_message_toast": "新的加密聊天消息", "chat_new_send": "发送请求", "chat_new_title": "新建对话", "chat_new_zaddr": "收款方 z 地址", diff --git a/src/app_network.cpp b/src/app_network.cpp index a0742c5..cc708a8 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1668,7 +1668,9 @@ void App::refreshTransactionData() !result.hushChatMetadata.empty()) { std::unordered_map chatTxTimes; for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp; - chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); + const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); + if (newMsgs > 0 && current_page_ != ui::NavPage::Chat) + ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab } NetworkRefreshService::applyTransactionRefreshResult( state_, cacheUpdate, std::move(result), std::time(nullptr)); @@ -1726,7 +1728,9 @@ void App::refreshRecentTransactionData() !result.hushChatMetadata.empty()) { std::unordered_map chatTxTimes; for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp; - chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); + const int newMsgs = chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr)); + if (newMsgs > 0 && current_page_ != ui::NavPage::Chat) + ui::Notifications::instance().info(TR("chat_new_message_toast")); // Q4: alert when not on the Chat tab } NetworkRefreshService::applyTransactionRefreshResult( state_, cacheUpdate, std::move(result), std::time(nullptr)); diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index db3cd11..91cbb00 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -16,6 +16,7 @@ #include "../material/draw_helpers.h" // TactileButton / LabeledInput / BeginOverlayDialog #include "../material/project_icons.h" // ICON_MD_* #include "../layout.h" // Layout::dpiScale() +#include "../notifications.h" // Notifications — add-to-contacts confirmation #include "imgui.h" #include // sodium_memzero — wipe typed plaintext on a wallet switch @@ -284,6 +285,16 @@ void RenderChatTab(App* app) ImGui::TextUnformatted(shorten(sel->peerZaddr, 20, 12).c_str()); ImGui::PopStyleColor(); ImGui::PopFont(); + // Header actions: copy the peer z-address (Q3), and add them to contacts when unknown (Q2). + if (ImGui::SmallButton(TR("copy_address"))) ImGui::SetClipboardText(sel->peerZaddr.c_str()); + if (book.findByAddress(sel->peerZaddr) < 0) { + ImGui::SameLine(); + if (ImGui::SmallButton(TR("chat_add_contact"))) { + data::AddressBookEntry e(sel->peerName, sel->peerZaddr); // label = shortened addr; rename in Contacts + if (book.addEntry(e)) Notifications::instance().success(TR("chat_contact_added")); + else Notifications::instance().error(TR("address_book_exists")); + } + } } ImGui::Separator(); @@ -291,7 +302,9 @@ void RenderChatTab(App* app) ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y - footerH), false); { const auto messages = store.conversation(s_selected_cid); - for (const auto& m : messages) { + for (std::size_t mi = 0; mi < messages.size(); ++mi) { + const auto& m = messages[mi]; + ImGui::PushID(static_cast(mi)); const bool outgoing = (m.direction == chat::ChatDirection::Outgoing); const bool request = (m.kind == chat::ChatMessageKind::ContactRequest); const bool failed = outgoing && m.delivery == chat::ChatDelivery::Failed; @@ -307,13 +320,20 @@ void RenderChatTab(App* app) ImGui::TextUnformatted(who.c_str()); ImGui::PopStyleColor(); ImGui::PopFont(); - // Body (wrapped). + // Body (wrapped). Right-click to copy (Q3). ImGui::PushFont(nameFont); ImGui::PushTextWrapPos(0.0f); ImGui::TextWrapped("%s", m.body.c_str()); ImGui::PopTextWrapPos(); ImGui::PopFont(); + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("##msgmenu"); + if (ImGui::BeginPopup("##msgmenu")) { + if (ImGui::MenuItem(TR("copy"))) ImGui::SetClipboardText(m.body.c_str()); + ImGui::EndPopup(); + } ImGui::Dummy(ImVec2(0.0f, 6.0f)); + ImGui::PopID(); } if (s_scroll_to_cid == s_selected_cid) { ImGui::SetScrollHereY(1.0f); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index d93cd01..50fafe5 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -226,6 +226,9 @@ void I18n::loadBuiltinEnglish() strings_["chat_new_message"] = "Message"; strings_["chat_new_send"] = "Send request"; strings_["chat_cancel"] = "Cancel"; + strings_["chat_add_contact"] = "Add contact"; + strings_["chat_contact_added"] = "Contact added — rename it in Contacts"; + strings_["chat_new_message_toast"] = "New encrypted chat message"; strings_["chat_toast_not_connected"] = "Not connected — chat message not sent."; strings_["chat_toast_no_zaddr"] = "No z-address available to send chat from."; strings_["chat_toast_lite_busy"] = "A send is already in progress, or no wallet is open.";