feat(chat): message-list rework, customization surface, smooth scroll
Chat tab overhaul: - Header/layout: single-row header (name + key-lock + compact click-to-copy address + right-aligned icon toolbar with a settings "notch"), composer moved below the message box (emoji toggle left of a bottom-flush input), tightened list-pane controls. - Message list: per-day date separators (Today/Yesterday/date), time-only group headers, tight same-sender grouping with iMessage-style merged corners, per-run peer avatar, delivery status (clock -> check), hover-reveal per-message time. Message base ~18px. - Customization: a settings "notch" gear opens a modal (also under Settings -> Chat & Contacts) with segmented controls for emoji style / bubble style / density / timestamps, sliders for poll rate + text size, a bubble-accent dropdown, Enter-to-send. Bubble style/accent/density/text-size/timestamps all applied live in the message loop. - Settings: app-wide clock-format control lives in Settings -> General; chat keeps a per-tab override. Both chat panes now use the app's smooth wheel-scroll. - i18n: new strings across all 8 languages; CJK subset rebuilt for the added glyphs. Inline contact rename, hide/mute, 0-conf fast-scan and the export path are carried through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "Passphrase ändern",
|
||||
"characters": "Zeichen",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Bernstein",
|
||||
"chat_accent_blue": "Blau",
|
||||
"chat_accent_green": "Grün",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Lila",
|
||||
"chat_accent_theme": "Design",
|
||||
"chat_add_contact": "Kontakt hinzufügen",
|
||||
"chat_awaiting_key": "Warten auf Antwort",
|
||||
"chat_bubble_minimal": "Minimal",
|
||||
"chat_bubble_rounded": "Abgerundet",
|
||||
"chat_bubble_square": "Eckig",
|
||||
"chat_cancel": "Abbrechen",
|
||||
"chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um",
|
||||
"chat_contact_request": "kontaktanfrage",
|
||||
"chat_copy_address_tip": "Zum Kopieren der Adresse klicken",
|
||||
"chat_density_comfortable": "Komfortabel",
|
||||
"chat_density_compact": "Kompakt",
|
||||
"chat_emoji_color": "Farbig",
|
||||
"chat_emoji_mono": "Monochrom",
|
||||
"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\".",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "Nachricht zu lang",
|
||||
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
|
||||
"chat_mute": "Stummschalten",
|
||||
"chat_new_button": "Neue Unterhaltung",
|
||||
"chat_new_button": "Neuer Chat",
|
||||
"chat_new_message": "Nachricht",
|
||||
"chat_new_message_toast": "Neue verschlüsselte Chat-Nachricht",
|
||||
"chat_new_send": "Anfrage senden",
|
||||
"chat_new_title": "Neue Unterhaltung",
|
||||
"chat_new_title": "Neuer Chat",
|
||||
"chat_new_zaddr": "z-Adresse des Empfängers",
|
||||
"chat_no_matches": "Keine Unterhaltungen entsprechen deiner Suche.",
|
||||
"chat_no_z_contacts": "Noch keine Kontakte mit geschützter Adresse",
|
||||
"chat_opt_bubble_accent": "Blasenfarbe",
|
||||
"chat_opt_bubble_style": "Blasenstil",
|
||||
"chat_opt_density": "Nachrichtendichte",
|
||||
"chat_opt_emoji": "Emoji-Stil",
|
||||
"chat_opt_enter_sends": "Eingabetaste sendet",
|
||||
"chat_opt_font_size": "Textgröße",
|
||||
"chat_opt_global_clock": "Globales Uhrzeitformat",
|
||||
"chat_opt_poll": "Abrufrate",
|
||||
"chat_opt_timestamp": "Zeitstempel",
|
||||
"chat_pick_contact": "Aus Kontakten wählen…",
|
||||
"chat_rename": "Kontakt umbenennen",
|
||||
"chat_rename_hint": "Kontaktname",
|
||||
"chat_renamed": "Kontakt umbenannt",
|
||||
"chat_retry": "Wiederholen",
|
||||
"chat_search": "Unterhaltungen durchsuchen",
|
||||
"chat_sec_appearance": "DARSTELLUNG",
|
||||
"chat_sec_messaging": "NACHRICHTEN",
|
||||
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
|
||||
"chat_send": "Senden",
|
||||
"chat_send_failed": "nicht gesendet",
|
||||
"chat_sending": "senden…",
|
||||
"chat_settings_done": "Fertig",
|
||||
"chat_settings_section": "CHAT & KONTAKTE",
|
||||
"chat_settings_tip": "Chat anpassen",
|
||||
"chat_settings_title": "Chat-Einstellungen",
|
||||
"chat_show_hidden": "Ausgeblendete anzeigen",
|
||||
"chat_time_now": "jetzt",
|
||||
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"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_today": "Heute",
|
||||
"chat_ts_12h": "12-Stunden",
|
||||
"chat_ts_24h": "24-Stunden",
|
||||
"chat_ts_global": "Global folgen",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Einblenden",
|
||||
"chat_unmute": "Stummschaltung aufheben",
|
||||
"chat_verify_key": "Identitätsschlüssel – zum Verifizieren vergleichen",
|
||||
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.",
|
||||
"chat_yesterday": "Gestern",
|
||||
"chat_you": "Du",
|
||||
"choose_icon": "Symbol wählen",
|
||||
"clear": "Leeren",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||
"click_copy_uri": "Klicken zum Kopieren der URI",
|
||||
"click_to_copy": "Klicken zum Kopieren",
|
||||
"clock_format": "Uhrzeitformat",
|
||||
"close": "Schließen",
|
||||
"conf_count": "%d Best.",
|
||||
"confirm_and_send": "Bestätigen & Senden",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
|
||||
"tt_change_pin": "Ihre Entsperr-PIN ändern",
|
||||
"tt_clear_ztx": "Lokal zwischengespeicherten Z-Transaktionsverlauf löschen",
|
||||
"tt_clock_format": "24- oder 12-Stunden-Uhr, app-weit. Der Chat-Tab kann sie überschreiben.",
|
||||
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
||||
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
|
||||
"tt_daemon_install_bundled": "Node stoppen, den installierten dragonxd mit der in diesem Wallet-Build enthaltenen Version überschreiben und dann neu starten",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "Cambiar frase de contraseña",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Ámbar",
|
||||
"chat_accent_blue": "Azul",
|
||||
"chat_accent_green": "Verde",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Morado",
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Añadir contacto",
|
||||
"chat_awaiting_key": "Esperando respuesta",
|
||||
"chat_bubble_minimal": "Mínima",
|
||||
"chat_bubble_rounded": "Redondeada",
|
||||
"chat_bubble_square": "Cuadrada",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contacto añadido: renómbralo en Contactos",
|
||||
"chat_contact_request": "solicitud de contacto",
|
||||
"chat_copy_address_tip": "Clic para copiar la dirección",
|
||||
"chat_density_comfortable": "Cómoda",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Color",
|
||||
"chat_emoji_mono": "Monocromo",
|
||||
"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\".",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "Mensaje demasiado largo",
|
||||
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
|
||||
"chat_mute": "Silenciar",
|
||||
"chat_new_button": "Nueva conversación",
|
||||
"chat_new_button": "Nuevo chat",
|
||||
"chat_new_message": "Mensaje",
|
||||
"chat_new_message_toast": "Nuevo mensaje de chat cifrado",
|
||||
"chat_new_send": "Enviar solicitud",
|
||||
"chat_new_title": "Nueva conversación",
|
||||
"chat_new_title": "Nuevo chat",
|
||||
"chat_new_zaddr": "Dirección z del destinatario",
|
||||
"chat_no_matches": "Ninguna conversación coincide con tu búsqueda.",
|
||||
"chat_no_z_contacts": "Aún no hay contactos con dirección blindada",
|
||||
"chat_opt_bubble_accent": "Color de burbuja",
|
||||
"chat_opt_bubble_style": "Estilo de burbuja",
|
||||
"chat_opt_density": "Densidad de mensajes",
|
||||
"chat_opt_emoji": "Estilo de emoji",
|
||||
"chat_opt_enter_sends": "Enter envía el mensaje",
|
||||
"chat_opt_font_size": "Tamaño del texto",
|
||||
"chat_opt_global_clock": "Formato de reloj global",
|
||||
"chat_opt_poll": "Frecuencia de sondeo",
|
||||
"chat_opt_timestamp": "Marcas de tiempo",
|
||||
"chat_pick_contact": "Elegir de contactos…",
|
||||
"chat_rename": "Renombrar contacto",
|
||||
"chat_rename_hint": "Nombre del contacto",
|
||||
"chat_renamed": "Contacto renombrado",
|
||||
"chat_retry": "Reintentar",
|
||||
"chat_search": "Buscar conversaciones",
|
||||
"chat_sec_appearance": "APARIENCIA",
|
||||
"chat_sec_messaging": "MENSAJES",
|
||||
"chat_select_hint": "Selecciona una conversación para verla.",
|
||||
"chat_send": "Enviar",
|
||||
"chat_send_failed": "no enviado",
|
||||
"chat_sending": "enviando…",
|
||||
"chat_settings_done": "Listo",
|
||||
"chat_settings_section": "CHAT Y CONTACTOS",
|
||||
"chat_settings_tip": "Personalizar chat",
|
||||
"chat_settings_title": "Ajustes de chat",
|
||||
"chat_show_hidden": "Ver ocultos",
|
||||
"chat_time_now": "ahora",
|
||||
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"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_today": "Hoy",
|
||||
"chat_ts_12h": "12 horas",
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reactivar",
|
||||
"chat_verify_key": "Clave de identidad: compárala para verificar",
|
||||
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
|
||||
"chat_yesterday": "Ayer",
|
||||
"chat_you": "Tú",
|
||||
"choose_icon": "Elegir Icono",
|
||||
"clear": "Limpiar",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "Clic para copiar dirección",
|
||||
"click_copy_uri": "Clic para copiar URI",
|
||||
"click_to_copy": "Clic para copiar",
|
||||
"clock_format": "Formato de hora",
|
||||
"close": "Cerrar",
|
||||
"conf_count": "%d conf",
|
||||
"confirm_and_send": "Confirmar y Enviar",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
|
||||
"tt_change_pin": "Cambiar su PIN de desbloqueo",
|
||||
"tt_clear_ztx": "Eliminar historial de z-transacciones en caché local",
|
||||
"tt_clock_format": "Reloj de 24 o 12 horas, en toda la app. El chat puede anularlo.",
|
||||
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
||||
"tt_custom_theme": "Tema personalizado activo",
|
||||
"tt_daemon_install_bundled": "Detiene el nodo, sobrescribe el dragonxd instalado con la versión incluida en esta compilación de la cartera y luego lo reinicia",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "Changer la phrase secrète",
|
||||
"characters": "caractères",
|
||||
"chat": "Discussion",
|
||||
"chat_accent_amber": "Ambre",
|
||||
"chat_accent_blue": "Bleu",
|
||||
"chat_accent_green": "Vert",
|
||||
"chat_accent_pink": "Rose",
|
||||
"chat_accent_purple": "Violet",
|
||||
"chat_accent_theme": "Thème",
|
||||
"chat_add_contact": "Ajouter un contact",
|
||||
"chat_awaiting_key": "En attente de réponse",
|
||||
"chat_bubble_minimal": "Minimale",
|
||||
"chat_bubble_rounded": "Arrondie",
|
||||
"chat_bubble_square": "Carrée",
|
||||
"chat_cancel": "Annuler",
|
||||
"chat_contact_added": "Contact ajouté — renommez-le dans Contacts",
|
||||
"chat_contact_request": "demande de contact",
|
||||
"chat_copy_address_tip": "Cliquer pour copier l'adresse",
|
||||
"chat_density_comfortable": "Confortable",
|
||||
"chat_density_compact": "Compacte",
|
||||
"chat_emoji_color": "Couleur",
|
||||
"chat_emoji_mono": "Monochrome",
|
||||
"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 ».",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "Message trop long",
|
||||
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
|
||||
"chat_mute": "Muet",
|
||||
"chat_new_button": "Nouvelle conversation",
|
||||
"chat_new_button": "Nouvelle discussion",
|
||||
"chat_new_message": "Message",
|
||||
"chat_new_message_toast": "Nouveau message chiffré",
|
||||
"chat_new_send": "Envoyer la demande",
|
||||
"chat_new_title": "Nouvelle conversation",
|
||||
"chat_new_title": "Nouvelle discussion",
|
||||
"chat_new_zaddr": "Adresse Z du destinataire",
|
||||
"chat_no_matches": "Aucune conversation ne correspond à votre recherche.",
|
||||
"chat_no_z_contacts": "Aucun contact avec adresse blindée pour l'instant",
|
||||
"chat_opt_bubble_accent": "Couleur de bulle",
|
||||
"chat_opt_bubble_style": "Style de bulle",
|
||||
"chat_opt_density": "Densité des messages",
|
||||
"chat_opt_emoji": "Style d'emoji",
|
||||
"chat_opt_enter_sends": "Entrée envoie le message",
|
||||
"chat_opt_font_size": "Taille du texte",
|
||||
"chat_opt_global_clock": "Format d'horloge global",
|
||||
"chat_opt_poll": "Fréquence d'actualisation",
|
||||
"chat_opt_timestamp": "Horodatage",
|
||||
"chat_pick_contact": "Choisir dans les contacts…",
|
||||
"chat_rename": "Renommer le contact",
|
||||
"chat_rename_hint": "Nom du contact",
|
||||
"chat_renamed": "Contact renommé",
|
||||
"chat_retry": "Réessayer",
|
||||
"chat_search": "Rechercher des conversations",
|
||||
"chat_sec_appearance": "APPARENCE",
|
||||
"chat_sec_messaging": "MESSAGERIE",
|
||||
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
|
||||
"chat_send": "Envoyer",
|
||||
"chat_send_failed": "non envoyé",
|
||||
"chat_sending": "envoi…",
|
||||
"chat_settings_done": "Terminé",
|
||||
"chat_settings_section": "CHAT ET CONTACTS",
|
||||
"chat_settings_tip": "Personnaliser le chat",
|
||||
"chat_settings_title": "Paramètres du chat",
|
||||
"chat_show_hidden": "Afficher masqués",
|
||||
"chat_time_now": "à l'instant",
|
||||
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"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_today": "Aujourd'hui",
|
||||
"chat_ts_12h": "12 heures",
|
||||
"chat_ts_24h": "24 heures",
|
||||
"chat_ts_global": "Suivre global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Afficher",
|
||||
"chat_unmute": "Réactiver",
|
||||
"chat_verify_key": "Clé d'identité — comparez pour vérifier",
|
||||
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
|
||||
"chat_yesterday": "Hier",
|
||||
"chat_you": "Vous",
|
||||
"choose_icon": "Choisir une icône",
|
||||
"clear": "Effacer",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "Cliquez pour copier l'adresse",
|
||||
"click_copy_uri": "Cliquez pour copier l'URI",
|
||||
"click_to_copy": "Cliquez pour copier",
|
||||
"clock_format": "Format d'horloge",
|
||||
"close": "Fermer",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmer & Envoyer",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
|
||||
"tt_change_pin": "Changer votre PIN de déverrouillage",
|
||||
"tt_clear_ztx": "Supprimer l'historique des z-transactions mis en cache localement",
|
||||
"tt_clock_format": "Horloge 24 h ou 12 h, dans toute l'app. Le chat peut la remplacer.",
|
||||
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
||||
"tt_custom_theme": "Thème personnalisé actif",
|
||||
"tt_daemon_install_bundled": "Arrêter le nœud, remplacer le dragonxd installé par la version intégrée dans cette version du portefeuille, puis redémarrer",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "パスフレーズを変更",
|
||||
"characters": "文字",
|
||||
"chat": "チャット",
|
||||
"chat_accent_amber": "琥珀",
|
||||
"chat_accent_blue": "青",
|
||||
"chat_accent_green": "緑",
|
||||
"chat_accent_pink": "ピンク",
|
||||
"chat_accent_purple": "紫",
|
||||
"chat_accent_theme": "テーマ",
|
||||
"chat_add_contact": "連絡先に追加",
|
||||
"chat_awaiting_key": "返信待ち",
|
||||
"chat_bubble_minimal": "ミニマル",
|
||||
"chat_bubble_rounded": "角丸",
|
||||
"chat_bubble_square": "角ばった",
|
||||
"chat_cancel": "キャンセル",
|
||||
"chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます",
|
||||
"chat_contact_request": "連絡リクエスト",
|
||||
"chat_copy_address_tip": "クリックしてアドレスをコピー",
|
||||
"chat_density_comfortable": "ゆったり",
|
||||
"chat_density_compact": "コンパクト",
|
||||
"chat_emoji_color": "カラー",
|
||||
"chat_emoji_mono": "モノクロ",
|
||||
"chat_emoji_search": "絵文字を検索",
|
||||
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
|
||||
"chat_empty_start": "「新しい会話」から始めましょう。",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "メッセージが長すぎます",
|
||||
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
|
||||
"chat_mute": "ミュート",
|
||||
"chat_new_button": "新しい会話",
|
||||
"chat_new_button": "新しいチャット",
|
||||
"chat_new_message": "メッセージ",
|
||||
"chat_new_message_toast": "新しい暗号化チャットメッセージ",
|
||||
"chat_new_send": "リクエストを送信",
|
||||
"chat_new_title": "新しい会話",
|
||||
"chat_new_title": "新しいチャット",
|
||||
"chat_new_zaddr": "宛先Zアドレス",
|
||||
"chat_no_matches": "検索に一致する会話がありません。",
|
||||
"chat_no_z_contacts": "シールドアドレスの連絡先はまだありません",
|
||||
"chat_opt_bubble_accent": "吹き出しの色",
|
||||
"chat_opt_bubble_style": "吹き出しスタイル",
|
||||
"chat_opt_density": "メッセージ密度",
|
||||
"chat_opt_emoji": "絵文字スタイル",
|
||||
"chat_opt_enter_sends": "Enterで送信",
|
||||
"chat_opt_font_size": "文字サイズ",
|
||||
"chat_opt_global_clock": "全体の時刻形式",
|
||||
"chat_opt_poll": "取得間隔",
|
||||
"chat_opt_timestamp": "タイムスタンプ",
|
||||
"chat_pick_contact": "連絡先から選択…",
|
||||
"chat_rename": "連絡先の名前を変更",
|
||||
"chat_rename_hint": "連絡先名",
|
||||
"chat_renamed": "連絡先の名前を変更しました",
|
||||
"chat_retry": "再送信",
|
||||
"chat_search": "会話を検索",
|
||||
"chat_sec_appearance": "外観",
|
||||
"chat_sec_messaging": "メッセージ",
|
||||
"chat_select_hint": "表示する会話を選択してください。",
|
||||
"chat_send": "送信",
|
||||
"chat_send_failed": "未送信",
|
||||
"chat_sending": "送信中…",
|
||||
"chat_settings_done": "完了",
|
||||
"chat_settings_section": "チャットと連絡先",
|
||||
"chat_settings_tip": "チャットのカスタマイズ",
|
||||
"chat_settings_title": "チャット設定",
|
||||
"chat_show_hidden": "非表示を表示",
|
||||
"chat_time_now": "たった今",
|
||||
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
|
||||
@@ -181,10 +212,16 @@
|
||||
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
||||
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
||||
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
||||
"chat_today": "今日",
|
||||
"chat_ts_12h": "12時間",
|
||||
"chat_ts_24h": "24時間",
|
||||
"chat_ts_global": "全体設定に従う",
|
||||
"chat_ts_global_short": "全体",
|
||||
"chat_unhide": "再表示",
|
||||
"chat_unmute": "ミュート解除",
|
||||
"chat_verify_key": "識別鍵 — 照合して確認",
|
||||
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
|
||||
"chat_yesterday": "昨日",
|
||||
"chat_you": "自分",
|
||||
"choose_icon": "アイコンを選択",
|
||||
"clear": "クリア",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "クリックしてアドレスをコピー",
|
||||
"click_copy_uri": "クリックしてURIをコピー",
|
||||
"click_to_copy": "クリックしてコピー",
|
||||
"clock_format": "時刻形式",
|
||||
"close": "閉じる",
|
||||
"conf_count": "%d 確認",
|
||||
"confirm_and_send": "確認して送金",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
||||
"tt_change_pin": "アンロック PIN を変更",
|
||||
"tt_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||
"tt_clock_format": "24時間または12時間表示(アプリ全体)。チャットで上書きできます。",
|
||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "암호 변경",
|
||||
"characters": "문자",
|
||||
"chat": "채팅",
|
||||
"chat_accent_amber": "황색",
|
||||
"chat_accent_blue": "파랑",
|
||||
"chat_accent_green": "초록",
|
||||
"chat_accent_pink": "분홍",
|
||||
"chat_accent_purple": "보라",
|
||||
"chat_accent_theme": "테마",
|
||||
"chat_add_contact": "연락처 추가",
|
||||
"chat_awaiting_key": "답장 대기 중",
|
||||
"chat_bubble_minimal": "미니멀",
|
||||
"chat_bubble_rounded": "둥근",
|
||||
"chat_bubble_square": "각진",
|
||||
"chat_cancel": "취소",
|
||||
"chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요",
|
||||
"chat_contact_request": "연락 요청",
|
||||
"chat_copy_address_tip": "클릭하여 주소 복사",
|
||||
"chat_density_comfortable": "편안하게",
|
||||
"chat_density_compact": "촘촘하게",
|
||||
"chat_emoji_color": "컬러",
|
||||
"chat_emoji_mono": "단색",
|
||||
"chat_emoji_search": "이모지 검색",
|
||||
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
|
||||
"chat_empty_start": "\"새 대화\"로 시작하세요.",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "메시지가 너무 깁니다",
|
||||
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
|
||||
"chat_mute": "음소거",
|
||||
"chat_new_button": "새 대화",
|
||||
"chat_new_button": "새 채팅",
|
||||
"chat_new_message": "메시지",
|
||||
"chat_new_message_toast": "새 암호화 채팅 메시지",
|
||||
"chat_new_send": "요청 보내기",
|
||||
"chat_new_title": "새 대화",
|
||||
"chat_new_title": "새 채팅",
|
||||
"chat_new_zaddr": "받는 사람 z-주소",
|
||||
"chat_no_matches": "검색과 일치하는 대화가 없습니다.",
|
||||
"chat_no_z_contacts": "보호 주소 연락처가 아직 없습니다",
|
||||
"chat_opt_bubble_accent": "말풍선 색상",
|
||||
"chat_opt_bubble_style": "말풍선 스타일",
|
||||
"chat_opt_density": "메시지 밀도",
|
||||
"chat_opt_emoji": "이모지 스타일",
|
||||
"chat_opt_enter_sends": "Enter로 전송",
|
||||
"chat_opt_font_size": "글자 크기",
|
||||
"chat_opt_global_clock": "전역 시간 형식",
|
||||
"chat_opt_poll": "폴링 주기",
|
||||
"chat_opt_timestamp": "타임스탬프",
|
||||
"chat_pick_contact": "연락처에서 선택…",
|
||||
"chat_rename": "연락처 이름 변경",
|
||||
"chat_rename_hint": "연락처 이름",
|
||||
"chat_renamed": "연락처 이름이 변경되었습니다",
|
||||
"chat_retry": "다시 시도",
|
||||
"chat_search": "대화 검색",
|
||||
"chat_sec_appearance": "모양",
|
||||
"chat_sec_messaging": "메시지",
|
||||
"chat_select_hint": "볼 대화를 선택하세요.",
|
||||
"chat_send": "전송",
|
||||
"chat_send_failed": "전송 안 됨",
|
||||
"chat_sending": "전송 중…",
|
||||
"chat_settings_done": "완료",
|
||||
"chat_settings_section": "채팅 및 연락처",
|
||||
"chat_settings_tip": "채팅 사용자 지정",
|
||||
"chat_settings_title": "채팅 설정",
|
||||
"chat_show_hidden": "숨김 보기",
|
||||
"chat_time_now": "방금",
|
||||
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
||||
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
||||
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
||||
"chat_today": "오늘",
|
||||
"chat_ts_12h": "12시간",
|
||||
"chat_ts_24h": "24시간",
|
||||
"chat_ts_global": "전역 설정 따르기",
|
||||
"chat_ts_global_short": "전역",
|
||||
"chat_unhide": "다시 표시",
|
||||
"chat_unmute": "음소거 해제",
|
||||
"chat_verify_key": "신원 키 — 비교하여 확인",
|
||||
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
|
||||
"chat_yesterday": "어제",
|
||||
"chat_you": "나",
|
||||
"choose_icon": "아이콘 선택",
|
||||
"clear": "지우기",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "클릭하여 주소 복사",
|
||||
"click_copy_uri": "클릭하여 URI 복사",
|
||||
"click_to_copy": "복사하려면 클릭",
|
||||
"clock_format": "시간 형식",
|
||||
"close": "닫기",
|
||||
"conf_count": "%d 확인",
|
||||
"confirm_and_send": "확인 후 전송",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
||||
"tt_change_pin": "잠금 해제 PIN 변경",
|
||||
"tt_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||
"tt_clock_format": "24시간 또는 12시간 형식(앱 전체). 채팅에서 재정의할 수 있습니다.",
|
||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "Alterar senha",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Âmbar",
|
||||
"chat_accent_blue": "Azul",
|
||||
"chat_accent_green": "Verde",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Roxo",
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Adicionar contato",
|
||||
"chat_awaiting_key": "Aguardando resposta",
|
||||
"chat_bubble_minimal": "Mínimo",
|
||||
"chat_bubble_rounded": "Arredondado",
|
||||
"chat_bubble_square": "Quadrado",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contato adicionado — renomeie em Contatos",
|
||||
"chat_contact_request": "solicitação de contato",
|
||||
"chat_copy_address_tip": "Clique para copiar o endereço",
|
||||
"chat_density_comfortable": "Confortável",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Colorido",
|
||||
"chat_emoji_mono": "Monocromático",
|
||||
"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\".",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "Mensagem muito longa",
|
||||
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
|
||||
"chat_mute": "Silenciar",
|
||||
"chat_new_button": "Nova conversa",
|
||||
"chat_new_button": "Novo chat",
|
||||
"chat_new_message": "Mensagem",
|
||||
"chat_new_message_toast": "Nova mensagem de chat criptografada",
|
||||
"chat_new_send": "Enviar solicitação",
|
||||
"chat_new_title": "Nova conversa",
|
||||
"chat_new_title": "Novo chat",
|
||||
"chat_new_zaddr": "Endereço-z do destinatário",
|
||||
"chat_no_matches": "Nenhuma conversa corresponde à sua pesquisa.",
|
||||
"chat_no_z_contacts": "Ainda não há contatos com endereço blindado",
|
||||
"chat_opt_bubble_accent": "Cor do balão",
|
||||
"chat_opt_bubble_style": "Estilo do balão",
|
||||
"chat_opt_density": "Densidade das mensagens",
|
||||
"chat_opt_emoji": "Estilo de emoji",
|
||||
"chat_opt_enter_sends": "Enter envia a mensagem",
|
||||
"chat_opt_font_size": "Tamanho do texto",
|
||||
"chat_opt_global_clock": "Formato de relógio global",
|
||||
"chat_opt_poll": "Taxa de atualização",
|
||||
"chat_opt_timestamp": "Carimbos de data/hora",
|
||||
"chat_pick_contact": "Escolher dos contatos…",
|
||||
"chat_rename": "Renomear contato",
|
||||
"chat_rename_hint": "Nome do contato",
|
||||
"chat_renamed": "Contato renomeado",
|
||||
"chat_retry": "Tentar novamente",
|
||||
"chat_search": "Pesquisar conversas",
|
||||
"chat_sec_appearance": "APARÊNCIA",
|
||||
"chat_sec_messaging": "MENSAGENS",
|
||||
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
|
||||
"chat_send": "Enviar",
|
||||
"chat_send_failed": "não enviada",
|
||||
"chat_sending": "enviando…",
|
||||
"chat_settings_done": "Concluído",
|
||||
"chat_settings_section": "CHAT E CONTATOS",
|
||||
"chat_settings_tip": "Personalizar chat",
|
||||
"chat_settings_title": "Configurações de chat",
|
||||
"chat_show_hidden": "Ver ocultas",
|
||||
"chat_time_now": "agora",
|
||||
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"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_today": "Hoje",
|
||||
"chat_ts_12h": "12 horas",
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reativar som",
|
||||
"chat_verify_key": "Chave de identidade — compare para verificar",
|
||||
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
|
||||
"chat_yesterday": "Ontem",
|
||||
"chat_you": "Você",
|
||||
"choose_icon": "Escolher Ícone",
|
||||
"clear": "Limpar",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "Clique para copiar o endereço",
|
||||
"click_copy_uri": "Clique para copiar a URI",
|
||||
"click_to_copy": "Clique para copiar",
|
||||
"clock_format": "Formato de hora",
|
||||
"close": "Fechar",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmar & Enviar",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
|
||||
"tt_change_pin": "Alterar seu PIN de desbloqueio",
|
||||
"tt_clear_ztx": "Excluir histórico de z-transações em cache local",
|
||||
"tt_clock_format": "Relógio de 24 ou 12 horas, em todo o app. O chat pode substituí-lo.",
|
||||
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
||||
"tt_custom_theme": "Tema personalizado ativo",
|
||||
"tt_daemon_install_bundled": "Parar o nó, sobrescrever o dragonxd instalado com a versão incluída nesta compilação da carteira e reiniciar",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "Сменить пароль",
|
||||
"characters": "символов",
|
||||
"chat": "Чат",
|
||||
"chat_accent_amber": "Янтарный",
|
||||
"chat_accent_blue": "Синий",
|
||||
"chat_accent_green": "Зелёный",
|
||||
"chat_accent_pink": "Розовый",
|
||||
"chat_accent_purple": "Фиолетовый",
|
||||
"chat_accent_theme": "Тема",
|
||||
"chat_add_contact": "Добавить контакт",
|
||||
"chat_awaiting_key": "Ожидание ответа",
|
||||
"chat_bubble_minimal": "Минимальный",
|
||||
"chat_bubble_rounded": "Скруглённый",
|
||||
"chat_bubble_square": "Прямоугольный",
|
||||
"chat_cancel": "Отмена",
|
||||
"chat_contact_added": "Контакт добавлен — переименуйте его в Контактах",
|
||||
"chat_contact_request": "запрос контакта",
|
||||
"chat_copy_address_tip": "Нажмите, чтобы скопировать адрес",
|
||||
"chat_density_comfortable": "Свободная",
|
||||
"chat_density_compact": "Компактная",
|
||||
"chat_emoji_color": "Цветной",
|
||||
"chat_emoji_mono": "Монохромный",
|
||||
"chat_emoji_search": "Поиск эмодзи",
|
||||
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
|
||||
"chat_empty_start": "Начните новый с помощью «Новый разговор».",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "Сообщение слишком длинное",
|
||||
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
|
||||
"chat_mute": "Отключить уведомления",
|
||||
"chat_new_button": "Новая переписка",
|
||||
"chat_new_button": "Новый чат",
|
||||
"chat_new_message": "Сообщение",
|
||||
"chat_new_message_toast": "Новое зашифрованное сообщение",
|
||||
"chat_new_send": "Отправить запрос",
|
||||
"chat_new_title": "Новая переписка",
|
||||
"chat_new_title": "Новый чат",
|
||||
"chat_new_zaddr": "Z-адрес получателя",
|
||||
"chat_no_matches": "Нет разговоров, соответствующих запросу.",
|
||||
"chat_no_z_contacts": "Пока нет контактов с защищённым адресом",
|
||||
"chat_opt_bubble_accent": "Цвет пузырька",
|
||||
"chat_opt_bubble_style": "Стиль пузырька",
|
||||
"chat_opt_density": "Плотность сообщений",
|
||||
"chat_opt_emoji": "Стиль эмодзи",
|
||||
"chat_opt_enter_sends": "Enter отправляет сообщение",
|
||||
"chat_opt_font_size": "Размер текста",
|
||||
"chat_opt_global_clock": "Глобальный формат времени",
|
||||
"chat_opt_poll": "Частота опроса",
|
||||
"chat_opt_timestamp": "Метки времени",
|
||||
"chat_pick_contact": "Выбрать из контактов…",
|
||||
"chat_rename": "Переименовать контакт",
|
||||
"chat_rename_hint": "Имя контакта",
|
||||
"chat_renamed": "Контакт переименован",
|
||||
"chat_retry": "Повторить",
|
||||
"chat_search": "Поиск разговоров",
|
||||
"chat_sec_appearance": "ВИД",
|
||||
"chat_sec_messaging": "СООБЩЕНИЯ",
|
||||
"chat_select_hint": "Выберите переписку для просмотра.",
|
||||
"chat_send": "Отправить",
|
||||
"chat_send_failed": "не отправлено",
|
||||
"chat_sending": "отправка…",
|
||||
"chat_settings_done": "Готово",
|
||||
"chat_settings_section": "ЧАТ И КОНТАКТЫ",
|
||||
"chat_settings_tip": "Настройка чата",
|
||||
"chat_settings_title": "Настройки чата",
|
||||
"chat_show_hidden": "Показать скрытые",
|
||||
"chat_time_now": "сейчас",
|
||||
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
|
||||
@@ -181,10 +212,16 @@
|
||||
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
||||
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
||||
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
||||
"chat_today": "Сегодня",
|
||||
"chat_ts_12h": "12 часов",
|
||||
"chat_ts_24h": "24 часа",
|
||||
"chat_ts_global": "Как глобально",
|
||||
"chat_ts_global_short": "Общий",
|
||||
"chat_unhide": "Показать",
|
||||
"chat_unmute": "Включить уведомления",
|
||||
"chat_verify_key": "Ключ личности — сравните для проверки",
|
||||
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
|
||||
"chat_yesterday": "Вчера",
|
||||
"chat_you": "Вы",
|
||||
"choose_icon": "Выбрать иконку",
|
||||
"clear": "Очистить",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
||||
"click_to_copy": "Нажмите для копирования",
|
||||
"clock_format": "Формат времени",
|
||||
"close": "Закрыть",
|
||||
"conf_count": "%d подтв.",
|
||||
"confirm_and_send": "Подтвердить и отправить",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
||||
"tt_change_pin": "Изменить PIN-код разблокировки",
|
||||
"tt_clear_ztx": "Удалить локально кешированную историю z-транзакций",
|
||||
"tt_clock_format": "24- или 12-часовой формат для всего приложения. Чат может переопределить.",
|
||||
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
||||
"tt_custom_theme": "Пользовательская тема активна",
|
||||
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
||||
|
||||
@@ -135,12 +135,25 @@
|
||||
"change_pass_title": "更改密码短语",
|
||||
"characters": "字符",
|
||||
"chat": "聊天",
|
||||
"chat_accent_amber": "琥珀色",
|
||||
"chat_accent_blue": "蓝色",
|
||||
"chat_accent_green": "绿色",
|
||||
"chat_accent_pink": "粉色",
|
||||
"chat_accent_purple": "紫色",
|
||||
"chat_accent_theme": "主题",
|
||||
"chat_add_contact": "添加联系人",
|
||||
"chat_awaiting_key": "等待回复",
|
||||
"chat_bubble_minimal": "极简",
|
||||
"chat_bubble_rounded": "圆角",
|
||||
"chat_bubble_square": "方形",
|
||||
"chat_cancel": "取消",
|
||||
"chat_contact_added": "已添加联系人——可在联系人中重命名",
|
||||
"chat_contact_request": "联系人请求",
|
||||
"chat_copy_address_tip": "点击复制地址",
|
||||
"chat_density_comfortable": "宽松",
|
||||
"chat_density_compact": "紧凑",
|
||||
"chat_emoji_color": "彩色",
|
||||
"chat_emoji_mono": "单色",
|
||||
"chat_emoji_search": "搜索表情",
|
||||
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
|
||||
"chat_empty_start": "点击\"新建会话\"开始。",
|
||||
@@ -156,21 +169,39 @@
|
||||
"chat_len_over": "消息过长",
|
||||
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
|
||||
"chat_mute": "静音",
|
||||
"chat_new_button": "新建对话",
|
||||
"chat_new_button": "新聊天",
|
||||
"chat_new_message": "消息",
|
||||
"chat_new_message_toast": "新的加密聊天消息",
|
||||
"chat_new_send": "发送请求",
|
||||
"chat_new_title": "新建对话",
|
||||
"chat_new_title": "新聊天",
|
||||
"chat_new_zaddr": "收款方 z 地址",
|
||||
"chat_no_matches": "没有与搜索匹配的会话。",
|
||||
"chat_no_z_contacts": "暂无使用隐私地址的联系人",
|
||||
"chat_opt_bubble_accent": "气泡颜色",
|
||||
"chat_opt_bubble_style": "气泡样式",
|
||||
"chat_opt_density": "消息密度",
|
||||
"chat_opt_emoji": "表情样式",
|
||||
"chat_opt_enter_sends": "回车发送消息",
|
||||
"chat_opt_font_size": "文字大小",
|
||||
"chat_opt_global_clock": "全局时间格式",
|
||||
"chat_opt_poll": "轮询频率",
|
||||
"chat_opt_timestamp": "时间戳",
|
||||
"chat_pick_contact": "从联系人中选择…",
|
||||
"chat_rename": "重命名联系人",
|
||||
"chat_rename_hint": "联系人名称",
|
||||
"chat_renamed": "联系人已重命名",
|
||||
"chat_retry": "重试",
|
||||
"chat_search": "搜索会话",
|
||||
"chat_sec_appearance": "外观",
|
||||
"chat_sec_messaging": "消息",
|
||||
"chat_select_hint": "选择一个对话以查看。",
|
||||
"chat_send": "发送",
|
||||
"chat_send_failed": "未发送",
|
||||
"chat_sending": "发送中…",
|
||||
"chat_settings_done": "完成",
|
||||
"chat_settings_section": "聊天与联系人",
|
||||
"chat_settings_tip": "聊天自定义",
|
||||
"chat_settings_title": "聊天设置",
|
||||
"chat_show_hidden": "显示已隐藏",
|
||||
"chat_time_now": "刚刚",
|
||||
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
|
||||
@@ -181,10 +212,16 @@
|
||||
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
||||
"chat_toast_request_queued": "联系人请求已排队。",
|
||||
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||
"chat_today": "今天",
|
||||
"chat_ts_12h": "12小时",
|
||||
"chat_ts_24h": "24小时",
|
||||
"chat_ts_global": "跟随全局",
|
||||
"chat_ts_global_short": "全局",
|
||||
"chat_unhide": "取消隐藏",
|
||||
"chat_unmute": "取消静音",
|
||||
"chat_verify_key": "身份密钥 — 对比以验证",
|
||||
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||
"chat_yesterday": "昨天",
|
||||
"chat_you": "我",
|
||||
"choose_icon": "选择图标",
|
||||
"clear": "清除",
|
||||
@@ -196,6 +233,7 @@
|
||||
"click_copy_address": "点击复制地址",
|
||||
"click_copy_uri": "点击复制 URI",
|
||||
"click_to_copy": "点击复制",
|
||||
"clock_format": "时间格式",
|
||||
"close": "关闭",
|
||||
"conf_count": "%d 确认",
|
||||
"confirm_and_send": "确认并发送",
|
||||
@@ -1388,6 +1426,7 @@
|
||||
"tt_change_pass": "更改钱包加密密码",
|
||||
"tt_change_pin": "更改您的解锁 PIN",
|
||||
"tt_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||
"tt_clock_format": "24 或 12 小时制,应用全局。聊天可覆盖。",
|
||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||
"tt_custom_theme": "自定义主题已激活",
|
||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <sodium.h>
|
||||
#include "../../util/logger.h"
|
||||
#include "../windows/balance_tab.h"
|
||||
#include "../windows/chat_tab.h" // RenderChatSettingsControls (shared Chat & Contacts controls)
|
||||
#include "../windows/console_tab.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
@@ -821,6 +822,25 @@ void RenderSettingsPage(App* app) {
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// --- Clock format (app-wide 24h / 12h — the Chat tab can override it in chat settings) ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("clock_format"));
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
int cf = app->settings() ? app->settings()->getTimeFormat() : 0;
|
||||
const char* cfItems[] = { TR("chat_ts_24h"), TR("chat_ts_12h") };
|
||||
ImGui::SetNextItemWidth(160.0f * Layout::dpiScale());
|
||||
if (ImGui::Combo("##ClockFormat", &cf, cfItems, 2) && app->settings()) {
|
||||
app->settings()->setTimeFormat(cf);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clock_format"));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// --- Font Scale slider (always visible) ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
@@ -1086,6 +1106,25 @@ void RenderSettingsPage(App* app) {
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// --- Clock format (app-wide 24h / 12h — the Chat tab can override it in chat settings) ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("clock_format"));
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
int cf = app->settings() ? app->settings()->getTimeFormat() : 0;
|
||||
const char* cfItems[] = { TR("chat_ts_24h"), TR("chat_ts_12h") };
|
||||
ImGui::SetNextItemWidth(160.0f * Layout::dpiScale());
|
||||
if (ImGui::Combo("##ClockFormat", &cf, cfItems, 2) && app->settings()) {
|
||||
app->settings()->setTimeFormat(cf);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clock_format"));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// --- Font Scale slider (always visible) ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
@@ -2445,6 +2484,21 @@ void RenderSettingsPage(App* app) {
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// CHAT & CONTACTS — card (same controls as the Chat tab's settings notch)
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("chat_settings_section"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||
ImGui::PushFont(body2);
|
||||
RenderChatSettingsControls(app, availWidth - pad * 2.0f); // card inner width (GlassCard doesn't narrow it)
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// ABOUT — card
|
||||
// ====================================================================
|
||||
|
||||
@@ -52,6 +52,20 @@ bool s_show_hidden = false; // when on, the list also shows hidden conversatio
|
||||
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
|
||||
|
||||
// Inline contact rename (Tier C): the conversation whose header name is being edited (empty = none),
|
||||
// the edit buffer, and a one-shot flag to grab keyboard focus the frame the field appears.
|
||||
std::string s_rename_cid;
|
||||
char s_rename_buf[128] = ""; // matches the Contacts tab's label editor capacity (no silent truncation)
|
||||
bool s_rename_focus = false;
|
||||
|
||||
// Chat customization modal (opened by the header settings "notch"). The same controls also render in
|
||||
// Settings → Chat & Contacts via RenderChatSettingsControls().
|
||||
bool s_show_chat_settings = false;
|
||||
|
||||
// Resolved once per frame from the chat/global timestamp settings — read by formatTime() (which has no
|
||||
// App access). true => 12-hour clock in the Chat tab.
|
||||
bool s_chat_time_12h = false;
|
||||
|
||||
// Effective draw-list font size for a material font (mirrors sidebar's ScaledFontSize).
|
||||
float scaledSize(ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; }
|
||||
|
||||
@@ -75,8 +89,42 @@ std::string formatTime(std::int64_t ts) {
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t); // UI thread only
|
||||
if (!tm) return "";
|
||||
char buf[40];
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%Y-%m-%d %I:%M %p" : "%Y-%m-%d %H:%M", tm);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Time-of-day only (no date), honoring the chat clock. Used on grouped meta lines + hover (the date
|
||||
// now lives in a per-day separator, so the group header doesn't repeat it). UI thread only.
|
||||
std::string chatTimeOnly(std::int64_t ts) {
|
||||
if (ts <= 0) return "";
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return "";
|
||||
char buf[24];
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%I:%M %p" : "%H:%M", tm);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Local calendar-day key (YYYYMMDD) for detecting day boundaries between messages. UI thread only.
|
||||
std::int64_t chatDayKey(std::int64_t ts) {
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return -1;
|
||||
return static_cast<std::int64_t>(tm->tm_year + 1900) * 10000 +
|
||||
static_cast<std::int64_t>(tm->tm_mon + 1) * 100 + tm->tm_mday;
|
||||
}
|
||||
|
||||
// Date-separator label: "Today" / "Yesterday" / "Jul 16, 2026". UI thread only.
|
||||
std::string chatDaySeparator(std::int64_t ts, std::int64_t todayKey, std::int64_t yestKey) {
|
||||
const std::int64_t k = chatDayKey(ts);
|
||||
if (k == todayKey) return TR("chat_today");
|
||||
if (k == yestKey) return TR("chat_yesterday");
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return "";
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", tm);
|
||||
std::strftime(buf, sizeof(buf), "%b %d, %Y", tm);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -117,6 +165,18 @@ ImU32 avatarColor(const std::string& seed) {
|
||||
return kPalette[h % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
||||
}
|
||||
|
||||
// Outgoing-bubble accent base color for a chat_bubble_accent preset (0 = theme primary).
|
||||
ImU32 bubbleAccentColor(int accent) {
|
||||
switch (accent) {
|
||||
case 1: return IM_COL32(0x42, 0x85, 0xF4, 255); // blue
|
||||
case 2: return IM_COL32(0x34, 0xA8, 0x53, 255); // green
|
||||
case 3: return IM_COL32(0x9C, 0x27, 0xB0, 255); // purple
|
||||
case 4: return IM_COL32(0xFF, 0xA7, 0x26, 255); // amber
|
||||
case 5: return IM_COL32(0xEC, 0x40, 0x7A, 255); // pink
|
||||
default: return material::Primary(); // theme
|
||||
}
|
||||
}
|
||||
|
||||
// Uppercase first glyph of a display name (UTF-8 aware) for a letter-avatar.
|
||||
std::string initialOf(const std::string& name) {
|
||||
std::size_t i = 0;
|
||||
@@ -313,6 +373,12 @@ void RenderChatTab(App* app)
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the Chat-tab clock once per frame: the chat override wins, else the global format.
|
||||
if (auto* cst = app->settings()) {
|
||||
const int ctf = cst->getChatTimeFormat(); // 0=follow global, 1=24h, 2=12h
|
||||
s_chat_time_12h = (ctf == 2) || (ctf == 0 && cst->getTimeFormat() == 1);
|
||||
}
|
||||
|
||||
// Build conversation summaries (single scan per conversation), sorted by most-recent activity.
|
||||
std::vector<ConvSummary> convs;
|
||||
int hiddenCount = 0;
|
||||
@@ -354,6 +420,13 @@ void RenderChatTab(App* app)
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_compose_cid = s_selected_cid;
|
||||
}
|
||||
// Abandon an in-progress header rename tied to a different conversation (switch/hide), so a
|
||||
// half-typed buffer can't silently reappear or land on the wrong contact (Tier C review).
|
||||
if (!s_rename_cid.empty() && s_rename_cid != s_selected_cid) {
|
||||
s_rename_cid.clear();
|
||||
s_rename_buf[0] = '\0';
|
||||
s_rename_focus = false;
|
||||
}
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f);
|
||||
@@ -371,39 +444,49 @@ void RenderChatTab(App* app)
|
||||
// ---- Left: new-conversation button + conversation list ----
|
||||
// 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);
|
||||
ImGui::BeginChild("##ChatList", ImVec2(listW, avail.y), ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll(); // wheel-driven lerp scroll, matching the rest of the app
|
||||
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).
|
||||
// "New chat" (accented) + "Show hidden (N)" toggle, side by side. Show-hidden only appears when
|
||||
// something is hidden; otherwise New chat spans the row.
|
||||
const float ldp = Layout::dpiScale();
|
||||
const bool hasHidden = hiddenCount > 0;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
const bool newClicked = material::TactileButton(TR("chat_new_button"), ImVec2(-FLT_MIN, 0.0f));
|
||||
ImGui::PopStyleColor(3);
|
||||
bool newClicked = false;
|
||||
if (hasHidden) {
|
||||
const std::string hlShow = std::string(TR("chat_show_hidden")) + " (" + std::to_string(hiddenCount) + ")";
|
||||
const std::string hlHide = TR("chat_hide_hidden");
|
||||
const std::string hl = s_show_hidden ? hlHide : hlShow;
|
||||
// Size the toggle to the WIDER of its two labels so it (and New chat) don't resize on click.
|
||||
const float hbw = std::max(ImGui::CalcTextSize(hlShow.c_str()).x,
|
||||
ImGui::CalcTextSize(hlHide.c_str()).x) + 16.0f * ldp;
|
||||
const float newW = std::max(80.0f * ldp,
|
||||
ImGui::GetContentRegionAvail().x - hbw - ImGui::GetStyle().ItemSpacing.x);
|
||||
newClicked = material::TactileButton(TR("chat_new_button"), ImVec2(newW, 0.0f));
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(hl.c_str(), ImVec2(hbw, 0.0f))) s_show_hidden = !s_show_hidden;
|
||||
} else {
|
||||
s_show_hidden = false; // nothing hidden → keep the toggle off
|
||||
newClicked = material::TactileButton(TR("chat_new_button"), ImVec2(-FLT_MIN, 0.0f));
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
if (newClicked) {
|
||||
s_show_new_convo = true;
|
||||
s_new_zaddr[0] = '\0';
|
||||
s_new_msg[0] = '\0';
|
||||
}
|
||||
// Search filter (Q8) — only worth showing once there's more than one conversation.
|
||||
if (convs.size() > 1) {
|
||||
// Search filter (Q8) — always available (not gated by conversation count or Show hidden).
|
||||
if (!convs.empty() || hasHidden) {
|
||||
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()) {
|
||||
@@ -489,47 +572,117 @@ void RenderChatTab(App* app)
|
||||
ImGui::SameLine(0.0f, 10.0f * Layout::dpiScale()); // breathing gutter between the list and thread panes
|
||||
|
||||
// ---- Right: selected conversation thread ----
|
||||
ImGui::BeginChild("##ChatThread", ImVec2(0, avail.y), true);
|
||||
{
|
||||
const ConvSummary* sel = nullptr;
|
||||
for (const auto& c : convs) if (c.cid == s_selected_cid) { sel = &c; break; }
|
||||
// Resolve the selection up front so we can reserve room for the composer BELOW the bordered message
|
||||
// box — the input sits outside/below the box now (no divider above it).
|
||||
const ConvSummary* sel = nullptr;
|
||||
for (const auto& c : convs) if (c.cid == s_selected_cid) { sel = &c; break; }
|
||||
|
||||
const float tdp = Layout::dpiScale();
|
||||
const float composerBoxH = ImGui::GetTextLineHeight() * 2.6f; // multi-line composer (Q6)
|
||||
// Reserve a stable composer strip below the box whenever a conversation is open (thin counter row +
|
||||
// the input row, which sits flush with the strip/window bottom). Same height for the waiting hint so
|
||||
// the box doesn't jump when a reply arrives.
|
||||
const float composerAreaH = sel ? (composerBoxH + metaSz + 12.0f * tdp) : 0.0f;
|
||||
|
||||
ImGui::BeginChild("##ChatThread", ImVec2(0, avail.y - composerAreaH), true);
|
||||
{
|
||||
if (sel) {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // viewing the thread clears its unread (Q1)
|
||||
// ── Header (V7 / Tier A+B): avatar + name on the left, a right-aligned icon toolbar,
|
||||
// then a click-to-copy address with a key-verify lock (or a "waiting for reply" chip).
|
||||
// ── Header (single row): avatar + name + key-lock + compact address (left), a right-aligned
|
||||
// icon toolbar (right). The address rides the name line to keep the header to one row.
|
||||
const float hdpi = Layout::dpiScale();
|
||||
ImGui::PushFont(material::Type().subtitle1());
|
||||
const float nameH = ImGui::GetTextLineHeight();
|
||||
const float ib = nameH + 8.0f * hdpi; // icon-button square == header row height
|
||||
const float gap = 3.0f * hdpi;
|
||||
const float rowH = ib;
|
||||
const float avR = nameH * 0.62f;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
ImFont* subFont = material::Type().subtitle1();
|
||||
const float subSz = scaledSize(subFont);
|
||||
const float nameH = subSz; // subtitle1 line height
|
||||
const float ib = nameH + 8.0f * hdpi; // icon-button square == header row height
|
||||
const float gap = 3.0f * hdpi;
|
||||
const float rowH = ib;
|
||||
const float avR = nameH * 0.62f;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
const float rightX = hp.x + ImGui::GetContentRegionAvail().x;
|
||||
const float textX = hp.x + 2.0f * avR + 10.0f * hdpi;
|
||||
ImDrawList* hdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 avC(hp.x + avR, hp.y + rowH * 0.5f); // avatar centered in the row
|
||||
|
||||
// Avatar (centered in the row).
|
||||
const ImVec2 avC(hp.x + avR, hp.y + rowH * 0.5f);
|
||||
hdl->AddCircleFilled(avC, avR, avatarColor(sel->cid), 24);
|
||||
{
|
||||
const std::string init = initialOf(sel->peerName);
|
||||
const ImVec2 isz = ImGui::CalcTextSize(init.c_str());
|
||||
hdl->AddText(ImVec2(avC.x - isz.x * 0.5f, avC.y - isz.y * 0.5f),
|
||||
const ImVec2 isz = subFont->CalcTextSizeA(subSz, FLT_MAX, 0.0f, init.c_str());
|
||||
hdl->AddText(subFont, subSz, ImVec2(avC.x - isz.x * 0.5f, avC.y - isz.y * 0.5f),
|
||||
IM_COL32(255, 255, 255, 235), init.c_str());
|
||||
}
|
||||
const bool hasAddr = !sel->peerZaddr.empty();
|
||||
const bool known = hasAddr && book.findByAddress(sel->peerZaddr) >= 0;
|
||||
const bool muted = app->settings() && app->settings()->isChatMuted(sel->cid);
|
||||
|
||||
// The toolbar's left edge is known up front (from the button count), so the name can be
|
||||
// clipped to it — a long contact label can't overrun the icons (mirrors the list-pane clip).
|
||||
const int nBtns = 3 + ((hasAddr && !known) ? 1 : 0);
|
||||
const bool hasAddr = !sel->peerZaddr.empty();
|
||||
const int bookIdx = hasAddr ? book.findByAddress(sel->peerZaddr) : -1;
|
||||
const bool known = bookIdx >= 0;
|
||||
const bool muted = app->settings() && app->settings()->isChatMuted(sel->cid);
|
||||
const bool renaming = (s_rename_cid == sel->cid);
|
||||
|
||||
// The toolbar's left edge is known up front (from the button count). A rename (edit) icon is
|
||||
// shown whenever there's an address to save the contact under; the settings "notch" gear is
|
||||
// always the rightmost icon.
|
||||
const int nBtns = 4 + (hasAddr ? 1 : 0);
|
||||
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
||||
ImGui::SetCursorScreenPos(ImVec2(textX, hp.y + (rowH - nameH) * 0.5f)); // name centered in row
|
||||
ImGui::PushClipRect(ImVec2(textX, hp.y), ImVec2(toolbarLeft - gap, hp.y + rowH), true);
|
||||
ImGui::TextUnformatted(sel->peerName.c_str());
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopFont();
|
||||
|
||||
// Compact address + lock (or waiting-chip) metrics, reserved to the right of the name.
|
||||
const bool showLock = hasAddr && !sel->peerPubKey.empty();
|
||||
const bool showWait = hasAddr && sel->peerPubKey.empty();
|
||||
const std::string sa = hasAddr ? shorten(sel->peerZaddr, 10, 6) : std::string();
|
||||
ImFont* icoFont = material::Type().iconSmall();
|
||||
const float icoSz = scaledSize(icoFont);
|
||||
const float addrTextW = hasAddr ? metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, sa.c_str()).x : 0.0f;
|
||||
const float lockW = showLock ? icoFont->CalcTextSizeA(icoSz, FLT_MAX, 0.0f, ICON_MD_LOCK).x + 5.0f * hdpi : 0.0f;
|
||||
const char* wl = showWait ? TR("chat_awaiting_key") : "";
|
||||
const float waitTextW = showWait ? metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, wl).x : 0.0f;
|
||||
const float waitChipW = showWait ? waitTextW + 14.0f * hdpi + 8.0f * hdpi : 0.0f; // chip pad + leading gap
|
||||
const float addrGroupW = lockW + addrTextW + waitChipW;
|
||||
const float nameSpacing = 12.0f * hdpi;
|
||||
const float nameCY = hp.y + (rowH - nameH) * 0.5f;
|
||||
const float nameW = subFont->CalcTextSizeA(subSz, FLT_MAX, 0.0f, sel->peerName.c_str()).x;
|
||||
const float nameAvail = std::max(40.0f * hdpi,
|
||||
toolbarLeft - gap - textX - (hasAddr ? addrGroupW + nameSpacing : 0.0f));
|
||||
|
||||
// Name — or an inline rename field (Tier C) that takes the whole name+address span.
|
||||
if (renaming) {
|
||||
const float nameMaxW = std::max(60.0f * hdpi, toolbarLeft - gap - textX);
|
||||
ImGui::SetCursorScreenPos(ImVec2(textX, nameCY));
|
||||
ImGui::PushFont(subFont);
|
||||
ImGui::SetNextItemWidth(nameMaxW);
|
||||
if (s_rename_focus) { ImGui::SetKeyboardFocusHere(); s_rename_focus = false; }
|
||||
const bool commit = ImGui::InputTextWithHint("##renamepeer", TR("chat_rename_hint"),
|
||||
s_rename_buf, sizeof(s_rename_buf), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
const bool cancel = ImGui::IsItemDeactivated() && !commit; // Escape or click-away
|
||||
ImGui::PopFont();
|
||||
if (commit) {
|
||||
std::string nm = s_rename_buf;
|
||||
while (!nm.empty() && std::isspace(static_cast<unsigned char>(nm.front()))) nm.erase(nm.begin());
|
||||
while (!nm.empty() && std::isspace(static_cast<unsigned char>(nm.back()))) nm.pop_back();
|
||||
if (!nm.empty() && hasAddr) {
|
||||
if (bookIdx >= 0) {
|
||||
if (nm != book.entries()[bookIdx].label) { // skip a no-op rewrite/save
|
||||
data::AddressBookEntry e = book.entries()[bookIdx];
|
||||
e.label = nm; // keep address/notes/scope/avatar
|
||||
book.updateEntry(static_cast<std::size_t>(bookIdx), e);
|
||||
book.save();
|
||||
Notifications::instance().success(TR("chat_renamed"));
|
||||
}
|
||||
} else if (book.addEntry(data::AddressBookEntry(nm, sel->peerZaddr))) {
|
||||
book.save();
|
||||
Notifications::instance().success(TR("chat_contact_added"));
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
s_rename_cid.clear();
|
||||
} else if (cancel) {
|
||||
s_rename_cid.clear();
|
||||
}
|
||||
} else {
|
||||
ImGui::PushClipRect(ImVec2(textX, hp.y), ImVec2(textX + nameAvail, hp.y + rowH), true);
|
||||
hdl->AddText(subFont, subSz, ImVec2(textX, nameCY), material::OnSurface(), sel->peerName.c_str());
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
|
||||
// Right-aligned icon toolbar: [add contact?] · export · mute · hide. Each is a frameless
|
||||
// IconButton with a hover pill + tooltip (the tooltip carries the wording the old text buttons had).
|
||||
@@ -544,14 +697,22 @@ void RenderChatTab(App* app)
|
||||
float bx = toolbarLeft;
|
||||
const float by = hp.y;
|
||||
|
||||
// Add to contacts — only when the peer isn't in the address book yet (Q2).
|
||||
if (hasAddr && !known) {
|
||||
// Rename (edit) the contact inline in the header. For a known peer it updates the label;
|
||||
// for an unknown one it creates the contact under the typed name (folds in the old
|
||||
// "add contact" quick action, but lets you name them) (Q2 / Tier C).
|
||||
if (hasAddr) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base; a.tooltip = TR("chat_add_contact");
|
||||
if (material::IconButton("##hdr_add", ICON_MD_PERSON_ADD, ifont, ImVec2(ib, ib), a)) {
|
||||
data::AddressBookEntry e(sel->peerName, sel->peerZaddr); // label = shortened addr; rename in Contacts
|
||||
if (book.addEntry(e)) Notifications::instance().success(TR("chat_contact_added"));
|
||||
else Notifications::instance().error(TR("address_book_exists"));
|
||||
material::IconButtonStyle a = base;
|
||||
a.tooltip = known ? TR("chat_rename") : TR("chat_add_contact");
|
||||
if (known) { a.color = material::OnSurfaceMedium(); }
|
||||
if (material::IconButton("##hdr_rename",
|
||||
known ? ICON_MD_EDIT : ICON_MD_PERSON_ADD, ifont, ImVec2(ib, ib), a)) {
|
||||
s_rename_cid = sel->cid;
|
||||
const std::string cur = known ? book.entries()[bookIdx].label : std::string();
|
||||
const std::size_t n = std::min(cur.size(), sizeof(s_rename_buf) - 1);
|
||||
std::memcpy(s_rename_buf, cur.data(), n);
|
||||
s_rename_buf[n] = '\0';
|
||||
s_rename_focus = true;
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
@@ -614,74 +775,101 @@ void RenderChatTab(App* app)
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
}
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
// Settings "notch" — the rightmost icon, on a faint pill so it reads as a distinct
|
||||
// corner tab. Opens the chat-customization modal.
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base;
|
||||
a.tooltip = TR("chat_settings_tip");
|
||||
a.restBg = material::WithAlpha(material::OnSurface(), 22); // the notch backing
|
||||
if (material::IconButton("##hdr_settings", ICON_MD_SETTINGS, ifont, ImVec2(ib, ib), a))
|
||||
s_show_chat_settings = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2: a key-verify lock (Tier B) + click-to-copy address (Q3), or a "waiting for reply" chip.
|
||||
ImGui::SetCursorScreenPos(ImVec2(textX, hp.y + rowH + 3.0f * hdpi));
|
||||
if (hasAddr) {
|
||||
// Lock glyph → we hold the peer's identity key; the tooltip shows a comparable fingerprint.
|
||||
if (!sel->peerPubKey.empty()) {
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::WithAlpha(material::Primary(), 220));
|
||||
ImGui::TextUnformatted(ICON_MD_LOCK);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered())
|
||||
// Compact address + key-lock (or waiting chip) placed right after the (capped) name, on the
|
||||
// same row. Hidden while renaming (the field takes the whole span).
|
||||
if (hasAddr && !renaming) {
|
||||
float ax = textX + std::min(nameW, nameAvail) + nameSpacing;
|
||||
// Key-verify lock — we hold the peer's identity key; hover shows a comparable fingerprint.
|
||||
if (showLock) {
|
||||
const float ly = hp.y + (rowH - icoSz) * 0.5f;
|
||||
hdl->AddText(icoFont, icoSz, ImVec2(ax, ly),
|
||||
material::WithAlpha(material::Primary(), 220), ICON_MD_LOCK);
|
||||
if (ImGui::IsMouseHoveringRect(ImVec2(ax, ly), ImVec2(ax + lockW, ly + icoSz)))
|
||||
material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str());
|
||||
ImGui::SameLine(0.0f, 5.0f * hdpi);
|
||||
ax += lockW;
|
||||
}
|
||||
// Click-to-copy shortened address (replaces the old "Copy Full Address" button).
|
||||
ImGui::PushFont(metaFont);
|
||||
const std::string sa = shorten(sel->peerZaddr, 20, 12);
|
||||
const ImVec2 tp = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 tsz = ImGui::CalcTextSize(sa.c_str());
|
||||
const bool addrClicked = ImGui::InvisibleButton("##hdr_copyaddr", tsz);
|
||||
const bool addrHov = ImGui::IsItemHovered();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
tp, addrHov ? material::OnSurface() : material::OnSurfaceMedium(), sa.c_str());
|
||||
if (addrHov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", TR("chat_copy_address_tip"));
|
||||
}
|
||||
if (addrClicked) {
|
||||
ImGui::SetClipboardText(sel->peerZaddr.c_str());
|
||||
Notifications::instance().success(TR("copied"));
|
||||
}
|
||||
// Waiting-for-reply chip when the peer's identity key isn't known yet (Tier B).
|
||||
if (sel->peerPubKey.empty()) {
|
||||
ImGui::SameLine(0.0f, 8.0f * hdpi);
|
||||
const char* wl = TR("chat_awaiting_key");
|
||||
const ImVec2 wsz = ImGui::CalcTextSize(wl);
|
||||
const float px = 7.0f * hdpi, py = 2.0f * hdpi;
|
||||
const ImVec2 cp = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 pmax(cp.x + wsz.x + 2.0f * px, cp.y + wsz.y + 2.0f * py);
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
cdl->AddRectFilled(cp, pmax, material::WithAlpha(material::Primary(), 38),
|
||||
(wsz.y + 2.0f * py) * 0.5f);
|
||||
cdl->AddText(ImVec2(cp.x + px, cp.y + py),
|
||||
// Click-to-copy compact address.
|
||||
const float ay = hp.y + (rowH - metaSz) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(ax, ay));
|
||||
const bool addrClicked = ImGui::InvisibleButton("##hdr_copyaddr", ImVec2(std::max(1.0f, addrTextW), metaSz));
|
||||
const bool addrHov = ImGui::IsItemHovered();
|
||||
hdl->AddText(metaFont, metaSz, ImVec2(ax, ay),
|
||||
addrHov ? material::OnSurface() : material::OnSurfaceMedium(), sa.c_str());
|
||||
if (addrHov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("chat_copy_address_tip")); }
|
||||
if (addrClicked) { ImGui::SetClipboardText(sel->peerZaddr.c_str()); Notifications::instance().success(TR("copied")); }
|
||||
ax += addrTextW;
|
||||
// Waiting-for-reply chip when the peer's identity key isn't known yet.
|
||||
if (showWait) {
|
||||
const float lead = 8.0f * hdpi, px = 7.0f * hdpi, py = 2.0f * hdpi;
|
||||
const ImVec2 cp(ax + lead, hp.y + (rowH - (metaSz + 2.0f * py)) * 0.5f);
|
||||
const ImVec2 pmax(cp.x + waitTextW + 2.0f * px, cp.y + metaSz + 2.0f * py);
|
||||
hdl->AddRectFilled(cp, pmax, material::WithAlpha(material::Primary(), 38), (metaSz + 2.0f * py) * 0.5f);
|
||||
hdl->AddText(metaFont, metaSz, ImVec2(cp.x + px, cp.y + py),
|
||||
material::WithAlpha(material::Primary(), 230), wl);
|
||||
ImGui::Dummy(ImVec2(wsz.x + 2.0f * px, wsz.y + 2.0f * py));
|
||||
}
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(0.0f, metaSz)); // keep the header height stable when there's no address
|
||||
}
|
||||
// Advance the layout cursor below the single header row, then divide it from the messages.
|
||||
ImGui::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
||||
ImGui::Separator();
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
const float composerBoxH = ImGui::GetTextLineHeight() * 2.6f; // multi-line composer (Q6)
|
||||
// Footer = composer box + the emoji-toggle/counter row (30dp tall button) + padding.
|
||||
const float footerH = composerBoxH + 30.0f * dp + 20.0f * dp;
|
||||
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y - footerH), false);
|
||||
// Inset the message list from the pane edges so bubbles don't hug the border/scrollbar (8px).
|
||||
// Zero the VERTICAL item spacing: the message loop reserves its own gaps via Dummy() (tight
|
||||
// within a run so the grouped/merged corners read right), so the theme's 6px would double them.
|
||||
const float origSpacingX = ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f * dp, 8.0f * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(origSpacingX, 0.0f));
|
||||
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y),
|
||||
ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll(); // smooth wheel scroll; syncs with the auto-scroll-to-bottom below
|
||||
bool atBottom = true;
|
||||
const ImVec2 msgWinMin = ImGui::GetWindowPos();
|
||||
const ImVec2 msgWinSize = ImGui::GetWindowSize();
|
||||
{
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const float bpad = 9.0f * dp, bround = 9.0f * dp;
|
||||
// Bubble style / accent / density / text-size from chat customization settings.
|
||||
auto* cs = app->settings();
|
||||
const int bubbleStyle = cs ? cs->getChatBubbleStyle() : 0; // 0 rounded, 1 square, 2 minimal
|
||||
const bool compact = cs && cs->getChatDensity() == 1;
|
||||
const float fscale = cs ? cs->getChatFontScale() : 1.0f;
|
||||
// Message bodies read at ~18px by default (bigger than the 15px body2 used in the list),
|
||||
// then the Text-size slider (fscale) multiplies it. nameSz is body2's draw size, so
|
||||
// nameSz / kFontBody2() is the DPI/font-scale factor; ×18 gives an 18px base. ImGui 1.92
|
||||
// rebakes the font at the requested size, so the larger text stays crisp.
|
||||
const float bodySz = nameSz * (18.0f / std::max(1.0f, Layout::kFontBody2())) * fscale;
|
||||
const float bpad = (compact ? 6.0f : 9.0f) * dp;
|
||||
const float bround = (bubbleStyle == 1 ? 2.0f : 9.0f) * dp; // square vs rounded/minimal
|
||||
const int outFill = (bubbleStyle == 2 ? 26 : 46); // minimal = fainter fill
|
||||
const int inFill = (bubbleStyle == 2 ? 14 : 22);
|
||||
const float groupGap = (compact ? 4.0f : 7.0f) * dp;
|
||||
const float msgGap = (compact ? 2.0f : 3.0f) * dp;
|
||||
const ImU32 accentBase = bubbleAccentColor(cs ? cs->getChatBubbleAccent() : 0);
|
||||
const auto messages = store.conversation(s_selected_cid);
|
||||
std::int64_t prevMin = -1; int prevDir = -1;
|
||||
// Grouping + per-day separators (Tier 1). Same-sender messages within kGroupWindow share
|
||||
// one meta header and stack tightly; a date pill is drawn once per calendar day.
|
||||
const std::int64_t nowTs = static_cast<std::int64_t>(std::time(nullptr));
|
||||
const std::int64_t todayKey = chatDayKey(nowTs);
|
||||
const std::int64_t yestKey = chatDayKey(nowTs - 86400);
|
||||
constexpr std::int64_t kGroupWindow = 300; // 5 minutes
|
||||
const float avR = 11.0f * dp; // in-thread peer avatar radius
|
||||
const float gutter = 2.0f * avR + 6.0f * dp; // incoming bubbles indent past the avatar
|
||||
ImFont* icf = material::Type().iconSmall();
|
||||
const float icfSz = scaledSize(icf);
|
||||
std::int64_t prevTs = -1; int prevDir = -1; std::int64_t prevDay = -1;
|
||||
for (std::size_t mi = 0; mi < messages.size(); ++mi) {
|
||||
const auto& m = messages[mi];
|
||||
ImGui::PushID(static_cast<int>(mi));
|
||||
@@ -689,49 +877,112 @@ void RenderChatTab(App* app)
|
||||
const bool request = (m.kind == chat::ChatMessageKind::ContactRequest);
|
||||
const bool failed = outgoing && m.delivery == chat::ChatDelivery::Failed;
|
||||
const bool sending = outgoing && m.delivery == chat::ChatDelivery::Sending;
|
||||
const std::int64_t minute = m.timestamp / 60;
|
||||
const bool startGroup = (prevDir != static_cast<int>(outgoing)) || (minute != prevMin);
|
||||
const std::int64_t day = chatDayKey(m.timestamp);
|
||||
const bool newDay = (day != prevDay);
|
||||
const bool startGroup = newDay || (prevDir != static_cast<int>(outgoing)) ||
|
||||
(prevTs < 0) || (m.timestamp - prevTs > kGroupWindow);
|
||||
// Does this bubble end its run? (for grouped-corner shaping + the avatar placement)
|
||||
bool lastInGroup = true;
|
||||
if (mi + 1 < messages.size()) {
|
||||
const auto& n = messages[mi + 1];
|
||||
const bool nOut = (n.direction == chat::ChatDirection::Outgoing);
|
||||
lastInGroup = (chatDayKey(n.timestamp) != day) || (nOut != outgoing) ||
|
||||
(n.timestamp - m.timestamp > kGroupWindow);
|
||||
}
|
||||
|
||||
const float availW = ImGui::GetContentRegionAvail().x;
|
||||
const float maxBubbleW = std::max(140.0f * dp, availW * 0.72f);
|
||||
const float innerW = maxBubbleW - 2.0f * bpad;
|
||||
|
||||
// Grouped meta line (sender + time), once per sender/minute run, aligned to the sender's side.
|
||||
// ── Date separator (once per calendar day): a centered pill.
|
||||
if (newDay) {
|
||||
if (mi > 0) ImGui::Dummy(ImVec2(0.0f, groupGap + 4.0f * dp));
|
||||
const std::string ds = chatDaySeparator(m.timestamp, todayKey, yestKey);
|
||||
const ImVec2 dsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, ds.c_str());
|
||||
const float dpx = 9.0f * dp, dpy = 2.0f * dp;
|
||||
const ImVec2 dcur = ImGui::GetCursorScreenPos();
|
||||
const float cxs = dcur.x + std::max(0.0f, (availW - dsz.x) * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(cxs - dpx, dcur.y), ImVec2(cxs + dsz.x + dpx, dcur.y + dsz.y + 2.0f * dpy),
|
||||
material::WithAlpha(material::OnSurface(), 24), (dsz.y + 2.0f * dpy) * 0.5f);
|
||||
dl->AddText(metaFont, metaSz, ImVec2(cxs, dcur.y + dpy), material::OnSurfaceMedium(), ds.c_str());
|
||||
ImGui::Dummy(ImVec2(availW, dsz.y + 2.0f * dpy + 4.0f * dp));
|
||||
}
|
||||
|
||||
// ── Grouped meta line (sender + time-only), once per run, on the sender's side.
|
||||
if (startGroup) {
|
||||
if (mi > 0 && !newDay) ImGui::Dummy(ImVec2(0.0f, groupGap));
|
||||
std::string meta = outgoing ? std::string(TR("chat_you")) : sel->peerName;
|
||||
const std::string when = formatTime(m.timestamp);
|
||||
const std::string when = chatTimeOnly(m.timestamp);
|
||||
if (!when.empty()) meta += " " + when;
|
||||
if (request) meta += " [" + std::string(TR("chat_contact_request")) + "]";
|
||||
const ImVec2 msz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, meta.c_str());
|
||||
const ImVec2 mp = ImGui::GetCursorScreenPos();
|
||||
dl->AddText(metaFont, metaSz, ImVec2(outgoing ? mp.x + availW - msz.x : mp.x, mp.y),
|
||||
dl->AddText(metaFont, metaSz,
|
||||
ImVec2(outgoing ? mp.x + availW - msz.x : mp.x + gutter, mp.y),
|
||||
material::OnSurfaceMedium(), meta.c_str());
|
||||
ImGui::Dummy(ImVec2(availW, metaSz + 3.0f * dp));
|
||||
}
|
||||
|
||||
// Direction-aligned rounded bubble.
|
||||
const ImVec2 tsz = nameFont->CalcTextSizeA(nameSz, innerW, innerW, m.body.c_str());
|
||||
// ── Direction-aligned bubble (rounding/fill/accent from settings; grouped corners).
|
||||
const ImVec2 tsz = nameFont->CalcTextSizeA(bodySz, innerW, innerW, m.body.c_str());
|
||||
const float bw = std::min(maxBubbleW, tsz.x + 2.0f * bpad);
|
||||
const float bh = tsz.y + 2.0f * bpad;
|
||||
const ImVec2 cur = ImGui::GetCursorScreenPos();
|
||||
const float bx = outgoing ? (cur.x + availW - bw) : cur.x;
|
||||
const float bx = outgoing ? (cur.x + availW - bw) : (cur.x + gutter);
|
||||
const ImVec2 bmin(bx, cur.y), bmax(bx + bw, cur.y + bh);
|
||||
const ImU32 bubCol = failed ? material::WithAlpha(material::Error(), 40)
|
||||
: outgoing ? material::WithAlpha(material::Primary(), 46)
|
||||
: material::WithAlpha(material::OnSurface(), 22);
|
||||
dl->AddRectFilled(bmin, bmax, bubCol, bround);
|
||||
dl->AddText(nameFont, nameSz, ImVec2(bx + bpad, cur.y + bpad), material::OnSurface(),
|
||||
: outgoing ? material::WithAlpha(accentBase, outFill)
|
||||
: material::WithAlpha(material::OnSurface(), inFill);
|
||||
// Flatten the corner between stacked bubbles from the same sender (iMessage-style).
|
||||
ImDrawFlags rf = ImDrawFlags_RoundCornersAll;
|
||||
if (bubbleStyle != 1) { // shape only rounded/minimal; square keeps its uniform corners
|
||||
if (outgoing) {
|
||||
if (!startGroup) rf &= ~ImDrawFlags_RoundCornersTopRight;
|
||||
if (!lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomRight;
|
||||
} else {
|
||||
if (!startGroup) rf &= ~ImDrawFlags_RoundCornersTopLeft;
|
||||
if (!lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomLeft;
|
||||
}
|
||||
}
|
||||
dl->AddRectFilled(bmin, bmax, bubCol, bround, rf);
|
||||
dl->AddText(nameFont, bodySz, ImVec2(bx + bpad, cur.y + bpad), material::OnSurface(),
|
||||
m.body.c_str(), nullptr, innerW);
|
||||
|
||||
// Peer avatar beside the LAST incoming bubble of a run (Tier 3).
|
||||
if (!outgoing && lastInGroup) {
|
||||
const ImVec2 avc(cur.x + avR, cur.y + bh - avR);
|
||||
dl->AddCircleFilled(avc, avR, avatarColor(sel->cid), 20);
|
||||
const std::string init = initialOf(sel->peerName);
|
||||
const ImVec2 isz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, init.c_str());
|
||||
dl->AddText(metaFont, metaSz, ImVec2(avc.x - isz.x * 0.5f, avc.y - isz.y * 0.5f),
|
||||
IM_COL32(255, 255, 255, 235), init.c_str());
|
||||
}
|
||||
ImGui::Dummy(ImVec2(availW, bh));
|
||||
|
||||
// Hover: reveal this message's own time at the outer bubble edge + right-click copy.
|
||||
if (ImGui::IsMouseHoveringRect(bmin, bmax)) {
|
||||
material::Tooltip("%s", formatTime(m.timestamp).c_str());
|
||||
const std::string ht = chatTimeOnly(m.timestamp);
|
||||
const ImVec2 hsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, ht.c_str());
|
||||
const float hx = outgoing ? (bmin.x - hsz.x - 6.0f * dp) : (bmax.x + 6.0f * dp);
|
||||
dl->AddText(metaFont, metaSz, ImVec2(hx, bmin.y + (bh - hsz.y) * 0.5f),
|
||||
material::WithAlpha(material::OnSurface(), 120), ht.c_str());
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("##msgmenu");
|
||||
}
|
||||
if (ImGui::BeginPopup("##msgmenu")) {
|
||||
if (ImGui::MenuItem(TR("copy"))) ImGui::SetClipboardText(m.body.c_str());
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
// Failed send → right-aligned "not sent" + Retry (Q7).
|
||||
|
||||
// Delivery status for your messages: clock while sending, check once broadcast (Tier 2).
|
||||
if (outgoing && !failed && (sending || lastInGroup)) {
|
||||
const char* glyph = sending ? ICON_MD_SCHEDULE : ICON_MD_DONE;
|
||||
const float gs = icfSz * 0.82f;
|
||||
const ImVec2 gsz = icf->CalcTextSizeA(gs, FLT_MAX, 0.0f, glyph);
|
||||
dl->AddText(icf, gs, ImVec2(bmax.x - gsz.x, bmax.y + 1.0f * dp),
|
||||
material::WithAlpha(material::OnSurface(), sending ? 95 : 135), glyph);
|
||||
ImGui::Dummy(ImVec2(0.0f, gs + 2.0f * dp)); // reserve the status glyph (no ItemSpacing now)
|
||||
}
|
||||
// Failed send → right-aligned "not sent" + Retry (kept — it's actionable).
|
||||
if (failed) {
|
||||
ImGui::Dummy(ImVec2(0.0f, 1.0f * dp));
|
||||
const float ftw = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, TR("chat_send_failed")).x;
|
||||
@@ -750,18 +1001,9 @@ void RenderChatTab(App* app)
|
||||
s_scroll_to_cid = sel->cid;
|
||||
}
|
||||
}
|
||||
// In-flight send → subtle right-aligned "sending…"; the op callback resolves it.
|
||||
if (sending) {
|
||||
ImGui::Dummy(ImVec2(0.0f, 1.0f * dp));
|
||||
const float sw = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, TR("chat_sending")).x;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, availW - sw));
|
||||
ImGui::PushFont(metaFont); ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(TR("chat_sending"));
|
||||
ImGui::PopStyleColor(); ImGui::PopFont();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0.0f, 5.0f * dp));
|
||||
ImGui::Dummy(ImVec2(0.0f, lastInGroup ? msgGap : msgGap * 0.5f)); // tight within a run
|
||||
ImGui::PopID();
|
||||
prevMin = minute; prevDir = static_cast<int>(outgoing);
|
||||
prevTs = m.timestamp; prevDir = static_cast<int>(outgoing); prevDay = day;
|
||||
}
|
||||
if (s_scroll_to_cid == s_selected_cid) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
@@ -770,6 +1012,7 @@ void RenderChatTab(App* app)
|
||||
atBottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ##ChatMessages WindowPadding + zeroed vertical ItemSpacing
|
||||
// Jump-to-latest pill when scrolled up (Q9). SetCursorScreenPos moves the parent cursor to
|
||||
// the child's bottom edge; save + restore it so the composer footer below stays anchored
|
||||
// (otherwise it renders ~8px too high only while the pill is shown).
|
||||
@@ -783,71 +1026,7 @@ void RenderChatTab(App* app)
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::SetCursorScreenPos(savedCursor);
|
||||
}
|
||||
|
||||
// Composer footer: message input + send (only once we know the peer's key), else a hint.
|
||||
ImGui::Separator();
|
||||
if (sel->peerPubKey.empty()) {
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted(TR("chat_waiting_reply"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
// Multi-line composer (Q6): Enter sends, Ctrl+Enter inserts a newline. A live byte counter
|
||||
// tracks the effective on-chain body cap = (512 − len("utf8:"))/2 − secretstream ABYTES.
|
||||
static constexpr int kBodyMaxBytes = (512 - 5) / 2 - 17; // = 236 (see chat_outgoing.cpp)
|
||||
const float sendW = 80.0f * Layout::dpiScale(); // scaled so the translated label never clips (B3)
|
||||
const float inputW = ImGui::GetContentRegionAvail().x - sendW - ImGui::GetStyle().ItemSpacing.x;
|
||||
const int used = static_cast<int>(std::strlen(s_compose));
|
||||
const bool overCap = used > kBodyMaxBytes;
|
||||
bool submit = ImGui::InputTextMultiline("##compose", s_compose, sizeof(s_compose),
|
||||
ImVec2(inputW, composerBoxH),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CtrlEnterForNewLine);
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
ImGui::BeginDisabled(overCap || s_compose[0] == '\0');
|
||||
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. The picker opens as an
|
||||
// overlay over the conversation-list pane (rendered there), not a floating window. The
|
||||
// toggle is a larger frameless button (Tier A) that lights up while the picker is open.
|
||||
const float emojiBtn = 30.0f * dp;
|
||||
{
|
||||
material::IconButtonStyle es;
|
||||
es.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
es.bgRounding = 8.0f * dp;
|
||||
if (s_show_emoji_picker) es.restBg = material::WithAlpha(material::Primary(), 46);
|
||||
if (material::IconButton("##emojitoggle", u8"🙂", material::Type().subtitle1(),
|
||||
ImVec2(emojiBtn, emojiBtn), es)) {
|
||||
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 — vertically centered on the button.
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, overCap ? material::Error() : material::OnSurfaceMedium());
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
||||
std::max(0.0f, (emojiBtn - ImGui::GetTextLineHeight()) * 0.5f));
|
||||
if (overCap) { ImGui::TextUnformatted(TR("chat_len_over")); ImGui::SameLine(); }
|
||||
const std::string counter = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||
const float cw = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, counter.c_str()).x;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
|
||||
std::max(0.0f, ImGui::GetContentRegionAvail().x - cw));
|
||||
ImGui::TextUnformatted(counter.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
if (submit && s_compose[0] != '\0' && !overCap) {
|
||||
app->sendChatMessage(sel->cid, s_compose);
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_scroll_to_cid = sel->cid;
|
||||
}
|
||||
}
|
||||
// Composer is rendered OUTSIDE this box, directly below it (see after EndChild).
|
||||
} else {
|
||||
if (convs.empty()) centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));
|
||||
else centeredEmptyState(ICON_MD_CHAT_BUBBLE_OUTLINE, TR("chat_select_hint"), nullptr);
|
||||
@@ -855,6 +1034,92 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// ── Composer — OUTSIDE / below the bordered message box, spanning its width. No divider above it.
|
||||
// Layout (all placed by absolute screen pos so nothing wraps to the window's left edge): a thin
|
||||
// byte-counter row just under the box, then the input row flush with the strip/window bottom, with
|
||||
// the emoji toggle on the LEFT of the input and Send on the right.
|
||||
if (sel) {
|
||||
const ImVec2 boxMin = ImGui::GetItemRectMin(); // the ##ChatThread child's rect
|
||||
const ImVec2 boxMax = ImGui::GetItemRectMax();
|
||||
const float cx = boxMin.x;
|
||||
const float cw = boxMax.x - boxMin.x;
|
||||
if (sel->peerPubKey.empty()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, boxMax.y + 8.0f * tdp));
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted(TR("chat_waiting_reply"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
// Multi-line composer (Q6). Enter behavior is setting-dependent (see below). A live byte
|
||||
// counter tracks the on-chain body cap = (512 − len("utf8:"))/2 − secretstream ABYTES.
|
||||
static constexpr int kBodyMaxBytes = (512 - 5) / 2 - 17; // = 236 (see chat_outgoing.cpp)
|
||||
const float sendW = 80.0f * tdp; // scaled so the translated label never clips (B3)
|
||||
const float emojiBtn = 28.0f * tdp;
|
||||
const float inGap = 6.0f * tdp;
|
||||
const int used = static_cast<int>(std::strlen(s_compose));
|
||||
const bool overCap = used > kBodyMaxBytes;
|
||||
|
||||
// Thin counter row (right-aligned) just under the box; over-cap tints + prefixes a label.
|
||||
std::string counterLine = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||
if (overCap) counterLine = std::string(TR("chat_len_over")) + " " + counterLine;
|
||||
const float cwid = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, counterLine.c_str()).x;
|
||||
ImGui::GetWindowDrawList()->AddText(metaFont, metaSz,
|
||||
ImVec2(cx + cw - cwid, boxMax.y + 3.0f * tdp),
|
||||
overCap ? material::Error() : material::OnSurfaceMedium(), counterLine.c_str());
|
||||
|
||||
// Input row, flush with the strip/window bottom: [emoji] [input] [Send].
|
||||
const float rowY = boxMax.y + composerAreaH - composerBoxH;
|
||||
const float inputX = cx + emojiBtn + inGap;
|
||||
const float inputW = std::max(60.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW);
|
||||
const float sendX = inputX + inputW + inGap;
|
||||
|
||||
// Emoji toggle on the left, vertically centered in the input row.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, rowY + (composerBoxH - emojiBtn) * 0.5f));
|
||||
{
|
||||
material::IconButtonStyle es;
|
||||
es.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
es.bgRounding = 8.0f * tdp;
|
||||
if (s_show_emoji_picker) es.restBg = material::WithAlpha(material::Primary(), 46);
|
||||
if (material::IconButton("##emojitoggle", u8"🙂", material::Type().subtitle1(),
|
||||
ImVec2(emojiBtn, emojiBtn), es)) {
|
||||
s_show_emoji_picker = !s_show_emoji_picker;
|
||||
s_emoji_search[0] = '\0';
|
||||
}
|
||||
}
|
||||
// Input.
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, rowY));
|
||||
// Enter-to-send (setting): on → Enter sends + Ctrl+Enter newline; off → Enter is a newline and
|
||||
// the Send button is the only way to send. NOTE: without EnterReturnsTrue, InputTextMultiline
|
||||
// returns true on EVERY edit (value-changed), so in off-mode the return value must NOT drive
|
||||
// submit — otherwise typing one character would fire a send.
|
||||
const bool enterSends = !app->settings() || app->settings()->getChatEnterSends();
|
||||
const ImGuiInputTextFlags composeFlags = enterSends
|
||||
? (ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CtrlEnterForNewLine)
|
||||
: ImGuiInputTextFlags_None;
|
||||
const bool inputReturned = ImGui::InputTextMultiline("##compose", s_compose, sizeof(s_compose),
|
||||
ImVec2(inputW, composerBoxH), composeFlags);
|
||||
bool submit = enterSends && inputReturned; // off-mode sends only via the Send button
|
||||
// Send.
|
||||
ImGui::SetCursorScreenPos(ImVec2(sendX, rowY));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
ImGui::BeginDisabled(overCap || s_compose[0] == '\0');
|
||||
if (material::TactileButton(TR("chat_send"), ImVec2(sendW, composerBoxH))) submit = true;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
if (submit && s_compose[0] != '\0' && !overCap) {
|
||||
app->sendChatMessage(sel->cid, s_compose);
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_scroll_to_cid = sel->cid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- New-conversation dialog (send a contact request to a z-address) — house BlurFloat overlay ----
|
||||
if (s_show_new_convo) {
|
||||
const float dp = Layout::dpiScale();
|
||||
@@ -923,6 +1188,26 @@ void RenderChatTab(App* app)
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
||||
if (s_show_chat_settings) {
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("chat_settings_title");
|
||||
ov.p_open = &s_show_chat_settings; // X / backdrop closes it
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 520.0f; ov.idSuffix = "chatsettings";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
RenderChatSettingsControls(app);
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * Layout::dpiScale()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
if (material::TactileButton(TR("chat_settings_done"), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||
s_show_chat_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResetChatTab()
|
||||
@@ -932,10 +1217,155 @@ void ResetChatTab()
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
sodium_memzero(s_new_zaddr, sizeof(s_new_zaddr));
|
||||
sodium_memzero(s_new_msg, sizeof(s_new_msg));
|
||||
sodium_memzero(s_rename_buf, sizeof(s_rename_buf));
|
||||
s_selected_cid.clear();
|
||||
s_scroll_to_cid.clear();
|
||||
s_compose_cid.clear();
|
||||
s_rename_cid.clear();
|
||||
s_rename_focus = false;
|
||||
s_show_new_convo = false;
|
||||
s_show_chat_settings = false;
|
||||
}
|
||||
|
||||
void RenderChatSettingsControls(App* app, float contentWidth)
|
||||
{
|
||||
auto* st = app ? app->settings() : nullptr;
|
||||
if (!st) return;
|
||||
const float dp = Layout::dpiScale();
|
||||
const float ctrlW = 250.0f * dp; // control column width (fits a 3-segment control comfortably)
|
||||
const float rowGap = 5.0f * dp;
|
||||
// Right-align controls to the row's true right edge. The Settings tab renders us inside a GlassCard
|
||||
// whose content region isn't narrowed to the card padding, so it passes an explicit contentWidth;
|
||||
// the chat modal's dialog content region is correct, so it passes 0 (auto).
|
||||
const float leftX = ImGui::GetCursorPosX();
|
||||
const float rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// Label left, control right-aligned within [leftX, leftX+rowW]. Leaves the cursor at the control origin.
|
||||
auto beginRow = [&](const char* label) {
|
||||
ImGui::Dummy(ImVec2(0.0f, rowGap));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), leftX + std::max(0.0f, rowW - ctrlW)));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
};
|
||||
auto section = [&](const char* key) {
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
ImGui::PushFont(material::Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::WithAlpha(material::Primary(), 235));
|
||||
ImGui::TextUnformatted(TR(key));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0.0f, 2.0f * dp));
|
||||
};
|
||||
// Segmented control (iOS-style): a rounded track with an inset pill on the selected segment.
|
||||
// Returns the (possibly changed) index. Reads the label via beginRow for a right-aligned control.
|
||||
auto segmented = [&](const char* label, const char* const* items, int count, int value) -> int {
|
||||
beginRow(label);
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const float seg = ctrlW / static_cast<float>(count);
|
||||
const float round = 7.0f * dp;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(origin, ImVec2(origin.x + ctrlW, origin.y + h),
|
||||
material::WithAlpha(material::OnSurface(), 20), round);
|
||||
int result = value;
|
||||
ImGui::PushID(label);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 mn(origin.x + i * seg, origin.y), mx(origin.x + (i + 1) * seg, origin.y + h);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
if (ImGui::InvisibleButton("##s", ImVec2(seg, h))) result = i;
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const bool sel = (value == i);
|
||||
if (sel) {
|
||||
const float in = 2.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(mn.x + in, mn.y + in), ImVec2(mx.x - in, mx.y - in),
|
||||
material::WithAlpha(material::Primary(), 210), std::max(1.0f, round - in));
|
||||
} else if (hov) {
|
||||
dl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 26), round);
|
||||
}
|
||||
const ImVec2 ts = ImGui::CalcTextSize(items[i]);
|
||||
// Center when the label fits; otherwise left-align with a small pad so the START stays
|
||||
// readable (a long translation clipped on the right, not clipped on BOTH sides).
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= seg - 2.0f * lpad) ? (seg - ts.x) * 0.5f : lpad;
|
||||
ImGui::PushClipRect(mn, mx, true);
|
||||
dl->AddText(ImVec2(mn.x + tx, mn.y + (h - ts.y) * 0.5f),
|
||||
sel ? IM_COL32(255, 255, 255, 236) : material::OnSurfaceMedium(), items[i]);
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(ctrlW, h)); // reserve the control's rect for layout flow
|
||||
return result;
|
||||
};
|
||||
|
||||
// ── Appearance ────────────────────────────────────────────────────────────────
|
||||
section("chat_sec_appearance");
|
||||
// Emoji style (monochrome / color). Color needs a FreeType build (native + the cross-built Windows
|
||||
// FreeType); requestFontRebuild swaps the atlas live.
|
||||
{
|
||||
int v = st->getChatEmojiColor() ? 1 : 0;
|
||||
const char* items[] = { TR("chat_emoji_mono"), TR("chat_emoji_color") };
|
||||
int nv = segmented(TR("chat_opt_emoji"), items, 2, v);
|
||||
if (nv != v) { st->setChatEmojiColor(nv == 1); st->save(); app->requestFontRebuild(); }
|
||||
}
|
||||
// Bubble style (segmented) + accent color (a 6-way dropdown — too many for a segmented control).
|
||||
{
|
||||
const char* items[] = { TR("chat_bubble_rounded"), TR("chat_bubble_square"), TR("chat_bubble_minimal") };
|
||||
int v = st->getChatBubbleStyle();
|
||||
int nv = segmented(TR("chat_opt_bubble_style"), items, 3, v);
|
||||
if (nv != v) { st->setChatBubbleStyle(nv); st->save(); }
|
||||
}
|
||||
{
|
||||
beginRow(TR("chat_opt_bubble_accent"));
|
||||
int v = st->getChatBubbleAccent();
|
||||
const char* items[] = { TR("chat_accent_theme"), TR("chat_accent_blue"), TR("chat_accent_green"),
|
||||
TR("chat_accent_purple"), TR("chat_accent_amber"), TR("chat_accent_pink") };
|
||||
if (ImGui::Combo("##chat_baccent", &v, items, 6)) { st->setChatBubbleAccent(v); st->save(); }
|
||||
}
|
||||
// Message density (segmented).
|
||||
{
|
||||
const char* items[] = { TR("chat_density_comfortable"), TR("chat_density_compact") };
|
||||
int v = st->getChatDensity();
|
||||
int nv = segmented(TR("chat_opt_density"), items, 2, v);
|
||||
if (nv != v) { st->setChatDensity(nv); st->save(); }
|
||||
}
|
||||
// Message text size (slider).
|
||||
{
|
||||
beginRow(TR("chat_opt_font_size"));
|
||||
float v = st->getChatFontScale();
|
||||
if (ImGui::SliderFloat("##chat_font", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
st->setChatFontScale(v); st->save();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Messaging ─────────────────────────────────────────────────────────────────
|
||||
section("chat_sec_messaging");
|
||||
// Message poll rate (full-node 0-conf fast-scan cadence) — slider.
|
||||
{
|
||||
beginRow(TR("chat_opt_poll"));
|
||||
float v = st->getChatPollRateSec();
|
||||
if (ImGui::SliderFloat("##chat_poll", &v, 0.5f, 15.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
st->setChatPollRateSec(v); st->save();
|
||||
}
|
||||
}
|
||||
// Chat timestamps (segmented) — overrides the app-wide clock (Settings → General) for this tab only.
|
||||
{
|
||||
const char* items[] = { TR("chat_ts_global_short"), TR("chat_ts_24h"), TR("chat_ts_12h") };
|
||||
int v = st->getChatTimeFormat();
|
||||
int nv = segmented(TR("chat_opt_timestamp"), items, 3, v);
|
||||
if (nv != v) { st->setChatTimeFormat(nv); st->save(); }
|
||||
}
|
||||
// Enter-to-send (checkbox).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, rowGap));
|
||||
bool v = st->getChatEnterSends();
|
||||
if (ImGui::Checkbox(TR("chat_opt_enter_sends"), &v)) { st->setChatEnterSends(v); st->save(); }
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -24,6 +24,18 @@ namespace ui {
|
||||
*/
|
||||
void RenderChatTab(App* app);
|
||||
|
||||
/**
|
||||
* @brief Render the chat-customization controls (emoji style, poll rate, bubble style/color,
|
||||
* density, text size, chat timestamps, Enter-to-send). Shared by the Chat tab's settings
|
||||
* "notch" modal and the Settings tab's Chat & Contacts section. The app-wide clock format
|
||||
* (which chat timestamps fall back to) lives separately in Settings → General.
|
||||
* @param app Pointer to the app instance.
|
||||
* @param contentWidth Explicit row width for right-aligning controls; pass the card's inner content
|
||||
* width from the Settings tab (whose GlassCard doesn't narrow the content region). 0 = auto
|
||||
* (use the current content region, correct inside the chat modal's dialog).
|
||||
*/
|
||||
void RenderChatSettingsControls(App* app, float contentWidth = 0.0f);
|
||||
|
||||
/**
|
||||
* @brief Securely wipe the Chat tab's UI-local state (composer / new-conversation
|
||||
* plaintext buffers + the selected-conversation ids). Called by
|
||||
|
||||
@@ -217,11 +217,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_you"] = "You";
|
||||
strings_["chat_contact_request"] = "contact request";
|
||||
strings_["chat_send_failed"] = "not sent";
|
||||
strings_["chat_new_button"] = "New conversation";
|
||||
strings_["chat_new_button"] = "New chat";
|
||||
strings_["chat_select_hint"] = "Select a conversation to view it.";
|
||||
strings_["chat_waiting_reply"] = "Waiting for this contact to reply — you can message them once they do.";
|
||||
strings_["chat_send"] = "Send";
|
||||
strings_["chat_new_title"] = "New conversation";
|
||||
strings_["chat_new_title"] = "New chat";
|
||||
strings_["chat_new_zaddr"] = "Recipient z-address";
|
||||
strings_["chat_new_message"] = "Message";
|
||||
strings_["chat_new_send"] = "Send request";
|
||||
@@ -263,6 +263,44 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_copy_address_tip"] = "Click to copy address";
|
||||
strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify";
|
||||
strings_["chat_awaiting_key"] = "Waiting for reply";
|
||||
strings_["chat_rename"] = "Rename contact";
|
||||
strings_["chat_rename_hint"] = "Contact name";
|
||||
strings_["chat_renamed"] = "Contact renamed";
|
||||
// Chat customization (gear modal + Settings → Chat & Contacts)
|
||||
strings_["chat_settings_title"] = "Chat settings";
|
||||
strings_["chat_settings_tip"] = "Chat customization";
|
||||
strings_["chat_settings_section"] = "CHAT & CONTACTS";
|
||||
strings_["chat_opt_emoji"] = "Emoji style";
|
||||
strings_["chat_emoji_mono"] = "Monochrome";
|
||||
strings_["chat_emoji_color"] = "Color";
|
||||
strings_["chat_opt_poll"] = "Message poll rate";
|
||||
strings_["chat_opt_bubble_style"] = "Bubble style";
|
||||
strings_["chat_bubble_rounded"] = "Rounded";
|
||||
strings_["chat_bubble_square"] = "Square";
|
||||
strings_["chat_bubble_minimal"] = "Minimal";
|
||||
strings_["chat_opt_bubble_accent"] = "Bubble color";
|
||||
strings_["chat_accent_theme"] = "Theme";
|
||||
strings_["chat_accent_blue"] = "Blue";
|
||||
strings_["chat_accent_green"] = "Green";
|
||||
strings_["chat_accent_purple"] = "Purple";
|
||||
strings_["chat_accent_amber"] = "Amber";
|
||||
strings_["chat_accent_pink"] = "Pink";
|
||||
strings_["chat_opt_density"] = "Message density";
|
||||
strings_["chat_density_comfortable"] = "Comfortable";
|
||||
strings_["chat_density_compact"] = "Compact";
|
||||
strings_["chat_opt_font_size"] = "Text size";
|
||||
strings_["chat_opt_timestamp"] = "Timestamps";
|
||||
strings_["chat_ts_global"] = "Follow global";
|
||||
strings_["chat_ts_24h"] = "24-hour";
|
||||
strings_["chat_ts_12h"] = "12-hour";
|
||||
strings_["chat_opt_enter_sends"] = "Enter sends message";
|
||||
strings_["chat_opt_global_clock"] = "Global clock format";
|
||||
strings_["chat_settings_done"] = "Done";
|
||||
strings_["chat_ts_global_short"] = "Global";
|
||||
strings_["chat_sec_appearance"] = "APPEARANCE";
|
||||
strings_["chat_sec_messaging"] = "MESSAGING";
|
||||
strings_["chat_today"] = "Today";
|
||||
strings_["chat_yesterday"] = "Yesterday";
|
||||
// Seed-phrase backup (full-node)
|
||||
strings_["seed_backup_button"] = "Seed phrase";
|
||||
strings_["tt_seed_backup"] = "Show and back up your wallet's 24-word recovery seed phrase";
|
||||
@@ -1148,6 +1186,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["network"] = "Network";
|
||||
strings_["theme"] = "Theme";
|
||||
strings_["language"] = "Language";
|
||||
strings_["clock_format"] = "Clock format";
|
||||
strings_["tt_clock_format"] = "24-hour or 12-hour clock, used across the app. The Chat tab can override it in its own settings.";
|
||||
strings_["dragonx_green"] = "DragonX (Green)";
|
||||
strings_["dark"] = "Dark";
|
||||
strings_["light"] = "Light";
|
||||
|
||||
Reference in New Issue
Block a user