diff options
-rw-r--r-- | README.org | 107 | ||||
-rw-r--r-- | lisp/mastodon-async.el | 7 | ||||
-rw-r--r-- | lisp/mastodon-auth.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-client.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-discover.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-http.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-inspect.el | 2 | ||||
-rw-r--r-- | lisp/mastodon-media.el | 32 | ||||
-rw-r--r-- | lisp/mastodon-notifications.el | 48 | ||||
-rw-r--r-- | lisp/mastodon-profile.el | 136 | ||||
-rw-r--r-- | lisp/mastodon-search.el | 70 | ||||
-rw-r--r-- | lisp/mastodon-tl.el | 588 | ||||
-rw-r--r-- | lisp/mastodon-toot.el | 41 | ||||
-rw-r--r-- | lisp/mastodon.el | 24 | ||||
-rw-r--r-- | test/ert-helper.el | 1 | ||||
-rw-r--r-- | test/mastodon-media-tests.el | 2 | ||||
-rw-r--r-- | test/mastodon-profile-tests.el | 288 | ||||
-rw-r--r-- | test/mastodon-search-tests.el | 5 | ||||
-rw-r--r-- | test/mastodon-tl-tests.el | 2 |
19 files changed, 1087 insertions, 274 deletions
@@ -6,48 +6,50 @@ This is a fork of of the great but seemingly dormant https://github.com/jdenen/m It adds the following features: -| Profiles: | | -| | display profile metadata fields | -| | display pinned toots first | -| | display relationship (follows you/followed by you) | -| | display toots/follows/followers counts | -| | links/tags/mentions in profile bios are active links | -| | show a lock icon for locked accounts | -| =R=, =C-c a=, =C-c r= | view/accept/reject follow requests | -| =V= | view your favorited toots | -| =i= | toggle pinning of toots | -| =S-C-P= | jump to your profile | -| =U= | update your profile bio note | -| =O= | jump to own profile | -| Notifications: | | -| | follow requests now also appear in notifications | -| =a=, =r= | accept/reject follow request | -| | notifications for when a user posts (=mastodon-tl--enable-notify-user-posts=) | -| Timelines: | | -| =C= | copy url of toot at point | -| =d= | delete your toot at point, and reload current timeline | -| =D= | delete and redraft toot at point, preserving reply/CW/visibility | -| =W=, =M=, =B= | (un)follow, (un)mute, (un)block author of toot at point | -| =k=, =K= | toggle bookmark of toot at point, view bookmarked toots | -| | display polls and vote on them | -| | images are links to the full image, can be zoomed/rotated/saved (see image keymap) | -| | images scale properly | -| | toot visibility (direct, followers only) icon appears in toot bylines | -| | display toot's number of favorites, boosts and replies | -| | customize option to cache images | -| Toots: | | -| | mention booster in replies by default | -| | replies preserve visibility status/CW of original toot | -| | autocompletion of user mentions, via =company-mode= (must be installed to work) | -| =C-c C-a= | media uploads, asynchronous | -| | media upload previews displayed in toot compose buffer | -| =C-c C-n= | and sensitive media/nsfw flag | -| =C-c C-e= | add emoji (if =emojify= installed) | -| | download and use your instance's custom emoji | -| | server's maximum toot length shown in toot compose buffer | -| Search: | | -| =S= | search (posts, users, tags) (NB: only posts you have interacted with are searched) | -| | | +| Profiles: | | +| | display profile metadata fields | +| | display pinned toots first | +| | display relationship (follows you/followed by you) | +| | display toots/follows/followers counts | +| | links/tags/mentions in profile bios are active links | +| | show a lock icon for locked accounts | +| =G= | view follow suggestions | +| =R=, =a=, =r= | view/accept/reject follow requests | +| =V= | view your favorited toots | +| =i= | toggle pinning of toots | +| =U= | update your profile bio note | +| =O= | jump to own profile | +| Notifications: | | +| | follow requests now also appear in notifications | +| =a=, =j= | accept/reject follow request | +| | notifications for when a user posts (=mastodon-tl--enable-notify-user-posts=) | +| Timelines: | | +| =C= | copy url of toot at point | +| =d= | delete your toot at point, and reload current timeline | +| =D= | delete and redraft toot at point, preserving reply/CW/visibility | +| =W=, =M=, =B= | (un)follow, (un)mute, (un)block author of toot at point | +| =k=, =K= | toggle bookmark of toot at point, view bookmarked toots | +| =I=, =c=, =d= | view, create, and delete filters | +| | display polls and vote on them | +| | images are links to the full image, can be zoomed/rotated/saved (see image keymap) | +| | images scale properly | +| | toot visibility (direct, followers only) icon appears in toot bylines | +| | display toot's number of favorites, boosts and replies | +| | play gifs and videos (requires =mpv= to be installed) | +| | customize option to cache images | +| Toots: | | +| | mention booster in replies by default | +| | replies preserve visibility status/CW of original toot | +| | autocompletion of user mentions, via =company-mode= (must be installed to work) | +| =C-c C-a= | media uploads, asynchronous | +| | media upload previews displayed in toot compose buffer | +| =C-c C-n= | and sensitive media/nsfw flag | +| =C-c C-e= | add emoji (if =emojify= installed) | +| | download and use your instance's custom emoji | +| | server's maximum toot length shown in toot compose buffer | +| Search: | | +| =S= | search (posts, users, tags) (NB: only posts you have interacted with are searched) | +| | | It also makes some small cosmetic changes to make timelines easier to read, and makes some functions asynchronous, based on https://github.com/ieure/mastodon.el. @@ -65,9 +67,26 @@ Works for federated, local, and home timelines and for notifications. It's prett To enable, it, add =(require 'mastodon-async)= to your =init.el=. Then you can view a timeline with one of the commands that begin with =mastodon-async--stream-=. +** translating toots + +You can translate toots with =mastodon-toot--translate-toot-text=. At the moment this requires [[https://codeberg.org/martianh/lingva.el][lingva.el]], a little interface I wrote to https://lingva.ml, to be installed to work. + +You could easily modify the simple function to use your emacs translator of choice (=google-translate=, =babel=, =go-translate=, etc.), you just need to fetch the toot's content with =(mastodon-tl--content toot)= and pass it to your translator function as its text argument. Here's what =mastodon-toot--translate-toot-text= looks like: + +#+begin_src emacs-lisp + (defun mastodon-toot--translate-toot-text () + "Translate text of toot at point. + Uses `lingva.el'." + (interactive) + (let* ((toot (mastodon-tl--property 'toot-json))) + (if toot + (lingva-translate nil (mastodon-tl--content toot)) + (message "No toot to translate?")))) +#+end_src + ** NB: dependency -This version depends on the library =request= (for uploading attachments). You can install it from MELPA, or https://github.com/tkf/emacs-request. +This version depends on the library =request= (for uploading attachments). If you installed =mastodon.el= from MELPA, it should be fine, else you can install it yourself from MELPA or https://github.com/tkf/emacs-request. ** NB: bugs @@ -77,7 +96,7 @@ This repo also incorporates fixes for two bugs that were never merged into the u ** contributing -Contributions are welcome. Registration is disabled by default on the gitea instance, but if you are interested, get in touch with me on mastodon: +Contributions are welcome. Feel free to open an issue or get in touch with me on mastodon: [[https://todon.nl/@mousebot][@mousebot@todon.nl]] diff --git a/lisp/mastodon-async.el b/lisp/mastodon-async.el index 6ff09e3..86547a1 100644 --- a/lisp/mastodon-async.el +++ b/lisp/mastodon-async.el @@ -1,12 +1,11 @@ ;;; mastodon-async.el --- Client for Mastodon -*- lexical-binding: t -*- -;; Copyright (C) 2017 Johnson Denen -;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Alex J. Griffith <griffitaj@gmail.com> +;; Copyright (C) 2017 Alex J. Griffith +;; Author: Alex J. Griffith <griffitaj@gmail.com> ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-auth.el b/lisp/mastodon-auth.el index f50f5ed..582f7bb 100644 --- a/lisp/mastodon-auth.el +++ b/lisp/mastodon-auth.el @@ -6,7 +6,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; 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 80338b1..4fc8db7 100644 --- a/lisp/mastodon-client.el +++ b/lisp/mastodon-client.el @@ -6,7 +6,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; 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 10abc59..7046070 100644 --- a/lisp/mastodon-discover.el +++ b/lisp/mastodon-discover.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; 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 05c9d2e..35fd070 100644 --- a/lisp/mastodon-http.el +++ b/lisp/mastodon-http.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1") (request "0.3.0")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. diff --git a/lisp/mastodon-inspect.el b/lisp/mastodon-inspect.el index b0270ee..15ee7ce 100644 --- a/lisp/mastodon-inspect.el +++ b/lisp/mastodon-inspect.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; 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 acce473..383e062 100644 --- a/lisp/mastodon-media.el +++ b/lisp/mastodon-media.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -272,6 +272,20 @@ Replace them with the referenced image." (put-text-property start end 'media-state 'loading) (mastodon-media--load-image-from-url image-url media-type start (- end start)))))))) + ;; (mastodon-media--moving-image-overlay start end))))))) + +;; (defun mastodon-media--moving-image-overlay (start end) +;; "Add play symbol overlay to moving image media items." +;; (let ((ov (make-overlay start end)) +;; (type (get-text-property start 'mastodon-media-type))) +;; (when (or (equal type "gifv") +;; (equal type "video")) +;; (overlay-put +;; ov +;; 'after-string +;; (propertize " " +;; 'face +;; '((:height 1.5 :inherit 'font-lock-comment-face))))))) (defun mastodon-media--get-avatar-rendering (avatar-url) "Return the string to be written that renders the avatar at AVATAR-URL." @@ -293,12 +307,16 @@ Replace them with the referenced image." t image-options)) " "))) -(defun mastodon-media--get-media-link-rendering (media-url &optional full-remote-url type) +(defun mastodon-media--get-media-link-rendering (media-url &optional full-remote-url type caption) "Return the string to be written that renders the image at MEDIA-URL. FULL-REMOTE-URL is used for `shr-browse-image'. TYPE is the attachment's type field on the server." - (let ((help-echo - "RET/i: load full image (prefix: copy URL), +/-: zoom, r: rotate, o: save preview")) + (let* ((help-echo-base "RET/i: load full image (prefix: copy URL), +/-: zoom, r: rotate, o: save preview") + (help-echo (if caption + (concat help-echo-base + "\n\"" + caption "\"") + help-echo-base))) (concat (propertize "[img]" 'media-url media-url @@ -310,9 +328,11 @@ TYPE is the attachment's type field on the server." 'mastodon-tab-stop 'image ; for do-link-action-at-point 'image-url full-remote-url ; for shr-browse-image 'keymap mastodon-tl--shr-image-map-replacement - 'help-echo (if (string= type "image") + 'help-echo (if (or (string= type "image") + (string= type nil) + (string= type "unknown")) ;handle borked images help-echo - (concat help-echo "\ntype: " type))) + (concat help-echo "\nC-RET: play " type " with mpv"))) " "))) (provide 'mastodon-media) diff --git a/lisp/mastodon-notifications.el b/lisp/mastodon-notifications.el index 1361099..6b253ec 100644 --- a/lisp/mastodon-notifications.el +++ b/lisp/mastodon-notifications.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -47,6 +47,8 @@ (autoload 'mastodon-tl--property "mastodon-tl.el") (autoload 'mastodon-tl--spoiler "mastodon-tl.el") (autoload 'mastodon-tl--toot-id "mastodon-tl.el") +(autoload 'mastodon-http--get-params-async-json "mastodon-http.el") +(defvar mastodon-tl--buffer-spec) (defvar mastodon-tl--display-media-p) (defvar mastodon-tl--buffer-spec) @@ -84,7 +86,8 @@ With no argument, the request is accepted. Argument REJECT means reject the request. Can be called in notifications view or in follow-requests view." (interactive) - (when (mastodon-tl--find-property-range 'toot-json (point)) + (if (not (mastodon-tl--find-property-range 'toot-json (point))) + (message "No follow request at point?") (let* ((toot-json (mastodon-tl--property 'toot-json)) (f-reqs-view-p (string= "follow_requests" (plist-get mastodon-tl--buffer-spec 'endpoint))) @@ -107,7 +110,8 @@ follow-requests view." nil nil))) (mastodon-http--triage response (lambda () - (unless f-reqs-view-p + (if f-reqs-view-p + (mastodon-profile--view-follow-requests) (mastodon-notifications--get)) (message "Follow request of %s (@%s) %s!" name handle (if reject @@ -142,7 +146,7 @@ Can be called in notifications view or in follow-requests view." (defun mastodon-notifications--favourite (note) "Format for a `favourite' NOTE." - (mastodon-notifications--format-note note 'favorite)) + (mastodon-notifications--format-note note 'favourite)) (defun mastodon-notifications--reblog (note) "Format for a `boost' NOTE." @@ -164,12 +168,18 @@ Status notifications are given when (status (mastodon-tl--field 'status note)) (follower (alist-get 'username (alist-get 'account note)))) (mastodon-notifications--insert-status - (if (or (equal type 'follow) - (equal type 'follow-request)) - ;; Using reblog with an empty id will mark this as something - ;; non-boostable/non-favable. - (cons '(reblog (id . nil)) note) - status) + (cond ((or (equal type 'follow) + (equal 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 (equal type 'favourite) + (equal type 'boost)) + note) + (t + status)) (if (or (equal type 'follow) (equal type 'follow-request)) (propertize (if (equal type 'follow) @@ -192,7 +202,7 @@ Status notifications are given when (mastodon-notifications--byline-concat (cond ((equal type 'boost) "Boosted") - ((equal type 'favorite) + ((equal type 'favourite) "Favourited") ((equal type 'follow-request) "Requested to follow") @@ -204,9 +214,12 @@ Status notifications are given when "Posted") ((equal type 'poll) "Posted a poll")))) - id))) + id + (when (or (equal type 'favourite) + (equal type 'boost)) + status)))) -(defun mastodon-notifications--insert-status (toot body author-byline action-byline id) +(defun mastodon-notifications--insert-status (toot body author-byline action-byline id &optional parent-toot) "Display the content and byline of timeline element TOOT. BODY will form the section of the toot above the byline. @@ -221,7 +234,8 @@ takes a single function. By default it is `mastodon-tl--byline-boosted'. ID is the notification's own id, which is attached as a property." - (mastodon-tl--insert-status toot body author-byline action-byline id)) + (when toot ; handle rare blank notif server bug + (mastodon-tl--insert-status toot body author-byline action-byline id parent-toot))) (defun mastodon-notifications--by-type (note) "Filters NOTE for those listed in `mastodon-notifications--types-alist'." @@ -235,8 +249,10 @@ ID is the notification's own id, which is attached as a property." (defun mastodon-notifications--timeline (json) "Format JSON in Emacs buffer." - (mapc #'mastodon-notifications--by-type json) - (goto-char (point-min))) + (if (equal json '[]) + (message "Looks like you have no (more) notifications for the moment.") + (mapc #'mastodon-notifications--by-type json) + (goto-char (point-min)))) (defun mastodon-notifications--get () "Display NOTIFICATIONS in buffer." diff --git a/lisp/mastodon-profile.el b/lisp/mastodon-profile.el index 21b40b3..6065bdd 100644 --- a/lisp/mastodon-profile.el +++ b/lisp/mastodon-profile.el @@ -5,7 +5,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1") (seq "1.0")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -55,12 +55,21 @@ (autoload 'mastodon-tl--toot-id "mastodon-tl") (autoload 'mastodon-tl--toot "mastodon-tl") (autoload 'mastodon-tl--init "mastodon-tl.el") +(autoload 'mastodon-tl--init-sync "mastodon-tl") (autoload 'mastodon-http--patch "mastodon-http") (autoload 'mastodon-http--patch-json "mastodon-http") +(autoload 'mastodon-notifications--follow-request-reject "mastodon-notifications") +(autoload 'mastodon-notifications--follow-request-accept "mastodon-notifications") +(autoload 'mastodon-tl--goto-next-item "mastodon-tl") +(autoload 'mastodon-tl--goto-prev-item "mastodon-tl") +(autoload 'mastodon-tl--goto-first-item "mastodon-tl") +(autoload 'mastodon-toot "mastodon") +(autoload 'mastodon-search--insert-users-propertized "mastodon-search") (defvar mastodon-instance-url) (defvar mastodon-tl--buffer-spec) (defvar mastodon-tl--update-point) +(defvar mastodon-mode-map) (defvar-local mastodon-profile--account nil "The data for the account being described in the current profile buffer.") @@ -72,6 +81,20 @@ map) "Keymap for `mastodon-profile-mode'.") +(defvar mastodon-profile--view-follow-requests-keymap + (let ((map ;(make-sparse-keymap))) + (copy-keymap mastodon-mode-map))) + (define-key map (kbd "r") #'mastodon-notifications--follow-request-reject) + (define-key map (kbd "a") #'mastodon-notifications--follow-request-accept) + (define-key map (kbd "n") #'mastodon-tl--goto-next-item) + (define-key map (kbd "p") #'mastodon-tl--goto-prev-item) + (define-key map (kbd "g") 'mastodon-profile--view-follow-requests) + ;; (define-key map (kbd "t") #'mastodon-toot) + ;; (define-key map (kbd "q") #'kill-current-buffer) + ;; (define-key map (kbd "Q") #'kill-buffer-and-window) + map) + "Keymap for viewing follow requests.") + (define-minor-mode mastodon-profile-mode "Toggle mastodon profile minor mode. @@ -146,10 +169,31 @@ extra keybindings." (defun mastodon-profile--view-follow-requests () "Open a new buffer displaying the user's follow requests." (interactive) - (mastodon-profile-mode) - (mastodon-tl--init "follow-requests" - "follow_requests" - 'mastodon-profile--add-author-bylines)) + (mastodon-tl--init-sync "follow-requests" + "follow_requests" + 'mastodon-profile--insert-follow-requests) + (use-local-map mastodon-profile--view-follow-requests-keymap) + (mastodon-tl--goto-first-item)) + +(defun mastodon-profile--insert-follow-requests (json) + "Insert the user's current follow requests. +JSON is the data returned by the server." + (insert (mastodon-tl--set-face + (concat "\n ------------\n" + " FOLLOW REQUESTS\n" + " ------------\n\n") + 'success) + (mastodon-tl--set-face + "[a/r - accept/reject request at point\n n/p - go to next/prev request]\n\n" + 'font-lock-comment-face)) + (if (equal json '[]) + (insert (propertize + "Looks like you have no follow requests for now." + 'face font-lock-comment-face + 'byline t + 'toot-id "0")) + (mastodon-search--insert-users-propertized json :note))) + ;; (mastodon-profile--add-author-bylines json))) (defun mastodon-profile--update-user-profile-note () "Fetch user's profile note and display for editing." @@ -163,6 +207,10 @@ extra keybindings." (buffer (get-buffer-create "*mastodon-update-profile*")) (inhibit-read-only t)) (switch-to-buffer-other-window buffer) + (setq-local header-line-format + (propertize + "Edit your profile note. C-c C-c to send, C-c C-k to cancel." + 'face font-lock-comment-face)) (mastodon-profile-update-mode t) (insert note) (goto-char (point-min)) @@ -336,7 +384,6 @@ Returns a list of lists." (mastodon-profile--insert-statuses-pinned pinned) (setq mastodon-tl--update-point (point))) ;updates to follow pinned toots (funcall update-function json))) - ;;(mastodon-tl--goto-next-toot) (goto-char (point-min)))) (defun mastodon-profile--get-toot-author () @@ -357,19 +404,24 @@ If toot is a boost, opens the profile of the booster." "Query for USER-HANDLE from current status and show that user's profile." (interactive (list - (let ((user-handles (mastodon-profile--extract-users-handles - (mastodon-profile--toot-json)))) - (completing-read "View profile of user [choose or enter any handle]: " - user-handles - nil ; predicate - 'confirm)))) - (let ((account (mastodon-profile--lookup-account-in-status - user-handle (mastodon-profile--toot-json)))) - (if account - (progn - (message "Loading profile of user %s..." user-handle) - (mastodon-profile--make-author-buffer account)) - (message "Cannot find a user with handle %S" user-handle)))) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (let ((user-handles (mastodon-profile--extract-users-handles + (mastodon-profile--toot-json)))) + (completing-read "View profile of user [choose or enter any handle]: " + user-handles + nil ; predicate + 'confirm))))) + (if (not (get-text-property (point) 'toot-json)) + (message "Looks like there's no toot or user at point?") + (let ((account (mastodon-profile--lookup-account-in-status + user-handle (mastodon-profile--toot-json)))) + (if account + (progn + (message "Loading profile of user %s..." user-handle) + (mastodon-profile--make-author-buffer account)) + (message "Cannot find a user with handle %S" user-handle))))) (defun mastodon-profile--my-profile () "Show the profile of the currently signed in user." @@ -384,22 +436,28 @@ FIELD is used to identify regions under 'account" (cdr (assoc field account))) (defun mastodon-profile--add-author-bylines (tootv) - "Convert TOOTV into a author-bylines and insert." + "Convert TOOTV into a author-bylines and insert. +Also insert their profile note. +Used to view a user's followers and those they're following." + ;;FIXME change the name of this fun now that we've edited what it does! (let ((inhibit-read-only t)) - (mapc (lambda (toot) - (let ((start-pos (point))) - (insert "\n" - (propertize - (mastodon-tl--byline-author `((account . ,toot))) - 'byline 't - 'toot-id (alist-get 'id toot) - 'base-toot-id (mastodon-tl--toot-id toot) - 'toot-json toot)) - (mastodon-media--inline-images start-pos (point)) - (insert "\n" - (mastodon-tl--render-text (alist-get 'note toot) nil) - "\n"))) - tootv))) + (when (not (equal tootv '[])) + (mapc (lambda (toot) + (let ((start-pos (point))) + (insert "\n" + (propertize + (mastodon-tl--byline-author `((account . ,toot))) + 'byline 't + 'toot-id (alist-get 'id toot) + 'base-toot-id (mastodon-tl--toot-id toot) + 'toot-json toot)) + (mastodon-media--inline-images start-pos (point)) + (insert "\n" + (propertize + (mastodon-tl--render-text (alist-get 'note toot) nil) + 'toot-json toot) ' + "\n"))) + tootv)))) (defun mastodon-profile--search-account-by-handle (handle) "Return an account based on a user's HANDLE. @@ -427,9 +485,13 @@ If the handle does not match a search return then retun NIL." These include the author, author of reblogged entries and any user mentioned." (when status - (let ((this-account (alist-get 'account status)) - (mentions (alist-get 'mentions status)) - (reblog (alist-get 'reblog status))) + (let ((this-account + (or (alist-get 'account status) ; status is a toot + status)) ; status is a user listing + (mentions (or (alist-get 'mentions (alist-get 'status status)) + (alist-get 'mentions status))) + (reblog (or (alist-get 'reblog (alist-get 'status status)) + (alist-get 'reblog status)))) (seq-filter 'stringp (seq-uniq diff --git a/lisp/mastodon-search.el b/lisp/mastodon-search.el index 78c2ab4..cbb1fba 100644 --- a/lisp/mastodon-search.el +++ b/lisp/mastodon-search.el @@ -1,12 +1,11 @@ ;;; mastodon-search.el --- Search functions for mastodon.el -*- lexical-binding: t -*- -;; Copyright (C) 2017-2019 Johnson Denen -;; Author: Johnson Denen <johnson.denen@gmail.com> -;; Marty Hiatt <martianhiatus@riseup.net> +;; Copyright (C) 2017-2019 Marty Hiatt +;; Author: Marty Hiatt <martianhiatus@riseup.net> ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -40,6 +39,7 @@ (autoload 'mastodon-tl--as-string "mastodon-tl") (autoload 'mastodon-auth--access-token "mastodon-auth") (autoload 'mastodon-http--get-search-json "mastodon-http") +(autoload 'mastodon-http--api "mastodon-http") (defvar mastodon-instance-url) (defvar mastodon-tl--link-keymap) @@ -58,7 +58,7 @@ "Prompt for a search QUERY and return accounts synchronously. Returns a nested list containing user handle, display name, and URL." (interactive "sSearch mastodon for: ") - (let* ((url (format "%s/api/v1/accounts/search" mastodon-instance-url)) + (let* ((url (mastodon-http--api "accounts/search")) ;; (buffer (format "*mastodon-search-%s*" query)) (response (if (equal mastodon-toot--enable-completion-for-mentions "following") (mastodon-http--get-search-json url query "following=true") @@ -77,8 +77,9 @@ Returns a nested list containing user handle, display name, and URL." (accts (alist-get 'accounts response)) (tags (alist-get 'hashtags response)) (statuses (alist-get 'statuses response)) - (user-ids (mapcar #'mastodon-search--get-user-info - accts)) ; returns a list of three-item lists + ;; this is now done in search--insert-users-propertized + ;; (user-ids (mapcar #'mastodon-search--get-user-info + ;; accts)) ; returns a list of three-item lists (tags-list (mapcar #'mastodon-search--get-hashtag-info tags)) ;; (status-list (mapcar #'mastodon-search--get-status-info @@ -89,28 +90,16 @@ Returns a nested list containing user handle, display name, and URL." status-ids-list))) (with-current-buffer (get-buffer-create buffer) (switch-to-buffer buffer) - (erase-buffer) (mastodon-mode) (let ((inhibit-read-only t)) + (erase-buffer) ;; user results: (insert (mastodon-tl--set-face (concat "\n ------------\n" " USERS\n" " ------------\n\n") 'success)) - (mapc (lambda (el) - (insert (propertize (car el) 'face 'mastodon-display-name-face) - " : \n : " - (propertize (concat "@" (car (cdr el))) - 'face 'mastodon-handle-face - 'mouse-face 'highlight - 'mastodon-tab-stop 'user-handle - 'keymap mastodon-tl--link-keymap - 'mastodon-handle (concat "@" (car (cdr el))) - 'help-echo (concat "Browse user profile of @" (car (cdr el)))) - " : \n" - "\n")) - user-ids) + (mastodon-search--insert-users-propertized accts :note) ;; hashtag results: (insert (mastodon-tl--set-face (concat "\n ------------\n" @@ -136,11 +125,44 @@ Returns a nested list containing user handle, display name, and URL." (mapc 'mastodon-tl--toot toots-list-json) (goto-char (point-min)))))) +(defun mastodon-search--insert-users-propertized (json &optional note) + "Insert users list into the buffer. +JSON is the data from the server.. If NOTE is non-nil, include +user's profile note. This is also called by +`mastodon-tl--get-follow-suggestions' and +`mastodon-profile--insert-follow-requests'." + (mapc (lambda (acct) + (let ((user (mastodon-search--get-user-info acct))) + (insert + (propertize + (concat (propertize (car user) + 'face 'mastodon-display-name-face + 'byline t + 'toot-id "0") + " : \n : " + (propertize (concat "@" (cadr user)) + 'face 'mastodon-handle-face + 'mouse-face 'highlight + 'mastodon-tab-stop 'user-handle + 'keymap mastodon-tl--link-keymap + 'mastodon-handle (concat "@" (cadr user)) + 'help-echo (concat "Browse user profile of @" (cadr user))) + " : \n" + (if note + (mastodon-tl--render-text (cadddr user) nil) + "") + "\n") + 'toot-json acct)))) ; so named for compat w other processing functions + json)) + (defun mastodon-search--get-user-info (account) - "Get user handle, display name and account URL from ACCOUNT." - (list (alist-get 'display_name account) + "Get user handle, display name, account URL and profile note from ACCOUNT." + (list (if (not (equal "" (alist-get 'display_name account))) + (alist-get 'display_name account) + (alist-get 'username account)) (alist-get 'acct account) - (alist-get 'url account))) + (alist-get 'url account) + (alist-get 'note account))) (defun mastodon-search--get-hashtag-info (tag) "Get hashtag name and URL from TAG." diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index cd27bd5..40cbcae 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -2,10 +2,11 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Author: Johnson Denen <johnson.denen@gmail.com> +;; Marty Hiatt <martianhiatus@riseup.net> ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -35,6 +36,8 @@ (require 'time-date) (require 'cl-lib) ; for cl-mapcar +(require 'mpv nil :no-error) + (autoload 'mastodon-auth--get-account-name "mastodon-auth") (autoload 'mastodon-http--api "mastodon-http") (autoload 'mastodon-http--get-json "mastodon-http") @@ -56,10 +59,19 @@ (autoload 'mastodon-http--get-json-async "mastodon-http") (autoload 'mastodon-profile--lookup-account-in-status "mastodon-profile") (autoload 'mastodon-profile-mode "mastodon-profile") -(autoload 'mastodon-notifications--get "mastodon-notifications") +;; make notifications--get available via M-x and outside our keymap: +(autoload 'mastodon-notifications--get "mastodon-notifications" + "Display NOTIFICATIONS in buffer." t) ; interactive +(autoload 'mastodon-search--insert-users-propertized "mastodon-search") +(autoload 'mastodon-search--get-user-info "mastodon-search") +(autoload 'mastodon-http--delete "mastodon-http") + +(when (require 'mpv nil :no-error) + (declare-function mpv-start "mpv")) (defvar mastodon-instance-url) (defvar mastodon-toot-timestamp-format) (defvar shr-use-fonts) ;; declare it since Emacs24 didn't have this +(defvar mastodon-mode-map) (defgroup mastodon-tl nil "Timelines in Mastodon." @@ -152,12 +164,42 @@ types of mastodon links and not just shr.el-generated ones.") (define-key map (kbd "u") 'mastodon-tl--update) ;; keep new my-profile binding; shr 'O' doesn't work here anyway (define-key map (kbd "O") 'mastodon-profile--my-profile) + (define-key map (kbd "<C-return>") 'mastodon-tl--mpv-play-video-at-point) (keymap-canonicalize map)) "The keymap to be set for shr.el generated image links. We need to override the keymap so tabbing will navigate to all types of mastodon links and not just shr.el-generated ones.") +(defvar mastodon-tl--view-filters-keymap + (let ((map ;(make-sparse-keymap))) + (copy-keymap mastodon-mode-map))) + (define-key map (kbd "d") 'mastodon-tl--delete-filter) + (define-key map (kbd "c") 'mastodon-tl--create-filter) + (define-key map (kbd "n") 'mastodon-tl--goto-next-item) + (define-key map (kbd "p") 'mastodon-tl--goto-prev-item) + (define-key map (kbd "TAB") 'mastodon-tl--goto-next-item) + (define-key map (kbd "g") 'mastodon-tl--view-filters) + (keymap-canonicalize map)) + "Keymap for viewing filters.") + +(defvar mastodon-tl--follow-suggestions-map + (let ((map ;(make-sparse-keymap))) + (copy-keymap mastodon-mode-map))) + (define-key map (kbd "n") 'mastodon-tl--goto-next-item) + (define-key map (kbd "p") 'mastodon-tl--goto-prev-item) + (define-key map (kbd "g") 'mastodon-tl--get-follow-suggestions) + (keymap-canonicalize map)) + "Keymap for viewing follow suggestions.") + +(defvar mastodon-tl--byline-link-keymap + (when (require 'mpv nil :no-error) + (let ((map (make-sparse-keymap))) + (define-key map (kbd "<C-return>") 'mastodon-tl--mpv-play-video-from-byline) + (keymap-canonicalize map))) + "The keymap to be set for the author byline. +The idea is that you can play media without navigating to it.") + (defun mastodon-tl--next-tab-item () "Move to the next interesting item. @@ -262,6 +304,27 @@ Optionally start from POS." (mastodon-tl--goto-toot-pos 'previous-single-property-change 'mastodon-tl--update)) +(defun mastodon-tl--goto-first-item () + "Jump to first toot or item in buffer. +Used on initializing a timeline or thread." + ;; goto-next-toot assumes we already have toots, and is therefore + ;; incompatible with any view where it is possible to have no items. + ;; when that is the case the call to goto-toot-pos loops infinitely + (goto-char (point-min)) + (mastodon-tl--goto-next-item)) + +(defun mastodon-tl--goto-next-item () + "Jump to next item, e.g. filter or follow request." + (interactive) + (mastodon-tl--goto-toot-pos 'next-single-property-change + 'next-line)) + +(defun mastodon-tl--goto-prev-item () + "Jump to previous item, e.g. filter or follow request." + (interactive) + (mastodon-tl--goto-toot-pos 'previous-single-property-change + 'previous-line)) + (defun mastodon-tl--remove-html (toot) "Remove unrendered tags from TOOT." (let* ((t1 (replace-regexp-in-string "<\/p>" "\n\n" toot)) @@ -288,15 +351,22 @@ Optionally start from POS." (mastodon-media--get-avatar-rendering avatar-url)) (propertize name '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 - (mastodon-tl--format-faves-count toot)) + ;; 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--get-endpoint))) + (mastodon-tl--format-faves-count toot))) " (" (propertize (concat "@" handle) 'face 'mastodon-handle-face 'mouse-face 'highlight - ;; TODO: Replace url browsing with native profile viewing 'mastodon-tab-stop 'user-handle 'account account 'shr-url profile-url @@ -306,19 +376,52 @@ Optionally start from POS." ")"))) (defun mastodon-tl--format-faves-count (toot) - "Format a favorites, boosts, replies count for a TOOT. -Used to help-echo when point is at the start of a byline, -i.e. where `mastodon-tl--goto-next-toot' leaves point." - (let ((toot-to-count - (or - ;; simply praying this order works - (alist-get 'status toot) ; notifications timeline - (alist-get 'reblog toot) ; boosts - toot))) ; everything else - (format "%s faves | %s boosts | %s replies" - (alist-get 'favourites_count toot-to-count) - (alist-get 'reblogs_count toot-to-count) - (alist-get 'replies_count toot-to-count)))) + "Format a favourites, boosts, replies count for a TOOT. +Used as a help-echo when point is at the start of a byline, i.e. +where `mastodon-tl--goto-next-toot' leaves point. Also displays a +toot's media types and optionally the binding to play moving +image media from the byline." + (let* ((toot-to-count + (or + ;; simply praying this order works + (alist-get 'status toot) ; notifications timeline + (alist-get 'reblog toot) ; boosts + toot)) ; everything else + (media-types (mastodon-tl--get-media-types toot)) + (format-faves (format "%s faves | %s boosts | %s replies" + (alist-get 'favourites_count toot-to-count) + (alist-get 'reblogs_count toot-to-count) + (alist-get 'replies_count toot-to-count))) + (format-media (when media-types + (format " | media: %s" + (mapconcat #'identity media-types " ")))) + (format-media-binding (when (and (or + (member "video" media-types) + (member "gifv" media-types)) + (require 'mpv nil :no-error)) + (format " | C-RET to view with mpv")))) + (format "%s" (concat format-faves format-media format-media-binding)))) + +(defun mastodon-tl--get-media-types (toot) + "Return a list of the media attachment types of the TOOT at point." + (let* ((attachments (mastodon-tl--field 'media_attachments toot))) + (mapcar (lambda (x) + (alist-get 'type x)) + attachments))) + +(defun mastodon-tl--get-attachments-for-byline (toot) + "Return a list of attachment URLs and types for TOOT. +The result is added as an attachments property to author-byline." + (let ((media-attachments (mastodon-tl--field 'media_attachments toot))) + (mapcar + (lambda (attachement) + (let ((remote-url + (or (alist-get 'remote_url attachement) + ;; fallback b/c notifications don't have remote_url + (alist-get 'url attachement))) + (type (alist-get 'type attachement))) + `(:url ,remote-url :type ,type))) + media-attachments))) (defun mastodon-tl--byline-boosted (toot) "Add byline for boosted data from TOOT." @@ -405,77 +508,92 @@ TIME-STAMP is assumed to be in the past." (defun mastodon-tl--byline (toot author-byline action-byline) "Generate byline for TOOT. -AUTHOR-BYLINE is function for adding the author portion of +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 +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'" - (let ((parsed-time (date-to-time (mastodon-tl--field 'created_at toot))) - (faved (equal 't (mastodon-tl--field 'favourited toot))) - (boosted (equal 't (mastodon-tl--field 'reblogged toot))) - (visibility (mastodon-tl--field 'visibility toot))) + (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)) + (faved (equal 't (mastodon-tl--field 'favourited toot))) + (boosted (equal 't (mastodon-tl--field 'reblogged toot))) + (visibility (mastodon-tl--field 'visibility toot))) (concat - ;; (propertize "\n | " 'face 'default) + ;; Boosted/favourited markers are not technically part of the byline, so + ;; we don't propertize them with 'byline t', as per the rest. This + ;; ensures that `mastodon-tl--goto-next-toot' puts point on + ;; author-byline, not before the (F) or (B) marker. Not propertizing like + ;; this makes the behaviour of these markers consistent whether they are + ;; displayed for an already boosted/favourited toot or as the result of + ;; the toot having just been favourited/boosted. + (concat (when boosted + (mastodon-tl--format-faved-or-boosted-byline "B")) + (when faved + (mastodon-tl--format-faved-or-boosted-byline "F"))) (propertize - (concat (when boosted - (format - (propertize "(%s) " - 'help-echo - (mastodon-tl--format-faves-count toot)) - (propertize "B" 'face 'mastodon-boost-fave-face))) - (when faved - (format - (propertize "(%s) " - 'help-echo - (mastodon-tl--format-faves-count toot)) - (propertize "F" 'face 'mastodon-boost-fave-face))) - ;; we propertize help-echo format faves for author name - ;; in `mastodon-tl--byline-author' - (funcall author-byline toot) - (cond ((equal visibility "direct") - (if (fontp (char-displayable-p #10r128274)) - " ✉" - " [direct]")) - ((equal visibility "private") - (if (fontp (char-displayable-p #10r9993)) - " 🔒" - " [followers]"))) - (funcall action-byline toot) - " " - ;; TODO: Once we have a view for toot (responses etc.) make - ;; this a tab stop and attach an action. - (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)) - (propertize "\n ------------\n" 'face 'default)) + (concat + ;; we propertize help-echo format faves for author name + ;; in `mastodon-tl--byline-author' + (funcall author-byline toot) + (cond ((equal visibility "direct") + (if (fontp (char-displayable-p #10r128274)) + " ✉" + " [direct]")) + ((equal visibility "private") + (if (fontp (char-displayable-p #10r9993)) + " 🔒" + " [followers]"))) + (funcall action-byline toot) + " " + ;; TODO: Once we have a view for toot (responses etc.) make + ;; this a tab stop and attach an action. + (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)) + (propertize "\n ------------\n" 'face 'default)) 'favourited-p faved 'boosted-p boosted 'byline t)))) +(defun mastodon-tl--format-faved-or-boosted-byline (letter) + "Format the byline marker for a boosted or favourited status. +LETTER is a string, either F or B." + (format "(%s) " + (propertize letter 'face 'mastodon-boost-fave-face))) + (defun mastodon-tl--render-text (string toot) "Return a propertized text rendering the given HTML string STRING. The contents comes from the given TOOT which is used in parsing links in the text. If TOOT is nil no parsing occurs." - (with-temp-buffer - (insert string) - (let ((shr-use-fonts mastodon-tl--enable-proportional-fonts) - (shr-width (when mastodon-tl--enable-proportional-fonts - (- (window-width) 1)))) - (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: - (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))))) - (buffer-string))) + (when string ; handle rare empty notif server bug + (with-temp-buffer + (insert string) + (let ((shr-use-fonts mastodon-tl--enable-proportional-fonts) + (shr-width (when mastodon-tl--enable-proportional-fonts + (- (window-width) 1)))) + (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: + (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))))) + (buffer-string)))) (defun mastodon-tl--process-link (toot start end url) "Process link URL in TOOT as hashtag, userhandle, or normal link. @@ -705,14 +823,14 @@ message is a link which unhides/hides the main body." (let ((preview-url (alist-get 'preview_url media-attachement)) (remote-url - (if (alist-get 'remote_url media-attachement) - (alist-get 'remote_url media-attachement) - ;; fallback b/c notifications don't have remote_url - (alist-get 'url media-attachement))) - (type (alist-get 'type media-attachement))) + (or (alist-get 'remote_url media-attachement) + ;; fallback b/c notifications don't have remote_url + (alist-get 'url media-attachement))) + (type (alist-get 'type media-attachement)) + (caption (alist-get 'description media-attachement))) (if mastodon-tl--display-media-p (mastodon-media--get-media-link-rendering - preview-url remote-url type) ; 2nd arg for shr-browse-url + preview-url remote-url type caption) ; 2nd arg for shr-browse-url (concat "Media::" preview-url "\n")))) media-attachements ""))) (if (not (and mastodon-tl--display-media-p @@ -734,7 +852,7 @@ Runs `mastodon-tl--render-text' and fetches poll or media." (mastodon-tl--get-poll toot)) (mastodon-tl--media toot)))) -(defun mastodon-tl--insert-status (toot body author-byline action-byline &optional id) +(defun mastodon-tl--insert-status (toot body author-byline action-byline &optional id parent-toot) "Display the content and byline of timeline element TOOT. BODY will form the section of the toot above the byline. @@ -758,7 +876,8 @@ a notification." 'toot-id (or id ; for notifications (alist-get 'id toot)) 'base-toot-id (mastodon-tl--toot-id toot) - 'toot-json toot) + 'toot-json toot + 'parent-toot parent-toot) "\n") (when mastodon-tl--display-media-p (mastodon-media--inline-images start-pos (point))))) @@ -839,6 +958,49 @@ a notification." (message "You voted for option %s: %s!" (car option) (cdr option))))))) +(defun mastodon-tl--find-first-video-in-attachments () + "Return the first media attachment that is a moving image." + (let ((attachments (mastodon-tl--property 'attachments)) + vids) + (mapc (lambda (x) + (let ((att-type (plist-get x :type))) + (when (or (string= "video" att-type) + (string= "gifv" att-type)) + (push x vids)))) + attachments) + (car vids))) + +(defun mastodon-tl--mpv-play-video-from-byline () + "Run `mastodon-tl--mpv-play-video-at-point' on first moving image in post." + (interactive) + (let* ((video (mastodon-tl--find-first-video-in-attachments)) + (url (plist-get video :url)) + (type (plist-get video :type))) + (mastodon-tl--mpv-play-video-at-point url type))) + +(defun mastodon-tl--mpv-play-video-at-point (&optional url type) + "Play the video or gif at point with an mpv process. +URL and TYPE are provided when called while point is on byline, +in which case play first video or gif from current toot." + (interactive) + (let ((url (or + ;; point in byline: + url + ;; point in toot: + (get-text-property (point) 'image-url))) + (type (or ;; in byline: + type + ;; point in toot: + (mastodon-tl--property 'mastodon-media-type)))) + (if url + (if (or (equal type "gifv") + (equal type "video")) + (progn + (message "'q' to kill mpv.") + (mpv-start "--loop" url)) + (message "no moving image here?")) + (message "no moving image here?")))) + (defun mastodon-tl--toot (toot) "Formats TOOT and insertes it into the buffer." (mastodon-tl--insert-status @@ -957,7 +1119,6 @@ webapp" (reblog (alist-get 'reblog json))) (if reblog (alist-get 'id reblog) id))) - (defun mastodon-tl--thread () "Open thread buffer for toot under `point'." (interactive) @@ -972,37 +1133,159 @@ webapp" (if (> (+ (length (alist-get 'ancestors context)) (length (alist-get 'descendants context))) 0) - (with-output-to-temp-buffer buffer - (switch-to-buffer buffer) - (mastodon-mode) - (setq mastodon-tl--buffer-spec - `(buffer-name ,buffer - endpoint ,(format "statuses/%s/context" id) - update-function - (lambda(toot) (message "END of thread.")))) - (let ((inhibit-read-only t)) - (mastodon-tl--timeline (vconcat - (alist-get 'ancestors context) - `(,toot) - (alist-get 'descendants context))))) + (progn + (with-output-to-temp-buffer buffer + (switch-to-buffer buffer) + (mastodon-mode) + (setq mastodon-tl--buffer-spec + `(buffer-name ,buffer + endpoint ,(format "statuses/%s/context" id) + update-function + (lambda (toot) (message "END of thread.")))) + (let ((inhibit-read-only t)) + (mastodon-tl--timeline (vconcat + (alist-get 'ancestors context) + `(,toot) + (alist-get 'descendants context))))) + (mastodon-tl--goto-next-toot)) (message "No Thread!")))) +(defun mastodon-tl--create-filter () + "Create a filter for a word. +Prompt for a context, must be a list containting at least one of \"home\", +\"notifications\", \"public\", \"thread\"." + (interactive) + (let* ((url (mastodon-http--api "filters")) + (word (read-string + (format "Word(s) to filter (%s): " (or (current-word) "")) + nil nil (or (current-word) ""))) + (contexts + (if (equal "" word) + (error "You must select at least one word for a filter") + (completing-read-multiple + "Contexts to filter [TAB for options]:" + '("home" "notifications" "public" "thread") + nil ; no predicate + t))) ; require-match, as context is mandatory + (contexts-processed + (if (equal nil contexts) + (error "You must select at least one context for a filter") + (mapcar (lambda (x) + (cons "context[]" x)) + contexts))) + (response (mastodon-http--post url (push + `("phrase" . ,word) + contexts-processed) + nil))) + (mastodon-http--triage response + (lambda () + (message "Filter created for %s!" word) + ;; reload if we are in filters view: + (when (string= (mastodon-tl--get-endpoint) + "filters") + (mastodon-tl--view-filters)))))) + +(defun mastodon-tl--view-filters () + "View the user's filters in a new buffer." + (interactive) + (mastodon-tl--init-sync "filters" + "filters" + 'mastodon-tl--insert-filters) + (use-local-map mastodon-tl--view-filters-keymap)) + +(defun mastodon-tl--insert-filters (json) + "Insert the user's current filters. +JSON is what is returned by by the server." + (insert (mastodon-tl--set-face + (concat "\n ------------\n" + " CURRENT FILTERS\n" + " ------------\n\n") + 'success) + (mastodon-tl--set-face + "[c - create filter\n d - delete filter at point\n n/p - go to next/prev filter]\n\n" + 'font-lock-comment-face)) + (if (equal json '[]) + (insert (propertize + "Looks like you have no filters for now." + 'face font-lock-comment-face + 'byline t + 'toot-id "0")) ; so point can move here when no filters + (mapc (lambda (x) + (mastodon-tl--insert-filter-string x) + (insert "\n\n")) + json))) + +(defun mastodon-tl--insert-filter-string (filter) + "Insert a single FILTER." + (let* ((phrase (alist-get 'phrase filter)) + (contexts (alist-get 'context filter)) + (id (alist-get 'id filter)) + (filter-string (concat "- \"" phrase "\" filtered in: " + (mapconcat #'identity contexts ", ")))) + (insert + (propertize filter-string + 'toot-id id ;for goto-next-filter compat + 'phrase phrase + ;;'help-echo "n/p to go to next/prev filter, c to create new filter, d to delete filter at point." + ;;'keymap mastodon-tl--view-filters-keymap + 'byline t)))) ;for goto-next-filter compat + +(defun mastodon-tl--delete-filter () + "Delete filter at point." + (interactive) + (let* ((filter-id (get-text-property (point) 'toot-id)) + (phrase (get-text-property (point) 'phrase)) + (url (mastodon-http--api + (format "filters/%s" filter-id)))) + (if (equal nil filter-id) + (error "No filter at point?") + (when (y-or-n-p (format "Delete this filter? "))) + (let ((response (mastodon-http--delete url))) + (mastodon-http--triage response (lambda () + (mastodon-tl--view-filters) + (message "Filter for \"%s\" deleted!" phrase))))))) + +(defun mastodon-tl--get-follow-suggestions () + "Display a buffer of suggested accounts to follow." + (interactive) + (mastodon-tl--init-sync "follow-suggestions" + "suggestions" + 'mastodon-tl--insert-follow-suggestions) + (use-local-map mastodon-tl--follow-suggestions-map)) + +(defun mastodon-tl--insert-follow-suggestions (response) + "Insert follow suggestions into buffer. +RESPONSE is the JSON returned by the server." + (insert (mastodon-tl--set-face + (concat "\n ------------\n" + " SUGGESTED ACCOUNTS\n" + " ------------\n\n") + 'success)) + (mastodon-search--insert-users-propertized response :note) + (goto-char (point-min))) + (defun mastodon-tl--follow-user (user-handle &optional notify) "Query for USER-HANDLE from current status and follow that user. If NOTIFY is \"true\", enable notifications when that user posts. If NOTIFY is \"false\", disable notifications when that user posts. Can be called to toggle NOTIFY on users already being followed." - (interactive - (list - (mastodon-tl--interactive-user-handles-get "follow"))) - (mastodon-tl--do-user-action-and-response user-handle "follow" nil notify)) + (interactive + (list + (mastodon-tl--interactive-user-handles-get "follow"))) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (mastodon-tl--do-user-action-and-response user-handle "follow" nil notify))) (defun mastodon-tl--enable-notify-user-posts (user-handle) "Query for USER-HANDLE and enable notifications when they post." (interactive (list (mastodon-tl--interactive-user-handles-get "enable"))) - (mastodon-tl--follow-user user-handle "true")) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (mastodon-tl--follow-user user-handle "true"))) (defun mastodon-tl--disable-notify-user-posts (user-handle) "Query for USER-HANDLE and disable notifications when they post." @@ -1016,14 +1299,20 @@ Can be called to toggle NOTIFY on users already being followed." (interactive (list (mastodon-tl--interactive-user-handles-get "unfollow"))) - (mastodon-tl--do-user-action-and-response user-handle "unfollow" t)) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (mastodon-tl--do-user-action-and-response user-handle "unfollow" t))) (defun mastodon-tl--block-user (user-handle) "Query for USER-HANDLE from current status and block that user." (interactive (list (mastodon-tl--interactive-user-handles-get "block"))) - (mastodon-tl--do-user-action-and-response user-handle "block")) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (mastodon-tl--do-user-action-and-response user-handle "block"))) (defun mastodon-tl--unblock-user (user-handle) "Query for USER-HANDLE from list of blocked users and unblock that user." @@ -1039,7 +1328,10 @@ Can be called to toggle NOTIFY on users already being followed." (interactive (list (mastodon-tl--interactive-user-handles-get "mute"))) - (mastodon-tl--do-user-action-and-response user-handle "mute")) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (mastodon-tl--do-user-action-and-response user-handle "mute"))) (defun mastodon-tl--unmute-user (user-handle) "Query for USER-HANDLE from list of muted users and unmute that user." @@ -1052,15 +1344,31 @@ Can be called to toggle NOTIFY on users already being followed." (defun mastodon-tl--interactive-user-handles-get (action) "Get the list of user-handles for ACTION from the current toot." - (let ((user-handles (mastodon-profile--extract-users-handles - (mastodon-profile--toot-json)))) - (completing-read (if (or (equal action "disable") - (equal action "enable")) - (format "%s notifications when user posts: " action) - (format "Handle of user to %s: " action)) - user-handles - nil ; predicate - 'confirm))) + (if (and (not (string-prefix-p "accounts" (mastodon-tl--get-endpoint))) ;profile view + (not (get-text-property (point) 'toot-json))) + (message "Looks like there's no toot or user at point?") + (let ((user-handles + (cond ((or (equal (buffer-name) "*mastodon-follow-suggestions*") + ;; follow suggests / search / foll requests compat: + (string-prefix-p "*mastodon-search" (buffer-name)) + (equal (buffer-name) "*mastodon-follow-requests*") + ;; profile view follows/followers compat: + ;; but not for profile statuses: + (and (string-prefix-p "accounts" (mastodon-tl--get-endpoint)) + (not (string-suffix-p "statuses" (mastodon-tl--get-endpoint))))) + ;; avoid tl--property here because it calls next-toot + ;; which breaks non-toot buffers like foll reqs etc.: + (list (alist-get 'acct (get-text-property (point) 'toot-json)))) + (t + (mastodon-profile--extract-users-handles + (mastodon-profile--toot-json)))))) + (completing-read (if (or (equal action "disable") + (equal action "enable")) + (format "%s notifications when user posts: " action) + (format "Handle of user to %s: " action)) + user-handles + nil ; predicate + 'confirm)))) (defun mastodon-tl--interactive-blocks-or-mutes-list-get (action) "Fetch the list of accounts for ACTION from the server. @@ -1090,11 +1398,13 @@ NOTIFY is only non-nil when called by `mastodon-tl--follow-user'." ;; if unmuting/unblocking, we got handle from mute/block list (mastodon-profile--search-account-by-handle user-handle) - ;; if muting/blocking, we select from handles in current status + ;; if muting/blocking, we select from handles in current status (mastodon-profile--lookup-account-in-status user-handle (mastodon-profile--toot-json)))) (user-id (mastodon-profile--account-field account 'id)) - (name (mastodon-profile--account-field account 'display_name)) + (name (if (not (equal "" (mastodon-profile--account-field account 'display_name))) + (mastodon-profile--account-field account 'display_name) + (mastodon-profile--account-field account 'username))) (url (mastodon-http--api (if notify (format "accounts/%s/%s?notify=%s" user-id action notify) @@ -1243,7 +1553,7 @@ is a no-op." ;; We need to re-schedule for an earlier time (cancel-timer mastodon-tl--timestamp-update-timer) (setq mastodon-tl--timestamp-update-timer - (run-at-time this-update + (run-at-time (time-to-seconds (time-subtract this-update (current-time))) nil ;; don't repeat #'mastodon-tl--update-timestamps-callback (current-buffer) nil))))))) @@ -1296,7 +1606,9 @@ from the start if it is nil." (copy-marker previous-timestamp)) ;; otherwise we are done for now; schedule a new run for when needed (setq mastodon-tl--timestamp-update-timer - (run-at-time mastodon-tl--timestamp-next-update + (run-at-time (time-to-seconds + (time-subtract mastodon-tl--timestamp-next-update + (current-time))) nil ;; don't repeat #'mastodon-tl--update-timestamps-callback buffer nil)))))))) @@ -1329,6 +1641,14 @@ UPDATE-FUNCTION is used to recieve more toots. JSON is the data returned from the server." (with-output-to-temp-buffer buffer (switch-to-buffer buffer) + ;; mastodon-mode wipes buffer-spec, so order must unforch be: + ;; 1 run update-function, 2 enable masto-mode, 3 set buffer spec. + ;; which means we cannot use buffer-spec for update-function + ;; unless we set it both before and after the others + (setq mastodon-tl--buffer-spec + `(buffer-name ,buffer + endpoint ,endpoint + update-function ,update-function)) (setq ;; Initialize with a minimal interval; we re-scan at least once ;; every 5 minutes to catch any timestamps we may have missed @@ -1336,8 +1656,6 @@ JSON is the data returned from the server." (seconds-to-time 300))) (funcall update-function json)) (mastodon-mode) - (when (equal endpoint "follow_requests") - (mastodon-profile-mode)) (with-current-buffer buffer (setq mastodon-tl--buffer-spec `(buffer-name ,buffer @@ -1345,11 +1663,21 @@ JSON is the data returned from the server." update-function ,update-function) mastodon-tl--timestamp-update-timer (when mastodon-tl--enable-relative-timestamps - (run-at-time mastodon-tl--timestamp-next-update + (run-at-time (time-to-seconds + (time-subtract mastodon-tl--timestamp-next-update + (current-time))) nil ;; don't repeat #'mastodon-tl--update-timestamps-callback (current-buffer) - nil))))) + nil))) + (unless + ;; for everything save profiles: + (string-prefix-p "accounts" endpoint)) + ;;(or (equal endpoint "notifications") + ;; (string-prefix-p "timelines" endpoint) + ;; (string-prefix-p "favourites" endpoint) + ;; (string-prefix-p "statuses" endpoint)) + (mastodon-tl--goto-first-item))) (defun mastodon-tl--init-sync (buffer-name endpoint update-function) "Initialize BUFFER-NAME with timeline targeted by ENDPOINT. @@ -1361,6 +1689,14 @@ Runs synchronously." (json (mastodon-http--get-json url))) (with-output-to-temp-buffer buffer (switch-to-buffer buffer) + ;; mastodon-mode wipes buffer-spec, so order must unforch be: + ;; 1 run update-function, 2 enable masto-mode, 3 set buffer spec. + ;; which means we cannot use buffer-spec for update-function + ;; unless we set it both before and after the others + (setq mastodon-tl--buffer-spec + `(buffer-name ,buffer + endpoint ,endpoint + update-function ,update-function)) (setq ;; Initialize with a minimal interval; we re-scan at least once ;; every 5 minutes to catch any timestamps we may have missed @@ -1375,11 +1711,17 @@ Runs synchronously." ,update-function) mastodon-tl--timestamp-update-timer (when mastodon-tl--enable-relative-timestamps - (run-at-time mastodon-tl--timestamp-next-update + (run-at-time (time-to-seconds + (time-subtract mastodon-tl--timestamp-next-update + (current-time))) nil ;; don't repeat #'mastodon-tl--update-timestamps-callback (current-buffer) - nil)))) + nil))) + (when ;(and (not (equal json '[])) + ;; for everything save profiles: + (not (string-prefix-p "accounts" endpoint)) + (mastodon-tl--goto-first-item))) buffer)) (provide 'mastodon-tl) diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el index 746b7d4..07ab400 100644 --- a/lisp/mastodon-toot.el +++ b/lisp/mastodon-toot.el @@ -2,10 +2,11 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Author: Johnson Denen <johnson.denen@gmail.com> +;; Marty Hiatt <martianhiatus@riseup.net> ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -69,6 +70,11 @@ (autoload 'mastodon-tl--toot-id "mastodon-tl") (autoload 'mastodon-toot "mastodon") +; for mastodon-toot--translate-toot-text +(autoload 'mastodon-tl--content "mastodon-tl") +(when (require 'lingva nil :no-error) + (declare-function lingva-translate "lingva")) + (defgroup mastodon-toot nil "Tooting in Mastodon." :prefix "mastodon-toot-" @@ -193,7 +199,10 @@ Remove MARKER if REMOVE is non-nil, otherwise add it." (unless remove (goto-char bol) (insert (format "(%s) " - (propertize marker 'face 'success))))))) + (propertize marker 'face 'success))))) + ;; leave point after the marker: + (unless remove + (mastodon-tl--goto-next-toot)))) (defun mastodon-toot--action (action callback) "Take ACTION on toot at point, then execute CALLBACK. @@ -255,7 +264,7 @@ Makes a POST request to the server." (mastodon-toot--action-success "F" byline-region remove)) (message (format "%s #%s" action id)))) - (message "Nothing to favorite here?!?")))) + (message "Nothing to favourite here?!?")))) (defun mastodon-toot--copy-toot-url () "Copy URL of toot at point." @@ -267,6 +276,25 @@ Makes a POST request to the server." (kill-new url) (message "Toot URL copied to the clipboard."))) +(defun mastodon-toot--copy-toot-text () + "Copy text of toot at point." + (interactive) + (let* ((toot (mastodon-tl--property 'toot-json))) + (kill-new (mastodon-tl--content toot)) + (message "Toot content copied to the clipboard."))) + +;; (when (require 'lingva nil :no-error) + (defun mastodon-toot--translate-toot-text () + "Translate text of toot at point. +Uses `lingva.el'." + (interactive) + (if mastodon-tl--buffer-spec + (let* ((toot (mastodon-tl--property 'toot-json))) + (if toot + (lingva-translate nil (mastodon-tl--content toot)) + (message "No toot to translate?"))) + (message "No mastodon buffer?"))) + (defun mastodon-toot--own-toot-p (toot) "Check if TOOT is user's own, e.g. for deleting it." (and (not (alist-get 'reblog toot)) @@ -589,11 +617,12 @@ candidate ARG. IGNORED remains a mystery." "Reply to toot at `point'." (interactive) (let* ((toot (mastodon-tl--property 'toot-json)) + (parent (mastodon-tl--property 'parent-toot)) ; for new notifs handling (id (mastodon-tl--as-string (mastodon-tl--field 'id toot))) (account (mastodon-tl--field 'account toot)) (user (alist-get 'acct account)) - (mentions (mastodon-toot--mentions toot)) - (boosted (mastodon-tl--field 'reblog toot)) + (mentions (mastodon-toot--mentions (or parent toot))) + (boosted (mastodon-tl--field 'reblog (or parent toot))) (booster (when boosted (alist-get 'acct (alist-get 'account toot))))) @@ -609,7 +638,7 @@ candidate ARG. IGNORED remains a mystery." mentions)) (concat (mastodon-toot--process-local user) mentions))) - id toot))) + id (or parent toot)))) (defun mastodon-toot--toggle-warning () "Toggle `mastodon-toot--content-warning'." diff --git a/lisp/mastodon.el b/lisp/mastodon.el index 6daa755..49abe26 100644 --- a/lisp/mastodon.el +++ b/lisp/mastodon.el @@ -6,7 +6,7 @@ ;; Maintainer: Marty Hiatt <martianhiatus@riseup.net> ;; Version: 0.10.0 ;; Package-Requires: ((emacs "27.1") (request "0.3.2") (seq "1.0")) -;; Homepage: https://git.blast.noho.st/mouse/mastodon.el +;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -33,7 +33,6 @@ ;;; Code: (require 'cl-lib) ; for `cl-some' call in mastodon -;; hack to make mastodon-toot customizes visible prior to running mastodon-toot: (require 'mastodon-toot) (declare-function discover-add-context-menu "discover") @@ -87,8 +86,12 @@ (autoload 'mastodon-tl--poll-vote "mastodon-http") ;; (autoload 'mastodon-toot--delete-and-redraft-toot "mastodon-toot") (autoload 'mastodon-profile--view-bookmarks "mastodon-profile") +(autoload 'mastoton-tl--view-filters "mastodon-tl") ;; (autoload 'mastodon-toot--bookmark-toot-toggle "mastodon-toot") +(when (require 'lingva nil :no-error) + (autoload 'mastodon-toot--translate-toot-text "mastodon-toot")) + (defgroup mastodon nil "Interface with Mastodon." :prefix "mastodon-" @@ -193,6 +196,10 @@ Use. e.g. \"%c\" for your locale's date and time format." (define-key map (kbd "v") #'mastodon-tl--poll-vote) (define-key map (kbd "k") #'mastodon-toot--bookmark-toot-toggle) (define-key map (kbd "K") #'mastodon-profile--view-bookmarks) + (define-key map (kbd "I") #'mastodon-tl--view-filters) + (define-key map (kbd "G") #'mastodon-tl--get-follow-suggestions) + (when (require 'lingva nil :no-error) + (define-key map (kbd "s") #'mastodon-toot--translate-toot-text)) map) "Keymap for `mastodon-mode'.") @@ -230,12 +237,17 @@ Use. e.g. \"%c\" for your locale's date and time format." (let* ((tls (list "home" "local" "federated" - (concat (mastodon-auth--user-acct) "-statuses") ; profile + (concat (mastodon-auth--user-acct) "-statuses") ; own profile "favourites" "search")) - (buffer (cl-some (lambda (el) - (get-buffer (concat "*mastodon-" el "*"))) - tls))) ; return first buff that exists + (buffer (or (cl-some (lambda (el) + (get-buffer (concat "*mastodon-" el "*"))) + tls) ; return first buff that exists + (cl-some (lambda (x) + (when + (string-prefix-p "*mastodon-" (buffer-name x)) + (get-buffer x))) + (buffer-list))))) ; catch any other masto buffer (if buffer (switch-to-buffer buffer) (mastodon-tl--get-home-timeline) diff --git a/test/ert-helper.el b/test/ert-helper.el index fc4da2d..f65649f 100644 --- a/test/ert-helper.el +++ b/test/ert-helper.el @@ -8,6 +8,7 @@ (load-file "lisp/mastodon-inspect.el") (load-file "lisp/mastodon-media.el") (load-file "lisp/mastodon-notifications.el") +(load-file "lisp/mastodon.el") (load-file "lisp/mastodon-profile.el") (load-file "lisp/mastodon-search.el") (load-file "lisp/mastodon-tl.el") diff --git a/test/mastodon-media-tests.el b/test/mastodon-media-tests.el index 0e1152a..abf9a1a 100644 --- a/test/mastodon-media-tests.el +++ b/test/mastodon-media-tests.el @@ -63,7 +63,7 @@ (should (string= "http://example.org/remote/img.png" (plist-get properties 'image-url))) (should (eq mastodon-tl--shr-image-map-replacement (plist-get properties 'keymap))) (should (string= "gifv" (plist-get properties 'mastodon-media-type))) - (should (string= "RET/i: load full image (prefix: copy URL), +/-: zoom, r: rotate, o: save preview\ntype: gifv" + (should (string= "RET/i: load full image (prefix: copy URL), +/-: zoom, r: rotate, o: save preview\nC-RET: play gifv with mpv" (plist-get properties 'help-echo)))))) (ert-deftest mastodon-media--load-image-from-url-avatar-with-imagemagic () diff --git a/test/mastodon-profile-tests.el b/test/mastodon-profile-tests.el new file mode 100644 index 0000000..ca323ea --- /dev/null +++ b/test/mastodon-profile-tests.el @@ -0,0 +1,288 @@ +;;; mastodon-profile-test.el --- Tests for mastodon-profile.el -*- lexical-binding: nil -*- + +(require 'el-mock) + +(defconst gargron-profile-json + '((id . "1") + (username . "Gargron") + (acct . "Gargron") + (display_name . "Eugen") + (locked . :json-false) + (bot . :json-false) + (discoverable . t) + (group . :json-false) + (created_at . "2016-03-16T00:00:00.000Z") + (note . "<p>Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.</p>") + (url . "https://mastodon.social/@Gargron") + (avatar . "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg") + (avatar_static . "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg") + (header . "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png") + (header_static . "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png") + (followers_count . 470905) + (following_count . 451) + (statuses_count . 70741) + (last_status_at . "2021-11-14") + (emojis . []) + (fields . [((name . "Patreon") + (value . "<a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>") + (verified_at)) + ((name . "Homepage") + (value . "<a href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">zeonfederated.com</span><span class=\"invisible\"></span></a>") + (verified_at . "2019-07-15T18:29:57.191+00:00"))]))) + +(defconst ccc-profile-json + '((id . "369027") + (username . "CCC") + (acct . "CCC@social.bau-ha.us") + (display_name . "") + (locked . :json-false) + (bot . :json-false) + (discoverable . :json-false) + (group . :json-false) + (created_at . "2018-06-03T00:00:00.000Z") + (note . "<p><a href=\"https://www.ccc.de/\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">ccc.de/</span><span class=\"invisible\"></span></a></p>") + (url . "https://social.bau-ha.us/@CCC") + (avatar . "https://files.mastodon.social/cache/accounts/avatars/000/369/027/original/6cfeb310f40e041a.jpg") + (avatar_static . "https://files.mastodon.social/cache/accounts/avatars/000/369/027/original/6cfeb310f40e041a.jpg") + (header . "https://files.mastodon.social/cache/accounts/headers/000/369/027/original/0d20bef6131b8139.jpg") + (header_static . "https://files.mastodon.social/cache/accounts/headers/000/369/027/original/0d20bef6131b8139.jpg") + (followers_count . 2733) + (following_count . 120) + (statuses_count . 1357) + (last_status_at . "2021-11-02") + (emojis . []) + (fields . []))) + +(defconst gargon-statuses-json + `[((id . "123456789012345678") + (created_at . "2021-11-11T11:11:11.111Z") + (in_reply_to_id) + (in_reply_to_account_id) + (sensitive . :json-false) + (spoiler_text . "") + (visibility . "public") + (language) + (uri . "https://mastodon.social/users/Gargron/statuses/123456789012345678/activity") + (url . "https://mastodon.social/users/Gargron/statuses/123456789012345678/activity") + (replies_count . 0) + (reblogs_count . 0) + (favourites_count . 0) + (favourited . :json-false) + (reblogged . :json-false) + (muted . :json-false) + (bookmarked . :json-false) + (content . "<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>") + (reblog) + (application) + (account ,@gargron-profile-json) + (media_attachments . []) + (mentions . []) + (tags . []) + (emojis . []) + (card) + (poll)) + ((id . "107279356043066700") + (created_at . "2021-11-11T00:00:00.000Z") + (in_reply_to_id) + (in_reply_to_account_id) + (sensitive . :json-false) + (spoiler_text . "") + (visibility . "public") + (language . "en") + (uri . "https://mastodon.social/users/Gargron/statuses/107279356043066700") + (url . "https://mastodon.social/@Gargron/107279356043066700") + (replies_count . 0) + (reblogs_count . 2) + (favourites_count . 0) + (favourited . :json-false) + (reblogged . :json-false) + (muted . :json-false) + (bookmarked . :json-false) + (content . "<p><span class=\"h-card\"><a href=\"https://social.bau-ha.us/@CCC\" class=\"u-url mention\">@<span>CCC</span></a></span> At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>") + (reblog) + (application + (name . "Web") + (website)) + (account ,@gargron-profile-json) + (media_attachments . []) + (mentions . [((id . "369027") + (username . "CCC") + (url . "https://social.bau-ha.us/@CCC") + (acct . "CCC@social.bau-ha.us"))]) + (tags . []) + (emojis . []) + (card) + (poll))]) + +(ert-deftest mastodon-profile--add-author-bylines () + "Should correctly format short infos about one account. + +When formatting Gargon's state we want to see +- the short description of that profile, +- the url of the avatar (yet to be loaded) +- the info attached to the name" + (with-mock + ;; Don't start any image loading: + (mock (mastodon-media--inline-images * *) => nil) + ;; Let's not do formatting as that makes it hard to not rely on + ;; window width and reflowing the text. + (mock (shr-render-region * *) => nil) + (if (version< emacs-version "27.1") + (mock (image-type-available-p 'imagemagick) => t) + (mock (image-transforms-p) => t)) + + (with-temp-buffer + (let ((mastodon-tl--show-avatars t) + (mastodon-tl--display-media-p t)) + (mastodon-profile--add-author-bylines (list gargron-profile-json))) + + (should + (equal + (buffer-substring-no-properties (point-min) (point-max)) + "\n Eugen (@Gargron)\n<p>Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.</p>\n")) + + ;; Check the avatar at pos 2 + (should + (equal + (get-text-property 2 'media-url) + "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg")) + (should + (equal + (get-text-property 2 'media-state) + 'needs-loading)) + + ;; Check the byline state + (should + (equal + (get-text-property 4 'byline) + t)) + (should + (equal + (get-text-property 4 'toot-id) + (alist-get 'id gargron-profile-json))) + (should + (equal + (get-text-property 4 'toot-json) + gargron-profile-json))))) + +(ert-deftest mastodon-profile--search-account-by-handle--removes-at () + "Should ignore a leading at-sign in user handle. + +The search will happen as if called without the \"@\"." + (with-mock + + (mock (mastodon-http--get-json + "https://instance.url/api/v1/accounts/search?q=gargron")) + + (let ((mastodon-instance-url "https://instance.url")) + ;; We don't check anything from the return value. We only care + ;; that the mocked fetch was called with the expected URL. + (mastodon-profile--search-account-by-handle "@gargron")))) + +(ert-deftest mastodon-profile--search-account-by-handle--filters-out-false-results () + "Should ignore results that don't match the searched handle." + (with-mock + (mock (mastodon-http--get-json *) + => + (vector ccc-profile-json gargron-profile-json)) + + (let ((mastodon-instance-url "https://instance.url")) + (should + (equal + (mastodon-profile--search-account-by-handle "Gargron") + gargron-profile-json))))) + +(ert-deftest mastodon-profile--search-account-by-handle--filtering-is-case-sensitive () + "Should ignore results that don't match the searched handle with exact case. + +TODO: We need to decide if this is actually desired or not." + (with-mock + (mock (mastodon-http--get-json *) => (vector gargron-profile-json)) + + (let ((mastodon-instance-url "https://instance.url")) + (should + (null + (mastodon-profile--search-account-by-handle "gargron")))))) + +(ert-deftest mastodon-profile--account-from-id--correct-url () + "Should use the expected url for looking up by account id." + (with-mock + + (mock (mastodon-http--get-json + "https://instance.url/api/v1/accounts/1234567")) + + (let ((mastodon-instance-url "https://instance.url")) + ;; We don't check anything from the return value. We only care + ;; that the mocked fetch was called with the expected URL. + (mastodon-profile--account-from-id "1234567")))) + +(ert-deftest mastodon-profile--make-author-buffer () + "Should set up the buffer as expected for the given author. + +This is a far more complicated test as the +mastodon-profile--make-author-buffer function does so much. There +is a bit too much mocking and this may be brittle but it should +help identify when things change unexpectedly. + +TODO: Consider separating the data retrieval and the actual +content generation in the function under test." + (with-mock + ;; Don't start any image loading: + (mock (mastodon-media--inline-images * *) => nil) + (if (version< emacs-version "27.1") + (mock (image-type-available-p 'imagemagick) => t) + (mock (image-transforms-p) => t)) + (mock (mastodon-http--get-json "https://instance.url/api/v1/accounts/1/statuses") + => + gargon-statuses-json) + (mock (mastodon-profile--get-statuses-pinned *) + => + []) + (mock (mastodon-profile--relationships-get "1") + => + [((id . "1") (following . :json-false) (showing_reblogs . :json-false) (notifying . :json-false) (followed_by . :json-false) (blocking . :json-false) (blocked_by . :json-false) (muting . :json-false) (muting_notifications . :json-false) (requested . :json-false) (domain_blocking . :json-false) (endorsed . :json-false) (note . ""))]) + ;; Let's not do formatting as that makes it hard to not rely on + ;; window width and reflowing the text. + (mock (shr-render-region * *) => nil) + ;; Don't perform the actual update call at the end. + ;;(mock (mastodon-tl--timeline *)) + + (let ((mastodon-tl--show-avatars t) + (mastodon-tl--display-media-p t) + (mastodon-instance-url "https://instance.url")) + (mastodon-profile--make-author-buffer gargron-profile-json) + + (should + (equal + (buffer-substring-no-properties (point-min) (point-max)) + (concat + "\n" + "[img] \n" + "Eugen\n" + "@Gargron\n" + " ------------\n" + "<p>Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.</p>\n" + "_ Patreon __ :: <a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>_ Homepage _ :: <a href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">zeonfederated.com</span><span class=\"invisible\"></span></a>\n" + " ------------\n" + " TOOTS: 70741 | FOLLOWERS: 470905 | FOLLOWING: 451\n" + " ------------\n" + "\n" + " ------------\n" + " TOOTS \n" + " ------------\n" + "\n" + "<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p> \n" + " Eugen (@Gargron) 2021-11-11 12:11:11\n" + " ------------\n" + "\n" + "\n" + "<p><span class=\"h-card\"><a href=\"https://social.bau-ha.us/@CCC\" class=\"u-url mention\">@<span>CCC</span></a></span> At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p> \n" + " Eugen (@Gargron) 2021-11-11 01:00:00\n" + " ------------\n" + "\n" + ))) + + ;; Until the function gets refactored this creates a non-temp + ;; buffer with Gargron's statuses which we want to delete (if + ;; the tests succeed). + (kill-buffer)))) diff --git a/test/mastodon-search-tests.el b/test/mastodon-search-tests.el index 996f786..e6d4cdb 100644 --- a/test/mastodon-search-tests.el +++ b/test/mastodon-search-tests.el @@ -126,7 +126,10 @@ (should (equal (mastodon-search--get-user-info mastodon-search--single-account-query) - '(": ( ) { : | : & } ; :" "mousebot" "https://todon.nl/@mousebot")))) + '(": ( ) { : | : & } ; :" + "mousebot" + "https://todon.nl/@mousebot" + "<p>poetry, writing, dmt, desertion, trash, black metal, translation, hegel, language, autonomia....</p><p><a href=\"https://anarchive.mooo.com\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">anarchive.mooo.com</span><span class=\"invisible\"></span></a><br /><a href=\"https://pleasantlybabykid.tumblr.com/\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleasantlybabykid.tumblr.com/</span><span class=\"invisible\"></span></a><br />IG: <a href=\"https://bibliogram.snopyta.org/u/martianhiatus\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">bibliogram.snopyta.org/u/marti</span><span class=\"invisible\">anhiatus</span></a><br />photos alt: <span class=\"h-card\"><a href=\"https://todon.eu/@goosebot\" class=\"u-url mention\">@<span>goosebot</span></a></span><br />git: <a href=\"https://git.blast.noho.st/mouse\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">git.blast.noho.st/mouse</span><span class=\"invisible\"></span></a></p><p>want to trade chapbooks or zines? hmu!</p><p>he/him or they/them</p>")))) (ert-deftest mastodon-search--get-hashtag-info () "Should build a list of hashtag name and URL." diff --git a/test/mastodon-tl-tests.el b/test/mastodon-tl-tests.el index dd07416..a569c89 100644 --- a/test/mastodon-tl-tests.el +++ b/test/mastodon-tl-tests.el @@ -559,7 +559,7 @@ a string or a numeric." (mock (mastodon-tl--relative-time-details 'fake-timestamp) => (cons "xxx ago" soon-in-the-future)) (mock (cancel-timer 'initial-timer)) - (mock (run-at-time soon-in-the-future nil + (mock (run-at-time * nil #'mastodon-tl--update-timestamps-callback (current-buffer) nil) => 'new-timer) |