diff --git a/README.org b/README.org index 4407010..c411288 100644 --- a/README.org +++ b/README.org @@ -117,6 +117,15 @@ In a room buffer: + You can customize settings in the ~ement~ group. - Set ~ement-auto-sync~ to sync new messages automatically. +*** Displaying symbols and emojis + +Emacs may not display certain symbols and emojis well by default. Based on [[https://emacs.stackexchange.com/questions/62049/override-the-default-font-for-emoji-characters][this question and answer]], you may find that the simplest way to fix this is to install an appropriate font, like [[https://www.google.com/get/noto/#emoji-zsye][Noto Emoji]], and then use this Elisp code: + +#+BEGIN_SRC elisp + (setf use-default-font-for-symbols nil) + (set-fontset-font t 'unicode "Noto Emoji" nil 'append) +#+END_SRC + * Changelog :PROPERTIES: :TOC: :depth 0 diff --git a/ement-room.el b/ement-room.el index 49c6077..e4b93fc 100644 --- a/ement-room.el +++ b/ement-room.el @@ -97,7 +97,7 @@ Used by `ement-room-send-message'.") "Suffix for Ement room buffer names." :type 'string) -(defcustom ement-room-message-format-spec "%B%R%t" +(defcustom ement-room-message-format-spec "%B%r%R%t" "Format messages according to this spec. It may contain these specifiers: @@ -107,6 +107,7 @@ It may contain these specifiers: %b Message body (plain-text) %B Message body (formatted if available) %i Event ID + %r Reactions %s Sender ID %S Sender display name %t Event timestamp, formatted according to @@ -190,6 +191,10 @@ See Info node `(elisp)Specified Space'." '((t (:inherit font-lock-comment-face))) "Membership events (join/part).") +(defface ement-room-reactions + '((t (:inherit font-lock-comment-face :height 0.9))) + "Reactions to messages.") + (defface ement-room-timestamp '((t (:inherit font-lock-comment-face))) "Event timestamps.") @@ -520,8 +525,31 @@ function to `ement-room-event-fns', which see." (declare (indent defun)) `(setf (alist-get ,type ement-room-event-fns nil nil #'string=) (lambda (event) + ;; TODO: Docstring. ,@body))) +(ement-room-defevent "m.reaction" + (pcase-let* (((cl-struct ement-event content) event) + ((map ('m.relates_to relates-to)) content) + ((map ('event_id related-id) ('rel_type rel-type) _key) relates-to)) + ;; TODO: Handle other rel_types? + (pcase rel-type + ("m.annotation" + ;; Look for related event in timeline. + (if-let ((related-event (cl-loop for event in (ement-room-timeline ement-room) + when (equal related-id (ement-event-id event)) + return event))) + ;; Found related event: add reaction to local slot and invalidate node. + (progn + (push event (map-elt (ement-event-local related-event) 'reactions)) + (ewoc-invalidate ement-ewoc (ement-room--ewoc-last-matching + (lambda (data) + (and (ement-event-p data) + (equal related-id (ement-event-id data))))))) + ;; No known related event: discard. + ;; TODO: Is this the correct thing to do? + nil))))) + (ement-room-defevent "m.typing" (pcase-let* (((cl-struct ement-event content) event) ((map ('user_ids user-ids)) content) @@ -558,6 +586,17 @@ buffer should be a room's buffer." (funcall pred (ewoc-data node))) finally return node)) +(defun ement-room--ewoc-last-matching (predicate) + "Return the last node in current buffer's EWOC matching PREDICATE. +PREDICATE is called with node's data. Searches backward from +last node." + ;; Intended to be like `ewoc-collect', but returning as soon as a match is found. + (cl-loop with node = (ewoc-nth ement-ewoc -1) + while node + when (funcall predicate (ewoc-data node)) + return node + do (setf node (ewoc-prev ement-ewoc node)))) + (defun ement-room--insert-ts-headers (&optional start-node end-node) "Insert timestamp headers into current buffer's `ement-ewoc'. Inserts headers between START-NODE and END-NODE, which default to @@ -764,6 +803,9 @@ seconds." :button-face 'ement-room-membership :value event) "") + ("m.reaction" + ;; Handled by defevent-based handler. + "") (_ (propertize (format "[sender:%s type:%s]" (ement-user-id (ement-event-sender event)) (ement-event-type event)) @@ -771,6 +813,22 @@ seconds." (propertize " " 'display ement-room-event-separator-display-property))) +(defun ement-room--format-reactions (event) + "Return formatted reactions to EVENT." + (if-let ((reactions (map-elt (ement-event-local event) 'reactions))) + (cl-labels ((format-key-senders + (ks) (pcase-let ((`(,key . ,senders) ks)) + (propertize (format "%s (%s)" key (length senders)) + 'help-echo (string-join senders ", "))))) + (cl-loop with keys-senders + for reaction in reactions + for key = (map-nested-elt (ement-event-content reaction) '(m.relates_to key)) + for sender-name = (ement-room--user-display-name (ement-event-sender reaction) ement-room) + do (push sender-name (alist-get key keys-senders nil nil #'string=)) + finally return (propertize (concat "\n " (string-join (mapcar #'format-key-senders keys-senders) " ")) + 'face 'ement-room-reactions))) + "")) + (cl-defun ement-room--format-message (event &optional (format ement-room-message-format-spec)) "Return EVENT formatted according to FORMAT. Format defaults to `ement-room-message-format-spec', which see." @@ -819,6 +877,7 @@ Format defaults to `ement-room-message-format-spec', which see." (?s (propertize (ement-user-id (ement-event-sender event)) 'face 'ement-room-user)) (?S (ement-room--format-user (ement-event-sender event) ement-room)) + (?r (ement-room--format-reactions event)) (?t (propertize (format-time-string ement-room-timestamp-format ;; Timestamps are in milliseconds. (/ (ement-event-origin-server-ts event) 1000)) diff --git a/ement-structs.el b/ement-structs.el index 04f4907..76d48f3 100644 --- a/ement-structs.el +++ b/ement-structs.el @@ -41,7 +41,9 @@ color) (cl-defstruct ement-event - id sender content origin-server-ts type unsigned) + id sender content origin-server-ts type unsigned + ;; The local slot is an alist used by the local client only. + local) (cl-defstruct ement-server name port uri-prefix) diff --git a/ement.el b/ement.el index 2c8285a..1bda828 100644 --- a/ement.el +++ b/ement.el @@ -372,15 +372,22 @@ To be called in `ement-sync-callback-hook'." (dolist (buffer buffers) (with-current-buffer buffer (cl-assert ement-room) + ;; Add the new events to the main timeline slot first, because some events can + ;; refer to other events, and we want them to be found in the timeline slot. + (setf (ement-room-timeline ement-room) (append (ement-room-timeline* ement-room) + (ement-room-timeline ement-room))) (when (ement-room-ephemeral ement-room) (ement-room--process-events (ement-room-ephemeral ement-room)) (setf (ement-room-ephemeral ement-room) nil)) (when (ement-room-timeline* ement-room) (ement-room--insert-events (ement-room-timeline* ement-room)) - ;; Move new events. - (setf (ement-room-timeline ement-room) (append (ement-room-timeline* ement-room) - (ement-room-timeline ement-room)) - (ement-room-timeline* ement-room) nil)))))) + ;; For now, we also call `--process-events' for ones that are defined with `ement-room-defevent'. + ;; FIXME: Unify this. + ;; HACK: Process these events in reverse order, so that later events + ;; (like reactions) which refer to earlier events can find them. + (ement-room--process-events (reverse (ement-room-timeline* ement-room))) + ;; Clear new events slot. + (setf (ement-room-timeline* ement-room) nil)))))) (defun ement--push-joined-room-events (session joined-room) "Push events for JOINED-ROOM into that room in SESSION."