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:
@@ -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,18 +2033,28 @@ 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);
|
||||
// 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) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user