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;
|
std::int64_t lastTs = 0;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
bool hidden = false; // shown only while "Show hidden" is on
|
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
|
// 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.cid = cid;
|
||||||
c.hidden = hidden;
|
c.hidden = hidden;
|
||||||
c.count = static_cast<int>(messages.size());
|
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
|
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.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 (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();
|
const auto& last = messages.back();
|
||||||
c.lastBody = last.body;
|
c.lastBody = last.body;
|
||||||
@@ -818,6 +821,12 @@ void RenderChatTab(App* app)
|
|||||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
: 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));
|
s_convs.push_back(std::move(c));
|
||||||
}
|
}
|
||||||
std::sort(s_convs.begin(), s_convs.end(),
|
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::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
||||||
ImGui::Separator();
|
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();
|
const float dp = Layout::dpiScale();
|
||||||
// Small message-list inset (the ##ChatThread WindowPadding above already holds content off
|
// 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).
|
// 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_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
||||||
strings_["chat_you"] = "You";
|
strings_["chat_you"] = "You";
|
||||||
strings_["chat_contact_request"] = "contact request";
|
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_send_failed"] = "not sent";
|
||||||
strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it";
|
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)";
|
strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)";
|
||||||
|
|||||||
Reference in New Issue
Block a user