branch: elpa/mastodon
commit fbeb1ec043e4e553e526e9e32bc3b74f16f448f2
Author: marty hiatt <[email protected]>
Commit: marty hiatt <[email protected]>
implement quote posts. #758
- quote toot command
- edit a quoted toot
- disable polls/attachments for quote toot composing
- check permission to quote
- display quoted toot partially in compose buffer, as per replies
- TODO: handle visibility limitations on the server in our compose buffer
---
lisp/mastodon-toot.el | 153 ++++++++++++++++++++++++++++++++++----------------
lisp/mastodon.el | 6 +-
2 files changed, 108 insertions(+), 51 deletions(-)
diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el
index 6865681b55..a518b1f7e5 100644
--- a/lisp/mastodon-toot.el
+++ b/lisp/mastodon-toot.el
@@ -738,7 +738,8 @@ MEDIA is the media_attachments data for a status from the
server."
media))
(defun mastodon-toot--set-toot-properties
- (reply-id visibility cw lang &optional scheduled scheduled-id media poll)
+ (reply-id visibility cw lang
+ &optional scheduled scheduled-id media poll quote-id)
"Set the toot properties for the current redrafted or edited toot.
REPLY-ID, VISIBILITY, CW, SCHEDULED, and LANG are the properties to set.
MEDIA is the media_attachments data for a status from the server."
@@ -755,6 +756,7 @@ MEDIA is the media_attachments data for a status from the
server."
(mastodon-toot--set-toot-media-attachments media))
(when poll
(mastodon-toot--server-poll-to-local poll))
+ (setq mastodon-toot-quote-id quote-id)
(mastodon-toot--refresh-attachments-display)
(mastodon-toot--update-status-fields)))
@@ -937,7 +939,8 @@ instance to edit a toot."
(symbol-name t)))
("spoiler_text" . ,mastodon-toot--content-warning)
("quote_approval_policy" . ,mastodon-toot-quote-policy)
- ("language" . ,mastodon-toot--language))
+ ("language" . ,mastodon-toot--language)
+ ("quoted_status_id" . ,mastodon-toot-quote-id))
;; Pleroma instances can't handle null-valued
;; scheduled_at args, so only add if non-nil
(when scheduled `(("scheduled_at" . ,scheduled)))))
@@ -1011,17 +1014,19 @@ instance to edit a toot."
(user-error "You can only edit your own toots")
(let* ((source (mastodon-toot--get-toot-source id))
(content (alist-get 'text source))
- (source-cw (alist-get 'spoiler_text source)))
+ (source-cw (alist-get 'spoiler_text source))
+ (quote (alist-get 'quote toot)))
(let-alist toot
(when (y-or-n-p "Edit this toot? ")
(mastodon-toot--compose-buffer nil .in_reply_to_id nil
content :edit)
(goto-char (point-max))
;; adopt reply-to-id, visibility, CW, language, and media:
- (mastodon-toot--set-toot-properties .in_reply_to_id .visibility
- source-cw .language nil nil
- ;; maintain media order:
- (reverse .media_attachments)
.poll)
+ (mastodon-toot--set-toot-properties
+ .in_reply_to_id .visibility source-cw .language nil nil
+ ;; maintain media order:
+ (reverse .media_attachments) .poll
+ (map-nested-elt quote '(quoted_status id)))
(setq mastodon-toot--edit-item-id id))))))))
(defun mastodon-toot--get-toot-source (id)
@@ -1339,31 +1344,35 @@ If that fails, return 4 as a fallback"
(alist-get 'max_media_attachments config))
4)) ; mastodon default as fallback
-(defun mastodon-toot-attach-media (file description)
- "Prompt for an attachment FILE with DESCRIPTION.
+(defun mastodon-toot-attach-media ()
+ "Prompt for an attachment file.
A preview is displayed in the new toot buffer, and the file
is uploaded asynchronously using `mastodon-toot--upload-attached-media'.
File is actually attached to the toot upon posting."
- (interactive "fFilename: \nsDescription: ")
- (let ((max-attachments (mastodon-toot--get-instance-max-attachments)))
- (when (>= (length mastodon-toot--media-attachments)
- max-attachments)
- ;; warn + pop the oldest one:
- (when (y-or-n-p
- (format "Maximum attachments (%s) reached: remove first one?"
- max-attachments))
- (pop mastodon-toot--media-attachments)))
- (if (file-directory-p file)
- (user-error "Looks like you chose a directory not a file")
- (setq mastodon-toot--media-attachments
- (nconc mastodon-toot--media-attachments
- `(((:contents . ,(mastodon-http--read-file-as-string file))
- (:description . ,description)
- (:filename . ,file)))))
- (mastodon-toot--refresh-attachments-display)
- ;; upload only most recent attachment:
- (mastodon-toot--upload-attached-media
- (car (last mastodon-toot--media-attachments))))))
+ (interactive)
+ (if mastodon-toot-quote-id
+ (user-error "You can't add attachments to quote toots.")
+ (let ((file (read-file-name "Filename: "))
+ (description (read-string "Description: "))
+ (max-attachments (mastodon-toot--get-instance-max-attachments)))
+ (when (>= (length mastodon-toot--media-attachments)
+ max-attachments)
+ ;; warn + pop the oldest one:
+ (when (y-or-n-p
+ (format "Maximum attachments (%s) reached: remove first one?"
+ max-attachments))
+ (pop mastodon-toot--media-attachments)))
+ (if (file-directory-p file)
+ (user-error "Looks like you chose a directory not a file")
+ (setq mastodon-toot--media-attachments
+ (nconc mastodon-toot--media-attachments
+ `(((:contents . ,(mastodon-http--read-file-as-string
file))
+ (:description . ,description)
+ (:filename . ,file)))))
+ (mastodon-toot--refresh-attachments-display)
+ ;; upload only most recent attachment:
+ (mastodon-toot--upload-attached-media
+ (car (last mastodon-toot--media-attachments)))))))
(defun mastodon-toot--attachment-descriptions ()
"Return a list of image descriptions for current attachments."
@@ -1463,9 +1472,11 @@ MAX is the maximum number set by their instance."
(defun mastodon-toot-create-poll ()
"Prompt for new poll options and return as a list."
(interactive)
- (if mastodon-toot-poll-use-transient
- (call-interactively #'mastodon-create-poll)
- (mastodon-toot--read-poll)))
+ (cond (mastodon-toot-quote-id
+ (user-error "You can't add a poll to a quote toot."))
+ (mastodon-toot-poll-use-transient
+ (call-interactively #'mastodon-create-poll))
+ (t (mastodon-toot--read-poll))))
(defun mastodon-toot--read-poll ()
"Read poll options."
@@ -1567,6 +1578,37 @@ If TRANSIENT, we are called from a transient, so nil
`( :options ,options :expiry-readable ,expiry-human
:expiry ,expiry-str :multi ,multiple))))))
+;;; QUOTE
+
+(defvar mastodon-toot-quote-approval-user
+ '("automatic" "manual" "denied" "unknown"))
+
+(defvar mastodon-toot-quote-approval-auto-or-manual
+ '("public" "followers" "following" "unsupported_policy"))
+
+(defvar-local mastodon-toot-quote-id nil)
+
+(defun mastodon-toot-quote ()
+ "Compose a toot quoting the toot at point."
+ (interactive)
+ ;; boosted toot or toot:
+ (let* ((quote-id (mastodon-tl--property 'base-item-id))
+ (json (mastodon-tl--property 'item-json))
+ (policy (alist-get 'quote_approval json))
+ ;;
https://docs.joinmastodon.org/methods/statuses/#form-data-parameters
+ ;; FIXME: "Quoting a private post will restrict the quoting post’s
+ ;; visibility to private or direct (if the given visibility is
+ ;; public or unlisted, private will be used instead)."
+ ;; So we need to set visibility when composing a quote toot:
+ (visibility (mastodon-tl--field 'visibility json))
+ (user-policy (alist-get 'current_user policy)))
+ (pcase user-policy
+ ("denied" (user-error "You don't have permission to quote this toot."))
+ ("unknown"
+ (when (y-or-n-p "Quote permission unknown. Proceed?")
+ (mastodon-toot nil nil nil quote-id json)))
+ (_ (mastodon-toot nil nil nil quote-id json)))))
+
;;; SCHEDULE
@@ -1709,22 +1751,23 @@ LONGEST is the length of the longest binding."
(mastodon-toot--formatted-kbinds-pairs formatted longest-kbind)
nil))))
-(defun mastodon-toot--format-reply-in-compose (reply-text)
+(defun mastodon-toot--format-reply-in-compose (reply-text &optional quote-text)
"Format a REPLY-TEXT for display in compose buffer docs."
- (let* ((rendered (mastodon-tl--render-text reply-text))
+ (let* ((rendered (mastodon-tl--render-text (or quote-text reply-text)))
(no-props (substring-no-properties rendered))
;; FIXME: this replaces \n at end of every post, so we have to trim:
(no-newlines (string-trim
(replace-regexp-in-string "[\n]+" " " no-props)))
- (reply-to (concat " Reply to: \"" no-newlines "\""))
- (crop (truncate-string-to-width reply-to
- mastodon-toot-orig-in-reply-length)))
+ (prompt (concat (if quote-text " Quoting: \"" " Reply to: \"")
+ no-newlines "\""))
+ (crop (truncate-string-to-width
+ prompt mastodon-toot-orig-in-reply-length)))
(if (> (length no-newlines)
(length crop)) ; we cropped:
(concat crop "\n")
- (concat reply-to "\n"))))
+ (concat prompt "\n"))))
-(defun mastodon-toot--display-docs-and-status-fields (&optional reply-text)
+(defun mastodon-toot--display-docs-and-status-fields (&optional reply-text
quote-text)
"Insert propertized text with documentation about `mastodon-toot-mode'.
Also includes and the status fields which will get updated based
on the status of NSFW, content warning flags, media attachments, etc.
@@ -1766,10 +1809,14 @@ REPLY-TEXT is the text of the toot being replied to."
(propertize "None "
'toot-attachments t)
"\n"
- (when reply-text
- (propertize
- (mastodon-toot--format-reply-in-compose reply-text)
- 'toot-reply t))
+ (cond (reply-text
+ (propertize
+ (mastodon-toot--format-reply-in-compose reply-text)
+ 'toot-reply t))
+ (quote-text
+ (propertize
+ (mastodon-toot--format-reply-in-compose nil quote-text)
+ 'toot-quote t)))
divider)
'face 'mastodon-toot-docs-face
'read-only "Edit your message below."
@@ -2029,7 +2076,8 @@ Added to `after-change-functions'."
;;; COMPOSE BUFFER FUNCTION
(defun mastodon-toot--compose-buffer
- (&optional reply-to-user reply-to-id reply-json initial-text edit)
+ (&optional reply-to-user reply-to-id reply-json initial-text edit
+ quote-id quote-json)
"Create a new buffer to capture text for a new toot.
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.
@@ -2057,6 +2105,9 @@ EDIT means we are editing an existing toot, not composing
a new one."
(switch-to-buffer-other-window buffer)
(text-mode)
(mastodon-toot-mode t)
+ ;; set quote id:
+ (when quote-id
+ (setq mastodon-toot-quote-id quote-id))
;; set visibility:
(setq mastodon-toot--visibility
(or (plist-get mastodon-profile-account-settings 'privacy)
@@ -2070,11 +2121,15 @@ EDIT means we are editing an existing toot, not
composing a new one."
(setq mastodon-toot--language
(mastodon-profile--get-preferences-pref 'posting:default:language))
;; display original toot:
- (if mastodon-toot-display-orig-in-reply-buffer
- (progn
- (mastodon-toot--display-docs-and-status-fields reply-text)
- (mastodon-toot--fill-reply-in-compose))
- (mastodon-toot--display-docs-and-status-fields))
+ ;; FIXME: display some quoted toot:
+ (cond (quote-id
+ (mastodon-toot--display-docs-and-status-fields
+ nil (mastodon-tl--field 'content quote-json))
+ (mastodon-toot--fill-reply-in-compose))
+ (mastodon-toot-display-orig-in-reply-buffer
+ (mastodon-toot--display-docs-and-status-fields reply-text)
+ (mastodon-toot--fill-reply-in-compose))
+ (t (mastodon-toot--display-docs-and-status-fields)))
;; `reply-to-user' (alone) is also used by `mastodon-tl-dm-user', so
;; perhaps we should not always call --setup-as-reply, or make its
;; workings conditional on reply-to-id. currently it only checks for
diff --git a/lisp/mastodon.el b/lisp/mastodon.el
index 9ec106889f..a676dc911d 100644
--- a/lisp/mastodon.el
+++ b/lisp/mastodon.el
@@ -414,13 +414,15 @@ FORCE means to fetch from the server in any case and
update
(alist-get 'version (mastodon-instance-data)))
;;;###autoload
-(defun mastodon-toot (&optional user reply-to-id reply-json)
+(defun mastodon-toot (&optional user reply-to-id reply-json
+ quote-id quote-json)
"Update instance with new toot. Content is captured in a new buffer.
If USER is non-nil, insert after @ symbol to begin new toot.
If REPLY-TO-ID is non-nil, attach new toot to a conversation.
If REPLY-JSON is the json of the toot being replied to."
(interactive)
- (mastodon-toot--compose-buffer user reply-to-id reply-json))
+ (mastodon-toot--compose-buffer user reply-to-id reply-json
+ nil nil quote-id quote-json))
;;;###autoload
(defun mastodon-notifications-get (&optional type buffer-name max-id)