diff --git a/README.md b/README.md index dc6b198..6761620 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ shows incoming and sent private messages newest-first; press `r` to refresh it manually, and it refreshes when a new private message arrives while the inbox is open. `:reply text` and `:r text` send to the latest private-message peer. Unread incoming private messages are marked with `*` until `:inbox` renders. +The inbox title shows a transient unread count when new private messages are +present. `:inbox clear` removes private messages and the reply target for this session. Private messages are per-session only and are not written to `messages.log`. diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index 825c8f1..1c7bf34 100644 --- a/docs/INTERFACE.md +++ b/docs/INTERFACE.md @@ -168,8 +168,9 @@ persisted to `messages.log` and are not included in exec `tail`, exec `dump`, Each participant keeps a bounded in-memory `:inbox` for the current session. Recipients see incoming private messages; senders see local sent-message copies. Unread incoming messages are marked with `*` until `:inbox` renders. -`:inbox` displays newest messages first, can be refreshed with `r`, and -refreshes automatically while open when a new private message arrives. +`:inbox` displays newest messages first, shows a transient unread count, can +be refreshed with `r`, and refreshes automatically while open when a new +private message arrives. `:inbox clear` removes the current session's private messages, unread count, and reply target. diff --git a/docs/USER_LIFECYCLE.md b/docs/USER_LIFECYCLE.md index d5934b8..eec3f63 100644 --- a/docs/USER_LIFECYCLE.md +++ b/docs/USER_LIFECYCLE.md @@ -38,9 +38,9 @@ The product path should stay short: reconnect. - `:inbox` is live enough for normal chat use: it can be refreshed with `r` and refreshes automatically when a new private message arrives while the - inbox is open. Incoming unread messages are marked with `*` until the inbox - renders them. `:inbox clear` removes private messages and the reply target - for the current session. + inbox is open. Incoming unread messages are marked with `*` and counted in + the inbox title until the inbox renders them. `:inbox clear` removes private + messages and the reply target for the current session. - `:reply` / `:r` keeps the private-message path keyboard-short: it answers the latest private-message peer in the current session without retyping a username. diff --git a/include/i18n.h b/include/i18n.h index d697665..b824867 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -50,6 +50,7 @@ typedef enum { I18N_INBOX_EMPTY, I18N_INBOX_SENT_TO_FORMAT, I18N_INBOX_CLEARED, + I18N_INBOX_UNREAD_FORMAT, I18N_NICK_INVALID, I18N_NICK_TAKEN_FORMAT, I18N_NICK_UNCHANGED, diff --git a/src/commands.c b/src/commands.c index 0b1177d..f9f92fb 100644 --- a/src/commands.c +++ b/src/commands.c @@ -138,9 +138,11 @@ static void append_inbox_output(client_t *client, char *output, size_t buf_size, size_t *pos) { whisper_t snapshot[WHISPER_INBOX_SIZE]; int snap_count; + int unread_count; pthread_mutex_lock(&client->whisper_lock); snap_count = client->whisper_inbox_count; + unread_count = client->unread_whispers; memcpy(snapshot, client->whisper_inbox, snap_count * sizeof(whisper_t)); for (int i = 0; i < snap_count; i++) { @@ -150,9 +152,18 @@ static void append_inbox_output(client_t *client, char *output, pthread_mutex_unlock(&client->whisper_lock); buffer_appendf(output, buf_size, pos, - "\033[1;36m%s\033[0m \033[2;37m· %d\033[0m\n", + "\033[1;36m%s\033[0m \033[2;37m· %d", i18n_text(client->ui_lang, I18N_INBOX_TITLE), snap_count); + if (unread_count > 0) { + buffer_appendf(output, buf_size, pos, + " · "); + buffer_appendf(output, buf_size, pos, + i18n_text(client->ui_lang, + I18N_INBOX_UNREAD_FORMAT), + unread_count); + } + buffer_appendf(output, buf_size, pos, "\033[0m\n"); if (snap_count == 0) { buffer_appendf(output, buf_size, pos, " \033[2;37m%s\033[0m\n", diff --git a/src/i18n_text.c b/src/i18n_text.c index 92f226e..3795839 100644 --- a/src/i18n_text.c +++ b/src/i18n_text.c @@ -141,6 +141,10 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = { "Private messages cleared\n", "私信已清空\n" ), + [I18N_INBOX_UNREAD_FORMAT] = I18N_STRING( + "%d new", + "%d 新" + ), [I18N_NICK_INVALID] = I18N_STRING( "Invalid username\n", "用户名无效\n" diff --git a/tests/test_user_lifecycle.sh b/tests/test_user_lifecycle.sh index 146eb87..c6d14eb 100755 --- a/tests/test_user_lifecycle.sh +++ b/tests/test_user_lifecycle.sh @@ -288,7 +288,9 @@ fi BOB_PID="" if grep -q '.*alice.*private lifecycle second' "$STATE_DIR/bob.log" && + grep -q '2 新' "$STATE_DIR/bob.log" && grep -q '\*.*alice.*private lifecycle second' "$STATE_DIR/bob.log" && + grep -q '1 新' "$STATE_DIR/alice.log" && grep -q '\*.*bob.*private lifecycle reply' "$STATE_DIR/alice.log"; then echo "✓ unread private messages are visibly marked in inbox" PASS=$((PASS + 1)) diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index 76e65fe..57c20b2 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -164,6 +164,10 @@ TEST(text_lookup_matches_language) { "cleared") != NULL); assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_CLEARED), "清空") != NULL); + assert(strstr(i18n_text(UI_LANG_EN, I18N_INBOX_UNREAD_FORMAT), + "new") != NULL); + assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_UNREAD_FORMAT), + "新") != NULL); assert(strstr(i18n_text(UI_LANG_EN, I18N_SEARCH_HEADER_FORMAT), "Search") != NULL); assert(strstr(i18n_text(UI_LANG_ZH, I18N_SEARCH_HEADER_FORMAT), diff --git a/tnt.1 b/tnt.1 index 026a489..2eb5fa5 100644 --- a/tnt.1 +++ b/tnt.1 @@ -256,7 +256,7 @@ page shows incoming messages and local sent-message copies for the current session. It refreshes automatically when a new private message arrives while it is open. Incoming unread messages are marked with .B * -until the inbox renders them. Use +and counted in the inbox title until the inbox renders them. Use .B :reply or .B :r