mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:44:38 +08:00
chat: whisper inbox with :inbox view + ✉ unread chip (UX-12)
Whispers used to flash on the recipient's terminal and disappear with
the next redraw. No history, no record, no signal if you weren't
looking.
Now whispers are stored per-recipient in a bounded inbox (16 slots,
FIFO eviction):
typedef struct { time_t timestamp; char from[]; char content[]; } whisper_t;
whisper_t whisper_inbox[16];
int whisper_inbox_count;
_Atomic int unread_whispers;
Sender side (:msg / :w):
- resolves target as before
- pushes the whisper into target->whisper_inbox under target->io_lock
(so two simultaneous senders to the same recipient don't tear the
ring)
- bumps target->unread_whispers atomically
- sends a single \a bell + triggers redraw_pending
- no longer writes whisper text directly to the channel (which used
to get clobbered by the next redraw)
Recipient side:
- new :inbox command in COMMAND mode prints the snapshot under
io_lock, in M7 chat-list style:
悄悄话 · whispers · 3
05-17 13:42 alice: 一会儿要不要喝咖啡
...
- viewing :inbox resets unread_whispers to 0
Title bar (extends UX-11):
- bright magenta "✉ N" chip alongside the yellow "★ N" mention chip
- same priority / degradation rules as ★
Whispers remain private — they're never broadcast to the room and
never persisted to messages.log. The inbox lives only in client_t,
so disconnecting drops it.
This commit is contained in:
parent
ddcecbea81
commit
87d6572156
3 changed files with 92 additions and 9 deletions
|
|
@ -7,6 +7,16 @@
|
|||
#include <libssh/libssh.h>
|
||||
#include <libssh/server.h>
|
||||
|
||||
/* One stored whisper. Kept per-recipient, not broadcast to the room
|
||||
* and not persisted to messages.log. Inbox is bounded; oldest slides
|
||||
* out FIFO. */
|
||||
#define WHISPER_INBOX_SIZE 16
|
||||
typedef struct {
|
||||
time_t timestamp;
|
||||
char from[MAX_USERNAME_LEN];
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
} whisper_t;
|
||||
|
||||
/* Client connection structure */
|
||||
typedef struct client {
|
||||
ssh_session session; /* SSH session */
|
||||
|
|
@ -37,6 +47,11 @@ typedef struct client {
|
|||
time_t last_active;
|
||||
atomic_bool redraw_pending;
|
||||
_Atomic int unread_mentions; /* @-mentions received since last reset */
|
||||
_Atomic int unread_whispers; /* whispers received since last :inbox view */
|
||||
/* Per-client whisper inbox. Pushes serialise on io_lock; readers are
|
||||
* the client's own thread inside :inbox handling. */
|
||||
whisper_t whisper_inbox[WHISPER_INBOX_SIZE];
|
||||
int whisper_inbox_count;
|
||||
bool mute_joins;
|
||||
pthread_t thread;
|
||||
atomic_bool connected;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@ void commands_dispatch(client_t *client) {
|
|||
"========================================\n"
|
||||
"list, users, who - Show online users\n"
|
||||
"nick/name <name> - Change nickname\n"
|
||||
"msg/w <user> <text> - Whisper to user\n"
|
||||
"msg/w <user> <text> - Whisper to user (private)\n"
|
||||
"inbox - Show whisper history\n"
|
||||
"last [N] - Show last N messages\n"
|
||||
"search <keyword> - Search message history\n"
|
||||
"mute-joins - Toggle join/leave notices\n"
|
||||
|
|
@ -155,12 +156,33 @@ void commands_dispatch(client_t *client) {
|
|||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
if (target) {
|
||||
char whisper[MAX_MESSAGE_LEN];
|
||||
snprintf(whisper, sizeof(whisper),
|
||||
"\r\n\033[35m[whisper from %s]: %s\033[0m\r\n",
|
||||
client->username, rest);
|
||||
client_send(target, whisper, strlen(whisper));
|
||||
/* Push into recipient's inbox. io_lock serialises so two
|
||||
* senders to the same recipient don't tear the ring. */
|
||||
pthread_mutex_lock(&target->io_lock);
|
||||
int slot;
|
||||
if (target->whisper_inbox_count < WHISPER_INBOX_SIZE) {
|
||||
slot = target->whisper_inbox_count++;
|
||||
} else {
|
||||
/* FIFO evict the oldest */
|
||||
memmove(&target->whisper_inbox[0],
|
||||
&target->whisper_inbox[1],
|
||||
(WHISPER_INBOX_SIZE - 1) * sizeof(whisper_t));
|
||||
slot = WHISPER_INBOX_SIZE - 1;
|
||||
}
|
||||
target->whisper_inbox[slot].timestamp = time(NULL);
|
||||
snprintf(target->whisper_inbox[slot].from,
|
||||
sizeof(target->whisper_inbox[slot].from),
|
||||
"%s", client->username);
|
||||
snprintf(target->whisper_inbox[slot].content,
|
||||
sizeof(target->whisper_inbox[slot].content),
|
||||
"%s", rest);
|
||||
pthread_mutex_unlock(&target->io_lock);
|
||||
|
||||
target->unread_whispers++;
|
||||
target->redraw_pending = true;
|
||||
/* Audible nudge — the title bar ✉ counter (UX-11 style)
|
||||
* carries the persistent signal. */
|
||||
client_send(target, "\a", 1);
|
||||
client_release(target);
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +195,35 @@ void commands_dispatch(client_t *client) {
|
|||
}
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "inbox") == 0) {
|
||||
/* Snapshot the inbox under io_lock so a concurrent sender doesn't
|
||||
* tear what we're rendering. Counter reset happens after copy. */
|
||||
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
||||
int snap_count;
|
||||
pthread_mutex_lock(&client->io_lock);
|
||||
snap_count = client->whisper_inbox_count;
|
||||
memcpy(snapshot, client->whisper_inbox,
|
||||
snap_count * sizeof(whisper_t));
|
||||
pthread_mutex_unlock(&client->io_lock);
|
||||
client->unread_whispers = 0;
|
||||
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"\033[1;36m悄悄话 · whispers\033[0m "
|
||||
"\033[2;37m· %d\033[0m\n", snap_count);
|
||||
if (snap_count == 0) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
" \033[2;37m(空)\033[0m\n");
|
||||
}
|
||||
for (int i = 0; i < snap_count; i++) {
|
||||
char ts[20];
|
||||
struct tm tmi;
|
||||
localtime_r(&snapshot[i].timestamp, &tmi);
|
||||
strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi);
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
" \033[90m%s\033[0m \033[35m%s\033[0m: %s\n",
|
||||
ts, snapshot[i].from, snapshot[i].content);
|
||||
}
|
||||
|
||||
} else if (strncmp(cmd, "nick ", 5) == 0 || strncmp(cmd, "name ", 5) == 0) {
|
||||
char *new_name = cmd + 5;
|
||||
while (*new_name == ' ') new_name++;
|
||||
|
|
|
|||
23
src/tui.c
23
src/tui.c
|
|
@ -371,11 +371,22 @@ void tui_render_screen(client_t *client) {
|
|||
unread_width = utf8_string_width(unread_buf) + 2; /* leading " · " minus initial space accounted later */
|
||||
}
|
||||
|
||||
/* Unread whispers chip — bright magenta envelope. Same priority as
|
||||
* the mentions chip; both signal "you missed something". */
|
||||
int whisper_count = client->unread_whispers;
|
||||
char whisper_buf[32] = "";
|
||||
int whisper_width = 0;
|
||||
if (whisper_count > 0) {
|
||||
snprintf(whisper_buf, sizeof(whisper_buf), "✉ %d", whisper_count);
|
||||
whisper_width = utf8_string_width(whisper_buf) + 2;
|
||||
}
|
||||
|
||||
/* Decide what fits. Reserve at least 1 col of gap between left and
|
||||
* right halves so they never visually touch. */
|
||||
int show_hint = 1;
|
||||
int show_mute = client->mute_joins ? 1 : 0;
|
||||
int show_unread = unread_count > 0 ? 1 : 0;
|
||||
int show_whisper = whisper_count > 0 ? 1 : 0;
|
||||
int show_chips = chip_count;
|
||||
|
||||
while (show_chips > 1) {
|
||||
|
|
@ -385,16 +396,17 @@ void tui_render_screen(client_t *client) {
|
|||
left_w += utf8_string_width(chips[i].value);
|
||||
}
|
||||
if (show_mute) left_w += mute_width;
|
||||
if (show_unread) left_w += unread_width + 1; /* + " " separator */
|
||||
if (show_unread) left_w += unread_width + 1;
|
||||
if (show_whisper) left_w += whisper_width + 1;
|
||||
int right_w = (show_hint ? hint_width + 1 /*trailing space*/ : 0);
|
||||
int needed = left_w + 1 /*min gap*/ + right_w;
|
||||
if (needed <= render_width) break;
|
||||
|
||||
/* Drop in priority order: hint → mute → mode chip → online count.
|
||||
* Unread is sticky — only dropped if everything else already is. */
|
||||
/* Drop priority: hint → mute → mode → online → whispers → mentions. */
|
||||
if (show_hint) { show_hint = 0; continue; }
|
||||
if (show_mute) { show_mute = 0; continue; }
|
||||
if (show_chips > 1) { show_chips--; continue; }
|
||||
if (show_whisper) { show_whisper = 0; continue; }
|
||||
if (show_unread) { show_unread = 0; continue; }
|
||||
break;
|
||||
}
|
||||
|
|
@ -421,6 +433,11 @@ void tui_render_screen(client_t *client) {
|
|||
" \033[1;33m%s\033[0m", unread_buf);
|
||||
left_width += unread_width + 1;
|
||||
}
|
||||
if (show_whisper) {
|
||||
buffer_appendf(left, sizeof(left), &lpos,
|
||||
" \033[1;35m%s\033[0m", whisper_buf);
|
||||
left_width += whisper_width + 1;
|
||||
}
|
||||
|
||||
int gap = render_width - left_width - (show_hint ? hint_width + 2 : 1);
|
||||
if (gap < 1) gap = 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue