;;; noise-switcher.el --- Minibuffer conversation switcher for Noise -*- lexical-binding: t; -*- ;;; Commentary: ;; Provides minibuffer completion for switching conversations. ;; Collects conversations and contacts from the database, presents them ;; with fuzzy matching in a vertico-style minibuffer completion. ;;; Code: (require 'noise-db) (defun noise-switcher () "Switch to a Signal conversation via minibuffer completion. Shows existing conversations and contacts without conversations." (interactive) (noise-db-init) (let* ((collection (noise-switcher--collection)) (choice (completing-read "Switch to chat: " (lambda (string pred action) (if (eq action 'metadata) '(metadata (display-sort-function . identity) (annotation-function . noise-switcher--annotate)) (complete-with-action action collection string pred))) nil t))) (when choice (let ((item (cdr (assoc choice collection)))) (if item (if (gethash "id" item) ;; It's a conversation (has an id) (message "Open conversation: %s (not yet implemented)" (gethash "name" item)) ;; It's just a contact — create conversation stub (message "New conversation with: %s (not yet implemented)" choice)) (message "Unknown selection: %s" choice)))))) (defun noise-switcher--collection () "Build the completion collection. Returns an alist of (display-string . hash-table)." (let ((conversations (noise-db-get-all-conversations)) (contacts (noise-db-get-all-contacts)) (collection nil)) ;; Add conversations (dolist (conv conversations) (let ((name (gethash "name" conv))) (push (cons name conv) collection))) ;; Add contacts without conversations (let ((conv-contacts (make-hash-table :test 'equal))) (dolist (conv conversations) (let ((members (noise-db-get-conversation-members (gethash "id" conv)))) (dolist (m members) (puthash (gethash "contact_id" m) t conv-contacts)))) (dolist (contact contacts) (let ((contact-id (gethash "id" contact)) (name (gethash "name" contact))) (unless (gethash contact-id conv-contacts) ;; Contact without conversation - add a stub (let ((ht (make-hash-table :test 'equal))) (puthash "name" name ht) (puthash "type" "direct" ht) (puthash "unread_count" 0 ht) (puthash "last_message_time" 0 ht) (push (cons name ht) collection)))))) collection)) (defun noise-switcher--annotate (candidate) "Annotate CANDIDATE with type, unread, and time info for completing-read." (let* ((collection (noise-switcher--collection)) (item (cdr (assoc candidate collection)))) (when item (let* ((type (gethash "type" item "direct")) (unread (gethash "unread_count" item 0)) (time (gethash "last_message_time" item 0)) (time-str (noise-switcher--format-time-annot time)) (annotation (format "%s" type))) (when (> unread 0) (setq annotation (concat annotation (format " · %d unread" unread)))) (when (> time 0) (setq annotation (concat annotation (format " · %s" time-str)))) (format " %s" annotation))))) (defun noise-switcher--format-time-annot (timestamp) "Format TIMESTAMP for annotation display." (if (<= timestamp 0) "" (let* ((msg-time (seconds-to-time timestamp)) (diff-days (/ (float-time (time-subtract (current-time) msg-time)) 86400))) (cond ((< diff-days 1) (format-time-string "%H:%M" msg-time)) ((< diff-days 7) (format-time-string "%a" msg-time)) (t (format-time-string "%b %e" msg-time)))))) (defun noise-switcher--format-candidate (name info) "Format a switcher candidate for display." (let* ((type (or (cdr (assoc "type" info)) "direct")) (unread (or (cdr (assoc "unread_count" info)) 0)) (time (or (cdr (assoc "last_message_time" info)) 0)) (time-str (if (> time 0) (noise-switcher--format-time-annot time) "")) (annotation (concat type (if (> unread 0) (format " · %d unread" unread) "") (if (> time 0) (format " · %s" time-str) "")))) (format "%-25s %s" name annotation))) (defun noise-switcher--filter (query candidates) "Filter CANDIDATES by QUERY using substring matching." (let ((q (downcase query))) (seq-filter (lambda (c) (string-match-p (regexp-quote q) (downcase c))) candidates))) (provide 'noise-switcher) ;;; noise-switcher.el ends here