diff --git a/ement-directory.el b/ement-directory.el index 73d30704..cfbb8834 100644 --- a/ement-directory.el +++ b/ement-directory.el @@ -180,7 +180,8 @@ Show up to LIMIT rooms. Interactively, with prefix, prompt for server and LIMIT. SINCE may be a next-batch token." - (interactive (let* ((session (ement-complete-session :prompt "Search on session: ")) + (interactive (let* ((session (ement-complete-session :prompt "Search on session: " + :pred #'ement-session-has-synced-p)) (server (if current-prefix-arg (read-string "Search on server: " nil nil (ement-server-name (ement-session-server session))) @@ -203,31 +204,32 @@ SINCE may be a next-batch token." ('total_room_count_estimate remaining)) results)) (ement-directory--view rooms :append-p since - :buffer-name (format "*Ement Directory: %s*" server) - :root-section-name (format "Ement Directory: %s" server) - :init-fn (lambda () - (setf (alist-get 'server ement-directory-etc) server - (alist-get 'session ement-directory-etc) session - (alist-get 'next-batch ement-directory-etc) next-batch - (alist-get 'limit ement-directory-etc) limit) - (setq-local revert-buffer-function revert-function) - (when remaining - ;; FIXME: The server seems to report all of the rooms on - ;; the server as remaining even when searching for a - ;; specific term like "emacs". - ;; TODO: Display this in a more permanent place (like a - ;; header or footer). - (message - (substitute-command-keys - "%s rooms remaining (use \\[ement-directory-next] to fetch more)") - remaining))))))) + :buffer-name (format "*Ement Directory: %s*" server) + :root-section-name (format "Ement Directory: %s" server) + :init-fn (lambda () + (setf (alist-get 'server ement-directory-etc) server + (alist-get 'session ement-directory-etc) session + (alist-get 'next-batch ement-directory-etc) next-batch + (alist-get 'limit ement-directory-etc) limit) + (setq-local revert-buffer-function revert-function) + (when remaining + ;; FIXME: The server seems to report all of the rooms on + ;; the server as remaining even when searching for a + ;; specific term like "emacs". + ;; TODO: Display this in a more permanent place (like a + ;; header or footer). + (message + (substitute-command-keys + "%s rooms remaining (use \\[ement-directory-next] to fetch more)") + remaining))))))) (ement-message "Listing %s rooms on %s..." limit server))) ;;;###autoload (cl-defun ement-directory-search (query &key server session since (limit 1000)) "View public rooms on SERVER matching QUERY. QUERY is a string used to filter results." - (interactive (let* ((session (ement-complete-session :prompt "Search on session: ")) + (interactive (let* ((session (ement-complete-session :prompt "Search on session: " + :pred #'ement-session-has-synced-p)) (server (if current-prefix-arg (read-string "Search on server: " nil nil (ement-server-name (ement-session-server session))) @@ -253,20 +255,20 @@ QUERY is a string used to filter results." ('total_room_count_estimate remaining)) results)) (ement-directory--view rooms :append-p since - :buffer-name (format "*Ement Directory: \"%s\" on %s*" query server) - :root-section-name (format "Ement Directory: \"%s\" on %s" query server) - :init-fn (lambda () - (setf (alist-get 'server ement-directory-etc) server - (alist-get 'session ement-directory-etc) session - (alist-get 'next-batch ement-directory-etc) next-batch - (alist-get 'limit ement-directory-etc) limit - (alist-get 'query ement-directory-etc) query) - (setq-local revert-buffer-function revert-function) - (when remaining - (message - (substitute-command-keys - "%s rooms remaining (use \\[ement-directory-next] to fetch more)") - remaining))))))) + :buffer-name (format "*Ement Directory: \"%s\" on %s*" query server) + :root-section-name (format "Ement Directory: \"%s\" on %s" query server) + :init-fn (lambda () + (setf (alist-get 'server ement-directory-etc) server + (alist-get 'session ement-directory-etc) session + (alist-get 'next-batch ement-directory-etc) next-batch + (alist-get 'limit ement-directory-etc) limit + (alist-get 'query ement-directory-etc) query) + (setq-local revert-buffer-function revert-function) + (when remaining + (message + (substitute-command-keys + "%s rooms remaining (use \\[ement-directory-next] to fetch more)") + remaining))))))) (ement-message "Searching for %S on %s..." query server))) (defun ement-directory-next () diff --git a/ement-lib.el b/ement-lib.el index 4dfc8363..0f625c34 100644 --- a/ement-lib.el +++ b/ement-lib.el @@ -167,7 +167,8 @@ include with the request (see Matrix spec)." ;; TODO: Document other arguments. ;; SPEC: 10.1.1. (declare (indent defun)) - (interactive (list (ement-complete-session) + (interactive (list (ement-complete-session :prompt "Make room as: " + :pred #'ement-session-has-synced-p) :name (read-string "New room name: ") :alias (read-string "New room alias (e.g. \"foo\" for \"#foo:matrix.org\"): ") :topic (read-string "New room topic: ") @@ -200,7 +201,8 @@ include with the request (see Matrix spec)." Then call function THEN with response data. Optional string arguments are NAME, ALIAS, and TOPIC." (declare (indent defun)) - (interactive (list (ement-complete-session) + (interactive (list (ement-complete-session :prompt "Make space as: " + :pred #'ement-session-has-synced-p) :name (read-string "New space name: ") :alias (read-string "New space alias (e.g. \"foo\" for \"#foo:matrix.org\"): ") :topic (read-string "New space topic: ") @@ -292,7 +294,8 @@ when necessary, and forget the room without prompting." "Ignore USER-ID on SESSION. If UNIGNORE-P (interactively, with prefix), un-ignore USER." (interactive (list (ement-complete-user-id) - (ement-complete-session) + (ement-complete-session :prompt "Ignore user as: " + :pred #'ement-session-has-synced-p) current-prefix-arg)) (pcase-let* (((cl-struct ement-session account-data) session) ;; TODO: Store session account-data events in an alist keyed on type. @@ -376,7 +379,8 @@ Uses the latest existing direct room with the user, or creates a new one automatically if necessary." ;; SPEC: 13.23.2. (interactive - (let* ((session (ement-complete-session)) + (let* ((session (ement-complete-session :prompt "Send as: " + :pred #'ement-session-has-synced-p)) (user-id (ement-complete-user-id)) (message (read-string "Message: "))) (list session user-id message))) @@ -449,7 +453,8 @@ new one automatically if necessary." "Set DISPLAY-NAME for user on SESSION. Sets global displayname." (interactive - (let* ((session (ement-complete-session)) + (let* ((session (ement-complete-session :prompt "Set name for: " + :pred #'ement-session-has-synced-p)) (display-name (read-string "Set display-name to: " nil nil (ement-user-displayname (ement-session-user session))))) (list display-name session))) @@ -772,14 +777,18 @@ THEN and ELSE are passed to `ement-api', which see." :content-type content-type :data data :data-type 'binary :then then :else else)) -(cl-defun ement-complete-session (&key (prompt "Session: ")) - "Return an Ement session selected with completion." +(cl-defun ement-complete-session (&key (pred #'identity) (prompt "Session: ")) + "Return an Ement session selected with completion. +PROMPT is passed to `completing-read'. Only offer sessions +matching PRED." (pcase (length ement-sessions) - (0 (user-error "No active sessions. Call `ement-connect' to log in")) - (1 (cdar ement-sessions)) - (_ (let* ((ids (mapcar #'car ement-sessions)) - (selected-id (completing-read prompt ids nil t))) - (alist-get selected-id ement-sessions nil nil #'equal))))) + (0 (user-error "No sessions. Call `ement-connect' to log in")) + (_ (let ((sessions (cl-remove-if-not pred ement-sessions :key #'cdr))) + (pcase (length sessions) + (1 (cdar sessions)) + (_ (let* ((ids (mapcar #'car sessions)) + (selected-id (completing-read prompt ids nil t))) + (alist-get selected-id sessions nil nil #'equal)))))))) (declare-function ewoc-locate "ewoc") (defun ement-complete-user-id () diff --git a/ement-notifications.el b/ement-notifications.el index 4df7e96b..229b6125 100644 --- a/ement-notifications.el +++ b/ement-notifications.el @@ -122,7 +122,8 @@ LIMIT may be a maximum number of events to return. ONLY may be the string \"highlight\" to only return notifications that have the highlight tweak set. THEN and ELSE may be callbacks passed to `ement-api', which see." - (interactive (list (ement-complete-session) + (interactive (list (ement-complete-session :prompt "Show notifications for: " + :pred #'ement-session-has-synced-p) :only (when current-prefix-arg "highlight"))) (if-let ((buffer (get-buffer "*Ement Notifications*"))) @@ -170,7 +171,8 @@ to `ement-api', which see." ;; FIXME: Naming things is hard. "Retrieve NUMBER older notifications on SESSION." ;; FIXME: Support multiple sessions. - (interactive (list (ement-complete-session) + (interactive (list (ement-complete-session :prompt "Retrieve notifications for: " + :pred #'ement-session-has-synced-p) (cl-typecase current-prefix-arg (null 100) (list (read-number "Number of messages: ")) @@ -292,7 +294,9 @@ to `ement-api', which see." ;; the command is asynchronous in that case, so the buffer can be displayed in the wrong ;; window. Fixing this would be hacky and awkward, but a partial solution is probably ;; possible. - (ement-notifications (ement-complete-session))) + (ement-notifications + (ement-complete-session :prompt "Show notifications for: " + :pred #'ement-session-has-synced-p))) ;;; Footer diff --git a/ement-room.el b/ement-room.el index e66136a8..52bbfa70 100644 --- a/ement-room.el +++ b/ement-room.el @@ -1775,7 +1775,8 @@ THEN may be a function to call after joining the room (and when buffer). It receives two arguments, the room and the session." (interactive (list (read-string "Join room (ID or alias): ") (or ement-session - (ement-complete-session)))) + (ement-complete-session :prompt "Join as: " + :pred #'ement-session-has-synced-p)))) (cl-assert id-or-alias) (cl-assert session) (unless (string-match-p ;; According to tulir in #matrix-dev:matrix.org, ": is not @@ -2426,7 +2427,11 @@ these all require at least version 29 of Emacs): (defun ement-room-view (room session) "Switch to a buffer showing ROOM on SESSION. Uses action `ement-view-room-display-buffer-action', which see." - (interactive (ement-complete-room :session (ement-complete-session) :suggest nil + (interactive (ement-complete-room + :session (ement-complete-session + :prompt "View as: " + :pred #'ement-session-has-synced-p) + :suggest nil :predicate (lambda (room) (not (ement--space-p room))))) (pcase-let* (((cl-struct ement-room (local (map buffer))) room)) diff --git a/ement.el b/ement.el index 0cb2fae4..16410598 100644 --- a/ement.el +++ b/ement.el @@ -225,7 +225,10 @@ the port, e.g. (cl-case (length ement-sessions) (0 (list :user-id (read-string "User ID: " nil 'ement-connect-user-id-history))) (1 (list :session (cdar ement-sessions))) - (otherwise (list :session (ement-complete-session)))))) + (otherwise (list :session (ement-complete-session + :prompt "Connect as user ID: " + :pred (lambda (session) + (not (ement-session-has-synced-p session))))))))) (let (sso-server-process) (cl-labels ((new-session () (unless (string-match (rx bos "@" (group (1+ (not (any ":")))) ; Username @@ -329,6 +332,9 @@ Ement: SSO login accepted; session token received. Connecting to Matrix server. (if session ;; Start syncing given session. (let ((user-id (ement-user-id (ement-session-user session)))) + (unless (hash-table-p (ement-session-events session)) + ;; HACK: Ensure session's events slot is a hash table (when disconnecting, it's cleared). + (setf (ement-session-events session) (make-hash-table :test #'equal))) ;; HACK: If session is already in ement-sessions, this replaces it. I think that's okay... (setf (alist-get user-id ement-sessions nil nil #'equal) session) (ement--sync session :timeout ement-initial-sync-timeout)) @@ -349,10 +355,8 @@ room buffers are left alive and can be read, but other commands in them won't work." (interactive (list (if current-prefix-arg (mapcar #'cdr ement-sessions) - (list (ement-complete-session))))) - (when ement-save-sessions - ;; Write sessions before we remove them from the variable. - (ement--write-sessions ement-sessions)) + (list (ement-complete-session :prompt "Disconnect: " + :pred #'ement-session-has-synced-p))))) (dolist (session sessions) (let ((user-id (ement-user-id (ement-session-user session)))) (when-let ((process (map-elt ement-syncs session))) @@ -362,11 +366,16 @@ in them won't work." (delete-process process)) ;; NOTE: I'd like to use `map-elt' here, but not until ;; is fixed, I guess. - (setf (alist-get session ement-syncs nil nil #'equal) nil - (alist-get user-id ement-sessions nil 'remove #'equal) nil))) - (unless ement-sessions - ;; HACK: If no sessions remain, clear the users table. It might be best - ;; to store a per-session users table, but this is probably good enough. + (setf (alist-get session ement-syncs) nil + ;; Set the session to an "emptied" one, which only retains slots needed to + ;; reconnect. + (alist-get user-id ement-sessions nil nil #'equal) (ement--emptied session)))) + (when ement-save-sessions + ;; Write sessions now that we have emptied the session. + (ement--write-sessions ement-sessions)) + (unless (cl-find-if #'ement-session-has-synced-p ement-sessions :key #'cdr) + ;; HACK: If no connected sessions remain, clear the users table. It might be best to + ;; store a per-session users table, but this is probably good enough. (clrhash ement-users)) ;; TODO: Should call this hook for each session with the session as argument. (run-hooks 'ement-disconnect-hook) @@ -873,6 +882,7 @@ Returns nil if unable to read `ement-sessions-file'." :token token :transaction-id transaction-id)))) (message "Ement: Writing sessions...") + ;; TODO: Use `persist'. (with-temp-file ement-sessions-file (pcase-let* ((print-level nil) (print-length nil) @@ -885,6 +895,22 @@ Returns nil if unable to read `ement-sessions-file'." ;; Ensure permissions are safe. (chmod ement-sessions-file #o600))) +(cl-defmethod ement--emptied ((session ement-session)) + "Return a copy of SESSION with most slots nulled. +The copy only includes these slots: user, server, token, and +transaction-id. This is suitable for persisting upon disconnect +so the session can be reconnected, but allowing most data to be +garbage-collected." + (pcase-let* (((cl-struct ement-session user server token transaction-id) session) + ((cl-struct ement-user (id user-id) username) user) + ((cl-struct ement-server (name server-name) uri-prefix) server)) + (make-ement-session :user (make-ement-user :id user-id + :username username) + :server (make-ement-server :name server-name + :uri-prefix uri-prefix) + :token token + :transaction-id transaction-id))) + (defun ement--kill-emacs-hook () "Function to be added to `kill-emacs-hook'. Writes Ement session to disk when enabled."