feat(chat): emoji picker + show-hidden toggle
- Emoji picker: a 🙂 button on the composer row opens a scrollable grid of ~150 common single-codepoint emoji (all verified present in the bundled NotoEmoji subset); clicking appends to the composer, respecting the byte cap. ImGui does no shaping, so the set is single-codepoint only (ZWJ sequences / flags omitted). - Show hidden: when there are hidden conversations, a "Show hidden (N)" toggle appears in the list; toggling it reveals them (dimmed) and the thread header's Hide button becomes Unhide. Off by default, and resets when nothing is hidden. 8-language strings; no new CJK glyphs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ bool s_show_new_convo = false;
|
||||
char s_new_zaddr[128] = "";
|
||||
char s_new_msg[256] = "";
|
||||
char s_search[80] = ""; // conversation-list filter (Q8)
|
||||
bool s_show_hidden = false; // when on, the list also shows hidden conversations (with an Unhide action)
|
||||
|
||||
// Effective draw-list font size for a material font (mirrors sidebar's ScaledFontSize).
|
||||
float scaledSize(ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; }
|
||||
@@ -136,6 +137,7 @@ struct ConvSummary {
|
||||
std::string lastBody;
|
||||
std::int64_t lastTs = 0;
|
||||
int count = 0;
|
||||
bool hidden = false; // shown only while "Show hidden" is on
|
||||
};
|
||||
|
||||
// Centered, muted, wrapped hint for the empty states.
|
||||
@@ -199,6 +201,42 @@ void centeredEmptyState(const char* icon, const char* title, const char* hint) {
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji picker popup: a scrollable grid of common single-codepoint emoji (verified present in the
|
||||
// bundled NotoEmoji subset). Clicking one appends its UTF-8 bytes to `buf` (respecting the buffer).
|
||||
// The set is single-codepoint only — ImGui does no shaping, so ZWJ sequences / flags wouldn't compose.
|
||||
void emojiPickerPopup(const char* popupId, char* buf, std::size_t bufSize) {
|
||||
static const char* const kEmoji =
|
||||
u8"😀😃😄😁😆😅😂🤣😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥳😏😒😞😔😟😕🙁😣😖😫😩🥺😢😭😤"
|
||||
u8"😠😡🤬🤯😳🥵🥶😱😨😰😥😓🤗🤔🤭🤫🤥😶😐😑😬🙄😯😦😧😮😲🥱😴🤤😪🤐🥴🤢🤮🤧😷🤒🤕😈👿💀👻👽🤖😺🙀"
|
||||
u8"👍👎👌✌🤞🤟🤘👏🙌👐🙏💪👋🤙👊✊🤛🤜👆👇👈👉"
|
||||
u8"❤🧡💛💚💙💜🖤🤍💔💕💞💗💖💘💝🔥⭐🎉🎊✨💯✅❌❓❗👀🎂🚀💰🎁🍺🍻☕🍕👑💎🌟💥";
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, 260.0f * dp)); // cap height → scrolls
|
||||
if (!ImGui::BeginPopup(popupId)) return;
|
||||
ImGui::PushFont(material::Type().subtitle1()); // larger + emoji-capable
|
||||
const float cell = 30.0f * dp;
|
||||
const int perRow = 9;
|
||||
int idx = 0;
|
||||
for (const char* s = kEmoji; *s;) {
|
||||
const unsigned char lead = static_cast<unsigned char>(*s);
|
||||
const int len = (lead >= 0xF0) ? 4 : (lead >= 0xE0) ? 3 : (lead >= 0xC0) ? 2 : 1;
|
||||
char glyph[5] = {0};
|
||||
for (int k = 0; k < len && s[k]; ++k) glyph[k] = s[k];
|
||||
ImGui::PushID(idx);
|
||||
if (ImGui::Button(glyph, ImVec2(cell, cell))) {
|
||||
const std::size_t cur = std::strlen(buf);
|
||||
const std::size_t add = std::strlen(glyph);
|
||||
if (cur + add < bufSize) { std::memcpy(buf + cur, glyph, add); buf[cur + add] = '\0'; }
|
||||
}
|
||||
ImGui::PopID();
|
||||
if ((idx + 1) % perRow != 0) ImGui::SameLine();
|
||||
s += len;
|
||||
++idx;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RenderChatTab(App* app)
|
||||
@@ -215,12 +253,16 @@ void RenderChatTab(App* app)
|
||||
|
||||
// Build conversation summaries (single scan per conversation), sorted by most-recent activity.
|
||||
std::vector<ConvSummary> convs;
|
||||
int hiddenCount = 0;
|
||||
for (const auto& cid : store.conversationIds()) {
|
||||
if (app->settings() && app->settings()->isChatHidden(cid)) continue; // hidden — a new message un-hides it
|
||||
const bool hidden = app->settings() && app->settings()->isChatHidden(cid);
|
||||
if (hidden) ++hiddenCount;
|
||||
if (hidden && !s_show_hidden) continue; // filtered out unless "Show hidden" is on
|
||||
const auto messages = store.conversation(cid);
|
||||
if (messages.empty()) continue;
|
||||
ConvSummary c;
|
||||
c.cid = cid;
|
||||
c.hidden = hidden;
|
||||
c.count = static_cast<int>(messages.size());
|
||||
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
|
||||
@@ -285,6 +327,18 @@ void RenderChatTab(App* app)
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGui::InputTextWithHint("##chatsearch", TR("chat_search"), s_search, sizeof(s_search));
|
||||
}
|
||||
// Show-hidden toggle — only when there are hidden conversations. Reveals them in the list (with
|
||||
// an Unhide action in the thread header).
|
||||
if (hiddenCount > 0) {
|
||||
ImGui::PushFont(metaFont);
|
||||
const std::string lbl = s_show_hidden
|
||||
? std::string(TR("chat_hide_hidden"))
|
||||
: std::string(TR("chat_show_hidden")) + " (" + std::to_string(hiddenCount) + ")";
|
||||
if (ImGui::SmallButton(lbl.c_str())) s_show_hidden = !s_show_hidden;
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
s_show_hidden = false; // nothing hidden → keep the toggle off
|
||||
}
|
||||
const std::string search = s_search;
|
||||
ImGui::Separator();
|
||||
if (convs.empty()) {
|
||||
@@ -335,8 +389,9 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
const float textX = avC.x + avR + pad;
|
||||
|
||||
// Name (top).
|
||||
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad), material::OnSurface(), c.peerName.c_str());
|
||||
// Name (top). Hidden conversations (shown via "Show hidden") render dimmed.
|
||||
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
|
||||
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
|
||||
// Time (top-right, muted) — compact relative form (Q5).
|
||||
const std::string when = relativeTime(c.lastTs);
|
||||
if (!when.empty()) {
|
||||
@@ -440,15 +495,20 @@ void RenderChatTab(App* app)
|
||||
app->settings()->setChatMuted(sel->cid, !muted);
|
||||
app->settings()->save();
|
||||
}
|
||||
// Hide conversation — drops it from the list (messages stay in the encrypted store; a
|
||||
// new incoming message brings it back). Deselect so the thread resets to the next one.
|
||||
// Hide / Unhide. Hiding drops it from the list (messages stay in the encrypted store);
|
||||
// Unhide (shown only for a hidden conversation while "Show hidden" is on) restores it.
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("chat_hide")) && app->settings()) {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // don't leave a phantom unread
|
||||
app->settings()->setChatHidden(sel->cid, true);
|
||||
app->settings()->save();
|
||||
s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
if (ImGui::SmallButton(sel->hidden ? TR("chat_unhide") : TR("chat_hide")) && app->settings()) {
|
||||
if (sel->hidden) {
|
||||
app->settings()->setChatHidden(sel->cid, false);
|
||||
app->settings()->save();
|
||||
} else {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // don't leave a phantom unread
|
||||
app->settings()->setChatHidden(sel->cid, true);
|
||||
app->settings()->save();
|
||||
s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
@@ -596,6 +656,13 @@ void RenderChatTab(App* app)
|
||||
if (material::TactileButton(TR("chat_send"), ImVec2(sendW, composerBoxH))) submit = true;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(3);
|
||||
// Emoji picker button (left) + byte counter (right) on one row.
|
||||
ImGui::PushFont(nameFont); // Body2 has emoji merged, so the glyph renders
|
||||
const bool emojiClicked = ImGui::SmallButton(u8"🙂");
|
||||
ImGui::PopFont();
|
||||
if (emojiClicked) ImGui::OpenPopup("##emojipick");
|
||||
emojiPickerPopup("##emojipick", s_compose, sizeof(s_compose));
|
||||
ImGui::SameLine();
|
||||
// Byte counter (Error tint over cap) with the over-cap label.
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, overCap ? material::Error() : material::OnSurfaceMedium());
|
||||
|
||||
@@ -253,6 +253,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_mute"] = "Mute";
|
||||
strings_["chat_unmute"] = "Unmute";
|
||||
strings_["chat_hide"] = "Hide";
|
||||
strings_["chat_unhide"] = "Unhide";
|
||||
strings_["chat_show_hidden"] = "Show hidden";
|
||||
strings_["chat_hide_hidden"] = "Hide hidden";
|
||||
strings_["chat_hidden_toast"] = "Conversation hidden — a new message brings it back";
|
||||
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
||||
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
||||
|
||||
Reference in New Issue
Block a user