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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user