From 1729f7dfadd4618c111550b008b9a1c184a82d6a Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 26 Oct 2026 12:00:27 -0500 Subject: [PATCH] feat(chat): show the on-chain part count for a long message in the composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A larger message (tier 2) is carried as one transaction split across up to 16 memo outputs, but the composer gave no sign of that. It now shows the part count beside the byte gauge the moment the body spans more than one memo output (computed the same way buildOutgoingMessage chunks it), and the gauge's hover tooltip adds the count plus "sent as one transaction split into these memo parts — it arrives as quickly as a normal message" (the honest answer: it's one atomic 0-conf tx, not N transactions — multi-transaction splitting is the unbuilt tier 3). New strings chat_parts_label / chat_parts_explain (English; 8 locales fall back). Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/chat_tab.cpp | 27 +++++++++++++++++++++++++++ src/util/i18n.cpp | 2 ++ 2 files changed, 29 insertions(+) diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 92d77af..80c2c4d 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -2019,6 +2019,14 @@ void RenderChatTab(App* app) // Fraction of the on-chain cap used, for the radial gauge (drawn inside the input, below). const float frac = kBodyMaxBytes > 0 ? static_cast(used) / kBodyMaxBytes : 0.0f; + // How this message chunks on-chain (mirrors buildOutgoingMessage): the hex ciphertext is + // (plaintext + the 17-byte secretstream tag) * 2; ≤512 hex fits one memo output, otherwise it + // splits into 510-hex chunks (512 minus a 2-char part index) — all carried in ONE transaction. + const int chatMsgParts = [&]() -> int { + const std::size_t hexLen = (static_cast(used) + 17) * 2; + return hexLen <= 512 ? 1 : static_cast((hexLen + 509) / 510); // ceil(hexLen/510) + }(); + // Input row: [emoji] [input] [Send]. The input's TOP is pinned 10px below the message box and // it grows DOWNWARD as it gets taller (the thread above shrinks), so the row bottom stays put. const float rowY = boxMax.y + composerAreaH - composerBoxH; @@ -2129,14 +2137,33 @@ void RenderChatTab(App* app) gdl->AddCircleFilled(ImVec2(ringC.x + std::cos(a1) * ringR, ringC.y + std::sin(a1) * ringR), thick * 0.6f, fillCol, 10); } + // Always-visible part count beside the gauge once the message spans >1 memo output, so it's + // clear how it chunks on-chain (it's still ONE transaction). Full explanation on hover. + if (chatMsgParts > 1 && !overCap) { + ImFont* cf = ImGui::GetFont(); + const float cfsz = std::max(10.0f, lineH * 0.42f); + char buf[24]; + std::snprintf(buf, sizeof buf, "%d %s", chatMsgParts, TR("chat_parts_label")); + const ImVec2 tsz = cf->CalcTextSizeA(cfsz, FLT_MAX, 0.0f, buf); + gdl->AddText(cf, cfsz, ImVec2(ringC.x - ringR - 6.0f * tdp - tsz.x, ringC.y - tsz.y * 0.5f), + material::WithAlpha(material::OnSurface(), 150), buf); + } // Exact count on hover (progressive disclosure; over-cap prefixes a label). const float hitR = ringR + thick; if (ImGui::IsMouseHoveringRect(ImVec2(ringC.x - hitR, ringC.y - hitR), ImVec2(ringC.x + hitR, ringC.y + hitR))) { std::string tip = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes); if (overCap) tip = std::string(TR("chat_len_over")) + " " + tip; + else if (chatMsgParts > 1) tip += " \xC2\xB7 " + std::to_string(chatMsgParts) + " " + TR("chat_parts_label"); ImGui::BeginTooltip(); ImGui::TextUnformatted(tip.c_str()); + if (chatMsgParts > 1 && !overCap) { + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f); + ImGui::TextUnformatted(TR("chat_parts_explain")); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); + } ImGui::EndTooltip(); } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 12dee80..6bbc893 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -293,6 +293,8 @@ void I18n::loadBuiltinEnglish() strings_["chat_export_confirm_path"] = "Will be saved to:"; strings_["chat_export_confirm_btn"] = "Export"; strings_["chat_len_over"] = "Message too long"; + strings_["chat_parts_label"] = "parts"; + strings_["chat_parts_explain"] = "This long message is sent as one transaction split into these memo parts — it arrives as quickly as a normal message."; strings_["chat_mute"] = "Mute"; strings_["chat_unmute"] = "Unmute"; strings_["chat_hide"] = "Hide";