Files
noise/noise-chat-list.el
2026-06-15 11:01:26 -04:00

169 lines
6.3 KiB
EmacsLisp

;;; noise-chat-list.el --- Chat list buffer for Noise -*- lexical-binding: t; -*-
;;; Commentary:
;; Provides the *signal* buffer — an ibuffer-style list of all conversations
;; with unread counts, last message previews, and timestamps.
;;; Code:
(require 'noise-db)
(require 'noise-new-chat)
(require 'noise-conversation)
(require 'cl-lib)
(defvar noise-chat-list-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "n") #'next-line)
(define-key map (kbd "p") #'previous-line)
(define-key map (kbd "RET") #'noise-chat-list-open)
(define-key map (kbd "m") #'noise-chat-list-mark-read)
(define-key map (kbd "c") #'noise-new-chat)
(define-key map (kbd "g") #'noise-chat-list-refresh)
(define-key map (kbd "/") #'noise-chat-list-filter)
(define-key map (kbd "q") #'quit-window)
map)
"Keymap for `noise-chat-list-mode'.")
(define-derived-mode noise-chat-list-mode special-mode "Signal Chats"
"Major mode for the Noise Signal chat list.
\\{noise-chat-list-mode-map}"
:group 'noise
(setq buffer-read-only t)
(setq-local header-line-format
(concat " U "
(format "%-18s" "Name")
(format "%-42s" "Last message")
"When"))
(when (bound-and-true-p evil-mode)
(let ((m (make-sparse-keymap)))
(define-key m (kbd "RET") #'noise-chat-list-open)
(setq-local evil-normal-state-local-map m))))
;;;###autoload
(defun noise-chat-list ()
"Display the Noise Signal chat list."
(interactive)
(noise-db-init)
(switch-to-buffer "*signal*")
(noise-chat-list-mode)
(noise-chat-list--render))
(defun noise-chat-list--render ()
"Render the conversation list in the *signal* buffer."
(let ((inhibit-read-only t)
(conversations (noise-db-get-all-conversations)))
(erase-buffer)
;; Clear old fringe overlays
(remove-overlays (point-min) (point-max) 'noise-fringe t)
(if (null conversations)
(insert "No conversations. Sync from signal-cli with `g`.\n")
(dolist (conv conversations)
(let ((unread (or (gethash "unread_count" conv) 0))
(row (noise-chat-list--format-row conv)))
(insert row "\n")
(when (> unread 0)
(let ((ov (make-overlay (line-beginning-position 0)
(line-beginning-position 0))))
(overlay-put ov 'noise-fringe t)
(overlay-put ov 'before-string
(propertize " " 'display
'(left-fringe filled-rectangle warning))))))))
(noise-chat-list--update-modeline conversations)
(goto-char (point-min))
(set-buffer-modified-p nil)))
(defun noise-chat-list--update-modeline (conversations)
"Update the mode line with chat statistics."
(let* ((total (length conversations))
(unread-total (cl-reduce #'+
(mapcar (lambda (c) (or (gethash "unread_count" c) 0))
conversations)
:initial-value 0)))
(setq mode-line-format
(format " ⌁ %s %d chats %s %s (%s) ─ U:%%- ─ Top ────────────"
(propertize "*signal*" 'face 'bold)
total
(if (> unread-total 0)
(format "· %d unread" unread-total)
"")
""
"Signal Chats"))))
(defun noise-chat-list--format-time (timestamp)
"Format TIMESTAMP (unix seconds) as a human-readable relative time string.
Today: HH:MM, this week: Ddd, older: Mon DD."
(let* ((now (current-time))
(msg-time (seconds-to-time timestamp))
(diff (float-time (time-subtract now msg-time)))
(one-day 86400)
(one-week (* 7 one-day)))
(cond
((< diff one-day)
(format-time-string "%H:%M" msg-time))
((< diff one-week)
(format-time-string "%a" msg-time))
(t
(format-time-string "%b %e" msg-time)))))
(defun noise-chat-list--format-row (conv)
"Format a single conversation row for display.
CONV is a hash table with string keys from noise-db.
The unread dot is rendered in the Emacs fringe, not inline."
(let* ((unread (or (gethash "unread_count" conv) 0))
(name (or (gethash "name" conv) "Unknown"))
(preview (or (gethash "last_message_preview" conv) ""))
(sender (or (gethash "last_message_sender" conv) ""))
(timestamp (or (gethash "last_message_time" conv) 0))
(unread-str (if (> unread 0) (format "%3d" unread) " "))
(name-str (truncate-string-to-width name 18 nil nil "…"))
(preview-prefix
(cond
((string= sender "") preview)
((string= sender "me") (concat "me: " preview))
(t (concat sender ": " preview))))
(preview-str (truncate-string-to-width preview-prefix 42 nil nil "…"))
(time-str (noise-chat-list--format-time timestamp)))
(concat unread-str " "
(propertize (format "%-18s" name-str) 'face 'bold)
(propertize (format "%-42s" preview-str) 'face 'shadow)
(propertize time-str 'face 'shadow))))
(defun noise-chat-list--conversation-at-point ()
"Return the conversation on the current line, or nil."
(let ((conversations (noise-db-get-all-conversations))
(idx (1- (line-number-at-pos))))
(nth idx conversations)))
(defun noise-chat-list-open ()
"Open the conversation at point."
(interactive)
(let ((conv (noise-chat-list--conversation-at-point)))
(if conv
(noise-conversation-open (gethash "id" conv))
(message "No conversation on this line."))))
(defun noise-chat-list-mark-read ()
"Mark the conversation at point as read."
(interactive)
(let ((conv (noise-chat-list--conversation-at-point)))
(if conv
(progn
(noise-db-mark-conversation-read (gethash "id" conv))
(noise-chat-list--render))
(message "No conversation on this line."))))
(defun noise-chat-list-refresh ()
"Refresh the chat list (re-render from database)."
(interactive)
(noise-chat-list--render)
(message "Chat list refreshed."))
(defun noise-chat-list-filter ()
"Filter the chat list with a search query."
(interactive)
(message "Filter: not yet implemented"))
(provide 'noise-chat-list)
;;; noise-chat-list.el ends here