feat(contacts,chat): a "Message" action linking Contacts to Chat

- Contacts: a "Message" context-menu item for shielded (z) contacts hands the
  address to the Chat tab (new OpenChatNewConversation entry point) and switches
  there with the New-conversation dialog prefilled; disabled with a reason for
  transparent-only contacts. (Kept to the safe context-menu + picker path, not
  the hand-drawn 4-slot row action cluster, to avoid mis-wiring the destructive
  Delete slot.)
- Chat: the New-conversation picker no longer silently hides transparent
  contacts — they're shown disabled with a "needs a shielded address to chat"
  tooltip instead of vanishing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 09:52:21 -05:00
co-authored by Claude Opus 4.8
parent 408393a547
commit ae16c06d24
4 changed files with 66 additions and 5 deletions
+37 -5
View File
@@ -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;