feat(chat): copy, add-to-contacts, and new-message toast (Q2/Q3/Q4)

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:52:12 -05:00
parent 46f93e380b
commit 055685a4c4
11 changed files with 55 additions and 4 deletions

View File

@@ -1668,7 +1668,9 @@ void App::refreshTransactionData()
!result.hushChatMetadata.empty()) {
std::unordered_map<std::string, std::int64_t> 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<std::string, std::int64_t> 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));

View File

@@ -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.h> // 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<int>(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);

View File

@@ -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.";