diff options
Diffstat (limited to 'lisp/mastodon-tl.el')
-rw-r--r-- | lisp/mastodon-tl.el | 1445 |
1 files changed, 365 insertions, 1080 deletions
diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index 8197315..dc538a9 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -1,4 +1,4 @@ -;;; mastodon-tl.el --- HTTP request/response functions for mastodon.el -*- lexical-binding: t -*- +;;; mastodon-tl.el --- Timeline functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017-2019 Johnson Denen ;; Copyright (C) 2020-2022 Marty Hiatt @@ -38,53 +38,50 @@ (require 'time-date) (require 'cl-lib) (require 'mastodon-iso) - (require 'mpv nil :no-error) +(autoload 'mastodon-auth--get-account-id "mastodon-auth") (autoload 'mastodon-auth--get-account-name "mastodon-auth") (autoload 'mastodon-http--api "mastodon-http") +(autoload 'mastodon-http--build-array-params-alist "mastodon-http") +(autoload 'mastodon-http--build-params-string "mastodon-http") +(autoload 'mastodon-http--delete "mastodon-http") (autoload 'mastodon-http--get-json "mastodon-http") +(autoload 'mastodon-http--get-json-async "mastodon-http") +(autoload 'mastodon-http--get-response-async "mastodon-http") +(autoload 'mastodon-http--post "mastodon-http") +(autoload 'mastodon-http--process-json "mastodon-http") +(autoload 'mastodon-http--put "mastodon-http") +(autoload 'mastodon-http--triage "mastodon-http") (autoload 'mastodon-media--get-avatar-rendering "mastodon-media") (autoload 'mastodon-media--get-media-link-rendering "mastodon-media") (autoload 'mastodon-media--inline-images "mastodon-media") (autoload 'mastodon-mode "mastodon") +(autoload 'mastodon-notifications--filter-types-list "mastodon-notifications") +(autoload 'mastodon-notifications-get "mastodon-notifications" + "Display NOTIFICATIONS in buffer." t) ; interactive +(autoload 'mastodon-profile--account-field "mastodon-profile") (autoload 'mastodon-profile--account-from-id "mastodon-profile") +(autoload 'mastodon-profile--extract-users-handles "mastodon-profile") +(autoload 'mastodon-profile--get-preferences-pref "mastodon-profile") +(autoload 'mastodon-profile--lookup-account-in-status "mastodon-profile") (autoload 'mastodon-profile--make-author-buffer "mastodon-profile") +(autoload 'mastodon-profile--my-profile "mastodon-profile") (autoload 'mastodon-profile--search-account-by-handle "mastodon-profile") -;; mousebot adds (autoload 'mastodon-profile--toot-json "mastodon-profile") -(autoload 'mastodon-profile--account-field "mastodon-profile") -(autoload 'mastodon-profile--extract-users-handles "mastodon-profile") -(autoload 'mastodon-profile--my-profile "mastodon-profile") -(autoload 'mastodon-toot--delete-toot "mastodon-toot") -(autoload 'mastodon-http--post "mastodon-http") -(autoload 'mastodon-http--triage "mastodon-http") -(autoload 'mastodon-http--get-json-async "mastodon-http") -(autoload 'mastodon-profile--lookup-account-in-status "mastodon-profile") +(autoload 'mastodon-profile--view-author-profile "mastodon-profile") (autoload 'mastodon-profile-mode "mastodon-profile") -;; 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--propertize-user "mastodon-search") -(autoload 'mastodon-search--insert-users-propertized "mastodon-search") (autoload 'mastodon-search--get-user-info "mastodon-search") -(autoload 'mastodon-http--delete "mastodon-http") -(autoload 'mastodon-profile--view-author-profile "mastodon-profile") -(autoload 'mastodon-profile--get-preferences-pref "mastodon-profile") -(autoload 'mastodon-http--get-response-async "mastodon-http") -(autoload 'mastodon-url-lookup "mastodon") -(autoload 'mastodon-auth--get-account-id "mastodon-auth") -(autoload 'mastodon-http--put "mastodon-http") -(autoload 'mastodon-http--process-json "mastodon-http") -(autoload 'mastodon-http--build-array-params-alist "mastodon-http") -(autoload 'mastodon-http--build-params-string "mastodon-http") -(autoload 'mastodon-notifications--filter-types-list "mastodon-notifications") -(autoload 'mastodon-toot--get-toot-edits "mastodon-toot") -(autoload 'mastodon-toot--update-status-fields "mastodon-toot") +(autoload 'mastodon-search--insert-users-propertized "mastodon-search") +(autoload 'mastodon-search--propertize-user "mastodon-search") (autoload 'mastodon-toot--compose-buffer "mastodon-toot") -(autoload 'mastodon-toot--set-toot-properties "mastodon-toot") -(autoload 'mastodon-toot--schedule-toot "mastodon-toot") +(autoload 'mastodon-toot--delete-toot "mastodon-toot") +(autoload 'mastodon-toot--get-toot-edits "mastodon-toot") (autoload 'mastodon-toot--iso-to-human "mastodon-toot") +(autoload 'mastodon-toot--schedule-toot "mastodon-toot") +(autoload 'mastodon-toot--set-toot-properties "mastodon-toot") +(autoload 'mastodon-toot--update-status-fields "mastodon-toot") +(autoload 'mastodon-url-lookup "mastodon") (defvar mastodon-toot--visibility) (defvar mastodon-toot-mode) @@ -97,6 +94,9 @@ (defvar shr-use-fonts) ;; declare it since Emacs24 didn't have this (defvar mastodon-mode-map) + +;;; CUSTOMIZES + (defgroup mastodon-tl nil "Timelines in Mastodon." :prefix "mastodon-tl-" @@ -123,9 +123,6 @@ nil." :group 'mastodon-tl :type 'boolean) -(defvar-local mastodon-tl--buffer-spec nil - "A unique identifier and functions for each Mastodon buffer.") - (defcustom mastodon-tl--show-avatars nil "Whether to enable display of user avatars in timelines." :group 'mastodon-tl @@ -170,6 +167,12 @@ timeline with a simple prefix argument, `C-u'." :group 'mastodon-tl :type '(boolean :tag "Whether to hide replies from the timelines.")) + +;;; VARIABLES + +(defvar-local mastodon-tl--buffer-spec nil + "A unique identifier and functions for each Mastodon buffer.") + (defvar-local mastodon-tl--update-point nil "When updating a mastodon buffer this is where new toots will be inserted. If nil `(point-min)' is used instead.") @@ -186,7 +189,8 @@ If nil `(point-min)' is used instead.") (defvar-local mastodon-tl--timestamp-update-timer nil "The timer that, when set will scan the buffer to update the timestamps.") -;; KEYMAPS + +;;; KEYMAPS (defvar mastodon-tl--link-keymap (let ((map (make-sparse-keymap))) @@ -235,60 +239,6 @@ types of mastodon links and not just shr.el-generated ones.") 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 - (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 - (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--view-lists-keymap - (let ((map ;(make-sparse-keymap))) - (copy-keymap mastodon-mode-map))) - (define-key map (kbd "D") 'mastodon-tl--delete-list) - (define-key map (kbd "C") 'mastodon-tl--create-list) - (define-key map (kbd "A") 'mastodon-tl--add-account-to-list) - (define-key map (kbd "R") 'mastodon-tl--remove-account-from-list) - (define-key map (kbd "E") 'mastodon-tl--edit-list) - (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--view-lists) - (keymap-canonicalize map)) - "Keymap for viewing lists.") - -(defvar mastodon-tl--list-name-keymap - (let ((map (make-sparse-keymap))) - (define-key map (kbd "<return>") 'mastodon-tl--view-timeline-list-at-point) - (define-key map (kbd "d") 'mastodon-tl--delete-list-at-point) - (define-key map (kbd "a") 'mastodon-tl--add-account-to-list-at-point) - (define-key map (kbd "r") 'mastodon-tl--remove-account-from-list-at-point) - (define-key map (kbd "e") 'mastodon-tl--edit-list-at-point) - (keymap-canonicalize map)) - "Keymap for when point is on list name.") - -(defvar mastodon-tl--scheduled-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "r") 'mastodon-tl--reschedule-toot) - (define-key map (kbd "c") 'mastodon-tl--cancel-scheduled-toot) - (define-key map (kbd "e") 'mastodon-tl--edit-scheduled-as-new) - (define-key map (kbd "<return>") 'mastodon-tl--edit-scheduled-as-new) - (keymap-canonicalize map)) - "Keymap for when point is on a scheduled toot.") - (defvar mastodon-tl--byline-link-keymap (when (require 'mpv nil :no-error) (let ((map (make-sparse-keymap))) @@ -298,17 +248,8 @@ types of mastodon links and not just shr.el-generated ones.") "The keymap to be set for the author byline. It is active where point is placed by `mastodon-tl--goto-next-toot.'") -(defun mastodon-tl--symbol (name) - "Return the unicode symbol (as a string) corresponding to NAME. -If symbol is not displayable, an ASCII equivalent is returned. If -NAME is not part of the symbol table, '?' is returned." - (if-let* ((symbol (alist-get name mastodon-tl--symbols))) - (if (char-displayable-p (string-to-char (car symbol))) - (car symbol) - (cdr symbol)) - "?")) - -;; NAV + +;;; NAV (defun mastodon-tl--next-tab-item () "Move to the next interesting item. @@ -320,7 +261,6 @@ This also skips tab items in invisible text, i.e. hidden spoiler text." (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 @@ -350,11 +290,9 @@ text, i.e. hidden spoiler text." (goto-char (car next-range)) (message "%s" (get-text-property (point) 'help-echo))))) - (defun mastodon-tl--goto-toot-pos (find-pos refresh &optional pos) "Search for toot with FIND-POS. If search returns nil, execute REFRESH function. - Optionally start from POS." (let* ((npos (funcall find-pos (or pos (point)) @@ -401,7 +339,8 @@ Used on initializing a timeline or thread." (mastodon-tl--goto-toot-pos 'previous-single-property-change 'previous-line)) -;; TIMELINES + +;;; TIMELINES (defun mastodon-tl--get-federated-timeline () "Opens federated timeline." @@ -448,6 +387,9 @@ Optionally load TAG timeline directly." 'mastodon-tl--timeline nil `(("limit" . ,mastodon-tl--timeline-posts-count)))) + +;;; BYLINES, etc. + (defun mastodon-tl--message-help-echo () "Call message on 'help-echo property at point. Do so if type of status at poins is not follow_request/follow." @@ -460,12 +402,6 @@ Do so if type of status at poins is not follow_request/follow." (string= type "follow")) ; no counts for these (message "%s" (get-text-property (point) 'help-echo)))))) -(defun mastodon-tl--remove-html (toot) - "Remove unrendered tags from TOOT." - (let* ((t1 (replace-regexp-in-string "<\/p>" "\n\n" toot)) - (t2 (replace-regexp-in-string "<\/?span>" "" t1))) - (replace-regexp-in-string "<span class=\"h-card\">" "" t2))) - (defun mastodon-tl--byline-author (toot &optional avatar) "Propertize author of TOOT. With arg AVATAR, include the account's avatar image." @@ -507,12 +443,12 @@ With arg AVATAR, include the account's avatar image." (propertize (concat "@" handle) 'face 'mastodon-handle-face 'mouse-face 'highlight - 'mastodon-tab-stop 'user-handle + 'mastodon-tab-stop 'user-handle 'account account - 'shr-url profile-url - 'keymap mastodon-tl--link-keymap + 'shr-url profile-url + 'keymap mastodon-tl--link-keymap 'mastodon-handle (concat "@" handle) - 'help-echo (concat "Browse user profile of @" handle)) + 'help-echo (concat "Browse user profile of @" handle)) ")"))) (defun mastodon-tl--format-faves-count (toot) @@ -553,9 +489,7 @@ image media from the byline." (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))) + (mastodon-tl--map-alist 'type attachments))) (defun mastodon-tl--get-attachments-for-byline (toot) "Return a list of attachment URLs and types for TOOT. @@ -581,74 +515,20 @@ The result is added as an attachments property to author-byline." " " (mastodon-tl--byline-author reblog))))) -(defun mastodon-tl--field (field toot) - "Return FIELD from TOOT. -Return value from boosted content if available." - (or (alist-get field (alist-get 'reblog toot)) - (alist-get field toot))) - -(defun mastodon-tl--relative-time-details (timestamp &optional current-time) - "Return 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 "just now" - 60)) - ((< seconds-difference (* 1.5 60)) - (cons "1 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 "1 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 "1 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 "1 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 "1 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) - "Return a string with a human readable TIMESTAMP 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--format-faved-or-boosted-byline (letter) + "Format the byline marker for a boosted or favourited status. +LETTER is a string, F for favourited, B for boosted, or K for bookmarked." + (let ((help-string (cond ((equal letter "F") + "favourited") + ((equal letter "B") + "boosted") + ((equal letter (or "🔖" "K")) + "bookmarked")))) + (format "(%s) " + (propertize letter 'face 'mastodon-boost-fave-face + ;; emojify breaks this for 🔖: + 'help-echo (format "You have %s this status." + help-string))))) (defun mastodon-tl--byline (toot author-byline action-byline &optional detailed-p) "Generate byline for TOOT. @@ -657,7 +537,6 @@ the byline that takes one variable. ACTION-BYLINE is a function for adding an action, such as boosting, favouriting and following to the byline. It also takes a single function. By default it is `mastodon-tl--byline-boosted'. - DETAILED-P means display more detailed info. For now this just means displaying toot client." (let* ((created-time @@ -734,10 +613,10 @@ this just means displaying toot client." 'face 'mastodon-display-name-face 'follow-link t 'mouse-face 'highlight - 'mastodon-tab-stop 'shr-url - 'shr-url app-url + 'mastodon-tab-stop 'shr-url + 'shr-url app-url 'help-echo app-url - 'keymap mastodon-tl--shr-map-replacement))))) + 'keymap mastodon-tl--shr-map-replacement))))) (if edited-time (concat " " @@ -761,36 +640,71 @@ this just means displaying toot client." (mastodon-toot--get-toot-edits (alist-get 'id toot))) 'byline t)))) -(defun mastodon-tl--format-edit-timestamp (timestamp) - "Convert edit TIMESTAMP into a descriptive string." - (let ((parsed (ts-human-duration - (ts-diff (ts-now) (ts-parse timestamp))))) - (cond ((> (plist-get parsed :days) 0) - (format "%s days ago" (plist-get parsed :days) (plist-get parsed :hours))) - ((> (plist-get parsed :hours) 0) - (format "%s hours ago" (plist-get parsed :hours) (plist-get parsed :minutes))) - ((> (plist-get parsed :minutes) 0) - (format "%s minutes ago" (plist-get parsed :minutes))) - (t ;; we failed to guess: - (format "%s days, %s hours, %s minutes ago" - (plist-get parsed :days) - (plist-get parsed :hours) - (plist-get parsed :minutes)))))) + +;;; TIMESTAMPS -(defun mastodon-tl--format-faved-or-boosted-byline (letter) - "Format the byline marker for a boosted or favourited status. -LETTER is a string, F for favourited, B for boosted, or K for bookmarked." - (let ((help-string (cond ((equal letter "F") - "favourited") - ((equal letter "B") - "boosted") - ((equal letter (or "🔖" "K")) - "bookmarked")))) - (format "(%s) " - (propertize letter 'face 'mastodon-boost-fave-face - ;; emojify breaks this for 🔖: - 'help-echo (format "You have %s this status." - help-string))))) +(defun mastodon-tl--relative-time-details (timestamp &optional current-time) + "Return 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 "just now" + 60)) + ((< seconds-difference (* 1.5 60)) + (cons "1 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 "1 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 "1 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 "1 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 "1 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) + "Return a string with a human readable TIMESTAMP 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))) + + +;;; RENDERING HTML, LINKS, HASHTAGS, HANDLES (defun mastodon-tl--render-text (string &optional toot) "Return a propertized text rendering the given HTML string STRING. @@ -933,40 +847,8 @@ the toot)." ;; If nothing matches we assume it is not a hashtag link: (t nil))) -(defun mastodon-tl--set-face (string face) - "Return 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--toggle-spoiler-text-in-toot () - "Toggle the visibility of the spoiler text in the current toot." - (interactive) - (let* ((toot-range (or (mastodon-tl--find-property-range - 'toot-json (point)) - (mastodon-tl--find-property-range - 'toot-json (point) t))) - (spoiler-range (when toot-range - (mastodon-tl--find-property-range - 'mastodon-content-warning-body - (car toot-range))))) - (cond ((null toot-range) - (message "No toot here")) - ((or (null spoiler-range) - (> (car spoiler-range) (cdr toot-range))) - (message "No content warning text here")) - (t - (mastodon-tl--toggle-spoiler-text (car spoiler-range)))))) + +;;; HYPERLINKS (defun mastodon-tl--make-link (string link-type) "Return a propertized version of STRING that will act like link. @@ -1024,6 +906,9 @@ Used for a mouse-click EVENT on a link." (interactive "e") (mastodon-tl--do-link-action-at-point (posn-point (event-end event)))) + +;;; CONTENT WARNINGS + (defun mastodon-tl--has-spoiler (toot) "Check if the given TOOT has a spoiler text. Spoiler text should initially be shown only while the main @@ -1031,6 +916,37 @@ content should be hidden." (let ((spoiler (mastodon-tl--field 'spoiler_text toot))) (and spoiler (> (length spoiler) 0)))) +(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--toggle-spoiler-text-in-toot () + "Toggle the visibility of the spoiler text in the current toot." + (interactive) + (let* ((toot-range (or (mastodon-tl--find-property-range + 'toot-json (point)) + (mastodon-tl--find-property-range + 'toot-json (point) t))) + (spoiler-range (when toot-range + (mastodon-tl--find-property-range + 'mastodon-content-warning-body + (car toot-range))))) + (cond ((null toot-range) + (message "No toot here")) + ((or (null spoiler-range) + (> (car spoiler-range) (cdr toot-range))) + (message "No content warning text here")) + (t + (mastodon-tl--toggle-spoiler-text (car spoiler-range)))))) + (defun mastodon-tl--clean-tabs-and-nl (string) "Remove tabs and newlines from STRING." (replace-regexp-in-string @@ -1071,6 +987,9 @@ message is a link which unhides/hides the main body." t) 'mastodon-content-warning-body t)))) + +;;; MEDIA + (defun mastodon-tl--media (toot) "Retrieve a media attachment link for TOOT if one exists." (let* ((media-attachments (mastodon-tl--field 'media_attachments toot)) @@ -1136,6 +1055,9 @@ HELP-ECHO, DISPLAY, and FACE are the text properties to add." help-echo (concat help-echo "\nC-RET: play " type " with mpv")))) + +;;; INSERT TOOTS + (defun mastodon-tl--content (toot) "Retrieve text content from TOOT. Runs `mastodon-tl--render-text' and fetches poll or media." @@ -1157,17 +1079,14 @@ BODY will form the section of the toot above the byline. AUTHOR-BYLINE is an optional function for adding the author portion of the byline that takes one variable. By default it is `mastodon-tl--byline-author'. - ACTION-BYLINE is also an optional function for adding an action, such as boosting favouriting and following to the byline. It also takes a single function. By default it is `mastodon-tl--byline-boosted'. - ID is that of the status if it is a notification, which is attached as a `toot-id' property if provided. If the status is a favourite or boost notification, BASE-TOOT is the JSON of the toot responded to. - DETAILED-P means display more detailed info. For now this just means displaying toot client." (let ((start-pos (point))) @@ -1191,6 +1110,9 @@ this just means displaying toot client." (when mastodon-tl--display-media-p (mastodon-media--inline-images start-pos (point))))) + +;; POLLS + (defun mastodon-tl--get-poll (toot) "If TOOT includes a poll, return it as a formatted string." (let* ((poll (mastodon-tl--field 'poll toot)) @@ -1200,9 +1122,7 @@ this just means displaying toot client." (voters-count (mastodon-tl--field 'voters_count poll)) (vote-count (mastodon-tl--field 'votes_count poll)) (options (mastodon-tl--field 'options poll)) - (option-titles (mapcar (lambda (x) - (alist-get 'title x)) - options)) + (option-titles (mastodon-tl--map-alist 'title options)) (longest-option (car (sort option-titles (lambda (x y) (> (length x) @@ -1270,13 +1190,9 @@ this just means displaying toot client." (poll (or (alist-get 'poll reblog) (mastodon-tl--field 'poll toot))) (options (mastodon-tl--field 'options poll)) - (options-titles (mapcar (lambda (x) - (alist-get 'title x)) - options)) + (options-titles (mastodon-tl--map-alist 'title options)) (options-number-seq (number-sequence 1 (length options))) - (options-numbers (mapcar (lambda(x) - (number-to-string x)) - options-number-seq)) + (options-numbers (mapcar #'number-to-string options-number-seq)) (options-alist (cl-mapcar 'cons options-numbers options-titles)) ;; we display both option number and the option title ;; but also store both as cons cell as cdr, as we need it below @@ -1308,6 +1224,9 @@ this just means displaying toot client." (message "You voted for option %s: %s!" (car option) (cdr option))))))) + +;; VIDEOS / MPV + (defun mastodon-tl--find-first-video-in-attachments () "Return the first media attachment that is a moving image." (let ((attachments (mastodon-tl--property 'attachments)) @@ -1351,6 +1270,9 @@ in which case play first video or gif from current toot." (message "no moving image here?")) (message "no moving image here?")))) + +;; INSERT TOOTS + (defun mastodon-tl--is-reply (toot) "Check if the TOOT is a reply to another one (and not boosted)." (and (null (mastodon-tl--field 'in_reply_to_id toot)) @@ -1383,10 +1305,13 @@ This function removes replies if user required." (mastodon-tl--get-buffer-property 'hide-replies nil :no-error) ;; loading a tl with a prefix arg: (mastodon-tl--hide-replies-p current-prefix-arg)) - (cl-remove-if-not #'mastodon-tl--is-reply toots) - toots))) + (cl-remove-if-not #'mastodon-tl--is-reply toots) + toots))) (goto-char (point-min))) + +;;; BUFFER SPEC + (defun mastodon-tl--get-update-function (&optional buffer) "Get the UPDATE-FUNCTION stored in `mastodon-tl--buffer-spec'. Optionally get it for BUFFER." @@ -1443,6 +1368,9 @@ HIDE-REPLIES is a flag indicating if replies are hidden in the current buffer." update-params ,update-params hide-replies ,hide-replies))) + +;;; BUFFERS + (defun mastodon-tl--get-buffer-type () "Return a symbol descriptive of current mastodon buffer type. Should work in all mastodon buffers. @@ -1567,32 +1495,49 @@ timeline." ;; Timeline called with C-u prefix (equal '(4) prefix)))) -(defun mastodon-tl--more-json (endpoint id) - "Return JSON for timeline ENDPOINT before ID." - (let* ((args `(("max_id" . ,(mastodon-tl--as-string id)))) - (url (mastodon-http--api endpoint))) - (mastodon-http--get-json url args))) + +;;; UTILITIES -(defun mastodon-tl--more-json-async (endpoint id &optional params callback &rest cbargs) - "Return JSON for timeline ENDPOINT before ID. -Then run CALLBACK with arguments CBARGS. -PARAMS is used to send any parameters needed to correctly update -the current view." - (let* ((args `(("max_id" . ,(mastodon-tl--as-string id)))) - (args (if params (push (car args) params) args)) - (url (mastodon-http--api endpoint))) - (apply 'mastodon-http--get-json-async url args callback cbargs))) +(defun mastodon-tl--map-alist (key alist) + "Return a list of values extracted from ALIST with KEY. +Key is a symbol, as with `alist-get'." + (mapcar (lambda (x) + (alist-get key x)) + alist)) -;; TODO -;; Look into the JSON returned here by Local -(defun mastodon-tl--updated-json (endpoint id &optional params) - "Return JSON for timeline ENDPOINT since ID. -PARAMS is used to send any parameters needed to correctly update -the current view." - (let* ((args `(("since_id" . ,(mastodon-tl--as-string id)))) - (args (if params (push (car args) params) args)) - (url (mastodon-http--api endpoint))) - (mastodon-http--get-json url args))) +(defun mastodon-tl--map-alist-vals-to-alist (key1 key2 alist) + "From ALIST, return an alist consisting of (val1 . val2) elements. +Values are accessed by `alist-get', using KEY1 and KEY2." + (mapcar (lambda (x) + (cons (alist-get key1 x) + (alist-get key2 x))) + alist)) + +(defun mastodon-tl--symbol (name) + "Return the unicode symbol (as a string) corresponding to NAME. +If symbol is not displayable, an ASCII equivalent is returned. If +NAME is not part of the symbol table, '?' is returned." + (if-let* ((symbol (alist-get name mastodon-tl--symbols))) + (if (char-displayable-p (string-to-char (car symbol))) + (car symbol) + (cdr symbol)) + "?")) + +(defun mastodon-tl--set-face (string face) + "Return the propertized STRING with the face property set to FACE." + (propertize string 'face face)) + +(defun mastodon-tl--field (field toot) + "Return FIELD from TOOT. +Return value from boosted content if available." + (or (alist-get field (alist-get 'reblog toot)) + (alist-get field toot))) + +(defun mastodon-tl--remove-html (toot) + "Remove unrendered tags from TOOT." + (let* ((t1 (replace-regexp-in-string "<\/p>" "\n\n" toot)) + (t2 (replace-regexp-in-string "<\/?span>" "" t1))) + (replace-regexp-in-string "<span class=\"h-card\">" "" t2))) (defun mastodon-tl--property (prop &optional backward) "Get property PROP for toot at point. @@ -1635,6 +1580,7 @@ webapp" (reblog (alist-get 'reblog json))) (if reblog (alist-get 'id reblog) id))) + ;;; THREADS (defun mastodon-tl--single-toot (id) @@ -1646,14 +1592,15 @@ ID is that of the toot to view." (mastodon-http--api (concat "statuses/" id))))) (if (equal (caar toot) 'error) (message "Error: %s" (cdar toot)) - (with-output-to-temp-buffer buffer - (switch-to-buffer buffer) - (mastodon-mode) - (mastodon-tl--set-buffer-spec buffer - (format "statuses/%s" id) - nil) + (with-current-buffer (get-buffer-create buffer) (let ((inhibit-read-only t)) - (mastodon-tl--toot toot :detailed-p)))))) + (switch-to-buffer buffer) + (mastodon-mode) + (mastodon-tl--set-buffer-spec buffer + (format "statuses/%s" id) + nil) + (let ((inhibit-read-only t)) + (mastodon-tl--toot toot :detailed-p))))))) (defun mastodon-tl--view-whole-thread () "From a thread view, view entire thread. @@ -1698,13 +1645,13 @@ view all branches of a thread." 0) ;; if we have a thread: (progn - (with-output-to-temp-buffer buffer - (switch-to-buffer buffer) - (mastodon-mode) - (mastodon-tl--set-buffer-spec buffer - endpoint - #'mastodon-tl--thread) + (with-current-buffer (get-buffer-create buffer) (let ((inhibit-read-only t)) + (switch-to-buffer buffer) + (mastodon-mode) + (mastodon-tl--set-buffer-spec buffer + endpoint + #'mastodon-tl--thread) (mastodon-tl--timeline (alist-get 'ancestors context)) (goto-char (point-max)) (move-marker marker (point)) @@ -1717,7 +1664,6 @@ view all branches of a thread." ;; else just print the lone toot: (mastodon-tl--single-toot id))))))) - (defun mastodon-tl--mute-thread () "Mute the thread displayed in the current buffer. Note that you can only (un)mute threads you have posted in." @@ -1776,499 +1722,8 @@ ID is that of the post the context is currently displayed for." (or (member (mastodon-auth--get-account-id) a-ids) (member (mastodon-auth--get-account-id) d-ids)))) -;;; LISTS - -(defun mastodon-tl--get-users-lists () - "Get the list of the user's lists from the server." - (let ((url (mastodon-http--api "lists"))) - (mastodon-http--get-json url))) - -(defun mastodon-tl--get-lists-names () - "Return a list of the user's lists' names." - (let ((lists (mastodon-tl--get-users-lists))) - (mapcar (lambda (x) - (alist-get 'title x)) - lists))) - -(defun mastodon-tl--get-list-by-name (name) - "Return the list data for list with NAME." - (let* ((lists (mastodon-tl--get-users-lists))) - (cl-loop for list in lists - if (string= (alist-get 'title list) name) - return list))) - -(defun mastodon-tl--get-list-id (name) - "Return id for list with NAME." - (let ((list (mastodon-tl--get-list-by-name name))) - (alist-get 'id list))) - -(defun mastodon-tl--get-list-name (id) - "Return name of list with ID." - (let* ((url (mastodon-http--api (format "lists/%s" id))) - (response (mastodon-http--get-json url))) - (alist-get 'title response))) - -(defun mastodon-tl--edit-list-at-point () - "Edit list at point." - (interactive) - (let ((id (get-text-property (point) 'list-id))) - (mastodon-tl--edit-list id))) - -(defun mastodon-tl--edit-list (&optional id) - "Prompt for a list and edit the name and replies policy. -If ID is provided, use that list." - (interactive) - (let* ((list-names (unless id (mastodon-tl--get-lists-names))) - (name-old (if id - (get-text-property (point) 'list-name) - (completing-read "Edit list: " - list-names))) - (id (or id (mastodon-tl--get-list-id name-old))) - (name-choice (read-string "List name: " name-old)) - (replies-policy (completing-read "Replies policy: " ; give this a proper name - '("followed" "list" "none") - nil t nil nil "list")) - (url (mastodon-http--api (format "lists/%s" id))) - (response (mastodon-http--put url - `(("title" . ,name-choice) - ("replies_policy" . ,replies-policy))))) - (mastodon-http--triage response - (lambda () - (with-current-buffer response - (let* ((json (mastodon-http--process-json)) - (name-new (alist-get 'title json))) - (message "list %s edited to %s!" name-old name-new))) - (when (mastodon-tl--buffer-type-eq 'lists) - (mastodon-tl--view-lists)))))) - -(defun mastodon-tl--view-timeline-list-at-point () - "View timeline of list at point." - (interactive) - (let ((list-id (get-text-property (point) 'list-id))) - (mastodon-tl--view-list-timeline list-id))) - -(defun mastodon-tl--view-list-timeline (&optional id) - "Prompt for a list and view its timeline. -If ID is provided, use that list." - (interactive) - (let* ((list-names (unless id (mastodon-tl--get-lists-names))) - (list-name (unless id (completing-read "View list: " list-names))) - (id (or id (mastodon-tl--get-list-id list-name))) - (endpoint (format "timelines/list/%s" id)) - (name (mastodon-tl--get-list-name id)) - (buffer-name (format "list-%s" name))) - (mastodon-tl--init buffer-name endpoint 'mastodon-tl--timeline))) - -(defun mastodon-tl--create-list () - "Create a new list. -Prompt for name and replies policy." - (interactive) - (let* ((title (read-string "New list name: ")) - (replies-policy (completing-read "Replies policy: " ; give this a proper name - '("followed" "list" "none") - nil t nil nil "list")) ; default - (response (mastodon-http--post (mastodon-http--api "lists") - `(("title" . ,title) - ("replies_policy" . ,replies-policy)) - nil))) - (mastodon-tl--list-action-triage response - (message "list %s created!" title)))) - -(defun mastodon-tl--delete-list-at-point () - "Delete list at point." - (interactive) - (let ((id (get-text-property (point) 'list-id))) - (mastodon-tl--delete-list id))) - -(defun mastodon-tl--delete-list (&optional id) - "Prompt for a list and delete it. -If ID is provided, delete that list." - (interactive) - (let* ((list-names (unless id (mastodon-tl--get-lists-names))) - (name (if id - (mastodon-tl--get-list-name id) - (completing-read "Delete list: " - list-names))) - (id (or id (mastodon-tl--get-list-id name))) - (url (mastodon-http--api (format "lists/%s" id)))) - (when (y-or-n-p (format "Delete list %s?" name)) - (let ((response (mastodon-http--delete url))) - (mastodon-tl--list-action-triage response - (message "list %s deleted!" name)))))) - -(defun mastodon-tl--view-lists () - "Show the user's lists in a new buffer." - (interactive) - (mastodon-tl--init-sync "lists" - "lists" - 'mastodon-tl--insert-lists) - (use-local-map mastodon-tl--view-lists-keymap)) - -(defun mastodon-tl--insert-lists (_json) - "Insert the user's lists from JSON." - ;; TODO: for now we don't use the JSON, we get it ourself again - (let* ((lists-names (mastodon-tl--get-lists-names))) - (erase-buffer) - (insert (mastodon-tl--set-face - (concat "\n ------------\n" - " YOUR LISTS\n" - " ------------\n\n") - 'success) - (mastodon-tl--set-face - "[C - create a list\n D - delete a list\ -\n A/R - add/remove account from a list\ -\n E - edit a list\n n/p - go to next/prev item]\n\n" - 'font-lock-comment-face)) - (mapc (lambda (x) - (mastodon-tl--print-list-accounts x) - (insert (propertize " ------------\n\n" - 'face 'success))) - lists-names) - (goto-char (point-min)))) -;; (mastodon-tl--goto-next-item))) ; causes another request! - -(defun mastodon-tl--print-list-accounts (list-name) - "Insert the accounts in list named LIST-NAME." - (let* ((id (mastodon-tl--get-list-id list-name)) - (accounts (mastodon-tl--accounts-in-list id))) - (insert - (propertize list-name - 'byline t ; so we nav here - 'toot-id "0" ; so we nav here - 'help-echo "RET: view list timeline, d: delete this list, \ -a: add account to this list, r: remove account from this list" - 'list t - 'face 'link - 'keymap mastodon-tl--list-name-keymap - 'list-name list-name - 'list-id id) - (propertize - "\n\n" - 'list t - 'keymap mastodon-tl--list-name-keymap - 'list-name list-name - 'list-id id) - (propertize - (mapconcat #'mastodon-search--propertize-user accounts - " ") - ;; (mastodon-search--insert-users-propertized accounts) - 'list t - 'keymap mastodon-tl--list-name-keymap - 'list-name list-name - 'list-id id)))) - -(defun mastodon-tl--get-users-followings () - "Return the list of followers of the logged in account." - (let* ((id (mastodon-auth--get-account-id)) - (url (mastodon-http--api (format "accounts/%s/following" id)))) - (mastodon-http--get-json url '(("limit" . "80"))))) ; max 80 accounts - -(defun mastodon-tl--add-account-to-list-at-point () - "Prompt for account and add to list at point." - (interactive) - (let ((id (get-text-property (point) 'list-id))) - (mastodon-tl--add-account-to-list id))) - -(defun mastodon-tl--add-account-to-list (&optional id account-id handle) - "Prompt for a list and for an account, add account to list. -If ID is provided, use that list. -If ACCOUNT-ID and HANDLE are provided use them rather than prompting." - (interactive) - (let* ((list-prompt (if handle - (format "Add %s to list: " handle) - "Add account to list: ")) - (list-name (if id - (get-text-property (point) 'list-name) - (completing-read list-prompt - (mastodon-tl--get-lists-names) nil t))) - (list-id (or id (mastodon-tl--get-list-id list-name))) - (followings (mastodon-tl--get-users-followings)) - (handles (mapcar (lambda (x) - (cons (alist-get 'acct x) - (alist-get 'id x))) - followings)) - (account (or handle (completing-read "Account to add: " - handles nil t))) - (account-id (or account-id (alist-get account handles nil nil 'equal))) - (url (mastodon-http--api (format "lists/%s/accounts" list-id))) - (response (mastodon-http--post url - `(("account_ids[]" . ,account-id))))) - (mastodon-tl--list-action-triage - response - (message "%s added to list %s!" account list-name)))) - -(defun mastodon-tl--add-toot-account-at-point-to-list () - "Prompt for a list, and add the account of the toot at point to it." - (interactive) - (let* ((toot (mastodon-tl--property 'toot-json)) - (account (mastodon-tl--field 'account toot)) - (account-id (mastodon-tl--field 'id account)) - (handle (mastodon-tl--field 'acct account))) - (mastodon-tl--add-account-to-list nil account-id handle))) - -(defun mastodon-tl--remove-account-from-list-at-point () - "Prompt for account and remove from list at point." - (interactive) - (let ((id (get-text-property (point) 'list-id))) - (mastodon-tl--remove-account-from-list id))) - -(defun mastodon-tl--remove-account-from-list (&optional id) - "Prompt for a list, select an account and remove from list. -If ID is provided, use that list." - (interactive) - (let* ((list-name (if id - (get-text-property (point) 'list-name) - (completing-read "Remove account from list: " - (mastodon-tl--get-lists-names) nil t))) - (list-id (or id (mastodon-tl--get-list-id list-name))) - (accounts (mastodon-tl--accounts-in-list list-id)) - (handles (mapcar (lambda (x) - (cons (alist-get 'acct x) - (alist-get 'id x))) - accounts)) - (account (completing-read "Account to remove: " - handles nil t)) - (account-id (alist-get account handles nil nil 'equal)) - (url (mastodon-http--api (format "lists/%s/accounts" list-id))) - (args (mastodon-http--build-array-params-alist "account_ids[]" `(,account-id))) - (response (mastodon-http--delete url args))) - (mastodon-tl--list-action-triage - response - (message "%s removed from list %s!" account list-name)))) - -(defun mastodon-tl--list-action-triage (response message) - "Call `mastodon-http--triage' on RESPONSE and display MESSAGE." - (mastodon-http--triage response - (lambda () - (when (mastodon-tl--buffer-type-eq 'lists) - (mastodon-tl--view-lists)) - message))) - -(defun mastodon-tl--accounts-in-list (list-id) - "Return the JSON of the accounts in list with LIST-ID." - (let* ((url (mastodon-http--api (format "lists/%s/accounts" list-id)))) - (mastodon-http--get-json url))) - -;;; SCHEDULED TOOTS - -(defun mastodon-tl--get-scheduled-toots (&optional id) - "Get the user's currently scheduled toots. -If ID, just return that toot." - (let* ((endpoint (if id - (format "scheduled_statuses/%s" id) - "scheduled_statuses")) - (url (mastodon-http--api endpoint))) - (mastodon-http--get-json url))) - -(defun mastodon-tl--reschedule-toot () - "Reschedule the scheduled toot at point." - (interactive) - (mastodon-toot--schedule-toot :reschedule)) - -(defun mastodon-tl--view-scheduled-toots () - "Show the user's scheduled toots in a new buffer." - (interactive) - (mastodon-tl--init-sync "scheduled-toots" - "scheduled_statuses" - 'mastodon-tl--insert-scheduled-toots)) - -(defun mastodon-tl--insert-scheduled-toots (json) - "Insert the user's scheduled toots, from JSON." - (let ((scheduleds (mastodon-tl--get-scheduled-toots))) - (erase-buffer) - (insert (mastodon-tl--set-face - (concat "\n ------------\n" - " YOUR SCHEDULED TOOTS\n" - " ------------\n\n") - 'success) - (mastodon-tl--set-face - "[n/p - prev/next\n r - reschedule\n e/RET - edit toot\n c - cancel]\n\n" - 'font-lock-comment-face)) - (mapc (lambda (x) - (mastodon-tl--insert-scheduled-toot x)) - scheduleds) - (goto-char (point-min)) - (when json - (mastodon-tl--goto-next-toot)))) - -(defun mastodon-tl--insert-scheduled-toot (toot) - "Insert scheduled TOOT into the buffer." - (let* ((id (alist-get 'id toot)) - (scheduled (alist-get 'scheduled_at toot)) - (params (alist-get 'params toot)) - (text (alist-get 'text params))) - (insert - (propertize (concat text - " | " - (mastodon-toot--iso-to-human scheduled)) - 'byline t ; so we nav here - 'toot-id "0" ; so we nav here - 'face 'font-lock-comment-face - 'keymap mastodon-tl--scheduled-map - 'scheduled-json toot - 'id id) - "\n"))) - -(defun mastodon-tl--copy-scheduled-toot-text () - "Copy the text of the scheduled toot at point." - (interactive) - (let* ((toot (get-text-property (point) 'toot)) - (params (alist-get 'params toot)) - (text (alist-get 'text params))) - (kill-new text))) - -(defun mastodon-tl--cancel-scheduled-toot (&optional id no-confirm) - "Cancel the scheduled toot at point. -ID is that of the scheduled toot to cancel. -NO-CONFIRM means there is no ask or message, there is only do." - (interactive) - (let* ((id (or id (get-text-property (point) 'id))) - (url (mastodon-http--api (format "scheduled_statuses/%s" id)))) - (when (or no-confirm - (y-or-n-p "Cancel scheduled toot?")) - (let ((response (mastodon-http--delete url))) - (mastodon-http--triage response - (lambda () - (mastodon-tl--view-scheduled-toots) - (unless no-confirm - (message "Toot cancelled!")))))))) - -(defun mastodon-tl--edit-scheduled-as-new () - "Edit scheduled status as new toot." - (interactive) - (let* ((toot (get-text-property (point) 'scheduled-json)) - (id (alist-get 'id toot)) - (scheduled (alist-get 'scheduled_at toot)) - (params (alist-get 'params toot)) - (text (alist-get 'text params)) - (visibility (alist-get 'visibility params)) - (cw (alist-get 'spoiler_text params)) - (lang (alist-get 'language params)) - ;; (poll (alist-get 'poll params)) - (reply-id (alist-get 'in_reply_to_id params))) - ;; (media (alist-get 'media_attachments toot))) - (mastodon-toot--compose-buffer) - (goto-char (point-max)) - (insert text) - ;; adopt properties from scheduled toot: - (mastodon-toot--set-toot-properties reply-id visibility cw - lang scheduled id))) - -;;; FILTERS - -(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 (string-empty-p 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)))) - (mastodon-http--triage response - (lambda () - (message "Filter created for %s!" word) - ;; reload if we are in filters view: - (when (mastodon-tl--buffer-type-eq '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 (seq-empty-p 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))))))) - -;;; FOLLOW SUGGESTIONS - -(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))) + +;;; FOLLOW/BLOCK/MUTE, ETC (defmacro mastodon-tl--do-if-toot (&rest body) "Execute BODY if we have a toot or user at point." @@ -2278,201 +1733,6 @@ RESPONSE is the JSON returned by the server." (message "Looks like there's no toot or user at point?") ,@body)) -;;; INSTANCES - -(defun mastodon-tl--view-own-instance (&optional brief) - "View details of your own instance. -BRIEF means show fewer details." - (interactive) - (mastodon-tl--view-instance-description :user brief)) - -(defun mastodon-tl--view-own-instance-brief () - "View brief details of your own instance." - (interactive) - (mastodon-tl--view-instance-description :user :brief)) - -(defun mastodon-tl--view-instance-description-brief () - "View brief details of the instance the current post's author is on." - (interactive) - (mastodon-tl--view-instance-description nil :brief)) - -(defun mastodon-tl--view-instance-description (&optional user brief instance) - "View the details of the instance the current post's author is on. -USER means to show the instance details for the logged in user. -BRIEF means to show fewer details. -INSTANCE is an instance domain name." - (interactive) - (if user - (let ((response (mastodon-http--get-json - (mastodon-http--api "instance") - nil ; params - nil ; silent - :vector))) - (mastodon-tl--instance-response-fun response brief)) - (mastodon-tl--do-if-toot - (let* ((profile-p (get-text-property (point) 'profile-json)) - (toot (if profile-p - (mastodon-tl--property 'profile-json) ; profile may have 0 toots - (mastodon-tl--property 'toot-json))) - (reblog (alist-get 'reblog toot)) - (account (or (alist-get 'account reblog) - (alist-get 'account toot))) - (url (if profile-p - (alist-get 'url toot) ; profile - (alist-get 'url account))) - (username (if profile-p - (alist-get 'username toot) ;; profile - (alist-get 'username account))) - (instance (if instance - (concat "https://" instance) - ;; pleroma URL is https://instance.com/users/username - (if (string-suffix-p "users/" (url-basepath url)) - (string-remove-suffix "/users/" - (url-basepath url)) - ;; mastodon: - (string-remove-suffix (concat "/@" username) - url)))) - (response (mastodon-http--get-json - (if user - (mastodon-http--api "instance") - (concat instance "/api/v1/instance")) - nil ; params - nil ; silent - :vector))) - (mastodon-tl--instance-response-fun response brief))))) - -(defun mastodon-tl--instance-response-fun (response brief) - "Display instance description RESPONSE in a new buffer. -BRIEF means to show fewer details." - (when response - (let ((buf (get-buffer-create "*mastodon-instance*"))) - (with-current-buffer buf - (switch-to-buffer-other-window buf) - (let ((inhibit-read-only t)) - (erase-buffer) - (special-mode) - (when brief - (setq response - (list (assoc 'uri response) - (assoc 'title response) - (assoc 'short_description response) - (assoc 'email response) - (cons 'contact_account - (list - (assoc 'username - (assoc 'contact_account response)))) - (assoc 'rules response) - (assoc 'stats response)))) - (mastodon-tl--print-json-keys response) - (mastodon-mode) - (mastodon-tl--set-buffer-spec (buffer-name buf) - "instance" - nil) - (goto-char (point-min))))))) - -(defun mastodon-tl--format-key (el pad) - "Format a key of element EL, a cons, with PAD padding." - (format (concat "%-" - (number-to-string pad) - "s: ") - (propertize - (prin1-to-string (car el)) - 'face '(:underline t)))) - -(defun mastodon-tl--print-json-keys (response &optional ind) - "Print the JSON keys and values in RESPONSE. -IND is the optional indentation level to print at." - (let* ((cars (mapcar - (lambda (x) (symbol-name (car x))) - response)) - (pad (1+ (cl-reduce #'max (mapcar #'length cars))))) - (while response - (let ((el (pop response))) - (cond - ;; vector of alists (fields, instance rules): - ((and (vectorp (cdr el)) - (not (seq-empty-p (cdr el))) - (consp (seq-elt (cdr el) 0))) - (insert - (mastodon-tl--format-key el pad) - "\n\n") - (seq-do #'mastodon-tl--print-instance-rules-or-fields (cdr el)) - (insert "\n")) - ;; vector of strings (media types): - ((and (vectorp (cdr el)) - (not (seq-empty-p (cdr el))) - (< 1 (seq-length (cdr el))) - (stringp (seq-elt (cdr el) 0))) - (when ind (indent-to ind)) - (insert - (mastodon-tl--format-key el pad) - "\n" - (seq-mapcat - (lambda (x) (concat x ", ")) - (cdr el) 'string) - "\n\n")) - ;; basic nesting: - ((consp (cdr el)) - (when ind (indent-to ind)) - (insert - (mastodon-tl--format-key el pad) - "\n\n") - (mastodon-tl--print-json-keys - (cdr el) (if ind (+ ind 4) 4))) - (t - ;; basic handling of raw booleans: - (let ((val (cond ((equal (cdr el) ':json-false) - "no") - ((equal (cdr el) 't) - "yes") - (t - (cdr el))))) - (when ind (indent-to ind)) - (insert (mastodon-tl--format-key el pad) - " " - (mastodon-tl--newline-if-long (cdr el)) - ;; only send strings straight to --render-text - ;; this makes hyperlinks work: - (if (not (stringp val)) - (mastodon-tl--render-text - (prin1-to-string val)) - (mastodon-tl--render-text val)) - "\n")))))))) - -(defun mastodon-tl--print-instance-rules-or-fields (alist) - "Print ALIST of instance rules or contact account or emoji fields." - (let ((key (cond ((alist-get 'id alist) - 'id) - ((alist-get 'name alist) - 'name) - ((alist-get 'shortcode alist) - 'shortcode))) - (value (cond ((alist-get 'id alist) - 'text) - ((alist-get 'value alist) - 'value) - ((alist-get 'url alist) - 'url)))) - (indent-to 4) - (insert - (format "%-5s: " - (propertize (alist-get key alist) - 'face '(:underline t))) - (mastodon-tl--newline-if-long (alist-get value alist)) - (format "%s" (mastodon-tl--render-text - (alist-get value alist))) - "\n"))) - -(defun mastodon-tl--newline-if-long (el) - "Return a newline string if the cdr of EL is over 50 characters long." - (let ((rend (if (stringp el) (mastodon-tl--render-text el) el))) - (if (and (sequencep rend) - (< 50 (length rend))) - "\n" - ""))) - -;;; FOLLOW/BLOCK/MUTE, ETC - (defun mastodon-tl--follow-user (user-handle &optional notify langs) "Query for USER-HANDLE from current status and follow that user. If NOTIFY is \"true\", enable notifications when that user posts. @@ -2621,9 +1881,7 @@ Action must be either \"unblock\" or \"unmute\"." "mutes"))) (url (mastodon-http--api endpoint)) (json (mastodon-http--get-json url)) - (accts (mapcar (lambda (user) - (alist-get 'acct user)) - json))) + (accts (mastodon-tl--map-get-accts json))) (when accts (completing-read (format "Handle of user to %s: " action) accts @@ -2688,6 +1946,7 @@ ARGS is an alist of any parameters to send with the request." ((eq notify nil) (message "User %s (@%s) %sed!" name user-handle action))))))) + ;; FOLLOW TAGS (defun mastodon-tl--get-tag-json (tag) @@ -2716,9 +1975,8 @@ If TAG provided, follow it." If TAG is provided, unfollow it." (interactive) (let* ((followed-tags-json (unless tag (mastodon-tl--followed-tags))) - (tags (unless tag (mapcar (lambda (x) - (alist-get 'name x)) - followed-tags-json))) + (tags (unless tag + (mastodon-tl--map-alist 'name followed-tags-json))) (tag (or tag (completing-read "Unfollow tag: " tags))) (url (mastodon-http--api (format "tags/%s/unfollow" tag))) @@ -2731,12 +1989,40 @@ If TAG is provided, unfollow it." "List followed tags. View timeline of tag user choses." (interactive) (let* ((followed-tags-json (mastodon-tl--followed-tags)) - (tags (mapcar (lambda (x) - (alist-get 'name x)) - followed-tags-json)) + (tags (mastodon-tl--map-alist 'name followed-tags-json)) (tag (completing-read "Tag: " tags))) (mastodon-tl--get-tag-timeline tag))) + +;;; UPDATING, etc. + +(defun mastodon-tl--more-json (endpoint id) + "Return JSON for timeline ENDPOINT before ID." + (let* ((args `(("max_id" . ,(mastodon-tl--as-string id)))) + (url (mastodon-http--api endpoint))) + (mastodon-http--get-json url args))) + +(defun mastodon-tl--more-json-async (endpoint id &optional params callback &rest cbargs) + "Return JSON for timeline ENDPOINT before ID. +Then run CALLBACK with arguments CBARGS. +PARAMS is used to send any parameters needed to correctly update +the current view." + (let* ((args `(("max_id" . ,(mastodon-tl--as-string id)))) + (args (if params (push (car args) params) args)) + (url (mastodon-http--api endpoint))) + (apply 'mastodon-http--get-json-async url args callback cbargs))) + +;; TODO +;; Look into the JSON returned here by Local +(defun mastodon-tl--updated-json (endpoint id &optional params) + "Return JSON for timeline ENDPOINT since ID. +PARAMS is used to send any parameters needed to correctly update +the current view." + (let* ((args `(("since_id" . ,(mastodon-tl--as-string id)))) + (args (if params (push (car args) params) args)) + (url (mastodon-http--api endpoint))) + (mastodon-http--get-json url args))) + ;; TODO: add this to new posts in some cases, e.g. in thread view. (defun mastodon-tl--reload-timeline-or-profile () "Reload the current timeline or profile page. @@ -2774,6 +2060,12 @@ when showing followers or accounts followed." (mastodon-tl--buffer-type-eq 'profile-followers) (mastodon-tl--buffer-type-eq 'profile-following))) +(defun mastodon-tl--get-link-header-from-response (headers) + "Get http Link header from list of http HEADERS." + ;; pleroma uses "link", so case-insensitive match required: + (when-let ((link-headers (alist-get "Link" headers nil nil 'cl-equalp))) + (split-string link-headers ", "))) + (defun mastodon-tl--more () "Append older toots to timeline, asynchronously." (interactive) @@ -2828,7 +2120,6 @@ HEADERS is the http headers returned in the response, if any." "Return `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) @@ -2863,9 +2154,7 @@ before (non-nil) or after (nil)" "Find (start . end) property range after/before START-POINT. Does so while PROPERTY is set to a consistent value (different from the value at START-POINT if that is set). - Return 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) @@ -2888,15 +2177,13 @@ START-POINT otherwise after START-POINT." 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." +If relative timestamps are disabled (i.e. if +`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 @@ -3010,11 +2297,8 @@ This location is defined by a non-nil value of (goto-char mastodon-tl--after-update-marker)))) (message "nothing to update"))))) -(defun mastodon-tl--get-link-header-from-response (headers) - "Get http Link header from list of http HEADERS." - ;; pleroma uses "link", so case-insensitive match required: - (when-let ((link-headers (alist-get "Link" headers nil nil 'cl-equalp))) - (split-string link-headers ", "))) + +;;; LOADING TIMELINES (defun mastodon-tl--init (buffer-name endpoint update-function &optional headers params hide-replies) @@ -3043,46 +2327,46 @@ RESPONSE is the data returned from the server by JSON and http headers, without it just the JSON." (let ((json (if headers (car response) response))) (if (not json) ; praying this is right here, else try "\n[]" - (message "Looks like nothing returned from endpoint: %s" endpoint) + (message "Looks like nothing returned from endpoint: %s" endpoint) (let* ((headers (if headers (cdr response) nil)) (link-header (mastodon-tl--get-link-header-from-response headers))) - (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 - (mastodon-tl--set-buffer-spec buffer - endpoint - update-function - link-header - update-params - hide-replies) - (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 - (mastodon-tl--set-buffer-spec buffer - endpoint - update-function - link-header - update-params - hide-replies) - (setq mastodon-tl--timestamp-update-timer - (when mastodon-tl--enable-relative-timestamps - (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))) - (unless (mastodon-tl--profile-buffer-p) - (mastodon-tl--goto-first-item))))))) + (with-current-buffer (get-buffer-create buffer) + (let ((inhibit-read-only t)) + (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 + (mastodon-tl--set-buffer-spec buffer + endpoint + update-function + link-header + update-params + hide-replies) + (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) + (mastodon-tl--set-buffer-spec buffer + endpoint + update-function + link-header + update-params + hide-replies) + (setq mastodon-tl--timestamp-update-timer + (when mastodon-tl--enable-relative-timestamps + (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))) + (unless (mastodon-tl--profile-buffer-p) + (mastodon-tl--goto-first-item)))))))) (defun mastodon-tl--init-sync (buffer-name endpoint update-function &optional note-type) "Initialize BUFFER-NAME with timeline targeted by ENDPOINT. @@ -3098,34 +2382,35 @@ Optional arg NOTE-TYPE means only get that type of note." (url (mastodon-http--api endpoint)) (buffer (concat "*mastodon-" buffer-name "*")) (json (mastodon-http--get-json url args))) - (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 - (mastodon-tl--set-buffer-spec buffer endpoint 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 - mastodon-tl--timestamp-next-update (time-add (current-time) - (seconds-to-time 300))) - (funcall update-function json)) - (mastodon-mode) - (with-current-buffer buffer - (mastodon-tl--set-buffer-spec buffer endpoint update-function nil args) - (setq mastodon-tl--timestamp-update-timer - (when mastodon-tl--enable-relative-timestamps - (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))) - (unless (mastodon-tl--profile-buffer-p) - (mastodon-tl--goto-first-item))) - buffer)) + (with-current-buffer (get-buffer-create buffer) + (let ((inhibit-read-only t)) + (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 + (mastodon-tl--set-buffer-spec buffer endpoint 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 + mastodon-tl--timestamp-next-update (time-add (current-time) + (seconds-to-time 300))) + (funcall update-function json) + (mastodon-mode) + (mastodon-tl--set-buffer-spec buffer endpoint update-function nil args) + (setq mastodon-tl--timestamp-update-timer + (when mastodon-tl--enable-relative-timestamps + (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))) + (unless (mastodon-tl--profile-buffer-p) + ;; FIXME: this breaks test (because test has empty buffer) + (mastodon-tl--goto-first-item))) + buffer))) (provide 'mastodon-tl) ;;; mastodon-tl.el ends here |