;;; 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 "%-20s" "Name") (format "%-40s" "Last message") "When"))) ;;;###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) (if (null conversations) (insert "No conversations. Sync from signal-cli with `g`.\n") (dolist (conv conversations) (insert (noise-chat-list--format-row conv) "\n"))) (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." (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)) (fringe (if (> unread 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 36 nil nil "…")) (time-str (noise-chat-list--format-time timestamp))) (concat " " fringe " " unread-str " " (propertize (format "%-18s" name-str) 'face 'bold) " " preview-str " " (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