aboutsummaryrefslogtreecommitdiff
path: root/lisp
diff options
context:
space:
mode:
Diffstat (limited to 'lisp')
-rw-r--r--lisp/mastodon-auth.el44
-rw-r--r--lisp/mastodon-client.el38
-rw-r--r--lisp/mastodon-http.el14
-rw-r--r--lisp/mastodon-inspect.el4
-rw-r--r--lisp/mastodon-media.el71
-rw-r--r--lisp/mastodon-tl.el581
-rw-r--r--lisp/mastodon-toot.el50
-rw-r--r--lisp/mastodon.el11
8 files changed, 670 insertions, 143 deletions
diff --git a/lisp/mastodon-auth.el b/lisp/mastodon-auth.el
index 83d7d04..e9889d9 100644
--- a/lisp/mastodon-auth.el
+++ b/lisp/mastodon-auth.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Homepage: https://github.com/jdenen/mastodon.el
;; Package-Requires: ((emacs "24.4"))
@@ -40,8 +40,11 @@
:prefix "mastodon-auth-"
:group 'mastodon)
-(defvar mastodon-auth--token nil
- "User access token.")
+(defvar mastodon-auth--token-alist nil
+ "Alist of User access tokens keyed by instance url.")
+
+(defvar mastodon-auth--acct-alist nil
+ "Alist of account accts (name@domain) keyed by instance url.")
(defun mastodon-auth--generate-token ()
"Make POST to generate auth token."
@@ -53,7 +56,8 @@
("username" . ,(read-string "Email: "))
("password" . ,(read-passwd "Password: "))
("scope" . "read write follow"))
- nil))
+ nil
+ :unauthenticated))
(defun mastodon-auth--get-token ()
"Make auth token request and return JSON response."
@@ -67,13 +71,31 @@
(json-read-from-string json-string))))
(defun mastodon-auth--access-token ()
- "Return `mastodon-auth--token'.
-
-Generate token and set `mastodon-auth--token' if nil."
- (or mastodon-auth--token
- (let* ((json (mastodon-auth--get-token))
- (token (plist-get json :access_token)))
- (setq mastodon-auth--token token))))
+ "Return the access token to use with the current `mastodon-instance-url'.
+
+Generate token and set if none known yet."
+ (let ((token
+ (cdr (assoc mastodon-instance-url mastodon-auth--token-alist))))
+ (unless token
+ (let ((json (mastodon-auth--get-token)))
+ (setq token (plist-get json :access_token))
+ (push (cons mastodon-instance-url token) mastodon-auth--token-alist)))
+ token))
+
+(defun mastodon-auth--get-account-name ()
+ "Request user credentials and return an account name."
+ (cdr (assoc
+ 'acct
+ (mastodon-http--get-json
+ (mastodon-http--api
+ "accounts/verify_credentials")))))
+
+(defun mastodon-auth--user-acct ()
+ "Return a mastodon user acct name."
+ (or (cdr (assoc mastodon-instance-url mastodon-auth--acct-alist))
+ (let ((acct (mastodon-auth--get-account-name)))
+ (push (cons mastodon-instance-url acct) mastodon-auth--acct-alist)
+ acct)))
(provide 'mastodon-auth)
;;; mastodon-auth.el ends here
diff --git a/lisp/mastodon-client.el b/lisp/mastodon-client.el
index b97197e..968cdf3 100644
--- a/lisp/mastodon-client.el
+++ b/lisp/mastodon-client.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Homepage: https://github.com/jdenen/mastodon.el
;; Package-Requires: ((emacs "24.4"))
@@ -30,6 +30,7 @@
;;; Code:
(require 'plstore)
+(defvar mastodon-instance-url)
(autoload 'mastodon-http--api "mastodon-http")
(autoload 'mastodon-http--post "mastodon-http")
@@ -39,8 +40,8 @@
:group 'mastodon
:type 'file)
-(defvar mastodon-client--client-details nil
- "Client id and secret.")
+(defvar mastodon-client--client-details-alist nil
+ "An alist of Client id and secrets keyed by the instance url.")
(defun mastodon-client--register ()
"POST client to Mastodon."
@@ -50,7 +51,8 @@
("redirect_uris" . "urn:ietf:wg:oauth:2.0:oob")
("scopes" . "read write follow")
("website" . "https://github.com/jdenen/mastodon.el"))
- nil))
+ nil
+ :unauthenticated))
(defun mastodon-client--fetch ()
"Return JSON from `mastodon-client--register' call."
@@ -72,8 +74,13 @@
Make `mastodon-client--fetch' call to determine client values."
(let ((plstore (plstore-open (mastodon-client--token-file)))
- (client (mastodon-client--fetch)))
- (plstore-put plstore "mastodon" client nil)
+ (client (mastodon-client--fetch))
+ ;; alexgriffith reported seeing ellipses in the saved output
+ ;; which indicate some output truncating. Nothing in `plstore-save'
+ ;; seems to ensure this cannot happen so let's do that ourselves:
+ (print-length nil)
+ (print-level nil))
+ (plstore-put plstore (concat "mastodon-" mastodon-instance-url) client nil)
(plstore-save plstore)
(plstore-close plstore)
client))
@@ -81,19 +88,24 @@ Make `mastodon-client--fetch' call to determine client values."
(defun mastodon-client--read ()
"Retrieve client_id and client_secret from `mastodon-client--token-file'."
(let* ((plstore (plstore-open (mastodon-client--token-file)))
- (mastodon (plstore-get plstore "mastodon")))
- (when mastodon
- (delete "mastodon" mastodon))))
+ (mastodon (plstore-get plstore (concat "mastodon-" mastodon-instance-url))))
+ (cdr mastodon)))
(defun mastodon-client ()
- "Return variable `mastodon-client--client-details' plist.
+ "Return variable client secrets to use for the current `mastodon-instance-url'..
Read plist from `mastodon-client--token-file' if variable is nil.
Fetch and store plist if `mastodon-client--read' returns nil."
- (or mastodon-client--client-details
- (setq mastodon-client--client-details
+ (let ((client-details
+ (cdr (assoc mastodon-instance-url mastodon-client--client-details-alist))))
+ (unless client-details
+ (setq client-details
(or (mastodon-client--read)
- (mastodon-client--store)))))
+ (mastodon-client--store)))
+ (push (cons mastodon-instance-url client-details)
+ mastodon-client--client-details-alist))
+ client-details))
(provide 'mastodon-client)
;;; mastodon-client.el ends here
+
diff --git a/lisp/mastodon-http.el b/lisp/mastodon-http.el
index 75cca2f..3240eef 100644
--- a/lisp/mastodon-http.el
+++ b/lisp/mastodon-http.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Package-Requires: ((emacs "24.4"))
;; Homepage: https://github.com/jdenen/mastodon.el
@@ -31,7 +31,6 @@
(require 'json)
(defvar mastodon-instance-url)
-(defvar mastodon-auth--token)
(autoload 'mastodon-auth--access-token "mastodon-auth")
(defvar mastodon-http--api-version "v1")
@@ -68,10 +67,10 @@ Open RESPONSE buffer if unsuccessful."
(funcall success)
(switch-to-buffer response))))
-(defun mastodon-http--post (url args headers)
+(defun mastodon-http--post (url args headers &optional unauthenticed-p)
"POST synchronously to URL with ARGS and HEADERS.
-Authorization header is included by default."
+Authorization header is included by default unless UNAUTHENTICED-P is non-nil."
(let ((url-request-method "POST")
(url-request-data
(when args
@@ -82,8 +81,10 @@ Authorization header is included by default."
args
"&")))
(url-request-extra-headers
- `(("Authorization" . ,(concat "Bearer " mastodon-auth--token))
- ,headers)))
+ (append
+ (unless unauthenticed-p
+ `(("Authorization" . ,(concat "Bearer " (mastodon-auth--access-token)))))
+ headers)))
(with-temp-buffer
(url-retrieve-synchronously url))))
@@ -107,6 +108,7 @@ Pass response buffer to CALLBACK function."
(decode-coding-string
(buffer-substring-no-properties (point) (point-max))
'utf-8)))
+ (kill-buffer)
(json-read-from-string json-string)))))
json-vector))
diff --git a/lisp/mastodon-inspect.el b/lisp/mastodon-inspect.el
index a44fb2c..62a91b5 100644
--- a/lisp/mastodon-inspect.el
+++ b/lisp/mastodon-inspect.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Package-Requires: ((emacs "24.4"))
;; Homepage: https://github.com/jdenen/mastodon.el
@@ -55,7 +55,7 @@
(interactive)
(mastodon-inspect--dump-json-in-buffer
(concat "*mastodon-inspect-toot-"
- (int-to-string (mastodon-tl--property 'toot-id))
+ (mastodon-tl--as-string (mastodon-tl--property 'toot-id))
"*")
(mastodon-tl--property 'toot-json)))
diff --git a/lisp/mastodon-media.el b/lisp/mastodon-media.el
index 612fad5..2decce4 100644
--- a/lisp/mastodon-media.el
+++ b/lisp/mastodon-media.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Homepage: https://github.com/jdenen/mastodon.el
;; Package-Requires: ((emacs "24.4"))
@@ -32,6 +32,8 @@
;; required by the server and client.
;;; Code:
+(defvar url-show-status)
+
(defgroup mastodon-media nil
"Inline Mastadon media."
:prefix "mastodon-media-"
@@ -125,7 +127,8 @@ BAIQCEAgAIEABAIsJVH58WqHw8FIgjUIQCAACAQgEIBAAAIBCAQgEIBAAAIBCAQgEAAEAhAIQCBA
fKRJkmVZjAQwh78A6vCRWJE8K+8AAAAASUVORK5CYII=")
"The PNG data for a generic 200x200 'broken image' view")
-(defun mastodon-media--process-image-response (status-plist marker image-options region-length)
+(defun mastodon-media--process-image-response
+ (status-plist marker image-options region-length)
"Callback function processing the url retrieve response for URL.
STATUS-PLIST is the usual plist of status events as per `url-retrieve'.
@@ -133,31 +136,33 @@ IMAGE-OPTIONS are the precomputed options to apply to the image.
MARKER is the marker to where the response should be visible.
REGION-LENGTH is the length of the region that should be replaced with the image.
"
- (let ((url-buffer (current-buffer))
- (is-error-response-p (eq :error (car status-plist))))
- (unwind-protect
- (let* ((data (unless is-error-response-p
- (goto-char (point-min))
- (search-forward "\n\n")
- (buffer-substring (point) (point-max))))
- (image (when data
- (apply #'create-image data (when image-options 'imagemagick)
- t image-options))))
- (switch-to-buffer (marker-buffer marker))
- ;; Save narrowing in our buffer
- (let ((inhibit-read-only t))
- (save-restriction
- (widen)
- (put-text-property marker (+ marker region-length) 'media-state 'loaded)
- (when image
- ;; We only set the image to display if we could load
- ;; it; we already have set a default image when we
- ;; added the tag.
- (put-text-property marker (+ marker region-length)
- 'display image))
- ;; We are done with the marker; release it:
- (set-marker marker nil)))
- (kill-buffer url-buffer)))))
+ (when (marker-buffer marker) ; only if the buffer hasn't been kill in the meantime
+ (let ((url-buffer (current-buffer))
+ (is-error-response-p (eq :error (car status-plist))))
+ (unwind-protect
+ (let* ((data (unless is-error-response-p
+ (goto-char (point-min))
+ (search-forward "\n\n")
+ (buffer-substring (point) (point-max))))
+ (image (when data
+ (apply #'create-image data (when image-options 'imagemagick)
+ t image-options))))
+ (with-current-buffer (marker-buffer marker)
+ ;; Save narrowing in our buffer
+ (let ((inhibit-read-only t))
+ (save-restriction
+ (widen)
+ (put-text-property marker
+ (+ marker region-length) 'media-state 'loaded)
+ (when image
+ ;; We only set the image to display if we could load
+ ;; it; we already have set a default image when we
+ ;; added the tag.
+ (put-text-property marker (+ marker region-length)
+ 'display image))
+ ;; We are done with the marker; release it:
+ (set-marker marker nil)))
+ (kill-buffer url-buffer)))))))
(defun mastodon-media--load-image-from-url (url media-type start region-length)
"Takes a URL and MEDIA-TYPE and load the image asynchronously.
@@ -171,7 +176,9 @@ MEDIA-TYPE is a symbol and either 'avatar or 'media-link."
((eq media-type 'media-link)
`(:max-height ,mastodon-media--preview-max-height))))))
(let ((buffer (current-buffer))
- (marker (copy-marker start)))
+ (marker (copy-marker start))
+ ;; Keep url.el from spamming us with messages about connecting to hosts:
+ (url-show-status nil))
(condition-case nil
;; catch any errors in url-retrieve so as to not abort
;; whatever called us
@@ -180,7 +187,10 @@ MEDIA-TYPE is a symbol and either 'avatar or 'media-link."
(list marker image-options region-length))
(error (with-current-buffer buffer
;; TODO: Consider adding retries
- (put-text-property marker (+ marker region-length) 'media-state 'loading-failed)
+ (put-text-property marker
+ (+ marker region-length)
+ 'media-state
+ 'loading-failed)
:loading-failed))))))
(defun mastodon-media--select-next-media-line ()
@@ -230,7 +240,8 @@ not been returned."
(put-text-property start end 'media-state 'invalid-url)
;; proceed to load this image asynchronously
(put-text-property start end 'media-state 'loading)
- (mastodon-media--load-image-from-url image-url media-type start (- end start)))))))
+ (mastodon-media--load-image-from-url
+ image-url media-type start (- end start)))))))
(defun mastodon-media--get-avatar-rendering (avatar-url)
"Returns the string to be written that renders the avatar at AVATAR-URL."
diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el
index 66452dd..252cefd 100644
--- a/lisp/mastodon-tl.el
+++ b/lisp/mastodon-tl.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Homepage: https://github.com/jdenen/mastodon.el
;; Package-Requires: ((emacs "24.4"))
@@ -40,19 +40,130 @@
(autoload 'mastodon-media--inline-images "mastodon-media")
(autoload 'mastodon-mode "mastodon")
(defvar mastodon-toot-timestamp-format)
+(defvar shr-use-fonts) ;; need to declare it since Emacs24 didn't have this
(defgroup mastodon-tl nil
"Timelines in Mastodon."
:prefix "mastodon-tl-"
:group 'mastodon)
+(defcustom mastodon-tl--enable-relative-timestamps t
+ "Nonnil to enable showing relative (to the current time) timestamps.
+
+This will require periodic updates of a timeline buffer to
+keep the timestamps current as time progresses."
+ :group 'mastodon-tl
+ :type '(boolean :tag "Enable relative timestamps and background updater task"))
+
+(defcustom mastodon-tl--enable-proportional-fonts nil
+ "Nonnil to enable using proportional fonts when rendering HTML.
+
+By default fixed width fonts are used."
+ :group 'mastodon-tl
+ :type '(boolean :tag "Enable using proportional rather than fixed \
+width fonts when rendering HTML text"))
+
(defvar mastodon-tl--buffer-spec nil
"A unique identifier and functions for each Mastodon buffer.")
+(make-variable-buffer-local 'mastodon-tl--buffer-spec)
(defvar mastodon-tl--show-avatars-p
(image-type-available-p 'imagemagick)
"A boolean value stating whether to show avatars in timelines.")
+(defvar mastodon-tl--display-media-p t
+ "A boolean value stating whether to show media in timelines.")
+
+(defvar mastodon-tl--timestamp-next-update nil
+ "The timestamp when the buffer should next be scanned to update the timestamps.")
+(make-variable-buffer-local 'mastodon-tl--timestamp-next-update)
+
+(defvar mastodon-tl--timestamp-update-timer nil
+ "The timer that, when set will scan the buffer to update the timestamps.")
+(make-variable-buffer-local 'mastodon-tl--timestamp-update-timer)
+
+(defvar mastodon-tl--link-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map [return] 'mastodon-tl--do-link-action-at-point)
+ (define-key map [mouse-2] 'mastodon-tl--do-link-action)
+ (define-key map [follow-link] 'mouse-face)
+ (define-key map [tab] 'mastodon-tl--next-tab-item)
+ (define-key map [M-tab] 'mastodon-tl--previous-tab-item)
+ (define-key map [S-tab] 'mastodon-tl--previous-tab-item)
+ (define-key map [backtab] 'mastodon-tl--previous-tab-item)
+ (keymap-canonicalize map))
+ "The keymap set for things in the buffer that act like links (except for shr.el generate links).
+
+This will make the region of text act like like a link with mouse
+highlighting, mouse click action tabbing to next/previous link
+etc.")
+
+(defvar mastodon-tl--shr-map-replacement
+ (let ((map (copy-keymap shr-map)))
+ ;; Replace the move to next/previous link bindings with our
+ ;; version that knows about more types of links.
+ (define-key map [remap shr-next-link] 'mastodon-tl--next-tab-item)
+ (define-key map [remap shr-previous-link] 'mastodon-tl--previous-tab-item)
+ (keymap-canonicalize map))
+ "The keymap to be set for shr.el generated links that are not images.
+
+We need to override the keymap so tabbing will navigate to all
+types of mastodon links and not just shr.el-generated ones.")
+
+(defvar mastodon-tl--shr-image-map-replacement
+ (let ((map (copy-keymap (if (boundp 'shr-image-map)
+ shr-image-map
+ shr-map))))
+ ;; Replace the move to next/previous link bindings with our
+ ;; version that knows about more types of links.
+ (define-key map [remap shr-next-link] 'mastodon-tl--next-tab-item)
+ (define-key map [remap shr-previous-link] 'mastodon-tl--previous-tab-item)
+ (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.")
+
+(defun mastodon-tl--next-tab-item ()
+ "Move to the next interesting item.
+
+This could be the next toot, link, or image; whichever comes first.
+Don't move if nothing else to move to is found, i.e. near the end of the buffer.
+This also skips tab items in invisible text, i.e. hidden spoiler text."
+ (interactive)
+ (let (next-range
+ (search-pos (point)))
+ (while (and (setq next-range (mastodon-tl--find-next-or-previous-property-range
+ 'mastodon-tab-stop search-pos nil))
+
+ (get-text-property (car next-range) 'invisible)
+ (setq search-pos (1+ (cdr next-range))))
+ ;; do nothing, all the action in in the while condition
+ )
+ (if (null next-range)
+ (message "Nothing else here.")
+ (goto-char (car next-range))
+ (message "%s" (get-text-property (point) 'help-echo)))))
+
+(defun mastodon-tl--previous-tab-item ()
+ "Move to the previous interesting item.
+
+This could be the previous toot, link, or image; whichever comes first.
+Don't move if nothing else to move to is found, i.e. near the start of the buffer.
+This also skips tab items in invisible text, i.e. hidden spoiler text."
+ (interactive)
+ (let (next-range
+ (search-pos (point)))
+ (while (and (setq next-range (mastodon-tl--find-next-or-previous-property-range
+ 'mastodon-tab-stop search-pos t))
+ (get-text-property (car next-range) 'invisible)
+ (setq search-pos (1- (car next-range))))
+ ;; do nothing, all the action in in the while condition
+ )
+ (if (null next-range)
+ (message "Nothing else before this.")
+ (goto-char (car next-range))
+ (message "%s" (get-text-property (point) 'help-echo)))))
(defun mastodon-tl--get-federated-timeline ()
"Opens federated timeline."
@@ -120,8 +231,10 @@ Optionally start from POS."
(handle (cdr (assoc 'acct account)))
(name (cdr (assoc 'display_name account)))
(avatar-url (cdr (assoc 'avatar account))))
+ ;; TODO: Once we have a view for a user (e.g. their posts
+ ;; timeline) make this a tab-stop and attach an action
(concat
- (when mastodon-tl--show-avatars-p
+ (when (and mastodon-tl--show-avatars-p mastodon-tl--display-media-p)
(mastodon-media--get-avatar-rendering avatar-url))
(propertize name 'face 'mastodon-display-name-face)
(propertize (concat " (@"
@@ -146,54 +259,204 @@ Return value from boosted content if available."
(or (cdr (assoc field (cdr (assoc 'reblog toot))))
(cdr (assoc field toot))))
+(defun mastodon-tl--relative-time-details (timestamp &optional current-time)
+ "Returns cons of (descriptive string . next change) for the TIMESTAMP.
+
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+
+The descriptive string is a human readable version relative to
+the current time while the next change timestamp give the first
+time that this description will change in the future.
+
+TIMESTAMP is assumed to be in the past."
+ (let* ((now (or current-time (current-time)))
+ (time-difference (time-subtract now timestamp))
+ (seconds-difference (float-time time-difference))
+ (regular-response
+ (lambda (seconds-difference multiplier unit-name)
+ (let ((n (floor (+ 0.5 (/ seconds-difference multiplier)))))
+ (cons (format "%d %ss ago" n unit-name)
+ (* (+ 0.5 n) multiplier)))))
+ (relative-result
+ (cond
+ ((< seconds-difference 60)
+ (cons "less than a minute ago"
+ 60))
+ ((< seconds-difference (* 1.5 60))
+ (cons "one minute ago"
+ 90)) ;; at 90 secs
+ ((< seconds-difference (* 60 59.5))
+ (funcall regular-response seconds-difference 60 "minute"))
+ ((< seconds-difference (* 1.5 60 60))
+ (cons "one hour ago"
+ (* 60 90))) ;; at 90 minutes
+ ((< seconds-difference (* 60 60 23.5))
+ (funcall regular-response seconds-difference (* 60 60) "hour"))
+ ((< seconds-difference (* 1.5 60 60 24))
+ (cons "one day ago"
+ (* 1.5 60 60 24))) ;; at a day and a half
+ ((< seconds-difference (* 60 60 24 6.5))
+ (funcall regular-response seconds-difference (* 60 60 24) "day"))
+ ((< seconds-difference (* 1.5 60 60 24 7))
+ (cons "one week ago"
+ (* 1.5 60 60 24 7))) ;; a week and a half
+ ((< seconds-difference (* 60 60 24 7 52))
+ (if (= 52 (floor (+ 0.5 (/ seconds-difference 60 60 24 7))))
+ (cons "52 weeks ago"
+ (* 60 60 24 7 52))
+ (funcall regular-response seconds-difference (* 60 60 24 7) "week")))
+ ((< seconds-difference (* 1.5 60 60 24 365))
+ (cons "one year ago"
+ (* 60 60 24 365 1.5))) ;; a year and a half
+ (t
+ (funcall regular-response seconds-difference (* 60 60 24 365.25) "year")))))
+ (cons (car relative-result)
+ (time-add timestamp (seconds-to-time (cdr relative-result))))))
+
+(defun mastodon-tl--relative-time-description (timestamp &optional current-time)
+ "Returns a string with a human readable description of TIMESTMAP relative to the current time.
+
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+
+E.g. this could return something like \"1 min ago\", \"yesterday\", etc.
+TIME-STAMP is assumed to be in the past."
+ (car (mastodon-tl--relative-time-details timestamp current-time)))
+
(defun mastodon-tl--byline (toot)
"Generate byline for TOOT."
(let ((id (cdr (assoc 'id toot)))
- (timestamp (mastodon-tl--field 'created_at toot))
- (faved (mastodon-tl--field 'favourited toot))
- (boosted (mastodon-tl--field 'reblogged toot)))
+ (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))))
+ (concat
+ (propertize "\n | " 'face 'default)
+ (propertize
+ (concat (when boosted
+ (format "(%s) "
+ (propertize "B" 'face 'mastodon-boost-fave-face)))
+ (when faved
+ (format "(%s) "
+ (propertize "F" 'face 'mastodon-boost-fave-face)))
+ (mastodon-tl--byline-author toot)
+ (mastodon-tl--byline-boosted 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 ------------" 'face 'default))
+ 'favourited-p faved
+ 'boosted-p boosted
+ 'toot-id id
+ 'toot-json toot))))
+
+(defun mastodon-tl--render-text (string)
+ "Returns a propertized text giving the rendering of the given HTML string."
+ (with-temp-buffer
+ (insert string)
+ (let ((shr-use-fonts mastodon-tl--enable-proportional-fonts)
+ (shr-width (when mastodon-tl--enable-proportional-fonts
+ (window-width))))
+ (shr-render-region (point-min) (point-max)))
+ ;; Make all links a tab stop recognized by our own logic and
+ ;; update keymaps where needed.
+ ;;
+ ;; TODO: Once we have views for users and tags we need to
+ ;; recognize these links and turn them into links to our own
+ ;; views.
+ (let (region)
+ (while (setq region (mastodon-tl--find-property-range
+ 'shr-url (or (cdr region) (point-min))))
+ (let* ((start (car region))
+ (end (cdr region))
+ (keymap (if (eq shr-map (get-text-property start 'keymap))
+ mastodon-tl--shr-map-replacement
+ mastodon-tl--shr-image-map-replacement)))
+ (add-text-properties start end
+ (list 'mastodon-tab-stop 'shr-url
+ 'keymap keymap)))))
+ (buffer-string)))
+
+(defun mastodon-tl--set-face (string face)
+ "Returns the propertized STRING with the face property set to FACE."
+ (propertize string 'face face))
+
+(defun mastodon-tl--toggle-spoiler-text (position)
+ "Toggle the visibility of the spoiler text at/after POSITION."
+ (let ((inhibit-read-only t)
+ (spoiler-text-region (mastodon-tl--find-property-range
+ 'mastodon-content-warning-body position nil)))
+ (if (not spoiler-text-region)
+ (message "No spoiler text here")
+ (add-text-properties (car spoiler-text-region) (cdr spoiler-text-region)
+ (list 'invisible
+ (not (get-text-property (car spoiler-text-region)
+ 'invisible)))))))
+(defun mastodon-tl--make-link (string link-type)
+ "Return a propertized version of STRING that will act like link.
+
+LINK-TYPE is the type of link to produce."
+ (let ((help-text (cond
+ ((eq link-type 'content-warning)
+ "Toggle hidden text")
+ (t
+ (error "unknown link type %s" link-type)))))
(propertize
- (concat (propertize "\n | " 'face 'default)
- (when boosted
- (format "(%s) "
- (propertize "B" 'face 'mastodon-boost-fave-face)))
- (when faved
- (format "(%s) "
- (propertize "F" 'face 'mastodon-boost-fave-face)))
- (mastodon-tl--byline-author toot)
- (mastodon-tl--byline-boosted toot)
- " "
- (format-time-string mastodon-toot-timestamp-format (date-to-time timestamp))
- (propertize "\n ------------" 'face 'default))
- 'favourited-p faved
- 'boosted-p boosted
- 'toot-id id
- 'toot-json toot)))
-
-(defun mastodon-tl--set-face (string face render)
- "Set the face of a string. If `render' is not nil
-also render the html"
- (propertize
- (with-temp-buffer
- (insert string)
- (when render
- (let ((shr-use-fonts nil))
- (shr-render-region (point-min) (point-max))))
- (buffer-string))
- 'face face))
+ string
+ 'mastodon-tab-stop link-type
+ 'mouse-face 'highlight
+ 'keymap mastodon-tl--link-keymap
+ 'help-echo help-text)))
+
+(defun mastodon-tl--do-link-action-at-point (position)
+ (interactive "d")
+ (let ((link-type (get-text-property position 'mastodon-tab-stop)))
+ (cond ((eq link-type 'content-warning)
+ (mastodon-tl--toggle-spoiler-text position))
+ (t
+ (error "unknown link type %s" link-type)))))
+
+(defun mastodon-tl--do-link-action (event)
+ (interactive "e")
+ (mastodon-tl--do-link-action-at-point (posn-point (event-end event))))
+
+(defun mastodon-tl--has-spoiler (toot)
+ "Check if the given TOOT has a spoiler text that should initially be shown only while the main content should be hidden."
+ (let ((spoiler (mastodon-tl--field 'spoiler_text toot)))
+ (and spoiler (> (length spoiler) 0))))
(defun mastodon-tl--spoiler (toot)
- "Retrieve spoiler message from TOOT."
+ "Render TOOT with spoiler message.
+
+This assumes TOOT is a toot with a spoiler message.
+The main body gets hidden and only the spoiler text and the
+content warning message are displayed. The content warning
+message is a link which unhides/hides the main body."
(let* ((spoiler (mastodon-tl--field 'spoiler_text toot))
- (string (mastodon-tl--set-face spoiler 'default t))
- (message (concat "\n ---------------"
- "\n Content Warning"
- "\n ---------------\n"))
- (cw (mastodon-tl--set-face message 'mastodon-cw-face nil)))
- (if (> (length string) 0)
- (replace-regexp-in-string "\n\n\n ---------------"
- "\n ---------------" (concat string cw))
- "")))
+ (string (mastodon-tl--set-face
+ ;; remove trailing whitespace
+ (replace-regexp-in-string "[\t\n ]*\\'" ""
+ (mastodon-tl--render-text spoiler))
+ 'default))
+ (message (concat "\n"
+ " ---------------\n"
+ " " (mastodon-tl--make-link "Content Warning"
+ 'content-warning)
+ "\n"
+ " ---------------\n"))
+ (cw (mastodon-tl--set-face message 'mastodon-cw-face)))
+ (concat
+ string
+ cw
+ (propertize (mastodon-tl--content toot)
+ 'invisible t
+ 'mastodon-content-warning-body t))))
(defun mastodon-tl--media (toot)
"Retrieve a media attachment link for TOOT if one exists."
@@ -202,32 +465,35 @@ also render the html"
(lambda (media-attachement)
(let ((preview-url
(cdr (assoc 'preview_url media-attachement))))
- (mastodon-media--get-media-link-rendering
- preview-url)))
+ (if mastodon-tl--display-media-p
+ (mastodon-media--get-media-link-rendering
+ preview-url)
+ (concat "Media::" preview-url "\n"))))
media-attachements "")))
- (if (not (equal media-string ""))
- (concat "\n" media-string ) "")))
+ (if (not (and mastodon-tl--display-media-p
+ (equal media-string "")))
+ (concat "\n" media-string)
+ "")))
(defun mastodon-tl--content (toot)
"Retrieve text content from TOOT."
- (let ((content (mastodon-tl--field 'content toot))
- (shr-use-fonts nil))
- (propertize (with-temp-buffer
- (insert content)
- (shr-render-region (point-min) (point-max))
- (buffer-string))
- 'face 'default)))
+ (let ((content (mastodon-tl--field 'content toot)))
+ (concat
+ (mastodon-tl--render-text content)
+ (mastodon-tl--media toot))))
(defun mastodon-tl--toot (toot)
"Display TOOT content and byline."
(insert
(concat
- (mastodon-tl--spoiler toot)
- ;; remove two trailing newlines
- (substring (mastodon-tl--content toot) 0 -2)
- (mastodon-tl--media toot)
- "\n\n"
+ ;; remove trailing whitespace
+
+ (replace-regexp-in-string
+ "[\t\n ]*\\'" ""
+ (if (mastodon-tl--has-spoiler toot)
+ (mastodon-tl--spoiler toot)
+ (mastodon-tl--content toot)))
(mastodon-tl--byline toot)
"\n\n")))
@@ -237,7 +503,8 @@ also render the html"
(goto-char (point-min))
(while (search-forward "\n\n\n | " nil t)
(replace-match "\n | "))
- (mastodon-media--inline-images))
+ (when mastodon-tl--display-media-p
+ (mastodon-media--inline-images)))
(defun mastodon-tl--get-update-function (&optional buffer)
"Get the UPDATE-FUNCTION stored in `mastodon-tl--buffer-spec'"
@@ -267,7 +534,7 @@ also render the html"
"&"
"?")
"max_id="
- (number-to-string id)))))
+ (mastodon-tl--as-string id)))))
(mastodon-http--get-json url)))
;; TODO
@@ -280,7 +547,7 @@ also render the html"
"&"
"?")
"since_id="
- (number-to-string id)))))
+ (mastodon-tl--as-string id)))))
(mastodon-http--get-json url)))
(defun mastodon-tl--property (prop &optional backward)
@@ -304,21 +571,47 @@ Move forward (down) the timeline unless BACKWARD is non-nil."
(goto-char (point-max))
(mastodon-tl--property 'toot-id t))
+(defun mastodon-tl--as-string(numeric)
+ "Convert NUMERIC to string."
+ (cond ((numberp numeric)
+ (number-to-string numeric))
+ ((stringp numeric) numeric)
+ (t (error
+ "Numeric:%s must be either a string or a number"
+ numeric))))
+
+(defun mastodon-tl--toot-id (json)
+ "Find approproiate toot id in JSON.
+
+If the toot has been boosted use the id found in the
+reblog portion of the toot. Otherwise, use the body of
+the toot. This is the same behaviour as the mastodon.social
+webapp"
+ (let ((id (cdr (assoc 'id json)))
+ (reblog (cdr (assoc 'reblog json))))
+ (if reblog (cdr (assoc 'id reblog)) id)))
+
(defun mastodon-tl--thread ()
"Open thread buffer for toot under `point'."
(interactive)
- (let* ((id (number-to-string (mastodon-tl--property 'toot-id)))
+ (let* ((id (mastodon-tl--as-string (mastodon-tl--toot-id
+ (mastodon-tl--property 'toot-json))))
(url (mastodon-http--api (format "statuses/%s/context" id)))
(buffer (format "*mastodon-thread-%s*" id))
(toot (mastodon-tl--property 'toot-json))
(context (mastodon-http--get-json url)))
(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."))))
(mastodon-tl--timeline (vconcat
(cdr (assoc 'ancestors context))
`(,toot)
- (cdr (assoc 'descendants context)))))
- (mastodon-mode)))
+ (cdr (assoc 'descendants context)))))))
(defun mastodon-tl--more ()
"Append older toots to timeline."
@@ -334,6 +627,148 @@ Move forward (down) the timeline unless BACKWARD is non-nil."
(funcall update-function json)
(goto-char point-before)))))
+(defun mastodon-tl--find-property-range (property start-point &optional search-backwards)
+ " Returns `nil` if no such range is found.
+
+If PROPERTY is set at START-POINT returns a range around
+START-POINT otherwise before/after START-POINT.
+
+SEARCH-BACKWARDS determines whether we pick point
+before (non-nil) or after (nil)"
+ (if (get-text-property start-point property)
+ ;; We are within a range, so look backwards for the start:
+ (cons (previous-single-property-change
+ (if (equal start-point (point-max)) start-point (1+ start-point))
+ property nil (point-min))
+ (next-single-property-change start-point property nil (point-max)))
+ (if search-backwards
+ (let* ((end (or (previous-single-property-change
+ (if (equal start-point (point-max))
+ start-point (1+ start-point))
+ property)
+ ;; we may either be just before the range or there
+ ;; is nothing at all
+ (and (not (equal start-point (point-min)))
+ (get-text-property (1- start-point) property)
+ start-point)))
+ (start (and
+ end
+ (previous-single-property-change end property nil (point-min)))))
+ (when end
+ (cons start end)))
+ (let* ((start (next-single-property-change start-point property))
+ (end (and start
+ (next-single-property-change start property nil (point-max)))))
+ (when start
+ (cons start end))))))
+
+(defun mastodon-tl--find-next-or-previous-property-range
+ (property start-point search-backwards)
+ "Finds (start . end) range after/before START-POINT where PROPERTY is set to a consistent value (different from the value at START-POINT if that is set).
+
+Returns nil if no such range exists.
+
+If SEARCH-BACKWARDS is non-nil it find a region before
+START-POINT otherwise after START-POINT.
+"
+ (if (get-text-property start-point property)
+ ;; We are within a range, we need to start the search from
+ ;; before/after this range:
+ (let ((current-range (mastodon-tl--find-property-range property start-point)))
+ (if search-backwards
+ (unless (equal (car current-range) (point-min))
+ (mastodon-tl--find-property-range
+ property (1- (car current-range)) search-backwards))
+ (unless (equal (cdr current-range) (point-max))
+ (mastodon-tl--find-property-range
+ property (1+ (cdr current-range)) search-backwards))))
+ ;; If we are not within a range, we can just defer to
+ ;; mastodon-tl--find-property-range directly.
+ (mastodon-tl--find-property-range property start-point search-backwards)))
+
+(defun mastodon-tl--consider-timestamp-for-updates (timestamp)
+ "Take note that TIMESTAMP is used in buffer and ajust timers as needed.
+
+This calculates the next time the text for TIMESTAMP will change
+and may adjust existing or future timer runs should that time
+before current plans to run the update function.
+
+The adjustment is only made if it is significantly (a few
+seconds) before the currently scheduled time. This helps reduce
+the number of occasions where we schedule an update only to
+schedule the next one on completion to be within a few seconds.
+
+If relative timestamps are
+disabled (`mastodon-tl--enable-relative-timestamps` is nil) this
+is a no-op."
+ (when mastodon-tl--enable-relative-timestamps
+ (let ((this-update (cdr (mastodon-tl--relative-time-details timestamp))))
+ (when (time-less-p this-update
+ (time-subtract mastodon-tl--timestamp-next-update
+ (seconds-to-time 10)))
+ (setq mastodon-tl--timestamp-next-update this-update)
+ (when mastodon-tl--timestamp-update-timer
+ ;; 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
+ nil ;; don't repeat
+ #'mastodon-tl--update-timestamps-callback
+ (current-buffer) nil)))))))
+
+(defun mastodon-tl--update-timestamps-callback (buffer previous-marker)
+ "Update the next few timestamp displays in BUFFER.
+
+Start searching for more timestamps from PREVIOUS-MARKER or
+from the start if it is nil."
+ ;; only do things if the buffer hasn't been killed in the meantime
+ (when (and mastodon-tl--enable-relative-timestamps ;; should be true but just in case...
+ (buffer-live-p buffer))
+ (save-excursion
+ (with-current-buffer buffer
+ (let ((previous-timestamp (if previous-marker
+ (marker-position previous-marker)
+ (point-min)))
+ (iteration 0)
+ next-timestamp-range)
+ (if previous-marker
+ ;; This is a follow-up call to process the next batch of
+ ;; timestamps.
+ ;; Release the marker to not slow things down.
+ (set-marker previous-marker nil)
+ ;; Otherwise this is a rew run, so let's initialize the next-run time.
+ (setq mastodon-tl--timestamp-next-update (time-add (current-time)
+ (seconds-to-time 300))
+ mastodon-tl--timestamp-update-timer nil))
+ (while (and (< iteration 5)
+ (setq next-timestamp-range
+ (mastodon-tl--find-property-range 'timestamp
+ previous-timestamp)))
+ (let* ((start (car next-timestamp-range))
+ (end (cdr next-timestamp-range))
+ (timestamp (get-text-property start 'timestamp))
+ (current-display (get-text-property start 'display))
+ (new-display (mastodon-tl--relative-time-description timestamp)))
+ (unless (string= current-display new-display)
+ (let ((inhibit-read-only t))
+ (add-text-properties
+ start end (list 'display
+ (mastodon-tl--relative-time-description timestamp)))))
+ (mastodon-tl--consider-timestamp-for-updates timestamp)
+ (setq iteration (1+ iteration)
+ previous-timestamp (1+ (cdr next-timestamp-range)))))
+ (if next-timestamp-range
+ ;; schedule the next batch from the previous location to
+ ;; start very soon in the future:
+ (run-at-time 0.1 nil #'mastodon-tl--update-timestamps-callback buffer
+ (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
+ nil ;; don't repeat
+ #'mastodon-tl--update-timestamps-callback
+ buffer nil))))))))
+
(defun mastodon-tl--update ()
"Update timeline with new toots."
(interactive)
@@ -346,7 +781,6 @@ Move forward (down) the timeline unless BACKWARD is non-nil."
(goto-char (point-min))
(funcall update-function json)))))
-
(defun mastodon-tl--init (buffer-name endpoint update-function)
"Initialize BUFFER-NAME with timeline targeted by ENDPOINT.
@@ -356,14 +790,25 @@ UPDATE-FUNCTION is used to recieve more toots."
(json (mastodon-http--get-json url)))
(with-output-to-temp-buffer buffer
(switch-to-buffer buffer)
+ (setq
+ ;; Initialize with a minimal interval; we re-scan at least once
+ ;; every 5 minutes to catch any timestamps we may have missed
+ mastodon-tl--timestamp-next-update (time-add (current-time)
+ (seconds-to-time 300)))
(funcall update-function json))
(mastodon-mode)
(with-current-buffer buffer
- (make-local-variable 'mastodon-tl--buffer-spec)
(setq mastodon-tl--buffer-spec
`(buffer-name ,buffer-name
endpoint ,endpoint update-function
- ,update-function)))
+ ,update-function)
+ mastodon-tl--timestamp-update-timer
+ (when mastodon-tl--enable-relative-timestamps
+ (run-at-time mastodon-tl--timestamp-next-update
+ nil ;; don't repeat
+ #'mastodon-tl--update-timestamps-callback
+ (current-buffer)
+ nil))))
buffer))
(provide 'mastodon-tl)
diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el
index 6ec3174..5db9d32 100644
--- a/lisp/mastodon-toot.el
+++ b/lisp/mastodon-toot.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Homepage: https://github.com/jdenen/mastodon.el
;; Package-Requires: ((emacs "24.4"))
@@ -48,7 +48,6 @@
map)
"Keymap for `mastodon-toot'.")
-
(defun mastodon-toot--action-success (marker &optional rm)
"Insert MARKER with 'success face in byline.
@@ -70,7 +69,7 @@ Remove MARKER if RM is non-nil."
"Take ACTION on toot at point, then execute CALLBACK."
(let* ((id (mastodon-tl--property 'toot-id))
(url (mastodon-http--api (concat "statuses/"
- (number-to-string id)
+ (mastodon-tl--as-string id)
"/"
action))))
(let ((response (mastodon-http--post url nil nil)))
@@ -79,7 +78,8 @@ Remove MARKER if RM is non-nil."
(defun mastodon-toot--toggle-boost ()
"Boost/unboost toot at `point'."
(interactive)
- (let* ((id (mastodon-tl--property 'toot-id))
+ (let* ((id (mastodon-tl--as-string
+ (mastodon-tl--property 'toot-id)))
(boosted (get-text-property (point) 'boosted-p))
(action (if boosted "unreblog" "reblog"))
(msg (if boosted "unboosted" "boosted"))
@@ -87,19 +87,20 @@ Remove MARKER if RM is non-nil."
(mastodon-toot--action action
(lambda ()
(mastodon-toot--action-success "B" remove)
- (message (format "%s #%d" msg id))))))
+ (message (format "%s #%s" msg id))))))
(defun mastodon-toot--toggle-favourite ()
"Favourite/unfavourite toot at `point'."
(interactive)
- (let* ((id (mastodon-tl--property 'toot-id))
+ (let* ((id (mastodon-tl--as-string
+ (mastodon-tl--property 'toot-id)))
(faved (get-text-property (point) 'favourited-p))
(action (if faved "unfavourite" "favourite"))
(remove (when faved t)))
(mastodon-toot--action action
(lambda ()
(mastodon-toot--action-success "F" remove)
- (message (format "%sd #%d" action id))))))
+ (message (format "%s #%s" action id))))))
(defun mastodon-toot--kill ()
"Kill `mastodon-toot-mode' buffer and window.
@@ -140,14 +141,40 @@ Set `mastodon-toot--content-warning' to nil."
(mastodon-http--triage response
(lambda () (message "Toot toot!"))))))
+(defun mastodon-toot--process-local (acct)
+ "Adds domain to local ACCT and replaces the curent user name with \"\".
+
+Mastodon requires the full user@domain, even in the case of local accts.
+eg. \"user\" -> \"user@local.social \" (when local.social is the domain of the
+mastodon-instance-url).
+eg. \"yourusername\" -> \"\"
+eg. \"feduser@fed.social\" -> \"feduser@fed.social\" "
+ (cond ((string-match-p "@" acct) (concat "@" acct " ")) ; federated acct
+ ((string= (mastodon-auth--user-acct) acct) "") ; your acct
+ (t (concat "@" acct "@" ; local acct
+ (cadr (split-string mastodon-instance-url "/" t)) " "))))
+
+(defun mastodon-toot--mentions (status)
+ "Extract mentions from STATUS and process them into a string."
+ (interactive)
+ (let ((mentions (cdr (assoc 'mentions status))))
+ (mapconcat (lambda(x) (mastodon-toot--process-local
+ (cdr (assoc 'acct x))))
+ ;; reverse does not work on vectors in 24.5
+ (reverse (append mentions nil))
+ "")))
+
(defun mastodon-toot--reply ()
"Reply to toot at `point'."
(interactive)
(let* ((toot (mastodon-tl--property 'toot-json))
- (id (number-to-string (mastodon-tl--field 'id toot)))
+ (id (mastodon-tl--as-string (mastodon-tl--field 'id toot)))
(account (mastodon-tl--field 'account toot))
- (user (cdr (assoc 'username account))))
- (mastodon-toot user id)))
+ (user (cdr (assoc 'acct account)))
+ (mentions (mastodon-toot--mentions toot)))
+ (mastodon-toot (when user (concat (mastodon-toot--process-local user)
+ mentions))
+ id)))
(defun mastodon-toot--toggle-warning ()
"Toggle `mastodon-toot--content-warning'."
@@ -209,7 +236,8 @@ e.g. mastodon-toot--send -> Send."
"If REPLY-TO-USER is provided, inject their handle into the message.
If REPLY-TO-ID is provided, set the MASTODON-TOOT--REPLY-TO-ID var."
(when reply-to-user
- (insert (format "@%s " reply-to-user))
+ (insert (format "%s " reply-to-user))
+ (make-variable-buffer-local 'mastodon-toot--reply-to-id)
(setq mastodon-toot--reply-to-id reply-to-id)))
(defun mastodon-toot--compose-buffer (reply-to-user reply-to-id)
diff --git a/lisp/mastodon.el b/lisp/mastodon.el
index c031774..7f02295 100644
--- a/lisp/mastodon.el
+++ b/lisp/mastodon.el
@@ -2,7 +2,7 @@
;; Copyright (C) 2017 Johnson Denen
;; Author: Johnson Denen <johnson.denen@gmail.com>
-;; Version: 0.7.1
+;; Version: 0.7.2
;; Package-Requires: ((emacs "24.4"))
;; Homepage: https://github.com/jdenen/mastodon.el
@@ -38,6 +38,8 @@
(autoload 'mastodon-tl--get-tag-timeline "mastodon-tl")
(autoload 'mastodon-tl--goto-next-toot "mastodon-tl")
(autoload 'mastodon-tl--goto-prev-toot "mastodon-tl")
+(autoload 'mastodon-tl--next-tab-item "mastodon-tl")
+(autoload 'mastodon-tl--previous-tab-item "mastodon-tl")
(autoload 'mastodon-tl--thread "mastodon-tl")
(autoload 'mastodon-tl--update "mastodon-tl")
(autoload 'mastodon-toot--compose-buffer "mastodon-toot")
@@ -131,7 +133,12 @@ If REPLY-TO-ID is non-nil, attach new toot to a conversation."
(define-key map (kbd "r") #'mastodon-toot--reply)
(define-key map (kbd "t") #'mastodon-tl--thread)
(define-key map (kbd "T") #'mastodon-tl--get-tag-timeline)
- (define-key map (kbd "u") #'mastodon-tl--update)))
+ (define-key map (kbd "u") #'mastodon-tl--update)
+ (define-key map [?\t] #'mastodon-tl--next-tab-item)
+ (define-key map [backtab] #'mastodon-tl--previous-tab-item)
+ (define-key map [?\S-\t] #'mastodon-tl--previous-tab-item)
+ (define-key map [?\M-\t] #'mastodon-tl--previous-tab-item)
+ ))
(with-eval-after-load 'mastodon
(when (require 'discover nil :noerror)