feat(chat): raise the composer to the tier-3 cap + show the transaction count
Front end of tier-3 larger messages. The composer body cap goes from 4000 to 16000 bytes (kHushChatMaxMessageChunks = 64 chunks ⇒ up to 4 transactions), so a message past the single-transaction ceiling now actually composes and sends via the tier-3 path. The buffer and static_assert grow to match. The beside-the-gauge indicator (and its hover tooltip) now distinguish the two tiers: while a long message still fits one transaction it shows the part count as before; once it spills across several it shows the TRANSACTION count — the real cost the user asked to see — and the tooltip explains it is split across several transactions, the recipient sees it only once every part has arrived, so it takes longer to deliver and costs a fee per transaction. New strings chat_txns_label / chat_parts_explain_multi (English; 8 locales fall back pending a translation pass). Tier 3 is now end to end: compose → split → broadcast → reassemble → display. A receive-side "incoming N/M parts" progress hint remains a future refinement (an incomplete large message simply appears once whole). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-15
@@ -63,17 +63,18 @@ int s_msgsel_head = 0; // drag-current byte offset
|
||||
bool s_msgsel_dragging = false;
|
||||
|
||||
// Composer + new-conversation UI state.
|
||||
char s_compose[4100] = ""; // must be >= kChatBodyMaxBytes + 1 (static_assert'd below)
|
||||
char s_compose[16400] = ""; // must be >= kChatBodyMaxBytes + 1 (static_assert'd below)
|
||||
std::string s_compose_cid; // the conversation s_compose is a draft for; draft is wiped when it changes
|
||||
// Live byte offset of the composer's text caret, kept in sync by composeInputCallback while the composer
|
||||
// is active (the callback only fires then). The emoji picker uses it to splice a glyph at the cursor
|
||||
// instead of always appending. -1 = unknown/never-focused => append at the end.
|
||||
int s_composeCursor = -1;
|
||||
// On-chain chat body cap in bytes. Larger-messages tier 2 splits the ciphertext across up to
|
||||
// kHushChatMaxMessageParts (16) memo outputs in one tx, so the body can be a few KB (protocol max ~4063:
|
||||
// 16 × 255 cipher bytes − 17 secretstream tag). Kept a touch under that so the composer never produces a
|
||||
// body that compose would reject as TooLong. The composer hard-caps input here; the emoji picker too.
|
||||
constexpr int kChatBodyMaxBytes = 4000;
|
||||
// On-chain chat body cap in bytes. Larger messages split the ciphertext across many memo outputs: tier 2
|
||||
// packs up to kHushChatMaxMessageParts (16) into ONE transaction (~4 KB), and tier 3 spills further across
|
||||
// several transactions up to kHushChatMaxMessageChunks (64) chunks total (protocol max ~16303: 64 × 255
|
||||
// cipher bytes − 17 secretstream tag). Kept a touch under that so the composer never produces a body that
|
||||
// compose would reject as TooLong. The composer hard-caps input here; the emoji picker too.
|
||||
constexpr int kChatBodyMaxBytes = 16000;
|
||||
static_assert(sizeof(s_compose) >= kChatBodyMaxBytes + 1, "compose buffer too small for the body cap");
|
||||
float s_composerAnimH = 0.0f; // animated composer-box height — grows with newlines, pushing the thread up
|
||||
float s_composerTargetH = 0.0f; // target height measured in the composer block (wrapped content), consumed next frame
|
||||
@@ -2021,11 +2022,14 @@ void RenderChatTab(App* app)
|
||||
|
||||
// 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.
|
||||
// splits into 510-hex chunks (512 minus a 2-char part index). Up to kHushChatMaxMessageParts (16)
|
||||
// chunks ride ONE transaction (tier 2); beyond that the message spills across several (tier 3).
|
||||
const int chatMsgParts = [&]() -> int {
|
||||
const std::size_t hexLen = (static_cast<std::size_t>(used) + 17) * 2;
|
||||
return hexLen <= 512 ? 1 : static_cast<int>((hexLen + 509) / 510); // ceil(hexLen/510)
|
||||
}();
|
||||
// How many transactions those chunks need (tier 3 is >1): the real cost the user asked to see.
|
||||
const int chatMsgTxns = (chatMsgParts + chat::kHushChatMaxMessageParts - 1) / chat::kHushChatMaxMessageParts;
|
||||
|
||||
// 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.
|
||||
@@ -2137,30 +2141,40 @@ 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.
|
||||
// Always-visible indicator beside the gauge once the message spans >1 memo output: the part
|
||||
// count while it still fits one transaction (tier 2), or the TRANSACTION count once it spills
|
||||
// across several (tier 3 — the real cost the user cares about). 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"));
|
||||
char buf[32];
|
||||
if (chatMsgTxns > 1)
|
||||
std::snprintf(buf, sizeof buf, "%d %s", chatMsgTxns, TR("chat_txns_label"));
|
||||
else
|
||||
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);
|
||||
material::WithAlpha(material::OnSurface(), chatMsgTxns > 1 ? 205 : 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");
|
||||
if (overCap) {
|
||||
tip = std::string(TR("chat_len_over")) + " " + tip;
|
||||
} else if (chatMsgTxns > 1) {
|
||||
tip += " \xC2\xB7 " + std::to_string(chatMsgParts) + " " + TR("chat_parts_label") +
|
||||
" \xC2\xB7 " + std::to_string(chatMsgTxns) + " " + TR("chat_txns_label");
|
||||
} 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::TextUnformatted(TR(chatMsgTxns > 1 ? "chat_parts_explain_multi" : "chat_parts_explain"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
@@ -294,7 +294,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_export_confirm_btn"] = "Export";
|
||||
strings_["chat_len_over"] = "Message too long";
|
||||
strings_["chat_parts_label"] = "parts";
|
||||
strings_["chat_txns_label"] = "txns";
|
||||
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_parts_explain_multi"] = "This message is too big for one transaction, so it is split across several. The recipient sees it once every part has arrived, so it takes longer to deliver and costs a network fee per transaction.";
|
||||
strings_["chat_mute"] = "Mute";
|
||||
strings_["chat_unmute"] = "Unmute";
|
||||
strings_["chat_hide"] = "Hide";
|
||||
|
||||
Reference in New Issue
Block a user