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";