diff --git a/res/lang/de.json b/res/lang/de.json index 9100aed..c3d4c10 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -148,6 +148,7 @@ "chat_export_warn": "Speichert die entschlüsselten Nachrichten als Klartext. Bewahre die Datei sicher auf.", "chat_hidden_toast": "Unterhaltung ausgeblendet – eine neue Nachricht holt sie zurück", "chat_hide": "Ausblenden", + "chat_hide_hidden": "Ausgeblendete verbergen", "chat_jump_latest": "Neueste", "chat_len_over": "Nachricht zu lang", "chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.", @@ -167,6 +168,7 @@ "chat_send": "Senden", "chat_send_failed": "nicht gesendet", "chat_sending": "senden…", + "chat_show_hidden": "Ausgeblendete anzeigen", "chat_time_now": "jetzt", "chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).", "chat_toast_lite_busy": "Es wird bereits gesendet, oder es ist keine Wallet geöffnet.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).", "chat_toast_request_queued": "Kontaktanfrage in Warteschlange.", "chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.", + "chat_unhide": "Einblenden", "chat_unmute": "Stummschaltung aufheben", "chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.", "chat_you": "Du", diff --git a/res/lang/es.json b/res/lang/es.json index 8aeadbe..187caea 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -148,6 +148,7 @@ "chat_export_warn": "Guarda los mensajes descifrados como texto sin cifrar. Guarda el archivo de forma segura.", "chat_hidden_toast": "Conversación oculta: un mensaje nuevo la recupera", "chat_hide": "Ocultar", + "chat_hide_hidden": "Ocultar ocultos", "chat_jump_latest": "Recientes", "chat_len_over": "Mensaje demasiado largo", "chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.", @@ -167,6 +168,7 @@ "chat_send": "Enviar", "chat_send_failed": "no enviado", "chat_sending": "enviando…", + "chat_show_hidden": "Ver ocultos", "chat_time_now": "ahora", "chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).", "chat_toast_lite_busy": "Ya hay un envío en curso, o no hay ningún monedero abierto.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).", "chat_toast_request_queued": "Solicitud de contacto en cola.", "chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.", + "chat_unhide": "Mostrar", "chat_unmute": "Reactivar", "chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.", "chat_you": "Tú", diff --git a/res/lang/fr.json b/res/lang/fr.json index 773188a..91c93c3 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -148,6 +148,7 @@ "chat_export_warn": "Enregistre les messages déchiffrés en texte clair. Conservez le fichier en lieu sûr.", "chat_hidden_toast": "Conversation masquée — un nouveau message la fait réapparaître", "chat_hide": "Masquer", + "chat_hide_hidden": "Masquer masqués", "chat_jump_latest": "Récents", "chat_len_over": "Message trop long", "chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.", @@ -167,6 +168,7 @@ "chat_send": "Envoyer", "chat_send_failed": "non envoyé", "chat_sending": "envoi…", + "chat_show_hidden": "Afficher masqués", "chat_time_now": "à l'instant", "chat_toast_compose_failed": "Impossible de composer le message (trop long ?).", "chat_toast_lite_busy": "Un envoi est déjà en cours, ou aucun portefeuille n'est ouvert.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).", "chat_toast_request_queued": "Demande de contact mise en file d'attente.", "chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.", + "chat_unhide": "Afficher", "chat_unmute": "Réactiver", "chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.", "chat_you": "Vous", diff --git a/res/lang/ja.json b/res/lang/ja.json index 5cf26d2..7852fcf 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -148,6 +148,7 @@ "chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。", "chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます", "chat_hide": "非表示", + "chat_hide_hidden": "非表示を隠す", "chat_jump_latest": "最新", "chat_len_over": "メッセージが長すぎます", "chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。", @@ -167,6 +168,7 @@ "chat_send": "送信", "chat_send_failed": "未送信", "chat_sending": "送信中…", + "chat_show_hidden": "非表示を表示", "chat_time_now": "たった今", "chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。", "chat_toast_lite_busy": "すでに送信処理が進行中か、ウォレットが開かれていません。", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。", "chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。", "chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。", + "chat_unhide": "再表示", "chat_unmute": "ミュート解除", "chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。", "chat_you": "自分", diff --git a/res/lang/ko.json b/res/lang/ko.json index ec3300a..dd90b73 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -148,6 +148,7 @@ "chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.", "chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다", "chat_hide": "숨기기", + "chat_hide_hidden": "숨김 접기", "chat_jump_latest": "최신", "chat_len_over": "메시지가 너무 깁니다", "chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.", @@ -167,6 +168,7 @@ "chat_send": "전송", "chat_send_failed": "전송 안 됨", "chat_sending": "전송 중…", + "chat_show_hidden": "숨김 보기", "chat_time_now": "방금", "chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).", "chat_toast_lite_busy": "이미 전송이 진행 중이거나 열린 지갑이 없습니다.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).", "chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.", "chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.", + "chat_unhide": "다시 표시", "chat_unmute": "음소거 해제", "chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.", "chat_you": "나", diff --git a/res/lang/pt.json b/res/lang/pt.json index 20107c8..85c2d46 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -148,6 +148,7 @@ "chat_export_warn": "Salva as mensagens descriptografadas como texto simples. Guarde o arquivo com segurança.", "chat_hidden_toast": "Conversa ocultada — uma nova mensagem a traz de volta", "chat_hide": "Ocultar", + "chat_hide_hidden": "Ocultar ocultas", "chat_jump_latest": "Recentes", "chat_len_over": "Mensagem muito longa", "chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.", @@ -167,6 +168,7 @@ "chat_send": "Enviar", "chat_send_failed": "não enviada", "chat_sending": "enviando…", + "chat_show_hidden": "Ver ocultas", "chat_time_now": "agora", "chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).", "chat_toast_lite_busy": "Já há um envio em andamento, ou nenhuma carteira está aberta.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).", "chat_toast_request_queued": "Solicitação de contato na fila.", "chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.", + "chat_unhide": "Mostrar", "chat_unmute": "Reativar som", "chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.", "chat_you": "Você", diff --git a/res/lang/ru.json b/res/lang/ru.json index a1cc8f8..177be48 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -148,6 +148,7 @@ "chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.", "chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его", "chat_hide": "Скрыть", + "chat_hide_hidden": "Скрыть скрытые", "chat_jump_latest": "Новые", "chat_len_over": "Сообщение слишком длинное", "chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.", @@ -167,6 +168,7 @@ "chat_send": "Отправить", "chat_send_failed": "не отправлено", "chat_sending": "отправка…", + "chat_show_hidden": "Показать скрытые", "chat_time_now": "сейчас", "chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).", "chat_toast_lite_busy": "Отправка уже выполняется, или кошелёк не открыт.", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).", "chat_toast_request_queued": "Запрос контакта поставлен в очередь.", "chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.", + "chat_unhide": "Показать", "chat_unmute": "Включить уведомления", "chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.", "chat_you": "Вы", diff --git a/res/lang/zh.json b/res/lang/zh.json index fff5e90..bf6b509 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -148,6 +148,7 @@ "chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。", "chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示", "chat_hide": "隐藏", + "chat_hide_hidden": "收起已隐藏", "chat_jump_latest": "最新", "chat_len_over": "消息过长", "chat_locked_hint": "解锁钱包以加载您的聊天记录。", @@ -167,6 +168,7 @@ "chat_send": "发送", "chat_send_failed": "未发送", "chat_sending": "发送中…", + "chat_show_hidden": "显示已隐藏", "chat_time_now": "刚刚", "chat_toast_compose_failed": "无法编写该消息(内容过长?)。", "chat_toast_lite_busy": "已有发送正在进行中,或未打开任何钱包。", @@ -176,6 +178,7 @@ "chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。", "chat_toast_request_queued": "联系人请求已排队。", "chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。", + "chat_unhide": "取消隐藏", "chat_unmute": "取消静音", "chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。", "chat_you": "我", diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index a31443e..a2ca91f 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -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(*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 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(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()); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index e9f1a50..235f0da 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";