;;; noise-new-chat.el --- Start new conversations from contacts -*- lexical-binding: t; -*- ;;; Commentary: ;; Provides `noise-new-chat' — start a new conversation by searching ;; contacts with minibuffer completion. Contacts are synced from ;; signal-cli via the listContacts JSON-RPC method and cached in the ;; local SQLite database. ;;; Code: (require 'noise-rpc) (require 'noise-db) (require 'cl-lib) (declare-function noise-chat-list--render "noise-chat-list") ;;;###autoload (defun noise-new-chat () "Start a new conversation by searching contacts. Syncs contacts from signal-cli when the local database has none. Candidates match on both name and phone number." (interactive) (noise-db-init) (unless (noise-db-get-all-contacts) (noise-new-chat-sync-contacts)) (let* ((contacts (noise-db-get-all-contacts)) (collection (mapcar (lambda (c) (cons (noise-new-chat--candidate c) c)) contacts))) (unless collection (user-error "No contacts found. Is signal-cli synced?")) (let* ((choice (completing-read "New chat with: " collection nil t)) (contact (cdr (assoc choice collection)))) (when contact (noise-new-chat-with-contact (gethash "id" contact) (gethash "name" contact)))))) ;;;###autoload (defun noise-new-chat-sync-contacts () "Fetch contacts from signal-cli and store them in the local database. Returns the number of contacts synced." (interactive) (noise-db-init) (let ((contacts (noise-rpc-call-for-account "listContacts")) (count 0)) (seq-do (lambda (c) (when-let ((id (noise-new-chat--contact-id c))) (noise-db-upsert-contact id :name (noise-new-chat--contact-name c) :color (noise-new-chat--field c "color") :profile-name (noise-new-chat--profile-name c)) (cl-incf count))) contacts) (when (called-interactively-p 'interactive) (message "Synced %d contacts from signal-cli" count)) count)) (defun noise-new-chat-with-contact (contact-id name) "Open (creating if needed) a direct conversation with CONTACT-ID. NAME is used as the conversation name for a new conversation. The conversation ID for a direct chat is the contact ID itself." (unless (noise-db-get-conversation contact-id) (noise-db-upsert-conversation contact-id :type "direct" :name (if (and name (not (string-empty-p name))) name contact-id) :members (list contact-id))) (when-let ((buf (get-buffer "*signal*"))) (with-current-buffer buf (noise-chat-list--render))) ;; Conversation buffers are not implemented yet; land on the chat list. (message "Conversation with %s ready (conversation buffer not yet implemented)" (or name contact-id))) (defun noise-new-chat--candidate (contact) "Format CONTACT (a hash table from noise-db) as a completion candidate. Includes the phone number so completion can match on it." (let ((id (gethash "id" contact)) (name (gethash "name" contact))) (if (or (null name) (string-empty-p name) (string= name id)) id (format "%s (%s)" name id)))) (defun noise-new-chat--field (obj key) "Get KEY from hash table OBJ, treating JSON null as nil." (when obj (let ((v (gethash key obj))) (unless (eq v :null) v)))) (defun noise-new-chat--contact-id (contact) "Extract the contact ID from a signal-cli CONTACT object. Prefers the phone number, falling back to the UUID." (or (noise-new-chat--field contact "number") (noise-new-chat--field contact "uuid"))) (defun noise-new-chat--profile-name (contact) "Extract the profile display name from a signal-cli CONTACT object." (when-let ((profile (noise-new-chat--field contact "profile"))) (let ((full (string-trim (concat (or (noise-new-chat--field profile "givenName") "") " " (or (noise-new-chat--field profile "familyName") ""))))) (unless (string-empty-p full) full)))) (defun noise-new-chat--contact-name (contact) "Pick the best display name for a signal-cli CONTACT object. Prefers the local contact name, then the profile name, then the username, then the phone number." (or (let ((name (noise-new-chat--field contact "name"))) (and name (not (string-empty-p name)) name)) (noise-new-chat--profile-name contact) (noise-new-chat--field contact "username") (noise-new-chat--contact-id contact))) (provide 'noise-new-chat) ;;; noise-new-chat.el ends here