diff options
-rw-r--r-- | README.org | 17 | ||||
-rw-r--r-- | lisp/.dir-locals.el | 7 | ||||
-rw-r--r-- | lisp/mastodon-async.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-auth.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-client.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-discover.el | 6 | ||||
-rw-r--r-- | lisp/mastodon-http.el | 39 | ||||
-rw-r--r-- | lisp/mastodon-inspect.el | 6 | ||||
-rw-r--r-- | lisp/mastodon-iso.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-media.el | 7 | ||||
-rw-r--r-- | lisp/mastodon-notifications.el | 488 | ||||
-rw-r--r-- | lisp/mastodon-profile.el | 25 | ||||
-rw-r--r-- | lisp/mastodon-search.el | 55 | ||||
-rw-r--r-- | lisp/mastodon-tl.el | 573 | ||||
-rw-r--r-- | lisp/mastodon-toot.el | 190 | ||||
-rw-r--r-- | lisp/mastodon-transient.el | 343 | ||||
-rw-r--r-- | lisp/mastodon-views.el | 6 | ||||
-rw-r--r-- | lisp/mastodon.el | 61 | ||||
-rw-r--r-- | mastodon-index.org | 9 | ||||
-rw-r--r-- | mastodon.info | 106 | ||||
-rw-r--r-- | mastodon.texi | 20 | ||||
-rw-r--r-- | screenshot-transient-1.jpg | bin | 0 -> 43851 bytes | |||
-rw-r--r-- | screenshot-transient-2.jpg | bin | 0 -> 24894 bytes |
23 files changed, 1420 insertions, 546 deletions
@@ -60,7 +60,7 @@ Or, with =use-package=: :ensure t) #+END_SRC -The minimum Emacs version is now 27.1. But if you are running an older version +The minimum Emacs version is now 28.1. But if you are running an older version it shouldn't be very hard to get it working. *** Emoji @@ -381,6 +381,11 @@ for =mastodon.el=. The repo is at [[https://github.com/rougier/mastodon-alt][mastodon-alt]]. +*** mastodon hydra + +A user made a hydra for handling basic mastodon.el commands. It's available at +https://holgerschurig.github.io/en/emacs-mastodon-hydra/. + *** Live-updating timelines: =mastodon-async-mode= (code taken from [[https://github.com/alexjgriffith/mastodon-future.el][mastodon-future]].) @@ -482,7 +487,7 @@ If you prefer emailing patches to the process described below, feel free to send If you'd like to support continued development of =mastodon.el=, I accept donations via paypal: [[https://paypal.me/martianh][paypal.me/martianh]]. If you would prefer a different -payment method, please write to me at <martianhiatus [at] riseup [dot] net> and I can +payment method, please write to me at <mousebot@disroot.org> and I can provide IBAN or other bank account details. I don't have a tech worker's income, so even a small tip would help out. @@ -508,3 +513,11 @@ Here's a (federated) timeline: Here's a notifcations view plus a compose buffer: [[file:screenshot-notifs+compose.png]] + +Here's a user settings transient (active values green, current server values commented and, if a boolean, underlined): + +[[file:screenshot-transient-1.jpg]] + +Here's a user profile fields transient (changed fields green, current server values commented): + +[[file:screenshot-transient-2.jpg]] diff --git a/lisp/.dir-locals.el b/lisp/.dir-locals.el index bcb8ba5..da012d6 100644 --- a/lisp/.dir-locals.el +++ b/lisp/.dir-locals.el @@ -1,7 +1,6 @@ -;;; Directory Local Variables +;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") -;; Preferred indentation style: ((nil . ((indent-tabs-mode . nil))) - ;; setting this makes package-lint look in the main file for deps: - (emacs-lisp-mode . ((package-lint-main-file . "mastodon.el")))) + (emacs-lisp-mode . ((elisp-flymake-byte-compile-load-path . load-path) + (package-lint-main-file . "mastodon.el")))) diff --git a/lisp/mastodon-async.el b/lisp/mastodon-async.el index 317be93..b059407 100644 --- a/lisp/mastodon-async.el +++ b/lisp/mastodon-async.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2017 Alex J. Griffith ;; Author: Alex J. Griffith <griffitaj@gmail.com> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Package-Requires: ((emacs "27.1")) ;; Homepage: https://codeberg.org/martianh/mastodon.el diff --git a/lisp/mastodon-auth.el b/lisp/mastodon-auth.el index 3796b7e..01639fb 100644 --- a/lisp/mastodon-auth.el +++ b/lisp/mastodon-auth.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org> ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-client.el b/lisp/mastodon-client.el index 6e55829..c0db3d6 100644 --- a/lisp/mastodon-client.el +++ b/lisp/mastodon-client.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org> ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-discover.el b/lisp/mastodon-discover.el index c34d85f..993cc27 100644 --- a/lisp/mastodon-discover.el +++ b/lisp/mastodon-discover.el @@ -1,10 +1,10 @@ ;;; mastodon-discover.el --- Use Mastodon.el with discover.el -*- lexical-binding: t -*- ;; Copyright (C) 2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-http.el b/lisp/mastodon-http.el index fbae8a7..1093de1 100644 --- a/lisp/mastodon-http.el +++ b/lisp/mastodon-http.el @@ -1,10 +1,10 @@ ;;; mastodon-http.el --- HTTP request/response functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -157,16 +157,18 @@ the request data. If it is :raw, just use the plain params." (let* ((url-request-data (when params (cond ((eq json :json) - (json-encode - params)) + (json-encode params)) ((eq json :raw) params) (t (mastodon-http--build-params-string params))))) (url-request-extra-headers (append url-request-extra-headers ; auth set in macro - (unless (assoc "Content-Type" headers) ; pleroma compat: - '(("Content-Type" . "application/x-www-form-urlencoded"))) + (if json + '(("Content-Type" . "application/json") + ("Accept" . "application/json")) + (unless (assoc "Content-Type" headers) ; pleroma compat: + '(("Content-Type" . "application/x-www-form-urlencoded")))) headers))) (with-temp-buffer (mastodon-http--url-retrieve-synchronously url))) @@ -298,11 +300,26 @@ Optionally specify the PARAMS to send." (with-current-buffer (mastodon-http--patch url params) (mastodon-http--process-json))) -(defun mastodon-http--patch (base-url &optional params) - "Make synchronous PATCH request to BASE-URL. -Optionally specify the PARAMS to send." +(defun mastodon-http--patch (url &optional params json) + "Make synchronous PATCH request to URL. +Optionally specify the PARAMS to send. +JSON means send params as JSON data." (mastodon-http--authorized-request "PATCH" - (let ((url (mastodon-http--concat-params-to-url base-url params))) + ;; NB: unlike POST, PATCHing only works if we use query params! + ;; so here, unless JSON arg, we use query params and do not set + ;; `url-request-data'. this is probably an error, i don't understand it. + (let* ((url-request-data + (when (and params json) + (encode-coding-string + (json-encode params) 'utf-8))) + ;; (mastodon-http--build-params-string params)))) + (url (unless json + (mastodon-http--concat-params-to-url url params))) + (headers (when json + '(("Content-Type" . "application/json") + ("Accept" . "application/json")))) + (url-request-extra-headers + (append url-request-extra-headers headers))) (mastodon-http--url-retrieve-synchronously url)))) ;; Asynchronous functions diff --git a/lisp/mastodon-inspect.el b/lisp/mastodon-inspect.el index 43c8ba4..4981943 100644 --- a/lisp/mastodon-inspect.el +++ b/lisp/mastodon-inspect.el @@ -1,10 +1,10 @@ ;;; mastodon-inspect.el --- Client for Mastodon -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-iso.el b/lisp/mastodon-iso.el index 8ea5635..6199cbe 100644 --- a/lisp/mastodon-iso.el +++ b/lisp/mastodon-iso.el @@ -1,7 +1,7 @@ ;;; mastodon-iso.el --- ISO language code lists for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2022 Marty Hiatt -;; Author: Marty Hiatt <martianhiatus@riseup.net> +;; Author: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-media.el b/lisp/mastodon-media.el index 2ec498e..8601410 100644 --- a/lisp/mastodon-media.el +++ b/lisp/mastodon-media.el @@ -1,10 +1,10 @@ ;;; mastodon-media.el --- Functions for inlining Mastodon media -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -38,6 +38,7 @@ (require 'image-mode) (autoload 'mastodon-tl--propertize-img-str-or-url "mastodon-tl") +(autoload 'mastodon-tl--image-trans-check "mastodon-tl") (defvar url-show-status) diff --git a/lisp/mastodon-notifications.el b/lisp/mastodon-notifications.el index 1c2aad7..f688f2d 100644 --- a/lisp/mastodon-notifications.el +++ b/lisp/mastodon-notifications.el @@ -1,10 +1,10 @@ ;;; mastodon-notifications.el --- Notification functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -30,7 +30,8 @@ ;;; Code: -(require 'mastodon) +(eval-when-compile (require 'subr-x)) +(require 'cl-lib) (autoload 'mastodon-http--api "mastodon-http") (autoload 'mastodon-http--get-params-async-json "mastodon-http") @@ -45,13 +46,22 @@ (autoload 'mastodon-tl--find-property-range "mastodon-tl") (autoload 'mastodon-tl--has-spoiler "mastodon-tl") (autoload 'mastodon-tl--init "mastodon-tl") -(autoload 'mastodon-tl--insert-status "mastodon-tl") (autoload 'mastodon-tl--property "mastodon-tl") (autoload 'mastodon-tl--reload-timeline-or-profile "mastodon-tl") (autoload 'mastodon-tl--spoiler "mastodon-tl") (autoload 'mastodon-tl--item-id "mastodon-tl") (autoload 'mastodon-tl--update "mastodon-tl") (autoload 'mastodon-views--view-follow-requests "mastodon-views") +(autoload 'mastodon-tl--current-filters "mastodon-views") +(autoload 'mastodon-tl--render-text "mastodon-tl") +(autoload 'mastodon-notifications-get "mastodon") +(autoload 'mastodon-tl--byline-uname-+-handle "mastodon-tl") +(autoload 'mastodon-tl--byline-username "mastodon-tl") +(autoload 'mastodon-tl--byline-handle "mastodon-tl") +(autoload 'mastodon-http--get-json "mastodon-http") +(autoload 'mastodon-media--get-avatar-rendering "mastodon-media") +(autoload 'mastodon-tl--image-trans-check "mastodon-tl") +(autoload 'mastodon-tl--symbol "mastodon-tl") (defgroup mastodon-tl nil "Nofications in mastodon.el." @@ -70,29 +80,31 @@ If unset, profile notes of any size will be displayed, which may make them unweildy." :type '(integer)) +(defcustom mastodon-notifications--images-in-notifs nil + "Whether to display attached images in notifications." + :type '(boolean)) + (defvar mastodon-tl--buffer-spec) (defvar mastodon-tl--display-media-p) +(defvar mastodon-mode-map) +(defvar mastodon-tl--fold-toots-at-length) +(defvar mastodon-tl--show-avatars) -(defvar mastodon-notifications--types-alist - '(("follow" . mastodon-notifications--follow) - ("favourite" . mastodon-notifications--favourite) - ("reblog" . mastodon-notifications--reblog) - ("mention" . mastodon-notifications--mention) - ("poll" . mastodon-notifications--poll) - ("follow_request" . mastodon-notifications--follow-request) - ("status" . mastodon-notifications--status) - ("update" . mastodon-notifications--edit)) - "Alist of notification types and their corresponding function.") +(defvar mastodon-notifications--types + '("favourite" "reblog" "mention" "poll" + "follow_request" "follow" "status" "update" + "severed_relationships" "moderation_warning") + "A list of notification types according to their name on the server.") (defvar mastodon-notifications--response-alist '(("Followed" . "you") - ("Favourited" . "your status from") - ("Boosted" . "your status from") + ("Favourited" . "your post") + ("Boosted" . "your post") ("Mentioned" . "you") ("Posted a poll" . "that has now ended") ("Requested to follow" . "you") ("Posted" . "a post") - ("Edited" . "a post from")) + ("Edited" . "their post")) "Alist of subjects for notification types.") (defvar mastodon-notifications--map @@ -106,8 +118,10 @@ make them unweildy." (defun mastodon-notifications--byline-concat (message) "Add byline for TOOT with MESSAGE." - (concat " " (propertize message 'face 'highlight) - " " (cdr (assoc message mastodon-notifications--response-alist)))) + (concat " " + (propertize message 'face 'mastodon-boosted-face) + " " (cdr (assoc message mastodon-notifications--response-alist)) + "\n")) (defun mastodon-notifications--follow-request-process (&optional reject) "Process the follow request at point. @@ -119,7 +133,9 @@ follow-requests view." (let* ((item-json (mastodon-tl--property 'item-json)) (f-reqs-view-p (string= "follow_requests" (plist-get mastodon-tl--buffer-spec 'endpoint))) - (f-req-p (or (string= "follow_request" (alist-get 'type item-json)) ;notifs + (f-req-p (or (string= "follow_request" + (mastodon-tl--property 'notification-type + :no-move)) f-reqs-view-p))) (if (not f-req-p) (user-error "No follow request at point?") @@ -153,40 +169,6 @@ Can be called in notifications view or in follow-requests view." (interactive) (mastodon-notifications--follow-request-process :reject)) -(defun mastodon-notifications--mention (note) - "Format for a `mention' NOTE." - (mastodon-notifications--format-note note 'mention)) - -(defun mastodon-notifications--follow (note) - "Format for a `follow' NOTE." - (mastodon-notifications--format-note note 'follow)) - -(defun mastodon-notifications--follow-request (note) - "Format for a `follow-request' NOTE." - (mastodon-notifications--format-note note 'follow-request)) - -(defun mastodon-notifications--favourite (note) - "Format for a `favourite' NOTE." - (mastodon-notifications--format-note note 'favourite)) - -(defun mastodon-notifications--reblog (note) - "Format for a `boost' NOTE." - (mastodon-notifications--format-note note 'boost)) - -(defun mastodon-notifications--status (note) - "Format for a `status' NOTE. -Status notifications are given when -`mastodon-tl--enable-notify-user-posts' has been set." - (mastodon-notifications--format-note note 'status)) - -(defun mastodon-notifications--poll (note) - "Format for a `poll' NOTE." - (mastodon-notifications--format-note note 'poll)) - -(defun mastodon-notifications--edit (note) - "Format for an `edit' NOTE." - (mastodon-notifications--format-note note 'edit)) - (defun mastodon-notifications--comment-note-text (str) "Add comment face to all text in STR with `shr-text' face only." (with-temp-buffer @@ -199,115 +181,264 @@ Status notifications are given when '(face (font-lock-comment-face shr-text))))) (buffer-string))) -(defun mastodon-notifications--format-note (note type) - "Format for a NOTE of TYPE." - ;; FIXME: apply/refactor filtering as per/with `mastodon-tl--toot' - (let* ((id (alist-get 'id note)) - (profile-note - (when (eq 'follow-request type) - (let ((str (mastodon-tl--field - 'note - (mastodon-tl--field 'account note)))) - (if mastodon-notifications--profile-note-in-foll-reqs-max-length - (string-limit str mastodon-notifications--profile-note-in-foll-reqs-max-length) - str)))) - (status (mastodon-tl--field 'status note)) - (follower (alist-get 'username (alist-get 'account note))) - (toot (alist-get 'status note)) - (filtered (mastodon-tl--field 'filtered toot)) - (filters (when filtered - (mastodon-tl--current-filters filtered)))) - (if (and filtered (assoc "hide" filters)) - nil - (mastodon-tl--insert-status - ;; toot - (cond ((or (eq type 'follow) - (eq type 'follow-request)) - ;; Using reblog with an empty id will mark this as something - ;; non-boostable/non-favable. - (cons '(reblog (id . nil)) note)) - ;; reblogs/faves use 'note' to process their own json - ;; not the toot's. this ensures following etc. work on such notifs - ((or (eq type 'favourite) - (eq type 'boost)) - note) - (t - status)) - ;; body - (let ((body (if-let ((match (assoc "warn" filters))) - (mastodon-tl--spoiler toot (cadr match)) - (mastodon-tl--clean-tabs-and-nl - (if (mastodon-tl--has-spoiler status) - (mastodon-tl--spoiler status) - (if (eq 'follow-request type) - (mastodon-tl--render-text profile-note) - (mastodon-tl--content status))))))) - (cond ((or (eq type 'follow) - (eq type 'follow-request)) - (if (eq type 'follow) - (propertize "Congratulations, you have a new follower!" - 'face 'default) +(defvar mastodon-notifications-grouped-types + '(follow reblog favourite) + "List of notification types for which grouping is implemented.") + +(defvar mastodon-notifications--action-alist + '((reblog . "Boosted") + (favourite . "Favourited") + (follow_request . "Requested to follow") + (follow . "Followed") + (mention . "Mentioned") + (status . "Posted") + (poll . "Posted a poll") + (update . "Edited")) + "Action strings keyed by notification type. +Types are those of the Mastodon API.") + +(defun mastodon-notifications--alist-by-value (str field json) + "From JSON, return the alist whose FIELD value matches STR. +JSON is a list of alists." + (cl-some (lambda (y) + (when (string= str (alist-get field y)) + y)) + json)) + +(defun mastodon-notifications--group-accounts (ids json) + "For IDS, return account data in JSON." + (cl-loop + for x in ids + collect (mastodon-notifications--alist-by-value x 'id json))) + +(defun mastodon-notifications--severance-body (group) + "Return a body for a severance notification GROUP." + ;; FIXME: actually implement this when we encounter one in the wild! + (let-alist (alist-get 'event group) + (concat .description ": " + .target_name + "\nRelationships affected: " + .relationships_count))) + +(defun mastodon-notifications--mod-warning-body (group) + "Return a body for a moderation warning notification GROUP." + (let-alist (alist-get ) + (concat .description ": " + .text + "\nStatuses: " + .status_ids + "\nfor account: " + .target_account))) + +(defun mastodon-notifications--format-note (group status accounts) + "Format for a GROUP notification. +STATUS is the status's JSON. +ACCOUNTS is data of the accounts that have reacted to the notification." + (let ((folded nil)) + ;; FIXME: apply/refactor filtering as per/with `mastodon-tl--toot' + (let-alist group + (let* ((type-sym (intern .type)) + (profile-note + (when (member type-sym '(follow_request)) + (let ((str (mastodon-tl--field 'note (car accounts)))) + (if mastodon-notifications--profile-note-in-foll-reqs-max-length + (string-limit str mastodon-notifications--profile-note-in-foll-reqs-max-length) + str)))) + (follower (when (member type-sym '(follow follow_request)) + (car accounts))) + (follower-name (mastodon-tl--field 'username follower)) + (filtered (mastodon-tl--field 'filtered status)) + (filters (when filtered + (mastodon-tl--current-filters filtered)))) + (unless (and filtered (assoc "hide" filters)) + (mastodon-notifications--insert-note + ;; toot + (if (member type-sym '(follow follow_request)) + follower + status) + ;; body + (let ((body (if-let ((match (assoc "warn" filters))) + (mastodon-tl--spoiler status (cadr match)) + (mastodon-tl--clean-tabs-and-nl + (cond ((mastodon-tl--has-spoiler status) + (mastodon-tl--spoiler status)) + ((eq type-sym 'follow_request) + (mastodon-tl--render-text profile-note)) + (t (mastodon-tl--content status))))))) + (cond + ((eq type-sym 'follow) + (propertize "Congratulations, you have a new follower!" + 'face 'default)) + ((eq type-sym 'follow_request) + (concat + (propertize (format "You have a follow request from %s" + follower-name) + 'face 'default) + (when mastodon-notifications--profile-note-in-foll-reqs (concat - (propertize - (format "You have a follow request from... %s" - follower) - 'face 'default) - (when mastodon-notifications--profile-note-in-foll-reqs - (concat - ":\n" - (mastodon-notifications--comment-note-text body)))))) - ((or (eq type 'favourite) - (eq type 'boost)) - (mastodon-notifications--comment-note-text body)) - (t body))) - ;; author-byline - (if (or (eq type 'follow) - (eq type 'follow-request) - (eq type 'mention)) - 'mastodon-tl--byline-author - (lambda (_status &rest _args) ; unbreak stuff - (mastodon-tl--byline-author note))) - ;; action-byline - (lambda (_status) - (mastodon-notifications--byline-concat - (cond ((eq type 'boost) - "Boosted") - ((eq type 'favourite) - "Favourited") - ((eq type 'follow-request) - "Requested to follow") - ((eq type 'follow) - "Followed") - ((eq type 'mention) - "Mentioned") - ((eq type 'status) - "Posted") - ((eq type 'poll) - "Posted a poll") - ((eq type 'edit) - "Edited")))) - id - ;; base toot - (when (or (eq type 'favourite) - (eq type 'boost)) - status))))) - -(defun mastodon-notifications--by-type (note) - "Filter NOTE for those listed in `mastodon-notifications--types-alist'. -Call its function in that list on NOTE." - (let* ((type (mastodon-tl--field 'type note)) - (fun (cdr (assoc type mastodon-notifications--types-alist))) - (start-pos (point))) - (when fun - (funcall fun note) - (when mastodon-tl--display-media-p - (mastodon-media--inline-images start-pos (point)))))) + ":\n" + (mastodon-notifications--comment-note-text body))))) + ((eq type-sym 'severed_relationships) + (mastodon-notifications--severance-body group)) + ((eq type-sym 'moderation_warning) + (mastodon-notifications--mod-warning-body group)) + ((member type-sym '(favourite reblog)) + (propertize + (mastodon-notifications--comment-note-text body))) + (t body))) + ;; author-byline + #'mastodon-tl--byline-author + ;; action-byline + (unless (member type-sym '(follow follow_request mention)) + (downcase + (mastodon-notifications--byline-concat + (alist-get type-sym mastodon-notifications--action-alist)))) + ;; action authors + (cond ((member type-sym '(follow follow_request mention)) + "") ;; mentions are normal statuses + (t (mastodon-notifications--byline-accounts + accounts status group))) + ;; action symbol: + (unless (eq type-sym 'mention) + (mastodon-tl--symbol type-sym)) + ;; base toot (no need for update/poll/?) + (when (member type-sym '(favourite reblog)) + status) + folded group accounts)))))) + +(defun mastodon-notifications--insert-note + (toot body author-byline action-byline action-authors action-symbol + &optional base-toot unfolded group accounts) + "Display the content and byline of timeline element TOOT. +BODY will form the section of the toot above the byline. +AUTHOR-BYLINE is an optional function for adding the author +portion of the byline that takes one variable. By default it is +`mastodon-tl--byline-author'. +ACTION-BYLINE is a string, obtained by calling +`mastodon-notifications--byline-concat'. +ACTION-AUTHORS is a string of those who have responded to the +current item, obtained by calling +`mastodon-notifications--byline-accounts'. +ACTION-SYMBOL is a symbol indicating a favourite, boost, or edit. +ID is that of the status if it is a notification, which is +attached as a `item-id' property if provided. If the +status is a favourite or boost notification, BASE-TOOT is the +JSON of the toot responded to. +UNFOLDED is a boolean meaning whether to unfold or fold item if +foldable. +GROUP is the notification group data. +ACCOUNTS is the notification accounts data." + (let* ((type (alist-get 'type (or group toot))) + (toot-foldable + (and mastodon-tl--fold-toots-at-length + (length> body mastodon-tl--fold-toots-at-length)))) + (insert + (propertize ;; top byline, body + byline: + (concat + (propertize ;; top byline + (if (equal type "mention") + "" + (concat action-symbol " " action-authors + action-byline)) + 'byline-top t) + (propertize ;; body only + body + 'toot-body t) ;; includes newlines etc. for folding + "\n" + ;; actual byline: + (mastodon-tl--byline toot author-byline nil nil + base-toot group + (if (member type '("follow" "follow_request")) + toot))) ;; account data! + 'item-type 'toot ;; for nav, actions, etc. + 'item-id (or (alist-get 'page_max_id group) ;; newest notif + (alist-get 'id toot)) ; toot id + 'base-item-id (mastodon-tl--item-id + ;; if status is a notif, get id from base-toot + ;; (-tl--item-id toot) will not work here: + (or base-toot + toot)) ; else normal toot with reblog check + 'item-json toot + 'base-toot base-toot + 'cursor-face 'mastodon-cursor-highlight-face + 'toot-foldable toot-foldable + 'toot-folded (and toot-foldable (not unfolded)) + ;; grouped notifs data: + 'notification-type type + 'notification-id (alist-get 'group_key group) + 'notification-group group + 'notification-accounts accounts + ;; for pagination: + 'notifications-min-id (alist-get 'page_min_id group) + 'notifications-max-id (alist-get 'page_max_id group)) + "\n"))) + +;; FIXME: REFACTOR with -tl--byline?: +;; we provide account directly, rather than let-alisting toot +;; almost everything is .account.field anyway +;; but toot still needed also, for attachments, etc. +(defun mastodon-notifications--byline-accounts + (accounts toot group &optional avatar) + "Propertize author byline ACCOUNTS for TOOT, the item responded to. +GROUP is the group notification data. +When AVATAR, include the account's avatar image. +When DOMAIN, force inclusion of user's domain in their handle." + (let ((total (alist-get 'notifications_count group)) + (accts 2)) + (concat + (string-trim ;; remove trailing newline + (cl-loop + for account in accounts + repeat accts + concat + (let-alist account + (concat + ;; avatar insertion moved up to `mastodon-tl--byline' by + ;; default to be outside 'byline propt. + (when (and avatar ; used by `mastodon-profile--format-user' + mastodon-tl--show-avatars + mastodon-tl--display-media-p + (mastodon-tl--image-trans-check)) + (mastodon-media--get-avatar-rendering .avatar)) + (let ((uname (mastodon-tl--display-or-uname account))) + (mastodon-tl--byline-handle toot nil account + uname 'mastodon-display-name-face)) + ", "))) + nil ", ") + (if (< accts total) + (let ((diff (- total accts))) + (propertize ;; help-echo remaining notifs authors: + (format " and %s other%s" diff (if (= 1 diff) "" "s")) + 'help-echo (mapconcat (lambda (a) + (propertize (alist-get 'username a) + 'face 'mastodon-display-name-face)) + (cddr accounts) ;; not first two + ", "))))))) + +(defun mastodon-notifications--render (json) + "Display grouped notifications in JSON." + ;; (setq masto-grouped-notifs json) + (let ((groups (alist-get 'notification_groups json))) + (cl-loop + for g in groups + for start-pos = (point) + for accounts = (mastodon-notifications--group-accounts + (alist-get 'sample_account_ids g) + (alist-get 'accounts json)) + for status = (mastodon-notifications--alist-by-value + (alist-get 'status_id g) 'id + (alist-get 'statuses json)) + do (mastodon-notifications--format-note g status accounts) + (when mastodon-tl--display-media-p + ;; images-in-notifs custom is handeld in + ;; `mastodon-tl--media-attachment', not here + (mastodon-media--inline-images start-pos (point)))))) (defun mastodon-notifications--timeline (json) "Format JSON in Emacs buffer." (if (seq-empty-p json) (user-error "Looks like you have no (more) notifications for now") - (mapc #'mastodon-notifications--by-type json) + (mastodon-notifications--render json) (goto-char (point-min)))) (defun mastodon-notifications--get-mentions () @@ -339,8 +470,7 @@ Status notifications are created when you call (defun mastodon-notifications--filter-types-list (type) "Return a list of notification types with TYPE removed." - (let ((types (mapcar #'car mastodon-notifications--types-alist))) - (remove type types))) + (remove type mastodon-notifications--types)) (defun mastodon-notifications--clear-all () "Clear all notifications." @@ -357,17 +487,43 @@ Status notifications are created when you call (defun mastodon-notifications--clear-current () "Dismiss the notification at point." (interactive) - (let* ((id (or (mastodon-tl--property 'item-id) - (mastodon-tl--field 'id - (mastodon-tl--property 'item-json)))) - (response - (mastodon-http--post (mastodon-http--api - (format "notifications/%s/dismiss" id))))) + (let* ((id (or (or (mastodon-tl--property 'notification-id) ;; grouped + (mastodon-tl--property 'item-id) + (mastodon-tl--field + 'id + (mastodon-tl--property 'item-json))))) + (endpoint (mastodon-http--api + (format "notifications/%s/dismiss" id) + "v2")) + (response (mastodon-http--post endpoint))) (mastodon-http--triage response (lambda (_) (when mastodon-tl--buffer-spec (mastodon-tl--reload-timeline-or-profile)) (message "Notification dismissed!"))))) +(defun mastodon-notifications--get-single-notif () + "Return a single notification JSON for v2 notifs." + (interactive) + (let* ((id (mastodon-tl--property + 'notification-id)) ;; grouped, doesn't work for ungrouped! + ;; (key (format "ungrouped-%s" + ;; (mastodon-tl--property 'item-id))) + (endpoint (mastodon-http--api + (format "notifications/%s" id) + "v2")) + (response (mastodon-http--get-json endpoint))) + (mastodon-http--triage + response (lambda (_) + (message "%s" (prin1-to-string response)))))) + +(defun mastodon-notifications--get-unread-count () + "Return the number of unread notifications for the current account." + ;; params: limit - max 1000, default 100, types[], exclude_types[], account_id + (let* ((endpoint "notifications/unread_count") + (url (mastodon-http--api endpoint)) + (resp (mastodon-http--get-json url))) + (alist-get 'count resp))) + (provide 'mastodon-notifications) ;;; mastodon-notifications.el ends here diff --git a/lisp/mastodon-profile.el b/lisp/mastodon-profile.el index 6410591..40f834c 100644 --- a/lisp/mastodon-profile.el +++ b/lisp/mastodon-profile.el @@ -1,10 +1,10 @@ ;;; mastodon-profile.el --- Functions for inspecting Mastodon profiles -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -84,6 +84,7 @@ (autoload 'mastodon-search--query "mastodon-search") (autoload 'mastodon-tl--field-status "mastodon-tl") +(defvar mastodon-active-user) (defvar mastodon-tl--horiz-bar) (defvar mastodon-tl--update-point) (defvar mastodon-toot--max-toot-chars) @@ -386,6 +387,8 @@ This is done after changing the setting on the server." Only do so if `mastodon-profile-account-settings' is nil." (mastodon-profile--fetch-server-account-settings :no-force)) +;; FIXME: this does one request per setting! should just do one request then +;; parse (defun mastodon-profile--fetch-server-account-settings (&optional no-force) "Fetch basic account settings from the server. Store the values in `mastodon-profile-account-settings'. @@ -859,13 +862,15 @@ These include the author, author of reblogged entries and any user mentioned." status)) ; status is a user listing (mentions (mastodon-tl--field-status 'mentions status)) (reblog (mastodon-tl--field-status 'reblog status))) - (seq-filter #'stringp - (seq-uniq - (seq-concatenate - 'list - (list (alist-get 'acct this-account)) - (mastodon-profile--extract-users-handles reblog) - (mastodon-tl--map-alist 'acct mentions))))))) + (seq-remove + (lambda (x) (string= x mastodon-active-user)) + (seq-filter #'stringp + (seq-uniq + (seq-concatenate + 'list + (list (alist-get 'acct this-account)) + (mastodon-profile--extract-users-handles reblog) + (mastodon-tl--map-alist 'acct mentions)))))))) (defun mastodon-profile--lookup-account-in-status (handle status) "Return account for HANDLE using hints in STATUS if possible." diff --git a/lisp/mastodon-search.el b/lisp/mastodon-search.el index 7fc4de3..25db7d8 100644 --- a/lisp/mastodon-search.el +++ b/lisp/mastodon-search.el @@ -1,8 +1,8 @@ ;;; mastodon-search.el --- Search functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Marty Hiatt -;; Author: Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Author: Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -28,8 +28,7 @@ ;;; Code: (require 'json) -(eval-when-compile - (require 'mastodon-tl)) +(require 'mastodon-tl) (autoload 'mastodon-auth--access-token "mastodon-auth") (autoload 'mastodon-http--api "mastodon-http") @@ -44,6 +43,7 @@ (autoload 'mastodon-tl--timeline "mastodon-tl") (autoload 'mastodon-tl--toot "mastodon-tl") (autoload 'mastodon-tl--buffer-property "mastodon-tl") +(autoload 'mastodon-http--api-v2 "mastodon-http") (defvar mastodon-toot--completion-style-for-mentions) (defvar mastodon-instance-url) @@ -97,6 +97,41 @@ QUERY is the string to search." (mastodon-search--view-trending "statuses" #'mastodon-tl--timeline)) +(defun mastodon-search--trending-links () + "Display a list of links trending on your instance." + (interactive) + (mastodon-search--view-trending "links" + #'mastodon-search--render-links)) + +(defun mastodon-search--render-links (links) + "Render trending LINKS." + (cl-loop for l in links + do (mastodon-search--render-link l))) + +(defun mastodon-search--render-link (link) + "Render a trending LINK." + (let-alist link + (insert + (propertize + (mastodon-tl--render-text + (concat "<a href=\"" .url "\">" .url "</a>\n" .title) + link) + 'item-type 'link + 'item-json link + 'shr-url .url + 'byline t ;; nav + 'help-echo + (substitute-command-keys + "\\[`mastodon-search--load-link-posts'] to view a link's timeline")) + ;; TODO: display card link author here + "\n\n"))) + +(defun mastodon-search--load-link-posts () + "Load timeline of posts containing link at point." + (interactive) + (let* ((url (mastodon-tl--property 'shr-url))) + (mastodon-tl--link-timeline url))) + (defun mastodon-search--view-trending (type print-fun) "Display a list of tags trending on your instance. TYPE is a string, either tags, statuses, or links. @@ -109,7 +144,8 @@ PRINT-FUN is the function used to print the data from the response." (offset '(("offset" . "0"))) (params (push limit offset)) (data (mastodon-http--get-json url params)) - (buffer (get-buffer-create (format "*mastodon-trending-%s*" type)))) + (buffer (get-buffer-create + (format "*mastodon-trending-%s*" type)))) (with-mastodon-buffer buffer #'mastodon-mode nil (mastodon-tl--set-buffer-spec (buffer-name buffer) (format "trends/%s" type) @@ -129,7 +165,8 @@ Optionally add string TYPE after HEADING." (defun mastodon-search--format-heading (str &optional type no-newline) "Format STR as a heading. -Optionally add string TYPE after HEADING." +Optionally add string TYPE after HEADING. +NO-NEWLINE means don't add add a newline at end." (mastodon-tl--set-face (concat "\n " mastodon-tl--horiz-bar "\n " (upcase str) " " @@ -153,7 +190,7 @@ is used for pagination." ;; TODO: handle no results (interactive "sSearch mastodon for: ") (let* ((url (mastodon-http--api-v2 "search")) - (following (when (or following (eq current-prefix-arg '(4))) + (following (when (or following (equal current-prefix-arg '(4))) "true")) (type (or type (if (eq current-prefix-arg '(4)) @@ -294,9 +331,7 @@ If NOTE is non-nil, include user's profile note. This is also (defun mastodon-search--get-user-info (account) "Get user handle, display name, account URL and profile note from ACCOUNT." - (list (if (not (string-empty-p (alist-get 'display_name account))) - (alist-get 'display_name account) - (alist-get 'username account)) + (list (mastodon-tl--display-or-uname account) (alist-get 'acct account) (alist-get 'url account) (alist-get 'note account))) diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index 8c41414..5bd1ec1 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -1,10 +1,10 @@ ;;; mastodon-tl.el --- Timeline functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -92,10 +92,12 @@ (autoload 'mastodon-toot--with-toot-item "mastodon-toot") (autoload 'mastodon-media--image-or-cached "mastodon-media") (autoload 'mastodon-toot--base-toot-or-item-json "mastodon-toot") +(autoload 'mastodon-search--load-link-posts "mastodon-search") (defvar mastodon-toot--visibility) (defvar mastodon-toot-mode) (defvar mastodon-active-user) +(defvar mastodon-notifications--images-in-notifs) (when (require 'mpv nil :no-error) (declare-function mpv-start "mpv")) @@ -151,18 +153,24 @@ nil." :type 'boolean) (defcustom mastodon-tl--symbols - '((reply . ("💬" . "R")) - (boost . ("🔁" . "B")) - (favourite . ("⭐" . "F")) - (bookmark . ("🔖" . "K")) - (media . ("📹" . "[media]")) - (verified . ("" . "V")) - (locked . ("🔒" . "[locked]")) - (private . ("🔒" . "[followers]")) - (direct . ("✉" . "[direct]")) - (edited . ("✍" . "[edited]")) - (replied . ("⬇" . "↓")) - (reply-bar . ("┃" . "|"))) + '((reply . ("💬" . "R")) + (boost . ("🔁" . "B")) + (reblog . ("🔁" . "B")) ;; server compat + (favourite . ("⭐" . "F")) + (bookmark . ("🔖" . "K")) + (media . ("📹" . "[media]")) + (verified . ("" . "V")) + (locked . ("🔒" . "[locked]")) + (private . ("🔒" . "[followers]")) + (direct . ("✉" . "[direct]")) + (edited . ("✍" . "[edited]")) + (update . ("✍" . "[edited]")) ;; server compat + (status . ("✍" . "[posted]")) + (replied . ("⬇" . "↓")) + (reply-bar . ("┃" . "|")) + (poll . ("📊" . "")) + (follow . ("👤" . "+")) + (follow_request . ("👤" . "+"))) "A set of symbols (and fallback strings) to be used in timeline. If a symbol does not look right (tofu), it means your font settings do not support it." @@ -278,6 +286,7 @@ etc.") ;; is already bound to w also (define-key map (kbd "u") #'mastodon-tl--update) (define-key map [remap shr-browse-url] #'mastodon-url-lookup) + (define-key map (kbd "M-RET") #'mastodon-search--load-link-posts) map) "The keymap to be set for shr.el generated links that are not images. We need to override the keymap so tabbing will navigate to all @@ -574,6 +583,19 @@ With a double PREFIX arg, limit results to your own instance." (concat "timelines/tag/" (if (listp tag) (car tag) tag)) ; must be /tag/:sth 'mastodon-tl--timeline nil params))) +(defun mastodon-tl--link-timeline (url) + "Load a link timeline, displaying posts containing URL." + (let ((params `(("url" . ,url)))) + (mastodon-tl--init "links" "timelines/link" + 'mastodon-tl--timeline nil + params))) + +(defun mastodon-tl--announcements () + "Display announcements from your instance." + (interactive) + (mastodon-tl--init "announcements" "announcements" + 'mastodon-tl--timeline nil nil nil nil :no-byline)) + ;;; BYLINES, etc. @@ -588,51 +610,102 @@ Do so if type of status at poins is not follow_request/follow." (string= type "follow")) ; no counts for these (message "%s" echo))))) -(defun mastodon-tl--byline-author (toot &optional avatar domain) +;; FIXME: now that this can also be used for non byline rendering, let's +;; remove the toot arg, and deal with attachments higher up (on real +;; author byline only) removing toot arg makes it easier to render notifs +;; that have no status (foll_reqs) +(defun mastodon-tl--byline-username (toot &optional account) + "Format a byline username from account in TOOT. +ACCOUNT is optionally acccount data to use." + (let-alist (or account (alist-get 'account toot)) + (propertize (if (not (string-empty-p .display_name)) + .display_name + .username) + 'face 'mastodon-display-name-face + ;; enable playing of videos when point is on byline: + ;; 'attachments (mastodon-tl--get-attachments-for-byline toot) + 'keymap mastodon-tl--byline-link-keymap + ;; echo faves count when point on post author name: + ;; which is where --goto-next-toot puts point. + 'help-echo + ;; but don't add it to "following"/"follows" on + ;; profile views: we don't have a tl--buffer-spec + ;; yet: + (unless (or (string-suffix-p "-followers*" (buffer-name)) + (string-suffix-p "-following*" (buffer-name))) + (mastodon-tl--format-byline-help-echo toot))))) + +(defun mastodon-tl--byline-handle (toot &optional domain account string face) + "Format a byline handle from account in TOOT. +DOMAIN is optionally added to the handle. +ACCOUNT is optionally acccount data to use. +STRING is optionally the string to propertize. +FACE is optionally the face to use. +The last two args allow for display a username as a clickable +handle." + (let-alist (or account (alist-get 'account toot)) + (propertize (or string + (concat "@" .acct + (when domain + (concat "@" + (url-host + (url-generic-parse-url .url)))))) + 'face (or face 'mastodon-handle-face) + 'mouse-face 'highlight + 'mastodon-tab-stop 'user-handle + 'account account + 'shr-url .url + 'keymap mastodon-tl--link-keymap + 'mastodon-handle (concat "@" .acct) + 'help-echo (concat "Browse user profile of @" .acct)))) + +(defun mastodon-tl--byline-uname-+-handle (data &optional domain account) + "Concatenate a byline username and handle. +DATA is the (toot) data to use. +DOMAIN is optionally a domain for the handle. +ACCOUNT is optionally acccount data to use." + (concat (mastodon-tl--byline-username data account) + " (" (mastodon-tl--byline-handle data domain account) ")")) + +(defun mastodon-tl--display-or-uname (account) + "Return display name or username from ACCOUNT data." + (if (not (string-empty-p (alist-get 'display_name account))) + (alist-get 'display_name account) + (alist-get 'username account))) + +(defun mastodon-tl--byline-author (toot &optional avatar domain base account) "Propertize author of TOOT. +If TOOT contains a reblog, return author of reblogged item. With arg AVATAR, include the account's avatar image. -When DOMAIN, force inclusion of user's domain in their handle." - (let-alist toot +When DOMAIN, force inclusion of user's domain in their handle. +BASE means to use data from the base item (reblog slot) if possible. +If BASE is nil, we are a boosted byline, so show less info. +ACCOUNT is optionally acccount data to use." + (let* ((data (if base + (mastodon-tl--toot-or-base toot) + toot)) + (account (or account (alist-get 'account data))) + (uname (mastodon-tl--display-or-uname account))) (concat - ;; avatar insertion moved up to `mastodon-tl--byline' by default to be - ;; outside 'byline propt. + ;; avatar insertion moved up to `mastodon-tl--byline' by default to + ;; be outside 'byline propt. (when (and avatar ; used by `mastodon-profile--format-user' mastodon-tl--show-avatars mastodon-tl--display-media-p (mastodon-tl--image-trans-check)) - (mastodon-media--get-avatar-rendering .account.avatar)) - ;; username: - (propertize (if (not (string-empty-p .account.display_name)) - .account.display_name - .account.username) - 'face 'mastodon-display-name-face - ;; enable playing of videos when point is on byline: - 'attachments (mastodon-tl--get-attachments-for-byline toot) - 'keymap mastodon-tl--byline-link-keymap - ;; echo faves count when point on post author name: - ;; which is where --goto-next-toot puts point. - 'help-echo - ;; but don't add it to "following"/"follows" on profile views: - ;; we don't have a tl--buffer-spec yet: - (unless (or (string-suffix-p "-followers*" (buffer-name)) - (string-suffix-p "-following*" (buffer-name))) - (mastodon-tl--format-byline-help-echo toot))) - ;; handle: - " (" - (propertize (concat "@" .account.acct - (when domain - (concat "@" - (url-host - (url-generic-parse-url .account.url))))) - 'face 'mastodon-handle-face - 'mouse-face 'highlight - 'mastodon-tab-stop 'user-handle - 'account .account - 'shr-url .account.url - 'keymap mastodon-tl--link-keymap - 'mastodon-handle (concat "@" .account.acct) - 'help-echo (concat "Browse user profile of @" .account.acct)) - ")"))) + (mastodon-media--get-avatar-rendering + (map-nested-elt data '(account avatar)))) + (if (not base) + ;; boost symbol: + (concat (mastodon-tl--symbol 'boost) + " " + ;; username as button: + (mastodon-tl--byline-handle + data domain account + ;; display uname not handle (for boosts): + uname 'mastodon-display-name-face)) + ;; normal combo author byline: + (mastodon-tl--byline-uname-+-handle data domain account))))) (defun mastodon-tl--format-byline-help-echo (toot) "Format a help-echo for byline of TOOT. @@ -680,13 +753,27 @@ The result is added as an attachments property to author-byline." :type .type))) media))) -(defun mastodon-tl--byline-boosted (toot) - "Add byline for boosted data from TOOT." +(defun mastodon-tl--byline-booster (toot) + "Add author byline for booster from TOOT. +Only return something if TOOT contains a reblog." (let ((reblog (alist-get 'reblog toot))) - (when reblog - (concat - "\n " (propertize "Boosted" 'face 'mastodon-boosted-face) - " " (mastodon-tl--byline-author reblog))))) + (if reblog + (mastodon-tl--byline-author toot) + ""))) + +(defun mastodon-tl--byline-booster-str (toot) + "Format boosted string for action byline. +Only return string if TOOT contains a reblog." + (let ((reblog (alist-get 'reblog toot))) + (if reblog + (concat + " " (propertize "boosted" 'face 'mastodon-boosted-face) "\n") + ""))) + +(defun mastodon-tl--byline-boost (toot) + "Format a boost action-byline element for TOOT." + (concat (mastodon-tl--byline-booster toot) + (mastodon-tl--byline-booster-str toot))) (defun mastodon-tl--format-faved-or-boosted-byline (letter) "Format the byline marker for a boosted or favourited status. @@ -709,36 +796,41 @@ LETTER is a string, F for favourited, B for boosted, or K for bookmarked." (image-type-available-p 'imagemagick) (image-transforms-p))) -(defun mastodon-tl--byline (toot author-byline action-byline - &optional detailed-p domain base-toot) +(defun mastodon-tl--byline (toot author-byline &optional detailed-p + domain base-toot group account) "Generate byline for TOOT. AUTHOR-BYLINE is a function for adding the author portion of the byline that takes one variable. ACTION-BYLINE is a function for adding an action, such as boosting, favouriting and following to the byline. It also takes a single function. -By default it is `mastodon-tl--byline-boosted'. +By default it is `mastodon-tl--byline-author' DETAILED-P means display more detailed info. For now this just means displaying toot client. When DOMAIN, force inclusion of user's domain in their handle. -BASE-TOOT is JSON for the base toot, if any." +BASE-TOOT is JSON for the base toot, if any. +GROUP is the notification group if any. +ACCOUNT is the notification account if any." (let* ((created-time - ;; bosts and faves in notifs view - ;; (makes timestamps be for the original toot not the boost/fave): - (or (mastodon-tl--field 'created_at - (mastodon-tl--field 'status toot)) - ;; all other toots, inc. boosts/faves in timelines: - ;; (mastodon-tl--field auto fetches from reblogs if needed): - (mastodon-tl--field 'created_at toot))) - (parsed-time (date-to-time created-time)) + (if group + (mastodon-tl--field 'latest_page_notification_at group) + ;; bosts and faves in notifs view + ;; (makes timestamps be for the original toot not the boost/fave): + (or (mastodon-tl--field 'created_at + (mastodon-tl--field 'status toot)) + ;; all other toots, inc. boosts/faves in timelines: + ;; (mastodon-tl--field auto fetches from reblogs if needed): + (mastodon-tl--field 'created_at toot)))) + (parsed-time (when created-time (date-to-time created-time))) (faved (eq t (mastodon-tl--field 'favourited toot))) (boosted (eq t (mastodon-tl--field 'reblogged toot))) (bookmarked (eq t (mastodon-tl--field 'bookmarked toot))) (visibility (mastodon-tl--field 'visibility toot)) - (account (alist-get 'account toot)) - (avatar-url (alist-get 'avatar account)) - (type (alist-get 'type toot)) + (type (alist-get 'type (or group toot))) (base-toot-maybe (or base-toot ;; show edits for notifs (mastodon-tl--toot-or-base toot))) ;; for boosts + (account (or account + (alist-get 'account base-toot-maybe))) + (avatar-url (alist-get 'avatar account)) (edited-time (alist-get 'edited_at base-toot-maybe)) (edited-parsed (when edited-time (date-to-time edited-time)))) (concat @@ -758,17 +850,18 @@ BASE-TOOT is JSON for the base toot, if any." (when bookmarked (mastodon-tl--format-faved-or-boosted-byline (mastodon-tl--symbol 'bookmark)))) - ;; we remove avatars from the byline also, so that they also do not mess - ;; with `mastodon-tl--goto-next-item': + ;; we remove avatars from the byline also, so that they also do not + ;; mess with `mastodon-tl--goto-next-item': (when (and mastodon-tl--show-avatars mastodon-tl--display-media-p (mastodon-tl--image-trans-check)) (mastodon-media--get-avatar-rendering avatar-url)) (propertize (concat - ;; we propertize help-echo format faves for author name - ;; in `mastodon-tl--byline-author' - (funcall author-byline toot nil domain) + ;; NB: action-byline (boost) is now added in insert-status, so no + ;; longer part of the byline. + ;; (base) author byline: + (funcall author-byline toot nil domain :base account) ;; visibility: (cond ((string= visibility "direct") (propertize (concat " " (mastodon-tl--symbol 'direct)) @@ -776,22 +869,22 @@ BASE-TOOT is JSON for the base toot, if any." ((string= visibility "private") (propertize (concat " " (mastodon-tl--symbol 'private)) 'help-echo visibility))) - ;;action byline: - (funcall action-byline toot) " " ;; timestamp: - (propertize - (format-time-string mastodon-toot-timestamp-format parsed-time) - 'timestamp parsed-time - 'display (if mastodon-tl--enable-relative-timestamps - (mastodon-tl--relative-time-description parsed-time) - parsed-time)) + (let ((ts (format-time-string + mastodon-toot-timestamp-format parsed-time))) + (propertize ts + 'timestamp parsed-time + 'display + (if mastodon-tl--enable-relative-timestamps + (mastodon-tl--relative-time-description parsed-time) + parsed-time) + 'help-echo ts)) ;; detailed: (when detailed-p - (let* ((app (alist-get 'application toot)) - (app-name (alist-get 'name app)) - (app-url (alist-get 'website app))) - (when app + (let* ((app-name (map-nested-elt toot '(application name))) + (app-url (map-nested-elt toot '(application website)))) + (when app-name (concat (propertize " via " 'face 'default) (propertize app-name @@ -826,6 +919,8 @@ BASE-TOOT is JSON for the base toot, if any." 'favourited-p faved 'boosted-p boosted 'bookmarked-p bookmarked + ;; enable playing of videos when point is on byline: + 'attachments (mastodon-tl--get-attachments-for-byline toot) 'edited edited-time 'edit-history (when edited-time (mastodon-toot--get-toot-edits @@ -874,17 +969,59 @@ links in the text. If TOOT is nil no parsing occurs." 0 (- (window-width) 3))))) (shr-render-region (point-min) (point-max))) - ;; Make all links a tab stop recognized by our own logic, make things point - ;; to our own logic (e.g. hashtags), and update keymaps where needed: + ;; Make all links a tab stop recognized by our own logic, make + ;; things point to our own logic (e.g. hashtags), and update keymaps + ;; where needed: (when toot (let (region) (while (setq region (mastodon-tl--find-property-range 'shr-url (or (cdr region) (point-min)))) (mastodon-tl--process-link toot (car region) (cdr region) - (get-text-property (car region) 'shr-url))))) + (get-text-property (car region) 'shr-url)) + (when (proper-list-p toot) ;; not on profile fields cons cells + ;; render card author maybe: + (let* ((card-url (map-nested-elt toot '(card url))) + (authors (map-nested-elt toot '(card authors))) + (url (buffer-substring (car region) (cdr region))) + (url-no-query (car (split-string url "?")))) + (when (and (string= url-no-query card-url) + ;; only if we have an account's data: + (alist-get 'account (car authors))) + (goto-char (point-max)) + (mastodon-tl--insert-card-authors authors))))))) (buffer-string)))) +(defun mastodon-tl--insert-card-authors (authors) + "Insert a string of card AUTHORS." + (let ((authors-str (format "Author%s: " + (if (< 1 (length authors)) "s" "")))) + (insert + (concat + "\n(" authors-str + (mapconcat #'mastodon-tl--format-card-author authors "\n") + ")\n")))) + +(defun mastodon-tl--format-card-author (data) + "Render card author DATA." + (when-let ((account (alist-get 'account data))) ;.account + (let-alist account ;.account + ;; FIXME: replace with refactored handle render fun + ;; in byline refactor branch: + (concat + (propertize .username + 'face 'mastodon-display-name-face + 'item-type 'user + 'item-id .id) + " " + (propertize (concat "@" .acct) + 'face 'mastodon-handle-face + 'mouse-face 'highlight + 'mastodon-tab-stop 'user-handle + 'keymap mastodon-tl--link-keymap + 'mastodon-handle (concat "@" .acct) + 'help-echo (concat "Browse user profile of @" .acct)))))) + (defun mastodon-tl--process-link (toot start end url) "Process link URL in TOOT as hashtag, userhandle, or normal link. START and END are the boundaries of the link in the toot." @@ -991,7 +1128,7 @@ the toot)." (url-generic-parse-url instance-url))) (parsed (url-generic-parse-url url)) (path (url-filename parsed)) - (split (string-split path "/"))) + (split (split-string path "/"))) (when (and (string= instance-host (url-host parsed)) (string-prefix-p "/tag" path)) ;; "/tag/" or "/tags/" (nth 2 split)))) @@ -1108,7 +1245,7 @@ content should be hidden." (save-excursion (goto-char (point-min)) (while (not (string= "No more items" ; improve this hack test! - (mastodon-tl--goto-next-item :no-refresh))) + (mastodon-tl--goto-next-item :no-refresh))) (let* ((json (mastodon-tl--property 'item-json :no-move)) (cw (alist-get 'spoiler_text json))) (when (not (string= "" cw)) @@ -1182,16 +1319,22 @@ SENSITIVE is a flag from the item's JSON data." (concat "Media:: " (if (and mastodon-tl--display-caption-not-url-when-no-media .description) - .description) - .preview_url))) - (if mastodon-tl--display-media-p + .description + .preview_url))) + (remote-url (or .remote_url .url))) + (if (and mastodon-tl--display-media-p + ;; if in notifs, also check notifs images custom: + (if (or (mastodon-tl--buffer-type-eq 'notifications) + (mastodon-tl--buffer-type-eq 'mentions)) + mastodon-notifications--images-in-notifs + t)) (mastodon-media--get-media-link-rendering ; placeholder: "[img]" - .preview_url (or .remote_url .url) ; for shr-browse-url + .preview_url remote-url ; for shr-browse-url .type .description sensitive) ;; return URL/caption: (concat (mastodon-tl--propertize-img-str-or-url (concat "Media:: " .preview_url) ; string - .preview_url .remote_url .type .description + .preview_url remote-url .type .description display-str 'shr-link .description sensitive) "\n"))))) @@ -1214,7 +1357,7 @@ SENSITIVE is a flag from the item's JSON data." 'face face 'mouse-face 'highlight 'mastodon-tab-stop 'image ; for do-link-action-at-point - 'image-url full-remote-url ; for shr-browse-image + 'image-url (or full-remote-url media-url) ; for shr-browse-image 'keymap mastodon-tl--shr-image-map-replacement 'image-description caption 'sensitive sensitive @@ -1521,7 +1664,7 @@ portion of the byline that takes one variable. By default it is ACTION-BYLINE is also an optional function for adding an action, such as boosting favouriting and following to the byline. It also takes a single function. By default it is -`mastodon-tl--byline-boosted'. +`mastodon-tl--byline-boost'. ID is that of the status if it is a notification, which is attached as a `item-id' property if provided. If the status is a favourite or boost notification, BASE-TOOT is the @@ -1532,12 +1675,11 @@ THREAD means the status will be displayed in a thread view. When DOMAIN, force inclusion of user's domain in their handle. UNFOLDED is a boolean meaning whether to unfold or fold item if foldable. NO-BYLINE means just insert toot body, used for folding." - (let* ((start-pos (point)) - (reply-to-id (alist-get 'in_reply_to_id toot)) + (let* ((reply-to-id (alist-get 'in_reply_to_id toot)) (after-reply-status-p (when (and thread reply-to-id) (mastodon-tl--after-reply-status reply-to-id))) - (type (alist-get 'type toot)) + ;; (type (alist-get 'type toot)) (toot-foldable (and mastodon-tl--fold-toots-at-length (length> body mastodon-tl--fold-toots-at-length)))) @@ -1547,7 +1689,8 @@ NO-BYLINE means just insert toot body, used for folding." (propertize ;; body only: (concat "\n" - ;; relpy symbol (broken): + (funcall action-byline toot) + ;; relpy symbol: (when (and after-reply-status-p thread) (concat (mastodon-tl--symbol 'replied) "\n")) @@ -1564,9 +1707,10 @@ NO-BYLINE means just insert toot body, used for folding." 'toot-body t) ;; includes newlines etc. for folding ;; byline: "\n" - (unless no-byline - (mastodon-tl--byline toot author-byline action-byline - detailed-p domain base-toot))) + (if no-byline + "" + (mastodon-tl--byline toot author-byline detailed-p + domain base-toot))) 'item-type 'toot 'item-id (or id ; notification's own id (alist-get 'id toot)) ; toot id @@ -1578,13 +1722,9 @@ NO-BYLINE means just insert toot body, used for folding." 'item-json toot 'base-toot base-toot 'cursor-face 'mastodon-cursor-highlight-face - 'notification-type type 'toot-foldable toot-foldable 'toot-folded (and toot-foldable (not unfolded))) - (if no-byline "" "\n")) - ;; media: - (when mastodon-tl--display-media-p - (mastodon-media--inline-images start-pos (point))))) + (if no-byline "" "\n")))) (defun mastodon-tl--is-reply (toot) "Check if the TOOT is a reply to another one (and not boosted). @@ -1654,15 +1794,16 @@ NO-BYLINE means just insert toot body, used for folding." (mastodon-tl--insert-status toot (mastodon-tl--clean-tabs-and-nl spoiler-or-content) - 'mastodon-tl--byline-author 'mastodon-tl--byline-boosted + #'mastodon-tl--byline-author #'mastodon-tl--byline-boost nil nil detailed-p thread domain unfolded no-byline)))) -(defun mastodon-tl--timeline (toots &optional thread domain) +(defun mastodon-tl--timeline (toots &optional thread domain no-byline) "Display each toot in TOOTS. This function removes replies if user required. THREAD means the status will be displayed in a thread view. When DOMAIN, force inclusion of user's domain in their handle." - (let ((toots ;; hack to *not* filter replies on profiles: + (let ((start-pos (point)) + (toots ;; hack to *not* filter replies on profiles: (if (eq (mastodon-tl--get-buffer-type) 'profile-statuses) toots (if (or ; we were called via --more*: @@ -1672,8 +1813,11 @@ When DOMAIN, force inclusion of user's domain in their handle." (cl-remove-if-not #'mastodon-tl--is-reply toots) toots)))) (mapc (lambda (toot) - (mastodon-tl--toot toot nil thread domain)) + (mastodon-tl--toot toot nil thread domain nil no-byline)) toots) + ;; media: + (when mastodon-tl--display-media-p + (mastodon-media--inline-images start-pos (point))) (goto-char (point-min)))) ;;; FOLDING @@ -1977,7 +2121,11 @@ call this function after it is set or use something else." ((string= "*mastodon-toot-edits*" buffer-name) 'toot-edits) ((string= "*masto-image*" (buffer-name)) - 'mastodon-image)))) + 'mastodon-image) + ((mastodon-tl--endpoint-str-= "timelines/link") + 'link-timeline) + ((mastodon-tl--endpoint-str-= "announcements") + 'announcements)))) (defun mastodon-tl--buffer-type-eq (type) "Return t if current buffer type is equal to symbol TYPE." @@ -2079,16 +2227,28 @@ BACKWARD means move backward (up) the timeline." (get-text-property (point) prop))))) (defun mastodon-tl--newest-id () - "Return item-id from the top of the buffer." + "Return item-id from the top of the buffer. +If we are in a notifications view, return `notifications-max-id'." (save-excursion (goto-char (point-min)) - (mastodon-tl--property 'item-id))) + (mastodon-tl--property + (if (eq (mastodon-tl--get-buffer-type) + (member (mastodon-tl--get-buffer-type) + '(mentions notifications))) + 'notifications-max-id + 'item-id)))) (defun mastodon-tl--oldest-id () - "Return item-id from the bottom of the buffer." + "Return item-id from the bottom of the buffer. +If we are in a notifications view, return `notifications-min-id'." (save-excursion (goto-char (point-max)) - (mastodon-tl--property 'item-id nil :backward))) + (mastodon-tl--property + (if (member (mastodon-tl--get-buffer-type) + '(mentions notifications)) + 'notifications-min-id + 'item-id) + nil :backward))) (defun mastodon-tl--as-string (numeric) "Convert NUMERIC to string." @@ -2128,6 +2288,9 @@ ID is that of the toot to view." #'mastodon-tl--update-toot) (mastodon-tl--toot toot :detailed-p) (goto-char (point-min)) + (when mastodon-tl--display-media-p + (mastodon-media--inline-images (point-min) + (point-max))) (mastodon-tl--goto-next-item :no-refresh))))) (defun mastodon-tl--update-toot (json) @@ -2186,6 +2349,11 @@ view all branches of a thread." (move-marker marker (point)) ;; print re-fetched toot: (mastodon-tl--toot toot :detailed-p :thread) + ;; inline images only for the toot + ;; (`mastodon-tl--timeline' handles the rest): + (when mastodon-tl--display-media-p + (mastodon-media--inline-images marker ;start-pos + (point))) (mastodon-tl--timeline (alist-get 'descendants context) :thread) ;; put point at the toot: @@ -2236,8 +2404,7 @@ If UNMUTE, unmute it." (defun mastodon-tl--map-account-id-from-toot (statuses) "Return a list of the account IDs of the author of each toot in STATUSES." (mapcar (lambda (status) - (alist-get 'id - (alist-get 'account status))) + (map-nested-elt status '(account id))) statuses)) (defun mastodon-tl--user-in-thread-p (id) @@ -2382,39 +2549,44 @@ LANGS is the accumulated array param alist if we re-run recursively." "Get the list of user-handles for ACTION from the current toot." (mastodon-tl--do-if-item (let ((user-handles - (cond ((or ; follow suggests / search / foll requests compat: - (mastodon-tl--buffer-type-eq 'follow-suggestions) - (mastodon-tl--buffer-type-eq 'search) - (mastodon-tl--buffer-type-eq 'follow-requests) - ;; profile follows/followers but not statuses: - (mastodon-tl--buffer-type-eq 'profile-followers) - (mastodon-tl--buffer-type-eq 'profile-following)) - ;; fetch 'item-json: - (list (alist-get 'acct - (mastodon-tl--property 'item-json :no-move)))) - ;; profile view, point in profile details, poss no toots - ;; needed for e.g. gup.pe groups which show no toots publically: - ((and (mastodon-tl--profile-buffer-p) - (get-text-property (point) 'profile-json)) - (list (alist-get 'acct - (mastodon-profile--profile-json)))) - (t - (mastodon-profile--extract-users-handles - (mastodon-profile--item-json)))))) - ;; return immediately if only 1 handle: - (if (eq 1 (length user-handles)) - (car user-handles) - (completing-read (cond ((or ; TODO: make this "enable/disable notifications" - (string= action "disable") - (string= action "enable")) - (format "%s notifications when user posts: " action)) - ((string-suffix-p "boosts" action) - (format "%s by user: " action)) - (t - (format "Handle of user to %s: " action))) - user-handles - nil ; predicate - 'confirm))))) + (cond + ((or ; follow suggests / search / foll requests compat: + (member (mastodon-tl--get-buffer-type) + '( follow-suggestions search follow-requests + ;; profile follows/followers but not statuses: + profile-followers profile-following))) + ;; fetch 'item-json: + (list (alist-get 'acct + (mastodon-tl--property 'item-json :no-move)))) + ;; profile view, point in profile details, poss no toots + ;; needed for e.g. gup.pe groups which show no toots publically: + ((and (mastodon-tl--profile-buffer-p) + (get-text-property (point) 'profile-json)) + (list (alist-get 'acct + (mastodon-profile--profile-json)))) + ;; (grouped) notifications: + ((member (mastodon-tl--get-buffer-type) '(mentions notifications)) + (append ;; those acting on item: + (cl-remove-duplicates + (cl-loop for a in (mastodon-tl--property + 'notification-accounts :no-move) + collect (alist-get 'acct a))) + ;; mentions in item: + (mastodon-profile--extract-users-handles + (mastodon-profile--item-json)))) + (t + (mastodon-profile--extract-users-handles + (mastodon-profile--item-json)))))) + (completing-read + (cond ((or ; TODO: make this "enable/disable notifications" + (string= action "disable") + (string= action "enable")) + (format "%s notifications when user posts: " action)) + ((string-suffix-p "boosts" action) + (format "%s by user: " action)) + (t (format "Handle of user to %s: " action))) + user-handles nil ; predicate + 'confirm)))) (defun mastodon-tl--get-blocks-or-mutes-list (action) "Fetch the list of accounts for ACTION from the server. @@ -2440,20 +2612,31 @@ NOTIFY is only non-nil when called by `mastodon-tl--follow-user'. LANGS is an array parameters alist of languages to filer user's posts by. REBLOGS is a boolean string like NOTIFY, enabling or disabling display of the user's boosts in your timeline." - (let* ((account (if negp - ;; unmuting/unblocking, handle from mute/block list - (mastodon-profile--search-account-by-handle user-handle) - (mastodon-profile--lookup-account-in-status - user-handle - (if (mastodon-tl--profile-buffer-p) - ;; profile view, use 'profile-json as status: - (mastodon-profile--profile-json) - ;; muting/blocking, select from handles in current status - (mastodon-profile--item-json))))) + (let* ((account + (cond + (negp ;; unmuting/unblocking, use mute/block list + (mastodon-profile--search-account-by-handle user-handle)) + ;; (grouped) notifications: + ((member (mastodon-tl--get-buffer-type) + '(mentions notifications)) + (let ((accounts (mastodon-tl--property 'notification-accounts))) + (or (cl-some (lambda (x) + (when (string= user-handle (alist-get 'acct x)) + x)) + accounts) + (mastodon-profile--lookup-account-in-status + user-handle + (mastodon-profile--item-json))))) + (t + (mastodon-profile--lookup-account-in-status + user-handle + (if (mastodon-tl--profile-buffer-p) + ;; profile view, use 'profile-json as status: + (mastodon-profile--profile-json) + ;; muting/blocking, select from handles in current status + (mastodon-profile--item-json)))))) (user-id (alist-get 'id account)) - (name (if (string-empty-p (alist-get 'display_name account)) - (alist-get 'username account) - (alist-get 'display_name account))) + (name (mastodon-tl--display-or-uname account)) (args (cond (notify `(("notify" . ,notify))) (langs langs) (reblogs `(("reblogs" . ,reblogs))) @@ -2503,8 +2686,7 @@ ARGS is an alist of any parameters to send with the request." ((assoc "languages[]" args #'string=) (message "User %s filtered by language(s): %s" name (mapconcat #'cdr args " "))) - ((and (eq notify nil) - (eq reblogs nil)) + ((not (or notify reblogs)) (if (and (string= action "follow") (eq t (alist-get 'requested json))) (message "Follow requested for user %s (@%s)!" name user-handle) @@ -2687,7 +2869,8 @@ the current view." (args (append args params)) (url (mastodon-http--api endpoint - (when (string-suffix-p "search" endpoint) + (when (or (string= endpoint "notifications") + (string-suffix-p "search" endpoint)) "v2")))) (apply #'mastodon-http--get-json-async url args callback cbargs))) @@ -2742,7 +2925,7 @@ Aims to respect any pagination in effect." ((eq type 'mentions) (mastodon-notifications--get-mentions)) ((eq type 'notifications) - (mastodon-notifications-get nil nil :force max-id)) + (mastodon-notifications-get nil nil max-id)) ((eq type 'profile-statuses-no-boosts) ;; TODO: max-id arg needed here also (mastodon-profile--open-statuses-no-reblogs)) @@ -3034,12 +3217,13 @@ This location is defined by a non-nil value of (interactive) ;; FIXME: actually these buffers should just reload by calling their own ;; load function (actually g is mostly mapped as such): - (if (or (mastodon-tl--buffer-type-eq 'trending-statuses) - (mastodon-tl--buffer-type-eq 'trending-tags) - (mastodon-tl--buffer-type-eq 'follow-suggestions) - (mastodon-tl--buffer-type-eq 'lists) - (mastodon-tl--buffer-type-eq 'filters) - (mastodon-tl--buffer-type-eq 'scheduled-statuses) + (if (or (member (mastodon-tl--get-buffer-type) + '(trending-statuses + trending-tags + follow-suggestions + lists + filters + scheduled-statuses)) (mastodon-tl--search-buffer-p)) (user-error "Update not available in this view") ;; FIXME: handle update for search and trending buffers @@ -3047,7 +3231,7 @@ This location is defined by a non-nil value of (update-function (mastodon-tl--update-function))) ;; update a thread, without calling `mastodon-tl--updated-json': (if (mastodon-tl--buffer-type-eq 'thread) - ;; load whole thread whole thread + ;; load whole thread: (let ((thread-id (mastodon-tl--thread-parent-id))) (funcall update-function thread-id) (message "Loaded full thread.")) @@ -3061,15 +3245,16 @@ This location is defined by a non-nil value of (mastodon-tl--set-after-update-marker) (goto-char (or mastodon-tl--update-point (point-min))) (funcall update-function json) - (when mastodon-tl--after-update-marker - (goto-char mastodon-tl--after-update-marker))))))))) + (if mastodon-tl--after-update-marker + (goto-char mastodon-tl--after-update-marker) + (mastodon-tl--goto-next-item))))))))) ;;; LOADING TIMELINES (defun mastodon-tl--init (buffer-name endpoint update-function &optional headers params - hide-replies instance) + hide-replies instance no-byline) "Initialize BUFFER-NAME with timeline targeted by ENDPOINT asynchronously. UPDATE-FUNCTION is used to recieve more toots. HEADERS means to also collect the response headers. Used for paginating @@ -3087,11 +3272,12 @@ a timeline from." #'mastodon-http--get-response-async #'mastodon-http--get-json-async) url params 'mastodon-tl--init* - buffer endpoint update-function headers params hide-replies instance))) + buffer endpoint update-function headers params hide-replies + instance no-byline))) (defun mastodon-tl--init* (response buffer endpoint update-function &optional headers - update-params hide-replies instance) + update-params hide-replies instance no-byline) "Initialize BUFFER with timeline targeted by ENDPOINT. UPDATE-FUNCTION is used to recieve more toots. RESPONSE is the data returned from the server by @@ -3122,7 +3308,7 @@ JSON and http headers, without it just the JSON." link-header update-params hide-replies ;; awful hack to fix multiple reloads: (alist-get "max_id" update-params nil nil #'string=)) - (mastodon-tl--do-init json update-function instance))))))) + (mastodon-tl--do-init json update-function instance no-byline))))))) (defun mastodon-tl--init-sync (buffer-name endpoint update-function &optional note-type params @@ -3165,14 +3351,15 @@ ENDPOINT-VERSION is a string, format Vx, e.g. V2." (mastodon-tl--do-init json update-function) buffer))) -(defun mastodon-tl--do-init (json update-fun &optional domain) +(defun mastodon-tl--do-init (json update-fun &optional domain no-byline) "Utility function for `mastodon-tl--init*' and `mastodon-tl--init-sync'. JSON is the data to call UPDATE-FUN on. When DOMAIN, force inclusion of user's domain in their handle." (remove-overlays) ; video overlays - (if domain ;; maybe our update-fun doesn't always have 3 args...: - (funcall update-fun json nil domain) - (funcall update-fun json)) + (cond (domain ;; maybe our update-fun doesn't always have 3 args...: + (funcall update-fun json nil domain)) + (no-byline (funcall update-fun json nil nil no-byline)) + (t (funcall update-fun json))) (setq ;; Initialize with a minimal interval; we re-scan at least once ;; every 5 minutes to catch any timestamps we may have missed diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el index 832d03f..4e116fa 100644 --- a/lisp/mastodon-toot.el +++ b/lisp/mastodon-toot.el @@ -1,10 +1,10 @@ ;;; mastodon-toot.el --- Minor mode for sending Mastodon toots -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen -;; Copyright (C) 2020-2022 Marty Hiatt +;; Copyright (C) 2020-2024 Marty Hiatt ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -53,6 +53,7 @@ (defvar mastodon-tl--enable-proportional-fonts) (defvar mastodon-profile-account-settings) (defvar mastodon-profile-acccount-preferences-data) +(defvar tp-transient-settings) (autoload 'iso8601-parse "iso8601") (autoload 'mastodon-auth--user-acct "mastodon-auth") @@ -100,6 +101,8 @@ (autoload 'mastodon-profile--get-preferences-pref "mastodon-profile") (autoload 'mastodon-views--get-own-instance "mastodon-views") (autoload 'mastodon-tl--image-trans-check "mastodon-tl") +(autoload 'mastodon-instance-data "mastodon") +(autoload 'mastodon-create-poll "mastodon-transient") ;; for mastodon-toot--translate-toot-text (autoload 'mastodon-tl--content "mastodon-tl") @@ -168,6 +171,10 @@ By default fixed width fonts are used." :type '(boolean :tag "Enable using proportional rather than fixed \ width fonts")) +(defcustom mastodon-toot-poll-use-transient t + "Whether to use the transient menu to create a poll." + :type '(boolean)) + (defvar-local mastodon-toot--content-warning nil "The content warning of the current toot.") @@ -175,9 +182,14 @@ width fonts")) "A flag indicating whether the toot should be marked as NSFW.") (defvar mastodon-toot-visibility-list - '(direct private unlisted public) + '(public unlisted private direct) "A list of the available toot visibility settings.") +(defvar mastodon-toot-visibility-settings-list + '("public" "unlisted" "private") + "A list of the available default toot visibility settings. +Like `mastodon-toot-visibility-list' but without direct.") + (defvar-local mastodon-toot--visibility nil "A string indicating the visibility of the toot being composed. Valid values are \"direct\", \"private\" (followers-only), @@ -193,8 +205,8 @@ change the setting on the server, see (defvar-local mastodon-toot--media-attachment-ids nil "A list of any media attachment ids of the toot being composed.") -(defvar-local mastodon-toot-poll nil - "A list of poll options for the toot being composed.") +(defvar mastodon-toot-poll nil + "A plist of poll options for the toot being composed.") (defvar-local mastodon-toot--language nil "The language of the toot being composed, in ISO 639 (two-letter).") @@ -285,7 +297,9 @@ data about the item boosted or favourited." Includes boosts, and notifications that display toots. This macro makes the local variable ID available." (declare (debug t)) - `(if (not (eq 'toot (mastodon-tl--property 'item-type :no-move))) + `(if (or (not (eq 'toot (mastodon-tl--property 'item-type :no-move))) + (member (mastodon-tl--property 'notification-type) + '("follow" "follow_request"))) (user-error "Looks like there's no toot at point?") (mastodon-tl--with-toot-helper (lambda (id) @@ -517,21 +531,34 @@ SUBTRACT means we are un-favouriting or unboosting, so we decrement." (defun mastodon-toot--list-boosters () "List the boosters of toot at point." (interactive) - (mastodon-toot--list-boosters-or-favers)) + ;; use grouped notifs data if present: + ;; only send accounts as arg if type matches notif type we are acting + ;; on, to prevent showing accounts for a boost notif when asking for + ;; favers, and vice versa. + (let* ((type (mastodon-tl--property 'notification-type :no-move)) + (accounts (when (string= type "reblog") + (mastodon-tl--property 'notification-accounts :no-move)))) + (mastodon-toot--list-boosters-or-favers nil accounts))) (defun mastodon-toot--list-favouriters () "List the favouriters of toot at point." (interactive) - (mastodon-toot--list-boosters-or-favers :favourite)) + (let* ((type (mastodon-tl--property 'notification-type :no-move)) + (accounts (when (string= type "favourite") + (mastodon-tl--property 'notification-accounts :no-move)))) + (mastodon-toot--list-boosters-or-favers :favourite accounts))) -(defun mastodon-toot--list-boosters-or-favers (&optional favourite) +(defun mastodon-toot--list-boosters-or-favers (&optional favourite accounts) "List the favouriters or boosters of toot at point. -With FAVOURITE, list favouriters, else list boosters." +With FAVOURITE, list favouriters, else list boosters. +ACCOUNTS is notfications accounts if any." (mastodon-toot--with-toot-item - (let* ((endpoint (if favourite "favourited_by" "reblogged_by")) - (url (mastodon-http--api (format "statuses/%s/%s" id endpoint))) - (params '(("limit" . "80"))) - (json (mastodon-http--get-json url params))) + (let* ((endpoint (unless accounts + (if favourite "favourited_by" "reblogged_by"))) + (url (unless accounts + (mastodon-http--api (format "statuses/%s/%s" id endpoint)))) + (params (unless accounts '(("limit" . "80")))) + (json (or accounts (mastodon-http--get-json url params)))) (if (eq (caar json) 'error) (user-error "%s (Status does not exist or is private)" (alist-get 'error json)) @@ -738,9 +765,9 @@ If toot is not empty, prompt to save text as a draft." Pushes `mastodon-toot-current-toot-text' to `mastodon-toot-draft-toots-list'." (interactive) - (unless (eq mastodon-toot-current-toot-text nil) + (unless (string= mastodon-toot-current-toot-text nil) (cl-pushnew mastodon-toot-current-toot-text - mastodon-toot-draft-toots-list :test 'equal) + mastodon-toot-draft-toots-list :test 'string=) (message "Draft saved!"))) (defun mastodon-toot--empty-p (&optional text-only) @@ -749,7 +776,7 @@ TEXT-ONLY means don't check for attachments or polls." (and (if text-only t (and (not mastodon-toot--media-attachments) - (not mastodon-toot-poll))) + (not (mastodon-toot-poll-var)))) (string-empty-p (mastodon-tl--clean-tabs-and-nl (mastodon-toot--remove-docs))))) @@ -849,13 +876,22 @@ to `emojify-user-emojis', and the emoji data is updated." (defun mastodon-toot--build-poll-params () "Return an alist of parameters for POSTing a poll status." - (append - (mastodon-http--build-array-params-alist - "poll[options][]" - (plist-get mastodon-toot-poll :options)) - `(("poll[expires_in]" . ,(plist-get mastodon-toot-poll :expiry))) - `(("poll[multiple]" . ,(symbol-name (plist-get mastodon-toot-poll :multi)))) - `(("poll[hide_totals]" . ,(symbol-name (plist-get mastodon-toot-poll :hide)))))) + (if mastodon-toot-poll-use-transient + (let-alist tp-transient-settings + (append + (mastodon-http--build-array-params-alist + "poll[options][]" + (list .one .two .three .four)) + (list (cons "poll[expires_in]" .expiry) + (cons "poll[multiple]" .multi) + (cons "poll[hide_totals]" .hide)))) + (append + (mastodon-http--build-array-params-alist + "poll[options][]" + (plist-get mastodon-toot-poll :options)) + `(("poll[expires_in]" . ,(plist-get mastodon-toot-poll :expiry))) + `(("poll[multiple]" . ,(symbol-name (plist-get mastodon-toot-poll :multi)))) + `(("poll[hide_totals]" . ,(symbol-name (plist-get mastodon-toot-poll :hide))))))) ;;; SEND TOOT FUNCTION @@ -874,26 +910,29 @@ instance to edit a toot." (endpoint (mastodon-http--api (if edit-id ; we are sending an edit: (format "statuses/%s" edit-id) "statuses"))) - (args-no-media (append `(("status" . ,toot) - ("in_reply_to_id" . ,mastodon-toot--reply-to-id) - ("visibility" . ,mastodon-toot--visibility) - ("sensitive" . ,(when mastodon-toot--content-nsfw - (symbol-name t))) - ("spoiler_text" . ,mastodon-toot--content-warning) - ("language" . ,mastodon-toot--language)) - ;; Pleroma instances can't handle null-valued - ;; scheduled_at args, so only add if non-nil - (when scheduled `(("scheduled_at" . ,scheduled))))) + (args-no-media + (append + `(("status" . ,toot) + ("in_reply_to_id" . ,mastodon-toot--reply-to-id) + ("visibility" . ,mastodon-toot--visibility) + ("sensitive" . ,(when mastodon-toot--content-nsfw + (symbol-name t))) + ("spoiler_text" . ,mastodon-toot--content-warning) + ("language" . ,mastodon-toot--language)) + ;; Pleroma instances can't handle null-valued + ;; scheduled_at args, so only add if non-nil + (when scheduled `(("scheduled_at" . ,scheduled))))) (args-media (when mastodon-toot--media-attachment-ids (mastodon-http--build-array-params-alist "media_ids[]" mastodon-toot--media-attachment-ids))) - (args-poll (when mastodon-toot-poll + (poll-var (mastodon-toot-poll-var)) + (args-poll (when poll-var (mastodon-toot--build-poll-params))) ;; media || polls: (args (if mastodon-toot--media-attachment-ids (append args-media args-no-media) - (if mastodon-toot-poll + (if poll-var (append args-no-media args-poll) args-no-media))) (prev-window-config mastodon-toot-previous-window-config)) @@ -920,6 +959,8 @@ instance to edit a toot." (lambda (_) ;; kill buffer: (mastodon-toot--kill) + ;; nil our poll var: + (set poll-var nil) (message "Toot %s!" (if scheduled "scheduled" "toot")) ;; cancel scheduled toot if we were editing it: (when scheduled-id @@ -1350,6 +1391,12 @@ which is used to attach it to a toot when posting." ;;; POLL +(defun mastodon-toot-poll-var () + "Return the correct poll var." + (if mastodon-toot-poll-use-transient + 'tp-transient-settings + 'mastodon-toot-poll)) + (defun mastodon-toot--fetch-max-poll-options (instance) "Return the maximum number of poll options from JSON data INSTANCE." (mastodon-toot--fetch-poll-field 'max_options instance)) @@ -1381,7 +1428,13 @@ MAX is the maximum number set by their instance." (defun mastodon-toot--create-poll () "Prompt for new poll options and return as a list." (interactive) - (let* ((instance (mastodon-http--get-json (mastodon-http--api "instance"))) + (if mastodon-toot-poll-use-transient + (mastodon-create-poll) + (mastodon-toot--read-poll))) + +(defun mastodon-toot--read-poll () + "Read poll options." + (let* ((instance (mastodon-instance-data)) (max-options (mastodon-toot--fetch-max-poll-options instance)) (count (mastodon-toot--read-poll-options-count max-options)) (length (mastodon-toot--fetch-max-poll-option-chars instance)) @@ -1406,12 +1459,11 @@ LENGTH is the maximum character length allowed for a poll option." (format "Poll option [%s/%s] [max %s chars]: " x count length)))) (longest (apply #'max (mapcar #'length choices)))) - (if (> longest length) - (progn - (user-error "Looks like you went over the max length. Try again") - (sleep-for 2) - (mastodon-toot--read-poll-options count length)) - choices))) + (if (not (> longest length)) + choices + (user-error "Looks like you went over the max length. Try again") + (sleep-for 2) + (mastodon-toot--read-poll-options count length)))) (defun mastodon-toot--read-poll-expiry () "Prompt for a poll expiry time. @@ -1440,10 +1492,11 @@ Return a cons of a human readable string, and a seconds-from-now string." "Remove poll from toot compose buffer. Sets `mastodon-toot-poll' to nil." (interactive) - (if (not mastodon-toot-poll) - (user-error "No poll?") - (setq mastodon-toot-poll nil) - (mastodon-toot--update-status-fields))) + (let ((var (mastodon-toot-poll-var))) + (if (not var) + (user-error "No poll?") + (set var nil) + (mastodon-toot--update-status-fields)))) (defun mastodon-toot--server-poll-to-local (json) "Convert server poll data JSON to a `mastodon-toot-poll' plist." @@ -1459,9 +1512,18 @@ Sets `mastodon-toot-poll' to nil." (mastodon-tl--human-duration expiry-seconds-rel))) (options (mastodon-tl--map-alist 'title .options)) (multiple (if (eq :json-false .multiple) nil t))) - (setq mastodon-toot-poll - `( :options ,options :expiry-readable ,expiry-human - :expiry ,expiry-str :multi ,multiple))))) + (if mastodon-toot-poll-use-transient + (setq tp-transient-settings + `((multi . ,multiple) + (expiry . ,expiry-str) + ;; (hide . ,hide) + (one . ,(nth 0 options)) + (two . ,(nth 1 options)) + (three . ,(nth 2 options)) + (four . ,(nth 3 options)))) + (setq mastodon-toot-poll + `( :options ,options :expiry-readable ,expiry-human + :expiry ,expiry-str :multi ,multiple)))))) ;;; SCHEDULE @@ -1668,7 +1730,7 @@ REPLY-TEXT is the text of the toot being replied to." 'read-only "Edit your message below." 'toot-post-header t)) ;; allow us to enter text after read-only header: - (propertize "\n" + (propertize "\n\n" 'rear-nonsticky t)))) (defun mastodon-toot--most-restrictive-visibility (reply-visibility) @@ -1678,14 +1740,8 @@ The default is given by `mastodon-toot--default-reply-visibility'." (let ((less-restrictive (member (intern mastodon-toot--default-reply-visibility) mastodon-toot-visibility-list))) (if (member (intern reply-visibility) less-restrictive) - mastodon-toot--default-reply-visibility - reply-visibility)))) - -(defun mastodon-toot--fill-buffer () - "Mark buffer, call `fill-region'." - (mark-whole-buffer) ; lisp code should not set mark - ;; (fill-region (point-min) (point-max)) ; but this doesn't work - (fill-region (region-beginning) (region-end))) + reply-visibility + mastodon-toot--default-reply-visibility)))) (defun mastodon-toot--render-reply-region-str (str) "Refill STR and prefix all lines with >, as reply-quote text." @@ -1693,10 +1749,11 @@ The default is given by `mastodon-toot--default-reply-visibility'." (insert str) ;; unfill first: (let ((fill-column (point-max))) - (mastodon-toot--fill-buffer)) + (fill-region (point-min) (point-max))) ;; then fill: - (mastodon-toot--fill-buffer) + (fill-region (point-min) (point-max)) ;; add our own prefix, pauschal: + (goto-char (point-min)) (save-match-data (while (re-search-forward "^" nil t) (replace-match " > "))) @@ -1744,7 +1801,8 @@ REPLY-REGION is a string to be injected into the buffer." (poll-region (mastodon-tl--find-property-range 'toot-post-poll-flag (point-min))) (toot-string (buffer-substring-no-properties (cdr header-region) - (point-max)))) + (point-max))) + (poll-var (mastodon-toot-poll-var))) (mastodon-toot--apply-fields-props count-region (format "%s/%s chars" @@ -1778,9 +1836,11 @@ REPLY-REGION is a string to be injected into the buffer." 'mastodon-cw-face) (mastodon-toot--apply-fields-props poll-region - (if mastodon-toot-poll "POLL" "") + (if (symbol-value poll-var) + "POLL" + "") 'mastodon-cw-face - (prin1-to-string mastodon-toot-poll)) + (prin1-to-string (symbol-value poll-var))) (mastodon-toot--apply-fields-props cw-region (if (and mastodon-toot--content-warning diff --git a/lisp/mastodon-transient.el b/lisp/mastodon-transient.el new file mode 100644 index 0000000..67ea667 --- /dev/null +++ b/lisp/mastodon-transient.el @@ -0,0 +1,343 @@ +;;; mastodon-transient.el --- transient menus for mastodon.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 martian hiatus + +;; Author: martian hiatus <mousebot@disroot.org> +;; Keywords: convenience + +;; 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: + +;; + +;;; Code: + +(require 'tp) +(require 'transient) + +(defvar mastodon-active-user) +(defvar mastodon-toot-visibility-settings-list) +(defvar mastodon-iso-639-regional) +(defvar mastodon-toot-poll) + +(autoload 'mastodon-toot-visibility-settings-list "mastodon-toot") +(autoload 'mastodon-http--get-json "mastodon-http") +(autoload 'mastodon-http--api "mastodon-http") +(autoload 'mastodon-http--triage "mastodon-http") +(autoload 'mastodon-http--patch "mastodon-http") +(autoload 'mastodon-profile--update-user-profile-note "mastodon-profile") +(autoload 'mastodon-toot--fetch-max-poll-options "mastodon-toot") +(autoload 'mastodon-toot--fetch-max-poll-option-chars "mastodon-toot") +(autoload 'mastodon-instance-data "mastodon") +(autoload 'mastodon-toot--update-status-fields "mastodon-toot") +(autoload 'mastodon-toot--read-poll-expiry "mastodon-toot") +(autoload 'mastodon-toot--poll-expiry-options-alist "mastodon-toot") +(autoload 'mastodon-toot--clear-poll "mastodon-toot") + +;;; UTILS + +;; some JSON fields that are returned under the "source" field need to be +;; sent back in the format source[key], while some others are sent kust as +;; key: +(defun mastodon-transient-parse-source-key (key) + "Parse mastodon source KEY. +If KEY needs to be source[key], format like so, else just return +the inner key part." + (let* ((split (split-string key "[][]")) + (array-key (cadr split))) + (if (or (= 1 (length split)) ;; no split + (member array-key '("privacy" "sensitive" "language"))) + key + array-key))) + +(defun mastodon-transient-parse-source-keys (alist) + "Parse ALIST containing source[key] keys." + (cl-loop for a in alist + collect (cons (mastodon-transient-parse-source-key (car a)) + (cdr a)))) + +(defun mastodon-transient-get-creds () + "Fetch account data." + (mastodon-http--get-json + (mastodon-http--api "accounts/verify_credentials") + nil :silent)) + +;; fields utils: +;; to PATCH fields, we just need fields[x][name] and fields[x][value] + +(defun mastodon-transient--fields-alist (fields) + "Convert fields in FIELDS to numbered conses. +The keys in the data are not numbered, so we convert the key into +the format fields.X.keyname." + (cl-loop + for f in fields + for count from 1 to 5 + collect + (cl-loop for x in f + collect + (cons (concat "fields." (number-to-string count) + "." (symbol-name (car x))) + (cdr x))))) + +(defun mastodon-transient-field-dot-to-array (key) + "Convert KEY from tp dot annotation to array[key] annotation." + (tp-dot-to-array (symbol-name key) nil "_attributes")) + +(defun mastodon-transient-dot-fields-to-arrays (alist) + "Parse fields ALIST in dot notation to array notation." + (cl-loop for y in alist + collect + (cons (mastodon-transient-field-dot-to-array (car y)) + (cdr y)))) + +;;; TRANSIENTS + +;; FIXME: PATCHing source vals as JSON request body doesn't work! existing +;; `mastodon-profile--update-preference' doesn't use it! it just uses +;; query params! strange thing is it works for non-source params +(transient-define-suffix mastodon-user-settings-update (&optional args) + "Update current user settings on the server." + :transient 'transient--do-exit + (interactive (list (transient-args 'mastodon-user-settings))) + (let* ((parsed (tp-parse-args-for-send args :strings)) + (strs (mastodon-transient-parse-source-keys parsed)) + (url (mastodon-http--api "accounts/update_credentials")) + (resp (mastodon-http--patch url strs))) ;; :json fails + (mastodon-http--triage + resp + (lambda (_) + (message "Settings updated!\n%s" (pp-to-string strs)))))) + +(transient-define-prefix mastodon-user-settings () + "A transient for setting current user settings." + :value (lambda () (tp-return-data + #'mastodon-transient-get-creds)) + [:description + (lambda () + (format "User settings for %s" mastodon-active-user)) + (:info + "Note: use the empty string (\"\") to remove a value from an option.")] + ;; strings + ["Account info" + ("n" "display name" "display_name" :alist-key display_name :class tp-option-str) + ("t" "update profile note" mastodon-update-profile-note) + ("f" "update profile fields" mastodon-profile-fields)] + ;; "choice" booleans (so we can PATCH :json-false explicitly): + ["Account options" + ("l" "locked" "locked" :alist-key locked :class tp-bool) + ("b" "bot" "bot" :alist-key bot :class tp-bool) + ("d" "discoverable" "discoverable" :alist-key discoverable :class tp-bool) + ("c" "hide follower/following lists" "source.hide_collections" + :alist-key source.hide_collections :class tp-bool) + ("i" "indexable" "source.indexable" :alist-key source.indexable :class tp-bool) + ] + ["Tooting options" + ("p" "default privacy" "source.privacy" :alist-key source.privacy + :class tp-option + :choices (lambda () mastodon-toot-visibility-settings-list)) + ("s" "mark sensitive" "source.sensitive" :alist-key source.sensitive :class tp-bool) + ("g" "default language" "source.language" :alist-key source.language :class tp-option + :choices (lambda () mastodon-iso-639-regional)) + ] + ["Update" + ("C-c C-c" "Save settings" mastodon-user-settings-update) + ("C-c C-k" :info "Revert all changes")] + (interactive) + (if (or (not (boundp 'mastodon-active-user)) + (not mastodon-active-user)) + (user-error "User not set") + (transient-setup 'mastodon-user-settings))) + +(transient-define-suffix mastodon-update-profile-note () + "Update current user profile note." + :transient 'transient--do-exit + (interactive) + (mastodon-profile--update-user-profile-note)) + +(transient-define-suffix mastodon-profile-fields-update (args) + "Update current user profile fields." + :transient 'transient--do-return + (interactive (list (transient-args 'mastodon-profile-fields))) + (let* (;; FIXME: maybe only changed also won't work with fields, as + ;; perhaps what is PATCHed overwrites whatever is on the server? + ;; (only-changed (tp-only-changed-args alist)) + (arrays (mastodon-transient-dot-fields-to-arrays args)) + (endpoint "accounts/update_credentials") + (url (mastodon-http--api endpoint)) + (resp (mastodon-http--patch url arrays))) ; :json))) + (mastodon-http--triage + resp (lambda (_) (message "Fields updated!"))))) + +(defun mastodon-transient-fetch-fields () + "Fetch profile fields (metadata)." + (tp-return-data #'mastodon-transient-get-creds nil 'fields) + (setq tp-transient-settings + (mastodon-transient--fields-alist tp-transient-settings))) + +(transient-define-prefix mastodon-profile-fields () + "A transient for setting profile fields." + :value (lambda () (mastodon-transient-fetch-fields)) + [:description + "Fields" + ["Name" + ("1 n" "" "fields.1.name" :alist-key fields.1.name :class mastodon-transient-field) + ("2 n" "" "fields.2.name" :alist-key fields.2.name :class mastodon-transient-field) + ("3 n" "" "fields.3.name" :alist-key fields.3.name :class mastodon-transient-field) + ("4 n" "" "fields.4.name" :alist-key fields.4.name :class mastodon-transient-field)] + ["Value" + ("1 v" "" "fields.1.value" :alist-key fields.1.value :class mastodon-transient-field) + ("2 v" "" "fields.2.value" :alist-key fields.2.value :class mastodon-transient-field) + ("3 v" "" "fields.3.value" :alist-key fields.3.value :class mastodon-transient-field) + ("4 v" "" "fields.4.value" :alist-key fields.4.value :class mastodon-transient-field)]] + ["Update" + ("C-c C-c" "Save settings" mastodon-profile-fields-update) + ("C-c C-k" :info "Revert all changes")] + (interactive) + (if (not mastodon-active-user) + (user-error "User not set") + (transient-setup 'mastodon-profile-fields))) + +(defun mastodon-transient-max-poll-opts () + "Return max poll options of user's instance." + (let ((instance (mastodon-instance-data))) + (mastodon-toot--fetch-max-poll-options instance))) + +(defun mastodon-transient-max-poll-opt-chars () + "Return max poll option characters of user's instance." + (let ((instance (mastodon-instance-data))) + (mastodon-toot--fetch-max-poll-option-chars instance))) + +(transient-define-prefix mastodon-create-poll () + "A transient for creating a poll." + ;; FIXME: handle existing polls when editing a toot + :value (lambda () tp-transient-settings) + ["Create poll" + (:info (lambda () + (format "Max options: %s" + (mastodon-transient-max-poll-opts)))) + (:info (lambda () + (format "Max option length: %s" + (mastodon-transient-max-poll-opt-chars))))] + ["Options" + ("m" "Multiple choice?" "multi" :alist-key multi + :class mastodon-transient-poll-bool) + ("h" "Hide vote count till expiry?" "hide" :alist-key hide + :class mastodon-transient-poll-bool) + ("e" "Expiry" "expiry" :alist-key expiry + :class mastodon-transient-expiry)] + ["Choices" + ("1" "" "1" :alist-key one :class mastodon-transient-poll-choice) + ("2" "" "2" :alist-key two :class mastodon-transient-poll-choice) + ("3" "" "3" :alist-key three :class mastodon-transient-poll-choice) + ("4" "" "4" :alist-key four :class mastodon-transient-poll-choice)] + ;; TODO: display the max number of options or add options cmd + ["Update" + ("C-c C-c" "Save and done" mastodon-create-poll-done) + ("C-c C-k" "Delete all" mastodon-clear-poll) + ("C-x C-k" :info "Revert all")] + (interactive) + (if (not mastodon-active-user) + (user-error "User not set") + (transient-setup 'mastodon-create-poll))) + +(transient-define-suffix mastodon-clear-poll () + "Clear current poll data." + :transient 'transient--do-stay + (interactive) + (mastodon-toot--clear-poll) + (transient-reset)) + +(transient-define-suffix mastodon-create-poll-done (args) + "Update current user profile fields." + :transient 'transient--do-exit + (interactive (list (transient-args 'mastodon-create-poll))) + ;; FIXME: if + ;; - no options filled in + ;; - no expiry + ;; then offer to cancel or warn / return to transient + (setq tp-transient-settings + (tp-bools-to-strs args)) + (mastodon-toot--update-status-fields)) + +;;; CLASSES + +(defclass mastodon-transient-field (tp-option-str) + ((always-read :initarg :always-read :initform t)) + "An infix option class for our options. +We always read.") + +(defclass mastodon-transient-opt (tp-option tp-option-var) + (())) + +(defclass mastodon-transient-poll-bool (tp-bool tp-option-var) + ()) + +(defclass mastodon-transient-poll-choice (tp-option-str tp-option-var) + ()) + +(defclass mastodon-transient-expiry (tp-option tp-option-var) + ()) + +(cl-defmethod transient-init-value ((obj mastodon-transient-field)) + "Initialize value of OBJ." + (let* ((prefix-val (oref transient--prefix value))) + ;; (arg (oref obj alist-key))) + (oset obj value + (tp-get-server-val obj prefix-val)))) + +(cl-defmethod tp-get-server-val ((obj mastodon-transient-field) data) + "Return the server value for OBJ from DATA. +If OBJ's key has dotted notation, drill down into the alist. Currently +only one level of nesting is supported." + ;; TODO: handle nested alist keys + (let* ((key (oref obj alist-key)) + (split (split-string (symbol-name key) "\\.")) + (num (string-to-number (cadr split)))) + (alist-get key + (nth (1- num) data) nil nil #'string=))) + +(cl-defmethod tp-arg-changed-p ((_obj mastodon-transient-field) cons) + "T if value of OBJ is changed from the server value. +CONS is a cons of the form \"(fields.1.name . val)\"." + (let* ((key-split (split-string + (symbol-name (car cons)) "\\.")) + (num (1- (string-to-number (nth 1 key-split)))) + (server-key (symbol-name (car cons))) + (server-elt (nth num tp-transient-settings))) + (not (equal (cdr cons) + (alist-get server-key server-elt nil nil #'string=))))) + +(cl-defmethod transient-infix-read ((_obj mastodon-transient-expiry)) + "Reader function for OBJ, a poll expiry." + (cdr (mastodon-toot--read-poll-expiry))) + +(cl-defmethod transient-format-value ((obj mastodon-transient-expiry)) + "Format function for OBJ, a poll expiry." + (let* ((cons (transient-infix-value obj)) + (value (when cons (cdr cons)))) + (if (not value) + "" + (let ((readable + (or (car + (rassoc value + (mastodon-toot--poll-expiry-options-alist))) + (concat value " secs")))) ;; editing a poll wont match expiry list + (propertize readable + 'face (if (tp-arg-changed-p obj cons) + 'transient-value + 'transient-inactive-value)))))) + +(provide 'mastodon-transient) +;;; mastodon-transient.el ends here diff --git a/lisp/mastodon-views.el b/lisp/mastodon-views.el index ac62b1f..8d356fb 100644 --- a/lisp/mastodon-views.el +++ b/lisp/mastodon-views.el @@ -1,8 +1,8 @@ ;;; mastodon-views.el --- Minor views functions for mastodon.el -*- lexical-binding: t -*- -;; Copyright (C) 2020-2022 Marty Hiatt -;; Author: Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> +;; Copyright (C) 2020-2024 Marty Hiatt +;; Author: Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon.el b/lisp/mastodon.el index faeae61..8560902 100644 --- a/lisp/mastodon.el +++ b/lisp/mastodon.el @@ -4,10 +4,10 @@ ;; Copyright (C) 2020-2022 Marty Hiatt ;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org> ;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> -;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> -;; Version: 1.0.27 -;; Package-Requires: ((emacs "27.1") (request "0.3.0") (persist "0.4")) +;; Marty Hiatt <mousebot@disroot.org> +;; Maintainer: Marty Hiatt <mousebot@disroot.org> +;; Version: 1.1.1 +;; Package-Requires: ((emacs "28.1") (request "0.3.0") (persist "0.4") (tp "0.1")) ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -38,13 +38,15 @@ ;;; Code: (require 'cl-lib) ; for `cl-some' call in mastodon (eval-when-compile (require 'subr-x)) -(require 'mastodon-http) -(require 'mastodon-toot) -(require 'mastodon-search) (require 'url) (require 'thingatpt) (require 'shr) +(require 'mastodon-http) +(require 'mastodon-toot) +(require 'mastodon-search) +(require 'mastodon-transient) + (declare-function discover-add-context-menu "discover") (declare-function emojify-mode "emojify") (declare-function request "request") @@ -148,7 +150,8 @@ Use. e.g. \"%c\" for your locale's date and time format." "Whether to use emojify.el to display emojis. From version 28, Emacs can display emojis natively. But currently, it doesn't seem to have a way to handle custom emoji, -while emojify,el has this feature and mastodon.el implements it.") +while emojify,el has this feature and mastodon.el implements it." + :type 'boolean) (defun mastodon-kill-window () "Quit window and delete helper." @@ -227,6 +230,7 @@ while emojify,el has this feature and mastodon.el implements it.") (define-key map (kbd "U") #'mastodon-profile--update-user-profile-note) (define-key map (kbd "V") #'mastodon-profile--view-favourites) (define-key map (kbd "K") #'mastodon-profile--view-bookmarks) + (define-key map (kbd ":") #'mastodon-user-settings) ;; minor views (define-key map (kbd "R") #'mastodon-views--view-follow-requests) (define-key map (kbd "S") #'mastodon-views--view-scheduled-toots) @@ -333,6 +337,15 @@ FORCE means to fetch from the server in any case and update ;; else just return the var: mastodon-profile-credential-account)) +(defvar mastodon-instance-data nil + "Instance data from the instance endpoint.") + +(defun mastodon-instance-data () + "Return `mastodon-instnace-data' or else fetch from instance endpoint." + (or mastodon-instance-data + (setq mastodon-instance-data + (mastodon-http--get-json (mastodon-http--api "instance"))))) + ;;;###autoload (defun mastodon-toot (&optional user reply-to-id reply-json) "Update instance with new toot. Content is captured in a new buffer. @@ -343,28 +356,25 @@ If REPLY-JSON is the json of the toot being replied to." (mastodon-toot--compose-buffer user reply-to-id reply-json)) ;;;###autoload -(defun mastodon-notifications-get (&optional type buffer-name force max-id) +(defun mastodon-notifications-get (&optional type buffer-name max-id) "Display NOTIFICATIONS in buffer. Optionally only print notifications of type TYPE, a string. BUFFER-NAME is added to \"*mastodon-\" to create the buffer name. -FORCE means do not try to update an existing buffer, but fetch -from the server and load anew." +MAX-ID is a request parameter for pagination." (interactive) (let* ((buffer-name (or buffer-name "notifications")) (buffer (concat "*mastodon-" buffer-name "*"))) - (if (and (not force) (get-buffer buffer)) - (progn (pop-to-buffer buffer '(display-buffer-same-window)) - (mastodon-tl--update)) - (message "Loading your notifications...") - (mastodon-tl--init-sync - buffer-name - "notifications" - 'mastodon-notifications--timeline - type - (when max-id - `(("max_id" . ,(mastodon-tl--buffer-property 'max-id))))) - (with-current-buffer buffer - (use-local-map mastodon-notifications--map))))) + (message "Loading your notifications...") + (mastodon-tl--init-sync + buffer-name + "notifications" + 'mastodon-notifications--timeline + type + (when max-id + `(("max_id" . ,(mastodon-tl--buffer-property 'max-id)))) + nil nil nil "v2") + (with-current-buffer (get-buffer-create buffer) + (use-local-map mastodon-notifications--map)))) ;; URL lookup: should be available even if `mastodon.el' not loaded: @@ -374,7 +384,8 @@ from the server and load anew." Does a WebFinger lookup on the server. URL can be arg QUERY-URL, or URL at point, or provided by the user. If a status or account is found, load it in `mastodon.el', if -not, just browse the URL in the normal fashion." +not, just browse the URL in the normal fashion. +If FORCE, do a lookup regardless of the result of `mastodon--fedi-url-p'." (interactive) (let* ((query (or query-url (mastodon-tl--property 'shr-url :no-move) diff --git a/mastodon-index.org b/mastodon-index.org index 76151d3..0545e54 100644 --- a/mastodon-index.org +++ b/mastodon-index.org @@ -93,11 +93,15 @@ | K | mastodon-profile--view-bookmarks | Open a new buffer displaying the user's bookmarks. | | V | mastodon-profile--view-favourites | Open a new buffer displaying the user's favourites. | | | mastodon-profile--view-preferences | View user preferences in another window. | +| | mastodon-profile-fields | A transient for setting profile fields. | +| | mastodon-profile-fields-update | Update current user profile fields. | | | mastodon-profile-mode | Toggle mastodon profile minor mode. | | | mastodon-profile-update-mode | Minor mode to update user profile. | +| | mastodon-search--load-link-posts | Load timeline of posts containing link at point. | | s | mastodon-search--query | Prompt for a search QUERY and return accounts, statuses, and hashtags. | | | mastodon-search--query-accounts-followed | Run an accounts search QUERY, limited to your followers. | | | mastodon-search--query-cycle | Cycle through search types: accounts, hashtags, and statuses. | +| | mastodon-search--trending-links | Display a list of links trending on your instance. | | | mastodon-search--trending-statuses | Display a list of statuses trending on your instance. | | | mastodon-search--trending-tags | Display a list of tags trending on your instance. | | | mastodon-search-mode | Toggle mastodon search minor mode. | @@ -190,10 +194,12 @@ | C-c C-n | mastodon-toot--toggle-nsfw | Toggle `mastodon-toot--content-nsfw'. | | a | mastodon-toot--translate-toot-text | Translate text of toot at point. | | E | mastodon-toot--view-toot-edits | View editing history of the toot at point in a popup buffer. | -| | mastodon-turn-on-discover | Turns on discover support | | | mastodon-toot-mode | Minor mode for composing toots. | +| | mastodon-update-profile-note | Update current user profile note. | | | mastodon-url-lookup | If a URL resembles a fediverse link, try to load in `mastodon.el'. | | | mastodon-url-lookup-force | Call `mastodon-url-lookup' without checking if URL is fedi-like. | +| : | mastodon-user-settings | A transient for setting current user settings. | +| | mastodon-user-settings-update | Update current user settings on the server. | | | mastodon-views--add-account-to-list | Prompt for a list and for an account, add account to list. | | | mastodon-views--add-account-to-list-at-point | Prompt for account and add to list at point. | | | mastodon-views--add-filter-kw | Add a keyword to filter at point. | @@ -258,6 +264,7 @@ | mastodon-media--hide-sensitive-media | Whether media marked as sensitive should be hidden. | | mastodon-media--preview-max-height | Max height of any media attachment preview to be shown in timelines. | | mastodon-mode-hook | Hook run when entering Mastodon mode. | +| mastodon-notifications--images-in-notifs | Whether to display attached images in notifications. | | mastodon-notifications--profile-note-in-foll-reqs | If non-nil, show a user's profile note in follow request notifications. | | mastodon-notifications--profile-note-in-foll-reqs-max-length | The max character length for user profile note in follow requests. | | mastodon-profile-mode-hook | Hook run after entering or leaving `mastodon-profile-mode'. | diff --git a/mastodon.info b/mastodon.info index 86531dd..6d44f1d 100644 --- a/mastodon.info +++ b/mastodon.info @@ -43,6 +43,7 @@ Usage * Customization:: * Commands and variables index:: * Alternative timeline layout:: +* mastodon hydra:: * Live-updating timelines mastodon-async-mode:: * Translating toots:: * Bookmarks and ‘mastodon.el’: Bookmarks and mastodonel. @@ -143,7 +144,7 @@ Then, require it and go. (use-package mastodon :ensure t) - The minimum Emacs version is now 27.1. But if you are running an + The minimum Emacs version is now 28.1. But if you are running an older version it shouldn’t be very hard to get it working. @@ -198,6 +199,7 @@ File: mastodon.info, Node: Usage, Next: Dependencies, Prev: Installation, Up * Customization:: * Commands and variables index:: * Alternative timeline layout:: +* mastodon hydra:: * Live-updating timelines mastodon-async-mode:: * Translating toots:: * Bookmarks and ‘mastodon.el’: Bookmarks and mastodonel. @@ -523,7 +525,7 @@ bindings, or run ‘M-X’ (upper-case ‘X’) to view all commands in the buffer with completion, and call one. -File: mastodon.info, Node: Alternative timeline layout, Next: Live-updating timelines mastodon-async-mode, Prev: Commands and variables index, Up: Usage +File: mastodon.info, Node: Alternative timeline layout, Next: mastodon hydra, Prev: Commands and variables index, Up: Usage 1.2.7 Alternative timeline layout --------------------------------- @@ -535,9 +537,18 @@ layout for ‘mastodon.el’. (https://github.com/rougier/mastodon-alt). -File: mastodon.info, Node: Live-updating timelines mastodon-async-mode, Next: Translating toots, Prev: Alternative timeline layout, Up: Usage +File: mastodon.info, Node: mastodon hydra, Next: Live-updating timelines mastodon-async-mode, Prev: Alternative timeline layout, Up: Usage -1.2.8 Live-updating timelines: ‘mastodon-async-mode’ +1.2.8 mastodon hydra +-------------------- + +A user made a hydra for handling basic mastodon.el commands. It’s +available at <https://holgerschurig.github.io/en/emacs-mastodon-hydra/>. + + +File: mastodon.info, Node: Live-updating timelines mastodon-async-mode, Next: Translating toots, Prev: mastodon hydra, Up: Usage + +1.2.9 Live-updating timelines: ‘mastodon-async-mode’ ---------------------------------------------------- (code taken from mastodon-future @@ -556,8 +567,8 @@ Then you can view a timeline with one of the commands that begin with File: mastodon.info, Node: Translating toots, Next: Bookmarks and mastodonel, Prev: Live-updating timelines mastodon-async-mode, Up: Usage -1.2.9 Translating toots ------------------------ +1.2.10 Translating toots +------------------------ You can translate toots with ‘mastodon-toot--translate-toot-text’ (‘a’ in a timeline). At the moment this requires lingva.el @@ -583,7 +594,7 @@ looks like: File: mastodon.info, Node: Bookmarks and mastodonel, Prev: Translating toots, Up: Usage -1.2.10 Bookmarks and ‘mastodon.el’ +1.2.11 Bookmarks and ‘mastodon.el’ ---------------------------------- ‘mastodon.el’ implements a basic bookmark record and handler. @@ -707,8 +718,8 @@ File: mastodon.info, Node: Supporting mastodonel, Next: Contributors, Prev: C If you’d like to support continued development of ‘mastodon.el’, I accept donations via paypal: paypal.me/martianh (https://paypal.me/martianh). If you would prefer a different payment -method, please write to me at <martianhiatus [at] riseup [dot] net> and -I can provide IBAN or other bank account details. +method, please write to me at <mousebot@disroot.org> and I can provide +IBAN or other bank account details. I don’t have a tech worker’s income, so even a small tip would help out. @@ -745,42 +756,55 @@ Here’s a (federated) timeline: + Here’s a user settings transient (active values green, current server +values commented and, if a boolean, underlined): + + + + + Here’s a user profile fields transient (changed fields green, current +server values commented): + + + + Tag Table: Node: Top219 -Node: README987 -Node: Installation1637 -Node: ELPA1926 -Node: MELPA2154 -Node: Repo2534 -Node: Emoji3027 -Node: Discover3621 -Node: Usage4173 -Node: Logging in to your instance4616 -Node: Timelines5613 -Ref: Keybindings6088 -Ref: Toot byline legend10928 -Node: Composing toots11237 -Ref: Keybindings (1)12789 -Ref: Autocompletion of mentions tags and emoji13324 -Ref: Draft toots14249 -Node: Other commands and account settings14720 -Node: Customization18084 -Node: Commands and variables index18962 -Node: Alternative timeline layout19478 -Node: Live-updating timelines mastodon-async-mode19883 -Node: Translating toots20735 -Node: Bookmarks and mastodonel21917 -Node: Dependencies22459 -Node: Network compatibility23093 -Node: Contributing23975 -Node: Bug reports24471 -Node: Fixes and features25382 -Node: Coding style25883 -Node: Supporting mastodonel26507 -Node: Contributors27074 -Node: screenshots27509 +Node: README1006 +Node: Installation1656 +Node: ELPA1945 +Node: MELPA2173 +Node: Repo2553 +Node: Emoji3046 +Node: Discover3640 +Node: Usage4192 +Node: Logging in to your instance4654 +Node: Timelines5651 +Ref: Keybindings6126 +Ref: Toot byline legend10966 +Node: Composing toots11275 +Ref: Keybindings (1)12827 +Ref: Autocompletion of mentions tags and emoji13362 +Ref: Draft toots14287 +Node: Other commands and account settings14758 +Node: Customization18122 +Node: Commands and variables index19000 +Node: Alternative timeline layout19516 +Node: mastodon hydra19892 +Node: Live-updating timelines mastodon-async-mode20224 +Node: Translating toots21063 +Node: Bookmarks and mastodonel22247 +Node: Dependencies22789 +Node: Network compatibility23423 +Node: Contributing24305 +Node: Bug reports24801 +Node: Fixes and features25712 +Node: Coding style26213 +Node: Supporting mastodonel26837 +Node: Contributors27389 +Node: screenshots27824 End Tag Table diff --git a/mastodon.texi b/mastodon.texi index e171408..761ac8f 100644 --- a/mastodon.texi +++ b/mastodon.texi @@ -57,6 +57,7 @@ Usage * Customization:: * Commands and variables index:: * Alternative timeline layout:: +* mastodon hydra:: * Live-updating timelines @samp{mastodon-async-mode}:: * Translating toots:: * Bookmarks and @samp{mastodon.el}: Bookmarks and @samp{mastodonel}. @@ -146,7 +147,7 @@ Or, with @samp{use-package}: :ensure t) @end lisp -The minimum Emacs version is now 27.1. But if you are running an older version +The minimum Emacs version is now 28.1. But if you are running an older version it shouldn't be very hard to get it working. @node Emoji @@ -187,6 +188,7 @@ Or, with @samp{use-package}: * Customization:: * Commands and variables index:: * Alternative timeline layout:: +* mastodon hydra:: * Live-updating timelines @samp{mastodon-async-mode}:: * Translating toots:: * Bookmarks and @samp{mastodon.el}: Bookmarks and @samp{mastodonel}. @@ -660,6 +662,12 @@ for @samp{mastodon.el}. The repo is at @uref{https://github.com/rougier/mastodon-alt, mastodon-alt}. +@node mastodon hydra +@subsection mastodon hydra + +A user made a hydra for handling basic mastodon.el commands. It's available at +@uref{https://holgerschurig.github.io/en/emacs-mastodon-hydra/}. + @node Live-updating timelines @samp{mastodon-async-mode} @subsection Live-updating timelines: @samp{mastodon-async-mode} @@ -810,7 +818,7 @@ There's no need for a blank line after the first docstring line (one is added au If you'd like to support continued development of @samp{mastodon.el}, I accept donations via paypal: @uref{https://paypal.me/martianh, paypal.me/martianh}. If you would prefer a different -payment method, please write to me at <martianhiatus [at] riseup [dot] net> and I can +payment method, please write to me at <mousebot@@disroot.org> and I can provide IBAN or other bank account details. I don't have a tech worker's income, so even a small tip would help out. @@ -846,4 +854,12 @@ Here's a notifcations view plus a compose buffer: @image{screenshot-notifs+compose,,,,png} +Here's a user settings transient (active values green, current server values commented and, if a boolean, underlined): + +@image{screenshot-transient-1,,,,jpg} + +Here's a user profile fields transient (changed fields green, current server values commented): + +@image{screenshot-transient-2,,,,jpg} + @bye
\ No newline at end of file diff --git a/screenshot-transient-1.jpg b/screenshot-transient-1.jpg Binary files differnew file mode 100644 index 0000000..268c2da --- /dev/null +++ b/screenshot-transient-1.jpg diff --git a/screenshot-transient-2.jpg b/screenshot-transient-2.jpg Binary files differnew file mode 100644 index 0000000..ff8b32e --- /dev/null +++ b/screenshot-transient-2.jpg |