aboutsummaryrefslogtreecommitdiff
path: root/lisp/mastodon-tl.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/mastodon-tl.el')
-rw-r--r--lisp/mastodon-tl.el588
1 files changed, 465 insertions, 123 deletions
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)