120 lines
4.6 KiB
EmacsLisp
120 lines
4.6 KiB
EmacsLisp
;;; 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 'noise-conversation)
|
|
(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)))
|
|
(noise-conversation-open 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
|