diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 7b05e24..15ad532 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -96,6 +96,8 @@ int composeInputCallback(ImGuiInputTextCallbackData* data) { bool s_show_new_convo = false; char s_new_zaddr[128] = ""; char s_new_msg[256] = ""; +char s_pending_new_zaddr[128] = ""; // cross-tab request from Contacts' "Message" action +bool s_pending_new_set = false; char s_search[80] = ""; // conversation-list filter (Q8) bool s_show_hidden = false; // when on, the list also shows hidden conversations (with an Unhide action) bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay (revive vs block) @@ -766,6 +768,17 @@ void RenderChatTab(App* app) return; } + // Consume a cross-tab "Message this contact" request (from Contacts): open the New-conversation + // dialog prefilled with the z-address. Runs every frame the tab renders, so it fires right after the + // Contacts tab switches us here. + if (s_pending_new_set) { + s_pending_new_set = false; + s_show_new_convo = true; + std::memcpy(s_new_zaddr, s_pending_new_zaddr, sizeof(s_new_zaddr)); + s_new_msg[0] = '\0'; + sodium_memzero(s_pending_new_zaddr, sizeof(s_pending_new_zaddr)); + } + // Resolve the Chat-tab clock once per frame: the chat override wins, else the global format. if (auto* cst = app->settings()) { const int ctf = cst->getChatTimeFormat(); // 0=follow global, 1=24h, 2=12h @@ -2020,17 +2033,27 @@ void RenderChatTab(App* app) const std::string activeHash = app->activeWalletScopeId(); // per-wallet contact scope int shown = 0; for (const auto& e : book.entries()) { - if (e.address.empty() || e.address[0] != 'z') continue; // chat requires a z-address + if (e.address.empty()) continue; // Respect the same per-wallet scope the Contacts tab enforces — don't leak another // wallet's scoped contact into this wallet's picker (global + legacy fail open). const bool visible = e.isGlobal() || (e.scope.rfind("w:", 0) == 0 ? (!activeHash.empty() && e.scope == activeHash) : true); if (!visible) continue; - ++shown; const std::string item = e.label + " " + shorten(e.address, 16, 8); - if (ImGui::Selectable(item.c_str())) { - std::strncpy(s_new_zaddr, e.address.c_str(), sizeof(s_new_zaddr) - 1); - s_new_zaddr[sizeof(s_new_zaddr) - 1] = '\0'; + // Show transparent contacts too, but DISABLED with a reason — chat needs a shielded + // address, so silently hiding them left the user wondering where a contact went. + if (dragonx::util::isShieldedAddress(e.address)) { + ++shown; + if (ImGui::Selectable(item.c_str())) { + std::strncpy(s_new_zaddr, e.address.c_str(), sizeof(s_new_zaddr) - 1); + s_new_zaddr[sizeof(s_new_zaddr) - 1] = '\0'; + } + } else { + ImGui::BeginDisabled(); + ImGui::Selectable(item.c_str(), false); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + material::Tooltip("%s", TR("chat_contact_needs_z")); } } if (shown == 0) { @@ -2242,6 +2265,8 @@ void ResetChatTab() sodium_memzero(s_compose, sizeof(s_compose)); sodium_memzero(s_new_zaddr, sizeof(s_new_zaddr)); sodium_memzero(s_new_msg, sizeof(s_new_msg)); + sodium_memzero(s_pending_new_zaddr, sizeof(s_pending_new_zaddr)); + s_pending_new_set = false; sodium_memzero(s_rename_buf, sizeof(s_rename_buf)); s_selected_cid.clear(); s_scroll_to_cid.clear(); @@ -2268,6 +2293,13 @@ void ResetChatTab() s_msgmenu_index = -1; } +void OpenChatNewConversation(const std::string& zaddr) +{ + std::strncpy(s_pending_new_zaddr, zaddr.c_str(), sizeof(s_pending_new_zaddr) - 1); + s_pending_new_zaddr[sizeof(s_pending_new_zaddr) - 1] = '\0'; + s_pending_new_set = true; +} + void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards) { auto* st = app ? app->settings() : nullptr; diff --git a/src/ui/windows/chat_tab.h b/src/ui/windows/chat_tab.h index 71fe390..8f7b93f 100644 --- a/src/ui/windows/chat_tab.h +++ b/src/ui/windows/chat_tab.h @@ -4,6 +4,8 @@ #pragma once +#include + namespace dragonx { class App; @@ -44,5 +46,11 @@ void RenderChatSettingsControls(App* app, float contentWidth = 0.0f, bool drawCa */ void ResetChatTab(); +/** + * @brief Ask the Chat tab to open its New-conversation dialog prefilled with this z-address (consumed + * on the next RenderChatTab frame). Used by the Contacts tab's "Message" action. + */ +void OpenChatNewConversation(const std::string& zaddr); + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index c018aef..8db2883 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -18,6 +18,7 @@ #include "../../util/platform.h" #include "image_picker.h" #include "transactions_tab.h" +#include "chat_tab.h" // OpenChatNewConversation — Contacts "Message" action #include "../sidebar.h" #include "../layout.h" #include "imgui.h" @@ -537,6 +538,15 @@ void RenderContactsTab(App* app) SetTransactionsAddressFilter(book.entries()[s_selected_index].address); app->setCurrentPage(ui::NavPage::History); }; + // Message a contact in Chat — only z-contacts (chat needs a shielded address to carry encrypted + // memos). Mirrors doViewTransactions: hand the address to the Chat tab and switch there. + auto doMessage = [&]() { + if (!selValid()) return; + const auto& e = book.entries()[s_selected_index]; + if (!isShieldedAddr(e.address)) return; + OpenChatNewConversation(e.address); + app->setCurrentPage(ui::NavPage::Chat); + }; // Add/edit form — a modal popup layered over the tab, with a live contact preview and an avatar // picker (default Z/T badge, a Material icon, or a custom image via the in-app image picker). @@ -1465,6 +1475,14 @@ void RenderContactsTab(App* app) if (ImGui::BeginPopup("##contactCtx")) { if (ImGui::MenuItem(TR("copy_address"))) doCopy(); if (ImGui::MenuItem(TR("view_transactions"))) doViewTransactions(); +#if DRAGONX_ENABLE_CHAT + { + const bool canMsg = selValid() && isShieldedAddr(book.entries()[s_selected_index].address); + if (ImGui::MenuItem(TR("contacts_message"), nullptr, false, canMsg)) doMessage(); + if (!canMsg && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + material::Tooltip("%s", TR("contacts_message_needs_z")); + } +#endif if (ImGui::MenuItem(TR("edit"))) openEdit(); ImGui::Separator(); if (ImGui::MenuItem(TR("delete"))) doDelete(); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 2c9b4ac..fad6a28 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -300,6 +300,9 @@ void I18n::loadBuiltinEnglish() strings_["chat_unblocked_toast"] = "Conversation unblocked"; strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6"; strings_["chat_no_z_contacts"] = "No shielded-address contacts yet"; + strings_["chat_contact_needs_z"] = "Needs a shielded (z) address to chat"; + strings_["contacts_message"] = "Message"; + strings_["contacts_message_needs_z"] = "Chat needs a shielded (z) address — this contact only has a transparent one"; strings_["chat_copy_address_tip"] = "Click to copy address"; strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify"; strings_["chat_awaiting_key"] = "Waiting for reply";