dotfiles/emacs/.emacs.d/elpa/ement-0.17/ement-notifications.el

301 lines
14 KiB
EmacsLisp

;;; ement-notifications.el --- Notifications support -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Author: Adam Porter <adam@alphapapa.net>
;; Maintainer: Adam Porter <adam@alphapapa.net>
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements support for Matrix notifications. It differs from
;; `ement-notify', which implements a kind of bespoke notification system for events
;; received via sync requests rather than Matrix's own notifications endpoint. These two
;; libraries currently integrate somewhat, as newly arriving events are handled and
;; notified about by `ement-notify', and old notifications are fetched and listed by
;; `ement-notifications' in the same "*Ement Notifications*" buffer.
;; In the future, these libraries will likely be consolidated and enhanced to more closely
;; follow the Matrix API's and Element client's examples.
;;; Code:
;;;; Requirements
(require 'cl-lib)
(require 'map)
(require 'ement-lib)
(require 'ement-room)
(require 'ement-notify)
;;;; Structs
(cl-defstruct ement-notification
"Represents a Matrix notification."
room-id event readp)
(defun ement-notifications--make (notification)
"Return an `ement-notification' struct for NOTIFICATION.
NOTIFICATION is an alist representing a notification returned
from the \"/notifications\" endpoint. The notification's event
is passed through `ement--make-event'."
(declare (function ement--make-event "ement"))
(pcase-let (((map room_id _actions _ts event read) notification))
(make-ement-notification :room-id room_id :readp read
:event (ement--make-event event))))
;;;; Variables
(declare-function ement-room-list "ement-room-list")
(defvar ement-notifications-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<return>") #'ement-notifications-jump)
(define-key map [mouse-1] #'ement-notifications-jump-mouse)
(define-key map [mouse-2] #'ement-notifications-jump-mouse)
(define-key map (kbd "S-<return>") #'ement-notify-reply)
(define-key map (kbd "M-g M-l") #'ement-room-list)
(define-key map (kbd "M-g M-m") #'ement-notify-switch-to-mentions-buffer)
(define-key map (kbd "M-g M-n") #'ement-notify-switch-to-notifications-buffer)
(define-key map [remap scroll-down-command] #'ement-notifications-scroll-down-command)
(define-key map [remap mwheel-scroll] #'ement-notifications-mwheel-scroll)
(make-composed-keymap (list map) 'view-mode-map))
"Map for Ement notification buffers.")
(cl-defun ement-notifications-jump (&optional (pos (point)))
"Jump to Matrix event at POS."
(interactive)
(let ((session (get-text-property pos 'session))
(room (get-text-property pos 'room))
(event (get-text-property pos 'event)))
(ement-view-room room session)
(ement-room-goto-event event)))
(defun ement-notifications-jump-mouse (event)
"Jump to Matrix event at EVENT."
(interactive "e")
(let ((pos (posn-point (event-start event))))
(if (button-at pos)
(push-button pos)
(ement-notifications-jump pos))))
(defvar ement-notifications-hook '(ement-notifications-log-to-buffer)
"Functions called for `ement-notifications' notifications.
Each function is called with two arguments, the session and the
`ement-notification' struct.")
(defvar-local ement-notifications-retro-loading nil
"Non-nil when earlier messages are being loaded.
Used to avoid overlapping requests.")
(defvar-local ement-notifications-metadata nil
"Metadata for `ement-notifications' buffers.")
;; Variables from other files.
(defvar ement-ewoc)
(defvar ement-session)
(defvar ement-notify-prism-background)
(defvar ement-room-message-format-spec)
(defvar ement-room-sender-in-left-margin)
;;;; Commands
;;;###autoload
(cl-defun ement-notifications
(session &key from limit only
(then (apply-partially #'ement-notifications-callback session)) else)
"Show the notifications buffer for SESSION.
FROM may be a \"next_token\" token from a previous request.
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)
:only (when current-prefix-arg
"highlight")))
(if-let ((buffer (get-buffer "*Ement Notifications*")))
(switch-to-buffer buffer)
(let ((endpoint "notifications")
(params (remq nil
(list (when from
(list "from" from))
(when limit
(list "limit" (number-to-string limit)))
(when only
(list "only" only))))))
(ement-api session endpoint :params params :then then :else else)
(ement-message "Fetching notifications for <%s>..." (ement-user-id (ement-session-user session))))))
(cl-defun ement-notifications-callback (session data &key (buffer (ement-notifications--log-buffer)))
"Callback for `ement-notifications' on SESSION which receives DATA."
(pcase-let (((map notifications next_token) data))
(with-current-buffer buffer
(setf (map-elt ement-notifications-metadata :next-token) next_token)
(cl-loop for notification across notifications
do (run-hook-with-args 'ement-notifications-hook
session (ement-notifications--make notification)))
;; TODO: Pass start/end nodes to `ement-room--insert-ts-headers' if possible.
(ement-room--insert-ts-headers)
(switch-to-buffer (current-buffer)))))
(defun ement-notifications-scroll-down-command ()
"Scroll down, and load NUMBER earlier messages when at top."
(interactive)
(condition-case _err
(scroll-down nil)
(beginning-of-buffer
(call-interactively #'ement-notifications-retro))))
(defun ement-notifications-mwheel-scroll (event)
"Scroll according to EVENT, loading earlier messages when at top."
(interactive "e")
(with-selected-window (posn-window (event-start event))
(mwheel-scroll event)
(when (= (point-min) (window-start))
(call-interactively #'ement-notifications-retro))))
(cl-defun ement-notifications-retro (session number)
;; FIXME: Naming things is hard.
"Retrieve NUMBER older notifications on SESSION."
;; FIXME: Support multiple sessions.
(interactive (list (ement-complete-session)
(cl-typecase current-prefix-arg
(null 100)
(list (read-number "Number of messages: "))
(number current-prefix-arg))))
(cl-assert (eq 'ement-notifications-mode major-mode))
(cl-assert (map-elt ement-notifications-metadata :next-token) nil
"No more notifications for %s" (ement-user-id (ement-session-user ement-session)))
(let ((buffer (current-buffer)))
(unless ement-notifications-retro-loading
(ement-notifications
session :limit number
:from (map-elt ement-notifications-metadata :next-token)
;; TODO: Use a :finally for resetting `ement-notifications-retro-loading'?
:then (lambda (data)
(unwind-protect
(ement-notifications-callback session data :buffer buffer)
(setf (buffer-local-value 'ement-notifications-retro-loading buffer) nil)))
:else (lambda (plz-error)
(setf (buffer-local-value 'ement-notifications-retro-loading buffer) nil)
(ement-api-error plz-error)))
(ement-message "Loading %s earlier messages..." number)
(setf ement-notifications-retro-loading t))))
;;;; Functions
(cl-defun ement-notifications-log-to-buffer (session notification &key (buffer-name "*Ement Notifications*"))
"Log EVENT in ROOM on SESSION to \"*Ement NOTIFICATIONS*\" buffer."
(with-demoted-errors "ement-notifications-log-to-buffer: %S"
(with-current-buffer (ement-notifications--log-buffer :name buffer-name)
(save-window-excursion
(when-let ((buffer-window (get-buffer-window (current-buffer))))
;; Select the buffer's window to avoid EWOC bug. (See #191.)
(select-window buffer-window))
;; TODO: Use the :readp slot to mark unread events.
(save-mark-and-excursion
(pcase-let* (((cl-struct ement-notification room-id event) notification)
(ement-session session)
(ement-room (or (cl-find room-id (ement-session-rooms session)
:key #'ement-room-id :test #'equal)
(error "ement-notifications-log-to-buffer: Can't find room <%s>; discarding notification" room-id)))
(ement-room-sender-in-left-margin nil)
(ement-room-message-format-spec "%o%O »%W %S> %B%R%t")
(new-node (ement-room--insert-event event))
(inhibit-read-only t)
(start) (end))
(ewoc-goto-node ement-ewoc new-node)
;; Apply the button properties only to the room and sender names,
;; allowing buttons in the rest of the message to remain separate.
(setf start (point)
end (save-excursion
(re-search-forward (rx "> "))))
(add-text-properties start end '( button (t)
category default-button
action ement-notify-button-action))
;; Apply the session, room, and event properties to the whole event.
(setf end (save-excursion
(if-let ((next-node (ewoc-next ement-ewoc new-node)))
(ewoc-location next-node)
(point-max))))
(add-text-properties start end
(list 'session session
'room ement-room
'event event))
;; Remove button face property from the whole event.
(alter-text-property start end 'face
(lambda (face)
(pcase face
('button nil)
((pred listp) (remq 'button face))
(_ face))))
(when ement-notify-prism-background
(add-face-text-property start end (list :background (ement-notifications--room-background-color ement-room)
:extend t)))))))))
(defun ement-notifications--room-background-color (room)
"Return a background color on which to display ROOM's messages."
(or (alist-get 'notify-background-color (ement-room-local room))
(setf (alist-get 'notify-background-color (ement-room-local room))
(let ((color (color-desaturate-name
(ement--prism-color (ement-room-id room) :contrast-with (face-foreground 'default))
50)))
(if (ement--color-dark-p (color-name-to-rgb (face-background 'default)))
(color-darken-name color 25)
(color-lighten-name color 25))))))
(cl-defun ement-notifications--log-buffer (&key (name "*Ement Notifications*"))
"Return an Ement notifications buffer named NAME."
(or (get-buffer name)
(with-current-buffer (get-buffer-create name)
(ement-notifications-mode)
(current-buffer))))
;;;; Mode
(define-derived-mode ement-notifications-mode ement-room-mode "Ement Notifications"
(setf ement-room-sender-in-left-margin nil
left-margin-width 0
right-margin-width 8)
(setq-local ement-room-message-format-spec "[%o%O] %S> %B%R%t"
bookmark-make-record-function #'ement-notifications-bookmark-make-record))
;;;; Bookmark support
(require 'bookmark)
(defun ement-notifications-bookmark-make-record ()
"Return a bookmark record for the current `ement-notifications' buffer."
(list (buffer-name)
;; It seems silly to have to record the buffer name twice, but the
;; `bookmark-make-record' function seems to override the bookmark name sometimes,
;; which makes the result useless unless we save the buffer name separately.
(cons 'buffer-name (buffer-name))
(cons 'handler #'ement-notifications-bookmark-handler)))
(defun ement-notifications-bookmark-handler (_bookmark)
"Show `ement-notifications' buffer for BOOKMARK."
;; FIXME: Handle multiple sessions.
;; FIXME: This doesn't work quite correctly when the buffer isn't already open, because
;; 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)))
;;; Footer
(provide 'ement-notifications)
;;; ement-notifications.el ends here