feat(chat): Accept/Block card for unsolicited contact requests
An unsolicited first message from a stranger (an incoming ContactRequest, with nothing sent by us and the peer not in contacts) now shows an Accept / Block card above the thread instead of looking like a normal conversation. Accept adds the peer to the address book (which clears the request state); Block deletes and blocks the conversation (reversible from the Blocked manager). ConvSummary gains a derived pendingRequest flag. (The list-row "Request" pill and a header Block button were left out to avoid the fragile hand-drawn header-toolbar geometry.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -537,6 +537,7 @@ struct ConvSummary {
|
||||
std::int64_t lastTs = 0;
|
||||
int count = 0;
|
||||
bool hidden = false; // shown only while "Show hidden" is on
|
||||
bool pendingRequest = false; // unsolicited incoming contact request from a stranger: awaiting Accept/Block
|
||||
};
|
||||
|
||||
// Per-frame memoization of the conversation-list build and the open-thread message list (both otherwise
|
||||
@@ -808,9 +809,11 @@ void RenderChatTab(App* app)
|
||||
c.cid = cid;
|
||||
c.hidden = hidden;
|
||||
c.count = static_cast<int>(messages.size());
|
||||
bool sentAny = false;
|
||||
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
||||
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
||||
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
||||
if (m.direction == chat::ChatDirection::Outgoing) sentAny = true;
|
||||
}
|
||||
const auto& last = messages.back();
|
||||
c.lastBody = last.body;
|
||||
@@ -818,6 +821,12 @@ void RenderChatTab(App* app)
|
||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||
// Unsolicited: first message is an incoming contact request, we've never replied, and the peer
|
||||
// isn't a saved contact → show an Accept/Block card instead of treating it as a normal chat.
|
||||
const auto& first = messages.front();
|
||||
c.pendingRequest = (idx < 0) && !sentAny && !c.peerZaddr.empty() &&
|
||||
first.direction == chat::ChatDirection::Incoming &&
|
||||
first.kind == chat::ChatMessageKind::ContactRequest;
|
||||
s_convs.push_back(std::move(c));
|
||||
}
|
||||
std::sort(s_convs.begin(), s_convs.end(),
|
||||
@@ -1340,6 +1349,42 @@ void RenderChatTab(App* app)
|
||||
ImGui::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
||||
ImGui::Separator();
|
||||
|
||||
// Stranger contact-request card: Accept (add to contacts) or Block (delete + block). Normal
|
||||
// ImGui flow, so ##ChatMessages below (sized from the remaining content height) shrinks to fit.
|
||||
if (sel->pendingRequest) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||
ImGui::TextUnformatted(TR("chat_request_title"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextWrapped("%s", TR("chat_request_body"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (ImGui::SmallButton(TR("chat_request_accept"))) {
|
||||
data::AddressBookEntry e;
|
||||
e.address = sel->peerZaddr;
|
||||
e.label = sel->peerName;
|
||||
book.addEntry(e); // book.revision() bumps → the request flag clears on the next rebuild
|
||||
Notifications::instance().info(TR("chat_request_accepted"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("chat_request_block"))) {
|
||||
const std::string cid = sel->cid, nm = sel->peerName; // copy before deleteConversation
|
||||
if (app->chatService().deleteConversation(cid, /*block=*/true)) {
|
||||
if (app->settings()) {
|
||||
app->settings()->setChatBlocked(cid, nm, true);
|
||||
app->settings()->setChatHidden(cid, false);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (s_selected_cid == cid) s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_blocked_toast"));
|
||||
} else {
|
||||
Notifications::instance().error(TR("chat_delete_failed"));
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
// Small message-list inset (the ##ChatThread WindowPadding above already holds content off
|
||||
// the glass edge; keep a little here so bubbles don't hug the scrollbar).
|
||||
|
||||
@@ -232,6 +232,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
||||
strings_["chat_you"] = "You";
|
||||
strings_["chat_contact_request"] = "contact request";
|
||||
strings_["chat_request_title"] = "New contact request";
|
||||
strings_["chat_request_body"] = "This person isn't in your contacts. Accept to add them, or block to delete this conversation and ignore them.";
|
||||
strings_["chat_request_accept"] = "Accept";
|
||||
strings_["chat_request_block"] = "Block";
|
||||
strings_["chat_request_accepted"] = "Contact added.";
|
||||
strings_["chat_send_failed"] = "not sent";
|
||||
strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it";
|
||||
strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)";
|
||||
|
||||
Reference in New Issue
Block a user