diff --git a/README.md b/README.md index 33035bc..dc6b198 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Ctrl+C - Exit chat :reply - Reply to latest private message :r - Short alias for :reply :inbox - Show private messages +:inbox clear - Clear private messages for this session :last [N] - Show last N messages from history (max 50, default 10) :search - Search message history (shows last 15 matches) :mute-joins - Toggle join/leave system notifications @@ -115,6 +116,7 @@ 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. +`: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`. **Special messages (INSERT mode)** diff --git a/docs/EASY_SETUP.md b/docs/EASY_SETUP.md index 12a5482..c66d589 100644 --- a/docs/EASY_SETUP.md +++ b/docs/EASY_SETUP.md @@ -82,6 +82,7 @@ Common commands: :msg send private message :reply reply to latest private message :inbox show private messages +:inbox clear clear private messages :last [N] recent messages :search search message history :lang en|zh switch UI language diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index 791226a..825c8f1 100644 --- a/docs/INTERFACE.md +++ b/docs/INTERFACE.md @@ -170,6 +170,8 @@ 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 clear` removes the current session's private messages, unread count, +and reply target. ### `help` diff --git a/docs/QUICKREF.md b/docs/QUICKREF.md index a84b017..88b7ba3 100644 --- a/docs/QUICKREF.md +++ b/docs/QUICKREF.md @@ -33,6 +33,7 @@ COMMANDS (COMMAND mode, prefix with :) reply reply to latest private message r alias for reply inbox show private messages, newest first + inbox clear clear private messages for this session last [N] last N messages from log (default 10, max 50) search search full history (case-insensitive, 15 results) mute-joins toggle join/leave notifications diff --git a/docs/USER_LIFECYCLE.md b/docs/USER_LIFECYCLE.md index b111d5b..d5934b8 100644 --- a/docs/USER_LIFECYCLE.md +++ b/docs/USER_LIFECYCLE.md @@ -39,7 +39,8 @@ The product path should stay short: - `: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. + 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. @@ -54,8 +55,8 @@ The product path should stay short: - first user opens `?`, checks `:users`, sends a public message, scrolls, uses `:last` and `:search` - first user toggles `:mute-joins`, sends two `:msg` messages, receives a - `:reply`, confirms private-message copies in `:inbox`, changes nickname, - sends `/me`, and exits + `:reply`, confirms private-message copies in `:inbox`, clears the inbox, + changes nickname, sends `/me`, and exits - second user opens `:inbox` before the private messages arrive, sees it auto-refresh after delivery, newest first, and replies without retyping the sender's username diff --git a/include/i18n.h b/include/i18n.h index 7fc2953..d697665 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -49,6 +49,7 @@ typedef enum { I18N_INBOX_TITLE, I18N_INBOX_EMPTY, I18N_INBOX_SENT_TO_FORMAT, + I18N_INBOX_CLEARED, I18N_NICK_INVALID, I18N_NICK_TAKEN_FORMAT, I18N_NICK_UNCHANGED, diff --git a/src/command_catalog.c b/src/command_catalog.c index 3817327..dafd615 100644 --- a/src/command_catalog.c +++ b/src/command_catalog.c @@ -50,11 +50,11 @@ static const command_catalog_entry_t entries[] = { }, { {TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}}, + I18N_STRING(":inbox, :inbox clear", ":inbox, :inbox clear"), + I18N_STRING("Show or clear private messages", "查看或清空私信"), I18N_STRING(":inbox", ":inbox"), - I18N_STRING("Show private messages", "查看私信"), - I18N_STRING(":inbox", ":inbox"), - I18N_STRING("Usage: inbox\n", "用法: inbox\n"), - 2, true, false + I18N_STRING("Usage: inbox [clear]\n", "用法: inbox [clear]\n"), + 2, false, false }, { {TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}}, @@ -239,6 +239,9 @@ bool command_catalog_args_valid(tnt_command_id_t id, const char *args) { if (!entry) { return false; } + if (id == TNT_COMMAND_INBOX) { + return !args || args[0] == '\0' || strcmp(args, "clear") == 0; + } if (entry->no_args) { return !args || args[0] == '\0'; } diff --git a/src/commands.c b/src/commands.c index 169f239..0b1177d 100644 --- a/src/commands.c +++ b/src/commands.c @@ -179,6 +179,15 @@ static void append_inbox_output(client_t *client, char *output, } } +static void clear_inbox(client_t *client) { + pthread_mutex_lock(&client->whisper_lock); + memset(client->whisper_inbox, 0, sizeof(client->whisper_inbox)); + client->whisper_inbox_count = 0; + client->unread_whispers = 0; + client->last_whisper_peer[0] = '\0'; + pthread_mutex_unlock(&client->whisper_lock); +} + bool commands_refresh_active_output(client_t *client) { char output[MAX_COMMAND_OUTPUT_LEN] = {0}; size_t pos = 0; @@ -361,8 +370,17 @@ void commands_dispatch(client_t *client) { } } else if (command_id == TNT_COMMAND_INBOX) { - output_kind = TNT_COMMAND_OUTPUT_INBOX; - append_inbox_output(client, output, sizeof(output), &pos); + const char *inbox_arg = arg; + while (*inbox_arg == ' ') inbox_arg++; + if (strcmp(inbox_arg, "clear") == 0) { + clear_inbox(client); + buffer_appendf(output, sizeof(output), &pos, "%s", + i18n_text(client->ui_lang, + I18N_INBOX_CLEARED)); + } else { + output_kind = TNT_COMMAND_OUTPUT_INBOX; + append_inbox_output(client, output, sizeof(output), &pos); + } } else if (command_id == TNT_COMMAND_NICK) { const char *new_name = arg; diff --git a/src/i18n_text.c b/src/i18n_text.c index 6ed9cd8..92f226e 100644 --- a/src/i18n_text.c +++ b/src/i18n_text.c @@ -137,6 +137,10 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = { "you -> %s", "你 -> %s" ), + [I18N_INBOX_CLEARED] = I18N_STRING( + "Private messages cleared\n", + "私信已清空\n" + ), [I18N_NICK_INVALID] = I18N_STRING( "Invalid username\n", "用户名无效\n" diff --git a/tests/test_user_lifecycle.sh b/tests/test_user_lifecycle.sh index c89c4d0..146eb87 100755 --- a/tests/test_user_lifecycle.sh +++ b/tests/test_user_lifecycle.sh @@ -238,6 +238,20 @@ send -- "q" expect "NORMAL" send -- ":" expect ":" +send -- "inbox clear\r" +expect "私信已清空" +expect "q:关闭" +send -- "q" +expect "NORMAL" +send -- ":" +expect ":" +send -- "reply should not send after clear\r" +expect "没有可回复的私信" +expect "q:关闭" +send -- "q" +expect "NORMAL" +send -- ":" +expect ":" send -- "nick alice2\r" expect "昵称已修改: alice -> alice2" expect "q:关闭" diff --git a/tests/unit/test_command_catalog.c b/tests/unit/test_command_catalog.c index d607105..fd8aa3d 100644 --- a/tests/unit/test_command_catalog.c +++ b/tests/unit/test_command_catalog.c @@ -43,6 +43,10 @@ TEST(matches_canonical_names_and_aliases) { assert(id == TNT_COMMAND_REPLY); assert(strcmp(args, "hello back") == 0); + assert(command_catalog_match("inbox clear", &id, &args)); + assert(id == TNT_COMMAND_INBOX); + assert(strcmp(args, "clear") == 0); + assert(command_catalog_match("language zh", &id, &args)); assert(id == TNT_COMMAND_LANG); assert(strcmp(args, "zh") == 0); @@ -75,6 +79,9 @@ TEST(validates_argument_shapes) { assert(command_catalog_args_valid(TNT_COMMAND_MSG, "alice hello")); assert(!command_catalog_args_valid(TNT_COMMAND_REPLY, "")); assert(command_catalog_args_valid(TNT_COMMAND_REPLY, "hello back")); + assert(command_catalog_args_valid(TNT_COMMAND_INBOX, NULL)); + assert(command_catalog_args_valid(TNT_COMMAND_INBOX, "clear")); + assert(!command_catalog_args_valid(TNT_COMMAND_INBOX, "clear now")); assert(!command_catalog_args_valid(TNT_COMMAND_SEARCH, "")); assert(command_catalog_args_valid(TNT_COMMAND_SEARCH, "needle")); @@ -103,12 +110,12 @@ TEST(generates_localized_help_sections) { assert(strstr(en, "Show online users") != NULL); assert(strstr(en, ":msg ") != NULL); assert(strstr(en, ":reply ") != NULL); - assert(strstr(en, "Show private messages") != NULL); + assert(strstr(en, "Show or clear private messages") != NULL); assert(strstr(en, ":support") == NULL); assert(strstr(zh, ":users, :list, :who") != NULL); assert(strstr(zh, "显示在线用户") != NULL); - assert(strstr(zh, "查看私信") != NULL); + assert(strstr(zh, "查看或清空私信") != NULL); assert(strstr(zh, ":msg ") != NULL); assert(strstr(zh, ":reply ") != NULL); assert(strstr(zh, "<用户>") == NULL); @@ -139,6 +146,12 @@ TEST(generates_localized_usage) { assert(strcmp(en, "Usage: reply \n" " r \n") == 0); + zh[0] = '\0'; + zh_pos = 0; + command_catalog_append_usage(zh, sizeof(zh), &zh_pos, + TNT_COMMAND_INBOX, UI_LANG_ZH); + assert(strcmp(zh, "用法: inbox [clear]\n") == 0); + en[0] = '\0'; en_pos = 0; command_catalog_append_usage(en, sizeof(en), &en_pos, diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index 06b4c2f..76e65fe 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -160,6 +160,10 @@ TEST(text_lookup_matches_language) { "you ->") != NULL); assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_SENT_TO_FORMAT), "你 ->") != NULL); + assert(strstr(i18n_text(UI_LANG_EN, I18N_INBOX_CLEARED), + "cleared") != NULL); + assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_CLEARED), + "清空") != 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 add4666..026a489 100644 --- a/tnt.1 +++ b/tnt.1 @@ -223,6 +223,7 @@ l l. :reply \fItext\fR Reply to latest private message :r \fItext\fR Short alias for :reply :inbox Show private messages, newest first +:inbox clear Clear private messages for this session :last [\fIN\fR] Show last N messages from history (1\-50, default 10) :search \fIkeyword\fR Case\-insensitive search; shows the last 15 matches :mute\-joins Toggle join/leave system notifications on/off @@ -259,7 +260,10 @@ until the inbox renders them. Use .B :reply or .B :r -to answer the latest private-message peer. Private messages are not written to +to answer the latest private-message peer. +.B :inbox clear +removes private messages and the reply target for this session. Private +messages are not written to .IR messages.log . .SH EXEC INTERFACE Commands can be run non\-interactively for scripting: