(require 'cl-lib)
(require 'tabulated-list)
(require 'ement)
(declare-function ement-notify-switch-to-mentions-buffer "ement-notify")
(declare-function ement-notify-switch-to-notifications-buffer "ement-notify")
(defvar ement-tabulated-room-list-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "SPC") #'ement-tabulated-room-list-next-unread)
(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)
map))
(defvar ement-tabulated-room-list-timestamp-colors nil
"List of colors used for timestamps.
Set automatically when `ement-tabulated-room-list-mode' is activated.")
(defvar ement-sessions)
(defgroup ement-tabulated-room-list nil
"Options for the room list buffer."
:group 'ement)
(defcustom ement-tabulated-room-list-auto-update t
"Automatically update the room list buffer."
:type 'boolean)
(defcustom ement-tabulated-room-list-avatars (display-images-p)
"Show room avatars in the room list."
:type 'boolean)
(defcustom ement-tabulated-room-list-simplify-timestamps t
"Only show the largest unit of time in a timestamp.
For example, \"1h54m3s\" becomes \"1h\"."
:type 'boolean)
(defface ement-tabulated-room-list-name
'((t (:inherit font-lock-function-name-face button)))
"Non-direct rooms.")
(defface ement-tabulated-room-list-direct
'((t (:weight normal :inherit (font-lock-constant-face ement-tabulated-room-list-name))))
"Direct rooms.")
(defface ement-tabulated-room-list-invited
'((t (:inherit italic ement-tabulated-room-list-name)))
"Invited rooms.")
(defface ement-tabulated-room-list-left
'((t (:strike-through t :inherit ement-tabulated-room-list-name)))
"Left rooms.")
(defface ement-tabulated-room-list-unread
'((t (:inherit bold ement-tabulated-room-list-name)))
"Unread rooms.")
(defface ement-tabulated-room-list-favourite '((t (:inherit (font-lock-doc-face ement-tabulated-room-list-name))))
"Favourite rooms.")
(defface ement-tabulated-room-list-low-priority '((t (:inherit (font-lock-comment-face ement-tabulated-room-list-name))))
"Low-priority rooms.")
(defface ement-tabulated-room-list-recent
'((t (:inherit font-lock-warning-face)))
"Latest timestamp of recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past 24
hours but at least one hour ago.")
(defface ement-tabulated-room-list-very-recent
'((t (:inherit error)))
"Latest timestamp of very recently updated rooms.
The foreground color is used to generate a gradient of colors
from recent to non-recent for rooms updated in the past hour.")
(require 'bookmark)
(defun ement-tabulated-room-list-bookmark-make-record ()
"Return a bookmark record for the `ement-tabulated-room-list' buffer."
(pcase-let* (((cl-struct ement-session user) ement-session)
((cl-struct ement-user (id session-id)) user))
(list (concat "Ement room list (" session-id ")")
(cons 'session-id session-id)
(cons 'handler #'ement-tabulated-room-list-bookmark-handler))))
(defun ement-tabulated-room-list-bookmark-handler (bookmark)
"Show Ement room list buffer for BOOKMARK."
(pcase-let* (((map session-id) bookmark))
(unless (alist-get session-id ement-sessions nil nil #'equal)
(user-error "Session %s not connected: call `ement-connect' first" session-id))
(ement-tabulated-room-list)))
(defun ement-tabulated-room-list-next-unread ()
"Show next unread room."
(interactive)
(unless (button-at (point))
(call-interactively #'forward-button))
(unless (cl-loop with starting-line = (line-number-at-pos)
if (equal "U" (elt (tabulated-list-get-entry) 0))
do (progn
(goto-char (button-end (button-at (point))))
(push-button (1- (point)))
(cl-return t))
else do (call-interactively #'forward-button)
while (> (line-number-at-pos) starting-line))
(message "No more unread rooms")))
(defun ement-tabulated-room-list (&rest _ignore)
"Show buffer listing joined rooms.
Calls `pop-to-buffer-same-window'. Interactively, with prefix,
call `pop-to-buffer'."
(interactive)
(with-current-buffer (get-buffer-create "*Ement Rooms*")
(ement-tabulated-room-list-mode)
(setq-local bookmark-make-record-function #'ement-tabulated-room-list-bookmark-make-record)
(funcall (if current-prefix-arg
#'pop-to-buffer #'pop-to-buffer-same-window)
(current-buffer))))
(defun ement-tabulated-room-list--timestamp-colors ()
"Return a vector of generated latest-timestamp colors for rooms.
Used in `ement-tabulated-room-list' and `ement-room-list'."
(if (or (equal "unspecified-fg" (face-foreground 'default nil 'default))
(equal "unspecified-bg" (face-background 'default nil 'default)))
(make-vector 134 "unspecified-fg")
(cl-coerce
(append (mapcar
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-tabulated-room-list-very-recent
nil 'default))
(color-name-to-rgb (face-foreground 'ement-tabulated-room-list-recent
nil 'default))
6))
(mapcar
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'ement-tabulated-room-list-recent
nil 'default))
(color-name-to-rgb (face-foreground 'default nil 'default))
24))
(mapcar
(lambda (rgb)
(pcase-let ((`(,r ,g ,b) rgb))
(color-rgb-to-hex r g b 2)))
(color-gradient (color-name-to-rgb (face-foreground 'default nil 'default))
(color-name-to-rgb (face-background 'default nil 'default))
104)))
'vector)))
(define-derived-mode ement-tabulated-room-list-mode tabulated-list-mode
"Ement-Tabulated-Room-List"
:group 'ement
(setf tabulated-list-format (vector
'("U" 1 t)
'(#("P" 0 1 (help-echo "Priority (favorite/low)")) 1 t)
'("B" 1 t)
'("d" 1 t) (list (propertize "🐱"
'help-echo "Avatar")
4 t) '("Name" 25 t) '("Topic" 35 t)
(list "Latest"
(if ement-tabulated-room-list-simplify-timestamps
6 20)
#'ement-tabulated-room-list-latest<
:right-align t)
'("Members" 7 ement-tabulated-room-list-members<)
'("Session" 15 t))
tabulated-list-sort-key '("Latest" . t)
ement-tabulated-room-list-timestamp-colors (ement-tabulated-room-list--timestamp-colors))
(add-hook 'tabulated-list-revert-hook #'ement-tabulated-room-list--set-entries nil 'local)
(tabulated-list-init-header)
(ement-tabulated-room-list--set-entries)
(tabulated-list-revert))
(defun ement-tabulated-room-list-action (event)
"Show buffer for room at EVENT or point."
(interactive "e")
(mouse-set-point event)
(pcase-let* ((room (tabulated-list-get-id))
(`[,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name ,_topic ,_latest ,_members ,user-id]
(tabulated-list-get-entry))
(session (alist-get user-id ement-sessions nil nil #'equal)))
(ement-view-room room session)))
(defun ement-tabulated-room-list-auto-update (_session)
"Automatically update the room list buffer.
Does so when variable `ement-tabulated-room-list-auto-update' is non-nil.
To be called in `ement-sync-callback-hook'."
(when (and ement-tabulated-room-list-auto-update
(buffer-live-p (get-buffer "*Ement Rooms*")))
(with-current-buffer (get-buffer "*Ement Rooms*")
(revert-buffer))))
(defun ement-tabulated-room-list--set-entries ()
"Set `tabulated-list-entries'."
(let ((entries (cl-loop for (_id . session) in ement-sessions
append (mapcar (lambda (room)
(ement-tabulated-room-list--entry session room))
(ement-session-rooms session)))))
(setf tabulated-list-entries
(cl-sort entries #'> :key (lambda (entry)
(or (ement-room-latest-ts (car entry)) 0))))))
(defun ement-tabulated-room-list--entry (session room)
"Return entry for ROOM in SESSION for `tabulated-list-entries'."
(pcase-let* (((cl-struct ement-room id canonical-alias display-name avatar topic latest-ts summary
(local (map buffer room-list-avatar)))
room)
((map ('m.joined_member_count member-count)) summary)
(e-alias (or canonical-alias
(setf (ement-room-canonical-alias room)
(ement--room-alias room))
id))
(e-unread (if (and buffer (buffer-modified-p buffer))
(propertize "U" 'help-echo "Unread") ""))
(e-buffer (if buffer (propertize "B" 'help-echo "Room has buffer") ""))
(e-avatar (if (and ement-tabulated-room-list-avatars avatar)
(or room-list-avatar
(if-let* ((avatar-image (get-text-property 0 'display avatar))
(new-avatar-string (propertize " " 'display
(ement--resize-image avatar-image
nil (frame-char-height)))))
(progn
(setf (alist-get 'room-list-avatar (ement-room-local room))
new-avatar-string)
new-avatar-string)
(ement-debug "nil avatar for room: " (ement-room-display-name room) (ement-room-canonical-alias room))
""))
""))
(name-face (cl-copy-list '(:inherit (ement-tabulated-room-list-name))))
(e-name (list (propertize (or display-name
(ement--room-display-name room))
'face name-face
'help-echo e-alias)
'action #'ement-tabulated-room-list-action))
(e-topic (if topic
(replace-regexp-in-string "\n" "" topic t t)
""))
(formatted-timestamp (if latest-ts
(ement--human-format-duration (- (time-convert nil 'integer) (/ latest-ts 1000))
t)
""))
(latest-face (when latest-ts
(let* ((difference-seconds (- (float-time) (/ latest-ts 1000)) )
(n (cl-typecase difference-seconds
((number 0 3599) (truncate (/ difference-seconds 600)))
((number 3600 86400) (+ 6 (truncate (/ difference-seconds 3600))))
(otherwise (min (/ (length ement-tabulated-room-list-timestamp-colors) 2)
(+ 24 (truncate (/ difference-seconds 86400 7))))))))
(list :foreground (elt ement-tabulated-room-list-timestamp-colors n)))))
(e-latest (or (when formatted-timestamp
(propertize formatted-timestamp
'value latest-ts
'face latest-face))
""))
(e-session (propertize (ement-user-id (ement-session-user session))
'value session))
(e-direct-p (if (ement--room-direct-p room session)
(propertize "d" 'help-echo "Direct room")
""))
(e-priority (cond ((ement--room-favourite-p room) "F")
((ement--room-low-priority-p room) "l")
(" ")))
(e-members (if member-count (number-to-string member-count) "")))
(when ement-tabulated-room-list-simplify-timestamps
(setf e-latest (replace-regexp-in-string
(rx bos (1+ digit) (1+ alpha) (group (1+ (1+ digit) (1+ alpha))))
"" e-latest t t 1)))
(when (and buffer (buffer-modified-p buffer))
(setf (map-elt name-face :inherit)
(cons 'ement-tabulated-room-list-unread (map-elt name-face :inherit))))
(when (ement--room-direct-p room session)
(setf (map-elt name-face :inherit)
(cons 'ement-tabulated-room-list-direct (map-elt name-face :inherit))))
(when (ement--room-favourite-p room)
(push 'ement-tabulated-room-list-favourite (map-elt name-face :inherit)))
(when (ement--room-low-priority-p room)
(push 'ement-tabulated-room-list-low-priority (map-elt name-face :inherit)))
(pcase (ement-room-type room)
('invite
(setf e-topic (concat (propertize "[invited]"
'face 'ement-tabulated-room-list-invited)
" " e-topic)
(map-elt name-face :inherit) (cons 'ement-tabulated-room-list-invited
(map-elt name-face :inherit))))
('leave
(setf e-topic (concat (propertize "[left]"
'face 'ement-tabulated-room-list-left)
" " e-topic)
(map-elt name-face :inherit) (cons (map-elt name-face :inherit)
'ement-tabulated-room-list-left))))
(list room (vector e-unread e-priority e-buffer e-direct-p
e-avatar e-name e-topic e-latest e-members
e-session
))))
(defun ement-tabulated-room-list-members< (a b)
"Return non-nil if entry A has fewer members than room B.
A and B should be entries from `tabulated-list-mode'."
(pcase-let* ((`(,_room [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,_latest ,a-members ,_session]) a)
(`(,_room [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,_latest ,b-members ,_session]) b))
(when (and a-members b-members)
(< (string-to-number a-members) (string-to-number b-members)))))
(defun ement-tabulated-room-list-latest< (a b)
"Return non-nil if entry A has fewer members than room B.
A and B should be entries from `tabulated-list-mode'."
(pcase-let* ((`(,_room-a [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,a-latest ,_a-members ,_session]) a)
(`(,_room-b [,_unread ,_priority ,_buffer ,_direct ,_avatar ,_name-for-list ,_topic ,b-latest ,_b-members ,_session]) b)
(a-latest (get-text-property 0 'value a-latest))
(b-latest (get-text-property 0 'value b-latest)))
(cond ((and a-latest b-latest)
(< a-latest b-latest))
(b-latest
nil)
(t t))))
(provide 'ement-tabulated-room-list)