diff --git a/README.md b/README.md index c140efa..33035bc 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Command output pages use `j/k`, `Ctrl+D/U`, and `g/G` for paging. `:inbox` 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. Private messages are per-session only and are not written to `messages.log`. **Special messages (INSERT mode)** diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index 3381322..791226a 100644 --- a/docs/INTERFACE.md +++ b/docs/INTERFACE.md @@ -167,8 +167,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. `:inbox` displays newest messages first, can be refreshed with `r`, -and refreshes automatically while open when a new private message arrives. +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. ### `help` diff --git a/docs/USER_LIFECYCLE.md b/docs/USER_LIFECYCLE.md index 03f6a64..b111d5b 100644 --- a/docs/USER_LIFECYCLE.md +++ b/docs/USER_LIFECYCLE.md @@ -38,7 +38,8 @@ 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. + inbox is open. Incoming unread messages are marked with `*` until the inbox + renders them. - `: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/ssh_server.h b/include/ssh_server.h index 5cbd34e..bb84c90 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -17,6 +17,7 @@ typedef struct { char to[MAX_USERNAME_LEN]; char content[MAX_MESSAGE_LEN]; bool outgoing; + bool unread; } whisper_t; typedef enum { diff --git a/src/commands.c b/src/commands.c index 84029e5..169f239 100644 --- a/src/commands.c +++ b/src/commands.c @@ -82,6 +82,7 @@ static void client_append_whisper(client_t *owner, const char *from, snprintf(owner->whisper_inbox[slot].content, sizeof(owner->whisper_inbox[slot].content), "%s", content); owner->whisper_inbox[slot].outgoing = outgoing; + owner->whisper_inbox[slot].unread = count_unread; snprintf(owner->last_whisper_peer, sizeof(owner->last_whisper_peer), "%s", outgoing ? to : from); if (count_unread) { @@ -142,6 +143,9 @@ static void append_inbox_output(client_t *client, char *output, snap_count = client->whisper_inbox_count; memcpy(snapshot, client->whisper_inbox, snap_count * sizeof(whisper_t)); + for (int i = 0; i < snap_count; i++) { + client->whisper_inbox[i].unread = false; + } client->unread_whispers = 0; pthread_mutex_unlock(&client->whisper_lock); @@ -157,6 +161,7 @@ static void append_inbox_output(client_t *client, char *output, for (int i = snap_count - 1; i >= 0; i--) { char ts[20]; char peer[MAX_USERNAME_LEN + 16]; + const char *marker = snapshot[i].unread ? "\033[1;35m*\033[0m" : " "; struct tm tmi; localtime_r(&snapshot[i].timestamp, &tmi); strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi); @@ -169,8 +174,8 @@ static void append_inbox_output(client_t *client, char *output, snprintf(peer, sizeof(peer), "%s", snapshot[i].from); } buffer_appendf(output, buf_size, pos, - " \033[90m%s\033[0m \033[35m%s\033[0m: %s\n", - ts, peer, snapshot[i].content); + " %s \033[90m%s\033[0m \033[35m%s\033[0m: %s\n", + marker, ts, peer, snapshot[i].content); } } diff --git a/tests/test_user_lifecycle.sh b/tests/test_user_lifecycle.sh index f4f4d0a..c89c4d0 100755 --- a/tests/test_user_lifecycle.sh +++ b/tests/test_user_lifecycle.sh @@ -273,6 +273,18 @@ else fi BOB_PID="" +if grep -q '.*alice.*private lifecycle second' "$STATE_DIR/bob.log" && + grep -q '\*.*alice.*private lifecycle second' "$STATE_DIR/bob.log" && + grep -q '\*.*bob.*private lifecycle reply' "$STATE_DIR/alice.log"; then + echo "✓ unread private messages are visibly marked in inbox" + PASS=$((PASS + 1)) +else + echo "✗ inbox unread marker missing" + sed -n '1,220p' "$STATE_DIR/bob.log" + sed -n '1,260p' "$STATE_DIR/alice.log" + FAIL=$((FAIL + 1)) +fi + TAIL_OUTPUT=$(ssh $SSH_EXEC_OPTS localhost "tail -n 10" 2>/dev/null || true) printf '%s\n' "$TAIL_OUTPUT" | grep -q 'hello lifecycle alpha' && printf '%s\n' "$TAIL_OUTPUT" | grep -q 'alice2 ships lifecycle' diff --git a/tnt.1 b/tnt.1 index 3f3ad1f..add4666 100644 --- a/tnt.1 +++ b/tnt.1 @@ -253,7 +253,9 @@ The .B :inbox 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. Use +it is open. Incoming unread messages are marked with +.B * +until the inbox renders them. Use .B :reply or .B :r