diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 043abbb..3a521b7 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index c3d4c10..9189287 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -139,6 +139,7 @@ "chat_cancel": "Abbrechen", "chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um", "chat_contact_request": "kontaktanfrage", + "chat_emoji_search": "Emoji suchen", "chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.", "chat_empty_start": "Starte eine mit \"Neue Unterhaltung\".", "chat_empty_title": "Noch keine Unterhaltungen", diff --git a/res/lang/es.json b/res/lang/es.json index 187caea..a2020a4 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -139,6 +139,7 @@ "chat_cancel": "Cancelar", "chat_contact_added": "Contacto añadido: renómbralo en Contactos", "chat_contact_request": "solicitud de contacto", + "chat_emoji_search": "Buscar emoji", "chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.", "chat_empty_start": "Inicia una con \"Nueva conversación\".", "chat_empty_title": "Aún no hay conversaciones", diff --git a/res/lang/fr.json b/res/lang/fr.json index 91c93c3..fba109d 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -139,6 +139,7 @@ "chat_cancel": "Annuler", "chat_contact_added": "Contact ajouté — renommez-le dans Contacts", "chat_contact_request": "demande de contact", + "chat_emoji_search": "Rechercher un emoji", "chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.", "chat_empty_start": "Commencez-en une avec « Nouvelle conversation ».", "chat_empty_title": "Aucune conversation pour l'instant", diff --git a/res/lang/ja.json b/res/lang/ja.json index 7852fcf..f64da30 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -139,6 +139,7 @@ "chat_cancel": "キャンセル", "chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます", "chat_contact_request": "連絡リクエスト", + "chat_emoji_search": "絵文字を検索", "chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。", "chat_empty_start": "「新しい会話」から始めましょう。", "chat_empty_title": "会話はまだありません", diff --git a/res/lang/ko.json b/res/lang/ko.json index dd90b73..469c616 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -139,6 +139,7 @@ "chat_cancel": "취소", "chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요", "chat_contact_request": "연락 요청", + "chat_emoji_search": "이모지 검색", "chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.", "chat_empty_start": "\"새 대화\"로 시작하세요.", "chat_empty_title": "아직 대화가 없습니다", diff --git a/res/lang/pt.json b/res/lang/pt.json index 85c2d46..665891c 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -139,6 +139,7 @@ "chat_cancel": "Cancelar", "chat_contact_added": "Contato adicionado — renomeie em Contatos", "chat_contact_request": "solicitação de contato", + "chat_emoji_search": "Pesquisar emoji", "chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.", "chat_empty_start": "Inicie uma com \"Nova conversa\".", "chat_empty_title": "Ainda não há conversas", diff --git a/res/lang/ru.json b/res/lang/ru.json index 177be48..98afaed 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -139,6 +139,7 @@ "chat_cancel": "Отмена", "chat_contact_added": "Контакт добавлен — переименуйте его в Контактах", "chat_contact_request": "запрос контакта", + "chat_emoji_search": "Поиск эмодзи", "chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.", "chat_empty_start": "Начните новый с помощью «Новый разговор».", "chat_empty_title": "Пока нет разговоров", diff --git a/res/lang/zh.json b/res/lang/zh.json index bf6b509..b9acf67 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -139,6 +139,7 @@ "chat_cancel": "取消", "chat_contact_added": "已添加联系人——可在联系人中重命名", "chat_contact_request": "联系人请求", + "chat_emoji_search": "搜索表情", "chat_empty_hint": "暂无对话。您收到的消息将显示在此处。", "chat_empty_start": "点击\"新建会话\"开始。", "chat_empty_title": "还没有会话", diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index a2ca91f..94d46de 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -49,6 +49,8 @@ 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) +bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open +char s_emoji_search[48] = ""; // emoji picker keyword filter // Effective draw-list font size for a material font (mirrors sidebar's ScaledFontSize). float scaledSize(ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; } @@ -201,40 +203,75 @@ 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; +// Common single-codepoint emoji (all verified present in the bundled NotoEmoji subset), each with +// keyword text for the picker's search box. Single-codepoint only — ImGui does no shaping, so ZWJ +// sequences / flags wouldn't compose. +struct EmojiEntry { const char* glyph; const char* keywords; }; +static const EmojiEntry kEmoji[] = { + {u8"😀","grin happy smile"},{u8"😃","smile happy joy"},{u8"😄","laugh happy smile"},{u8"😁","grin beam happy"}, + {u8"😆","laugh haha"},{u8"😅","sweat laugh nervous"},{u8"😂","joy laugh tears cry funny"},{u8"🤣","rofl laugh rolling funny"}, + {u8"😊","blush smile happy"},{u8"😇","angel innocent halo"},{u8"🙂","slight smile"},{u8"🙃","upside down silly"}, + {u8"😉","wink"},{u8"😌","relieved calm"},{u8"😍","heart eyes love"},{u8"🥰","love hearts adore"},{u8"😘","kiss blow"}, + {u8"😗","kiss"},{u8"😙","kiss smile"},{u8"😚","kiss closed"},{u8"😋","yum tasty tongue"},{u8"😛","tongue playful"}, + {u8"😝","tongue squint"},{u8"😜","wink tongue crazy"},{u8"🤪","zany crazy silly"},{u8"🤨","raised eyebrow suspicious"}, + {u8"🧐","monocle thinking"},{u8"🤓","nerd geek glasses"},{u8"😎","cool sunglasses"},{u8"🥳","party celebrate hat"}, + {u8"😏","smirk"},{u8"😒","unamused meh"},{u8"😞","disappointed sad"},{u8"😔","pensive sad"},{u8"😟","worried"}, + {u8"😕","confused"},{u8"🙁","frown sad"},{u8"😣","persevere"},{u8"😖","confounded"},{u8"😫","tired"},{u8"😩","weary"}, + {u8"🥺","pleading puppy beg"},{u8"😢","cry sad tear"},{u8"😭","sob cry bawl"},{u8"😤","huff triumph steam"}, + {u8"😠","angry mad"},{u8"😡","rage mad angry"},{u8"🤬","curse swear angry"},{u8"🤯","mind blown shock"}, + {u8"😳","flushed embarrassed"},{u8"🥵","hot heat sweat"},{u8"🥶","cold freezing"},{u8"😱","scream fear shock"}, + {u8"😨","fearful scared"},{u8"😰","anxious sweat"},{u8"😥","sad sweat"},{u8"😓","sweat"},{u8"🤗","hug"}, + {u8"🤔","think hmm"},{u8"🤭","giggle oops hand"},{u8"🤫","shush quiet"},{u8"🤥","lie"},{u8"😶","no mouth blank"}, + {u8"😐","neutral meh"},{u8"😑","expressionless"},{u8"😬","grimace awkward"},{u8"🙄","eye roll"},{u8"😯","hushed surprise"}, + {u8"😦","frown open"},{u8"😧","anguished"},{u8"😮","wow surprise oh"},{u8"😲","astonished shock"},{u8"🥱","yawn tired bored"}, + {u8"😴","sleep zzz"},{u8"🤤","drool"},{u8"😪","sleepy"},{u8"🤐","zipper quiet secret"},{u8"🥴","woozy dizzy drunk"}, + {u8"🤢","nausea sick"},{u8"🤮","vomit sick"},{u8"🤧","sneeze sick"},{u8"😷","mask sick"},{u8"🤒","sick thermometer"}, + {u8"🤕","hurt bandage injured"},{u8"😈","devil imp evil"},{u8"👿","devil angry"},{u8"💀","skull dead"},{u8"👻","ghost boo"}, + {u8"👽","alien"},{u8"🤖","robot bot"},{u8"😺","cat smile"},{u8"🙀","cat scream"}, + {u8"👍","thumbs up like yes good"},{u8"👎","thumbs down dislike no bad"},{u8"👌","ok perfect"},{u8"✌","peace victory"}, + {u8"🤞","fingers crossed luck"},{u8"🤟","love you"},{u8"🤘","rock horns"},{u8"👏","clap applause"}, + {u8"🙌","raise hands celebrate"},{u8"👐","open hands"},{u8"🙏","pray thanks please"},{u8"💪","muscle strong flex"}, + {u8"👋","wave hi hello bye"},{u8"🤙","call shaka"},{u8"👊","fist punch bump"},{u8"✊","fist raised"},{u8"🤛","fist left"}, + {u8"🤜","fist right"},{u8"👆","up point"},{u8"👇","down point"},{u8"👈","left point"},{u8"👉","right point"}, + {u8"❤","heart red love"},{u8"🧡","heart orange"},{u8"💛","heart yellow"},{u8"💚","heart green"},{u8"💙","heart blue"}, + {u8"💜","heart purple"},{u8"🖤","heart black"},{u8"🤍","heart white"},{u8"💔","broken heart"},{u8"💕","hearts love"}, + {u8"💞","revolving hearts"},{u8"💗","growing heart"},{u8"💖","sparkling heart"},{u8"💘","heart arrow cupid"}, + {u8"💝","heart gift ribbon"},{u8"🔥","fire lit hot flame"},{u8"⭐","star"},{u8"🎉","party tada celebrate confetti"}, + {u8"🎊","confetti party"},{u8"✨","sparkles shiny"},{u8"💯","hundred perfect 100"},{u8"✅","check yes done tick"}, + {u8"❌","cross no wrong x"},{u8"❓","question mark"},{u8"❗","exclamation mark"},{u8"👀","eyes look"},{u8"🎂","cake birthday"}, + {u8"🚀","rocket launch"},{u8"💰","money bag cash"},{u8"🎁","gift present"},{u8"🍺","beer"},{u8"🍻","beers cheers"}, + {u8"☕","coffee tea"},{u8"🍕","pizza"},{u8"👑","crown king queen"},{u8"💎","diamond gem"},{u8"🌟","glowing star"}, + {u8"💥","boom explosion"}, +}; + +// Emoji picker overlay: fills the conversation-list pane (cancel + keyword search at the top, then a +// grid). Clicking an emoji appends its UTF-8 bytes to `buf` (the composer), respecting the buffer. +void renderEmojiPickerOverlay(char* buf, std::size_t bufSize) { + if (ImGui::SmallButton(TR("chat_cancel"))) { s_show_emoji_picker = false; s_emoji_search[0] = '\0'; return; } + ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##emojisearch", TR("chat_emoji_search"), s_emoji_search, sizeof(s_emoji_search)); + ImGui::Separator(); + + const std::string q = s_emoji_search; + ImGui::BeginChild("##emojigrid", ImVec2(0, 0), false); 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(*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'; } + const float cell = 32.0f * Layout::dpiScale(); + const float step = cell + ImGui::GetStyle().ItemSpacing.x; + const int perRow = std::max(1, static_cast(ImGui::GetContentRegionAvail().x / step)); + int shown = 0; + for (const auto& e : kEmoji) { + if (!q.empty() && !containsCI(e.keywords, q)) continue; + ImGui::PushID(shown); + if (ImGui::Button(e.glyph, ImVec2(cell, cell))) { + const std::size_t cur = std::strlen(buf), add = std::strlen(e.glyph); + if (cur + add < bufSize) { std::memcpy(buf + cur, e.glyph, add); buf[cur + add] = '\0'; } } ImGui::PopID(); - if ((idx + 1) % perRow != 0) ImGui::SameLine(); - s += len; - ++idx; + if (++shown % perRow != 0) ImGui::SameLine(); } ImGui::PopFont(); - ImGui::EndPopup(); + ImGui::EndChild(); } } // namespace @@ -310,7 +347,10 @@ void RenderChatTab(App* app) // Faint sidebar tint so the list reads as distinct from the thread pane (V6). ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::OnSurface(), 10))); ImGui::BeginChild("##ChatList", ImVec2(listW, avail.y), true); - { + if (s_show_emoji_picker) { + // Emoji picker overlay takes over the conversation-list pane while open (search + cancel + grid). + renderEmojiPickerOverlay(s_compose, sizeof(s_compose)); + } else { // Accented "New conversation" action (house tactile style). ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary())); @@ -656,12 +696,12 @@ 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. + // Emoji picker button (left) + byte counter (right) on one row. The picker opens as an + // overlay over the conversation-list pane (rendered there), not a floating window. 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)); + if (emojiClicked) { s_show_emoji_picker = !s_show_emoji_picker; s_emoji_search[0] = '\0'; } ImGui::SameLine(); // Byte counter (Error tint over cap) with the over-cap label. ImGui::PushFont(metaFont); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 235f0da..277cc64 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -255,6 +255,7 @@ void I18n::loadBuiltinEnglish() strings_["chat_hide"] = "Hide"; strings_["chat_unhide"] = "Unhide"; strings_["chat_show_hidden"] = "Show hidden"; + strings_["chat_emoji_search"] = "Search emoji"; 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";