diff options
| author | marty hiatt <martianhiatus [a t] riseup [d o t] net> | 2022-11-26 10:49:03 +0100 | 
|---|---|---|
| committer | marty hiatt <martianhiatus [a t] riseup [d o t] net> | 2022-11-26 10:49:03 +0100 | 
| commit | 6f017799fa13dd53015ce4159202893f2a590888 (patch) | |
| tree | 95f13b29a6e2615e2f139f7d45a9eade0b8e08e8 /lisp/mastodon-toot.el | |
| parent | 14b7547c385648565eba8a4bac3dc8afa5ebf978 (diff) | |
| parent | 55c91270734da9e6a11060b3bea7aad152d40680 (diff) | |
Merge branch 'develop' into HEAD
Diffstat (limited to 'lisp/mastodon-toot.el')
| -rw-r--r-- | lisp/mastodon-toot.el | 509 | 
1 files changed, 314 insertions, 195 deletions
| diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el index 125eeea..27e7ce5 100644 --- a/lisp/mastodon-toot.el +++ b/lisp/mastodon-toot.el @@ -41,11 +41,7 @@  (require 'cl-lib)  (require 'persist) -(when (require 'company nil :noerror) -  (declare-function company-mode-on "company") -  (declare-function company-begin-backend "company") -  (declare-function company-grab-symbol "company") -  (defvar company-backends)) +(require 'mastodon-iso)  (defvar mastodon-instance-url)  (defvar mastodon-tl--buffer-spec) @@ -76,9 +72,13 @@  (autoload 'mastodon-toot "mastodon")  (autoload 'mastodon-profile--get-source-pref "mastodon-profile")  (autoload 'mastodon-profile--update-preference "mastodon-profile") -(autoload 'mastodon-profile-fetch-server-account-settings "mastodon-profile") +(autoload 'mastodon-profile--fetch-server-account-settings "mastodon-profile")  (autoload 'mastodon-tl--render-text "mastodon-tl") -(autoload 'mastodon-profile-fetch-server-account-settings-maybe "mastodon-profile") +(autoload 'mastodon-profile--fetch-server-account-settings-maybe "mastodon-profile") +(autoload 'mastodon-http--build-array-params-alist "mastodon-http") +(autoload 'mastodon-tl--get-endpoint "mastodon-tl") +(autoload 'mastodon-http--put "mastodon-http") +(autoload 'mastodon-tl--return-fave-char "mastodon-tl")  ;; for mastodon-toot--translate-toot-text  (autoload 'mastodon-tl--content "mastodon-tl") @@ -100,18 +100,24 @@    :group 'mastodon-toot    :type 'integer) -(defcustom mastodon-toot--enable-completion -  (if (require 'company nil :noerror) t nil) +(defcustom mastodon-toot--enable-completion t    "Whether to enable completion of mentions and hashtags. +Used for completion in toot compose buffer." +  :group 'mastodon-toot +  :type 'boolean) + +(defcustom mastodon-toot--use-company-for-completion nil +  "Whether to enable company for completion. -Used for completion in toot compose buffer. +When non-nil, `company-mode' is enabled in the toot compose +buffer, and mastodon completion backends are added to +`company-capf'. -This is only used if company mode is installed." +You need to install company yourself to use this."    :group 'mastodon-toot    :type 'boolean) -(defcustom mastodon-toot--completion-style-for-mentions -  (if (require 'company nil :noerror) "following" "off") +(defcustom mastodon-toot--completion-style-for-mentions "all"    "The company completion style to use for mentions."    :group 'mastodon-toot    :type '(choice @@ -155,7 +161,7 @@ Valid values are \"direct\", \"private\" (followers-only),  This is determined by the account setting on the server. To  change the setting on the server, see -`mastodon-toot-set-default-visibility'.") +`mastodon-toot--set-default-visibility'.")  (defvar-local mastodon-toot--media-attachments nil    "A list of the media attachments of the toot being composed.") @@ -166,9 +172,14 @@ change the setting on the server, see  (defvar-local mastodon-toot-poll nil    "A list of poll options for the toot being composed.") +(defvar-local mastodon-toot--language nil +  "The language of the toot being composed, in ISO 639 (two-letter).") +  (defvar-local mastodon-toot--reply-to-id nil    "Buffer-local variable to hold the id of the toot being replied to.") +(defvar-local mastodon-toot--edit-toot-id nil +  "The id of the toot being edited.")  (defvar-local mastodon-toot-previous-window-config nil    "A list of window configuration prior to composing a toot. @@ -177,6 +188,9 @@ Takes its form from `window-configuration-to-register'.")  (defvar mastodon-toot--max-toot-chars nil    "The maximum allowed characters count for a single toot.") +(defvar-local mastodon-toot-completions nil +  "The data of completion candidates for the current completion at point.") +  (defvar mastodon-toot-current-toot-text nil    "The text of the toot being composed.") @@ -186,6 +200,21 @@ For the moment we just put all composed toots in here, as we want  to also capture toots that are 'sent' but that don't successfully  send.") +(defvar mastodon-toot-handle-regex +  (concat +   ;; preceding space or bol [boundary doesn't work with @] +   "\\([\n\t ]\\|^\\)" +   "\\(?2:@[1-9a-zA-Z._-]+" ; a handle +   "\\(@[^ \n\t]*\\)?\\)" ; with poss domain, * = allow only @ +   "\\b")) + +(defvar mastodon-toot-tag-regex +  (concat +   ;; preceding space or bol [boundary doesn't work with #] +   "\\([\n\t ]\\|^\\)" +   "\\(?2:#[1-9a-zA-Z_]+\\)" ; tag +   "\\b")) ; boundary +  (defvar mastodon-toot-mode-map    (let ((map (make-sparse-keymap)))      (define-key map (kbd "C-c C-c") #'mastodon-toot--send) @@ -198,10 +227,11 @@ send.")      (define-key map (kbd "C-c C-a") #'mastodon-toot--attach-media)      (define-key map (kbd "C-c !") #'mastodon-toot--clear-all-attachments)      (define-key map (kbd "C-c C-p") #'mastodon-toot--create-poll) +    (define-key map (kbd "C-c C-l") #'mastodon-toot--set-toot-lang)      map)    "Keymap for `mastodon-toot'.") -(defun mastodon-toot-set-default-visibility () +(defun mastodon-toot--set-default-visibility ()    "Set the default visibility for toots on the server."    (interactive)    (let ((vis (completing-read "Set default visibility to:" @@ -214,6 +244,7 @@ send.")  NO-TOOT means we are not calling from a toot buffer."    (mastodon-http--get-json-async     (mastodon-http--api "instance") +   nil     'mastodon-toot--get-max-toot-chars-callback no-toot))  (defun mastodon-toot--get-max-toot-chars-callback (json-response @@ -266,7 +297,7 @@ boosting, or bookmarking toots."                                            (mastodon-tl--as-string id)                                            "/"                                            action)))) -    (let ((response (mastodon-http--post url nil nil))) +    (let ((response (mastodon-http--post url)))        (mastodon-http--triage response callback))))  (defun mastodon-toot--toggle-boost-or-favourite (type) @@ -313,7 +344,9 @@ TYPE is a symbol, either 'favourite or 'boost."                                          (list 'boosted-p (not boosted))                                        (list 'favourited-p (not faved))))                 (mastodon-toot--action-success -                (if boost-p "B" "F") +                (if boost-p +                    (mastodon-tl--return-boost-char) +                  (mastodon-tl--return-fave-char))                  byline-region remove))               (message (format "%s #%s" (if boost-p msg action) id))))))        (message (format "Nothing to %s here?!?" action-string))))) @@ -366,9 +399,12 @@ TYPE is a symbol, either 'favourite or 'boost."        (message (format "Nothing to %s here?!?" action)))))  (defun mastodon-toot--copy-toot-url () -  "Copy URL of toot at point." +  "Copy URL of toot at point. +If the toot is a fave/boost notification, copy the URLof the +base toot."    (interactive) -  (let* ((toot (mastodon-tl--property 'toot-json)) +  (let* ((toot (or (mastodon-tl--property 'base-toot) +                   (mastodon-tl--property 'toot-json)))           (url (if (mastodon-tl--field 'reblog toot)                    (alist-get 'url (alist-get 'reblog toot))                  (alist-get 'url toot)))) @@ -376,9 +412,12 @@ TYPE is a symbol, either 'favourite or 'boost."      (message "Toot URL copied to the clipboard.")))  (defun mastodon-toot--copy-toot-text () -  "Copy text of toot at point." +  "Copy text of toot at point. +If the toot is a fave/boost notification, copy the text of the +base toot."    (interactive) -  (let* ((toot (mastodon-tl--property 'toot-json))) +  (let* ((toot (or (mastodon-tl--property 'base-toot) +                   (mastodon-tl--property 'toot-json))))      (kill-new (mastodon-tl--content toot))      (message "Toot content copied to the clipboard."))) @@ -408,7 +447,8 @@ Uses `lingva.el'."  (defun mastodon-toot--pin-toot-toggle ()    "Pin or unpin user's toot at point."    (interactive) -  (let* ((toot (mastodon-tl--property 'toot-json)) +  (let* ((toot (or (mastodon-tl--property 'base-toot) ;fave/boost notifs +                   (mastodon-tl--property 'toot-json)))           (pinnable-p (mastodon-toot--own-toot-p toot))           (pinned-p (equal (alist-get 'pinned toot) t))           (action (if pinned-p "unpin" "pin")) @@ -433,7 +473,8 @@ Uses `lingva.el'."    "Delete and redraft user's toot at point synchronously.  NO-REDRAFT means delete toot only."    (interactive) -  (let* ((toot (mastodon-tl--property 'toot-json)) +  (let* ((toot (or (mastodon-tl--property 'base-toot) ;fave/boost notifs +                   (mastodon-tl--property 'toot-json)))           (id (mastodon-tl--as-string (mastodon-tl--toot-id toot)))           (url (mastodon-http--api (format "statuses/%s" id)))           (toot-cw (alist-get 'spoiler_text toot)) @@ -458,7 +499,7 @@ NO-REDRAFT means delete toot only."                                           toot-visibility                                           toot-cw))))))))) -(defun mastodon-toot-set-cw (&optional cw) +(defun mastodon-toot--set-cw (&optional cw)    "Set content warning to CW if it is non-nil."    (unless (string-empty-p cw)      (setq mastodon-toot--content-warning t) @@ -477,7 +518,7 @@ REPLY-ID, TOOT-VISIBILITY, and TOOT-CW of deleted toot are preseved."        (when reply-id          (setq mastodon-toot--reply-to-id reply-id))        (setq mastodon-toot--visibility toot-visibility) -      (mastodon-toot-set-cw toot-cw) +      (mastodon-toot--set-cw toot-cw)        (mastodon-toot--update-status-fields))))  (defun mastodon-toot--kill (&optional cancel) @@ -497,13 +538,13 @@ CANCEL means the toot was not sent, so we save the toot text as a draft."    "Kill new-toot buffer/window. Does not POST content to Mastodon.  If toot is not empty, prompt to save text as a draft."    (interactive) -  (if (mastodon-toot-empty-p) +  (if (mastodon-toot--empty-p)        (mastodon-toot--kill)      (when (y-or-n-p "Save draft toot?") -      (mastodon-toot-save-draft)) +      (mastodon-toot--save-draft))      (mastodon-toot--kill))) -(defun mastodon-toot-save-draft () +(defun mastodon-toot--save-draft ()    "Save the current compose toot text as a draft.  Pushes `mastodon-toot-current-toot-text' to  `mastodon-toot-draft-toots-list'." @@ -513,9 +554,9 @@ Pushes `mastodon-toot-current-toot-text' to                  mastodon-toot-draft-toots-list :test 'equal)      (message "Draft saved!"))) -(defun mastodon-toot-empty-p (&optional text-only) -  "Return t if no text, attachments, or polls have been added to the compose buffer. -TEXT-ONLY means don't check for attachments." +(defun mastodon-toot--empty-p (&optional text-only) +  "Return t if toot has no text, attachments, or polls. +TEXT-ONLY means don't check for attachments or polls."    (and (if text-only             t           (not mastodon-toot--media-attachments) @@ -615,7 +656,8 @@ to `emojify-user-emojis', and the emoji data is updated."  (defun mastodon-toot--build-poll-params ()    "Return an alist of parameters for POSTing a poll status."    (append -   (mastodon-toot--make-poll-options-params +   (mastodon-http--build-array-params-alist +    "poll[options][]"      (plist-get mastodon-toot-poll :options))     `(("poll[expires_in]" .  ,(plist-get mastodon-toot-poll :expiry)))     `(("poll[multiple]" . ,(symbol-name (plist-get mastodon-toot-poll :multi)))) @@ -624,23 +666,33 @@ to `emojify-user-emojis', and the emoji data is updated."  (defun mastodon-toot--send ()    "POST contents of new-toot buffer to Mastodon instance and kill buffer.  If media items have been attached and uploaded with -`mastodon-toot--attach-media', they are attached to the toot." +`mastodon-toot--attach-media', they are attached to the toot. +If `mastodon-toot--edit-toot-id' is non-nil, PUT contents to +instance to edit a toot."    (interactive) -  (let* ((toot (mastodon-toot--remove-docs)) -         (endpoint (mastodon-http--api "statuses")) -         (spoiler (when (and (not (mastodon-toot-empty-p)) +  (let* ((edit-p (if mastodon-toot--edit-toot-id t nil)) +         (toot (mastodon-toot--remove-docs)) +         (endpoint +          (if edit-p +              ;; we are sending an edit: +              (mastodon-http--api (format "statuses/%s" +                                          mastodon-toot--edit-toot-id)) +            (mastodon-http--api "statuses"))) +         (spoiler (when (and (not (mastodon-toot--empty-p))                               mastodon-toot--content-warning) -                    (read-string "Warning: " mastodon-toot--content-warning-from-reply-or-redraft))) +                    (read-string "Warning: " +                                 mastodon-toot--content-warning-from-reply-or-redraft)))           (args-no-media `(("status" . ,toot)                            ("in_reply_to_id" . ,mastodon-toot--reply-to-id)                            ("visibility" . ,mastodon-toot--visibility)                            ("sensitive" . ,(when mastodon-toot--content-nsfw                                              (symbol-name t))) -                          ("spoiler_text" . ,spoiler))) +                          ("spoiler_text" . ,spoiler) +                          ("language" . ,mastodon-toot--language)))           (args-media (when mastodon-toot--media-attachments -                       (mapcar (lambda (id) -                                 (cons "media_ids[]" id)) -                               mastodon-toot--media-attachment-ids))) +                       (mastodon-http--build-array-params-alist +                        "media_ids[]" +                        mastodon-toot--media-attachment-ids)))           (args-poll (when mastodon-toot-poll                        (mastodon-toot--build-poll-params)))           ;; media || polls: @@ -660,16 +712,89 @@ If media items have been attached and uploaded with            ((and mastodon-toot--max-toot-chars                  (> (length toot) mastodon-toot--max-toot-chars))             (message "Looks like your toot is longer than that maximum allowed length.")) -          ((mastodon-toot-empty-p) +          ((mastodon-toot--empty-p)             (message "Empty toot. Cowardly refusing to post this."))            (t -           (let ((response (mastodon-http--post endpoint args nil))) +           (let ((response (if edit-p +                               ;; we are sending an edit: +                               (mastodon-http--put endpoint args) +                             (mastodon-http--post endpoint args))))               (mastodon-http--triage response                                      (lambda ()                                        (mastodon-toot--kill)                                        (message "Toot toot!")                                        (mastodon-toot--restore-previous-window-config prev-window-config)))))))) +;; EDITING TOOTS: + +(defun mastodon-toot--edit-toot-at-point () +  "Edit the user's toot at point." +  (interactive) +  (let ((toot (or (mastodon-tl--property 'base-toot); fave/boost notifs +                  (mastodon-tl--property 'toot-json)))) +    (if (not (mastodon-toot--own-toot-p toot)) +        (message "You can only edit your own toots.") +      (let* ((id (mastodon-tl--as-string (mastodon-tl--toot-id toot))) +             (source (mastodon-toot--get-toot-source id)) +             (content (alist-get 'text source)) +             (source-cw (alist-get 'spoiler_text source)) +             (toot-visibility (alist-get 'visibility toot)) +             (reply-id (alist-get 'in_reply_to_id toot))) +        (when (y-or-n-p "Edit this toot? ") +          (mastodon-toot--compose-buffer) +          (goto-char (point-max)) +          (insert content) +          ;; adopt reply-to-id, visibility and CW: +          (when reply-id +            (setq mastodon-toot--reply-to-id reply-id)) +          (setq mastodon-toot--visibility toot-visibility) +          (mastodon-toot--set-cw source-cw) +          (mastodon-toot--update-status-fields) +          (setq mastodon-toot--edit-toot-id id)))))) + +(defun mastodon-toot--get-toot-source (id) +  "Fetch the source JSON of toot with ID." +  (let ((url (mastodon-http--api (format "/statuses/%s/source" id)))) +    (mastodon-http--get-json url nil :silent))) + +(defun mastodon-toot--get-toot-edits (id) +  "Return the edit history of toot with ID." +  (let* ((url (mastodon-http--api (format "statuses/%s/history" id)))) +    (mastodon-http--get-json url))) + +(defun mastodon-toot--view-toot-edits () +  "View editing history of the toot at point in a popup buffer." +  (interactive) +  (let ((history (mastodon-tl--property 'edit-history))) +    (with-current-buffer (get-buffer-create "*mastodon-toot-edits*") +      (let ((inhibit-read-only t)) +        (special-mode) +        (erase-buffer) +        (let ((count 1)) +          (mapc (lambda (x) +                  (insert (propertize (if (= count 1) +                                          (format "%s [original]:\n" count) +                                        (format "%s:\n" count)) +                                      'face 'font-lock-comment-face) +                          (mastodon-toot--insert-toot-iter x) +                          "\n") +                  (cl-incf count)) +                history)) +        (switch-to-buffer-other-window (current-buffer)) +        (setq-local header-line-format +                    (propertize +                     (format "Edits to toot by %s:" +                             (alist-get 'username +                                        (alist-get 'account (car history)))) +                     'face font-lock-comment-face)))))) + +(defun mastodon-toot--insert-toot-iter (it) +  "Insert iteration IT of toot." +  (let ((content (alist-get 'content it)) +        (account (alist-get 'account it))) +    ;; TODO: handle polls, media +    (mastodon-tl--render-text content))) +  (defun mastodon-toot--restore-previous-window-config (config)    "Restore the window CONFIG after killing the toot compose buffer.  Buffer-local variable `mastodon-toot-previous-window-config' holds the config." @@ -703,116 +828,75 @@ eg. \"feduser@fed.social\" -> \"feduser@fed.social\"."                 (reverse (append mentions nil))                 ""))) -(defun mastodon-toot--mentions-company-meta (candidate) -  "Format company completion CANDIDATE's meta field." -  (format " %s" -          (get-text-property 0 'meta candidate))) - -(defun mastodon-toot--mentions-company-annotation (candidate) -  "Format company completion CANDIDATE's annotation." -  (format " %s" (get-text-property 0 'annot candidate))) - -(defun mastodon-toot--mentions-company-make-candidate (candidate) -  "Construct a company completion CANDIDATE for display." -  (let ((display-name (car candidate)) -        (handle (cadr candidate)) -        (url (caddr candidate))) -    (propertize handle 'annot display-name 'meta url))) - -(defun mastodon-toot--tags-company-make-candidate (candidate) -  "Construct a company completion CANDIDATE for display." -  (let ((tag (concat "#" (car candidate))) -        (url (cadr candidate))) -    (propertize tag 'annot url 'meta url))) - -(defun mastodon-toot--company-build-candidates (query list-fun make-fun) -  "Build a list of completion candidates for a company backend. -QUERY is the search prefix, LIST-FUN builds a list of items to -match against, and MAKE-FUN builds the actual cadidate list item -for display by company." -  (let ((query (substring query 1)) ; remove @ or # for search -        (res)) -    (dolist (item (funcall list-fun query)) -      (when (or (string-prefix-p query (substring (cadr item) 1) t) -                (string-prefix-p query (car item) t)) -        (push (funcall make-fun item) res))) -    res)) - -(defun mastodon-toot--mentions-company-candidates (query) -  "Given a company QUERY, build a list of candidates. -The query can match both user handles and display names." -  (mastodon-toot--company-build-candidates -   query -   'mastodon-search--search-accounts-query -   'mastodon-toot--mentions-company-make-candidate)) - -(defun mastodon-toot--tags-company-candidates (query) -  "Given a company QUERY, build a list of candidates. -The query is matched against a tag search on the server." -  (mastodon-toot--company-build-candidates -   query -   'mastodon-search--search-tags-query -   'mastodon-toot--tags-company-make-candidate)) - -(defun mastodon-toot--make-company-backend -    (command _backend-name str-prefix candidates-fun annot-fun meta-fun -             &optional arg -             &rest ignored) -  "Make a company backend for `mastodon-toot-mode'. -COMMAND, ARG, IGNORED are all company backend args. -COMMAND is either prefix, to fetch a prefix query, candidates, to -build a list of candidates with query ARG, annotation, to format -an annotation for candidate ARG, or meta, to format meta info for -candidate ARG. IGNORED remains a mystery. - -BACKEND-NAME is the backend's name, STR-PREFIX is used to search -for matches, CANDIDATES-FUN, ANNOT-FUN, and META-FUN are -functions called on ARG to generate formatted candidates, annotation, and -meta fields respectively." -  (interactive (list 'interactive)) -  (cl-case command -    (interactive (company-begin-backend (quote backend-name))) -    (prefix (when (and (bound-and-true-p mastodon-toot-mode) ; if masto toot minor mode -                       (save-excursion -                         (forward-whitespace -1) -                         (forward-whitespace 1) -                         (looking-at str-prefix))) -              (concat str-prefix (company-grab-symbol)))) -    (candidates (funcall candidates-fun arg)) -    (annotation (funcall annot-fun arg)) -    (meta (funcall meta-fun arg)))) - -(defun mastodon-toot-mentions (command &optional arg &rest ignored) -  "A company completion backend for toot mentions. -COMMAND is either prefix, to fetch a prefix query, candidates, to -build a list of candidates with query ARG, annotation, to format -an annotation for candidate ARG, or meta, to format meta info for -candidate ARG. IGNORED remains a mystery." -  (mastodon-toot--make-company-backend -   command -   'mastodon-toot-mentions -   "@" -   'mastodon-toot--mentions-company-candidates -   'mastodon-toot--mentions-company-annotation -   'mastodon-toot--mentions-company-meta -   arg -   ignored)) - -(defun mastodon-toot-tags (command &optional arg &rest ignored) -  "A company completion backend for toot tags. -COMMAND is either prefix, to fetch a prefix query, candidates, to -build a list of candidates with query ARG, annotation, to format -an annotation for candidate ARG, or meta, to format meta info for -candidate ARG. IGNORED remains a mystery." -  (mastodon-toot--make-company-backend -   command -   'mastodon-toot-tags -   "#" -   'mastodon-toot--tags-company-candidates -   'mastodon-toot--mentions-company-annotation -   'mastodon-toot--mentions-company-meta -   arg -   ignored)) +(defun mastodon-toot--get-bounds (regex) +  "Get bounds of tag or handle before point." +  ;; needed because # and @ are not part of any existing thing at point +  (save-match-data +    (save-excursion +      ;; match full handle inc. domain, or tag including # +      ;; (see the regexes for subexp 2) +      (when (re-search-backward regex nil :no-error) +        (cons (match-beginning 2) +              (match-end 2)))))) + +(defun mastodon-toot--mentions-capf () +  "Build a mentions completion backend for `completion-at-point-functions'." +  (let* ((bounds +          (mastodon-toot--get-bounds mastodon-toot-handle-regex)) +         (start (car bounds)) +         (end (cdr bounds))) +    (when bounds +      (list start +            end +            ;; only search when necessary: +            (completion-table-dynamic +             (lambda (_) +               ;; TODO: do we really need to set a local var here +               ;; just for the annotation-function? +               (setq mastodon-toot-completions +                     (mastodon-search--search-accounts-query +                      (buffer-substring-no-properties start end) +                      :capf)))) +            :exclusive 'no +            :annotation-function +            (lambda (candidate) +              (concat " " +                      (mastodon-toot--mentions-annotation-fun candidate))))))) + +(defun mastodon-toot--tags-capf () +  "Build a tags completion backend for `completion-at-point-functions'." +  (let* ((bounds +          (mastodon-toot--get-bounds mastodon-toot-tag-regex)) +         (start (car bounds)) +         (end (cdr bounds))) +    (when bounds +      (list start +            end +            ;; only search when necessary: +            (completion-table-dynamic +             (lambda (_) +               (setq mastodon-toot-completions +                     (let ((tags (mastodon-search--search-tags-query +                                  (buffer-substring-no-properties start end)))) +                       (mapcar (lambda (x) +                                 (list (concat "#" (car x)) +                                       (cdr x))) +                               tags))))) +            :exclusive 'no +            :annotation-function +            (lambda (candidate) +              (concat " " +                      (mastodon-toot--tags-annotation-fun candidate))))))) + +(defun mastodon-toot--mentions-annotation-fun (candidate) +  "Given a handle completion CANDIDATE, return its annotation string, a username." +  (caddr (assoc candidate mastodon-toot-completions))) + +(defun mastodon-toot--tags-annotation-fun (candidate) +  "Given a tag string CANDIDATE, return an annotation, the tag's URL." +  ;; FIXME check the list returned here? should be cadr +  ;;or make it an alist and use cdr +  (caadr (assoc candidate mastodon-toot-completions)))  (defun mastodon-toot--reply ()    "Reply to toot at `point'. @@ -960,12 +1044,6 @@ which is used to attach it to a toot when posting."                  mastodon-toot--media-attachments))        (list "None"))) -(defun mastodon-toot--make-poll-options-params (options) -  "Return an parameter query alist from poll OPTIONS." -  (let ((key "poll[options][]")) -    (cl-loop for o in options -             collect `(,key . ,o)))) -  (defun mastodon-toot--fetch-max-poll-options ()    "Return the maximum number of poll options."    (mastodon-toot--fetch-poll-field 'max_options)) @@ -976,7 +1054,7 @@ which is used to attach it to a toot when posting."        50)) ; masto default  (defun mastodon-toot--fetch-poll-field (field) -  "Return FIELD from the poll settings from the user's instance. " +  "Return FIELD from the poll settings from the user's instance."    (let* ((instance (mastodon-http--get-json (mastodon-http--api "instance"))))      (alist-get field                 (alist-get 'polls @@ -1008,7 +1086,8 @@ MAX is the maximum number set by their instance."      (message "poll created!")))  (defun mastodon-toot--read-poll-options (count length) -  "Read a list of options for poll of LENGTH options." +  "Read a list of options for poll with COUNT options. +LENGTH is the maximum character length allowed for a poll option."    (cl-loop for x from 1 to count             collect (read-string (format "Poll option [%s/%s] [max %s chars]: " x count length)))) @@ -1035,6 +1114,19 @@ MAX is the maximum number set by their instance."      ("14 days" . ,(number-to-string (* 60 60 24 14)))      ("30 days" . ,(number-to-string (* 60 60 24 30))))) +(defun mastodon-toot--set-toot-lang () +  "Prompt for a language and set `mastodon-toot--language'. +Return its two letter ISO 639 1 code." +  (interactive) +  (let* ((langs (mapcar (lambda (x) +                          (cons (cadr x) +                                (car x))) +                        mastodon-iso-639-1)) +         (choice (completing-read "Language for this toot: " +                                  langs))) +    (setq mastodon-toot--language +          (alist-get choice langs nil nil 'equal)))) +  ;; we'll need to revisit this if the binds get  ;; more diverse than two-chord bindings  (defun mastodon-toot--get-mode-kbinds () @@ -1162,26 +1254,28 @@ REPLY-JSON is the full JSON of the toot being replied to."        (setq mastodon-toot--reply-to-id reply-to-id)        (unless (equal mastodon-toot--visibility reply-visibility)          (setq mastodon-toot--visibility reply-visibility)) -      (mastodon-toot-set-cw reply-cw)))) +      (mastodon-toot--set-cw reply-cw))))  (defun mastodon-toot--update-status-fields (&rest _args)    "Update the status fields in the header based on the current state."    (ignore-errors  ;; called from after-change-functions so let's not leak errors -    (let ((inhibit-read-only t) -          (header-region (mastodon-tl--find-property-range 'toot-post-header +    (let* ((inhibit-read-only t) +           (header-region (mastodon-tl--find-property-range 'toot-post-header +                                                            (point-min))) +           (count-region (mastodon-tl--find-property-range 'toot-post-counter                                                             (point-min))) -          (count-region (mastodon-tl--find-property-range 'toot-post-counter +           (visibility-region (mastodon-tl--find-property-range +                               'toot-post-visibility (point-min))) +           (nsfw-region (mastodon-tl--find-property-range 'toot-post-nsfw-flag                                                            (point-min))) -          (visibility-region (mastodon-tl--find-property-range -                              'toot-post-visibility (point-min))) -          (nsfw-region (mastodon-tl--find-property-range 'toot-post-nsfw-flag -                                                         (point-min))) -          (cw-region (mastodon-tl--find-property-range 'toot-post-cw-flag -                                                       (point-min)))) +           (cw-region (mastodon-tl--find-property-range 'toot-post-cw-flag +                                                        (point-min))) +           (toot-string (buffer-substring-no-properties (cdr header-region) +                                                        (point-max))))        (add-text-properties (car count-region) (cdr count-region)                             (list 'display                                   (format "%s/%s characters" -                                         (- (point-max) (cdr header-region)) +                                         (mastodon-toot--count-toot-chars toot-string)                                           (number-to-string mastodon-toot--max-toot-chars))))        (add-text-properties (car visibility-region) (cdr visibility-region)                             (list 'display @@ -1201,6 +1295,26 @@ REPLY-JSON is the full JSON of the toot being replied to."                             (list 'invisible (not mastodon-toot--content-warning)                                   'face 'mastodon-cw-face))))) +(defun mastodon-toot--count-toot-chars (toot-string) +  "Count the characters in TOOT-STRING. +URLs always = 23, and domain names of handles are not counted. +This is how mastodon does it." +  (with-temp-buffer +    (switch-to-buffer (current-buffer)) +    (insert toot-string) +    (goto-char (point-min)) +    ;; handle URLs +    (while (search-forward-regexp "\\w+://[^ \n]*" nil t) ; URL +      (replace-match "xxxxxxxxxxxxxxxxxxxxxxx")) ; 23 x's +    ;; handle @handles +    (goto-char (point-min)) +    (while (search-forward-regexp (concat "\\(?2:@[^ @\n]+\\)" ; a handle only +                                          "\\(@[^ \n]+\\)?" ; with poss domain +                                          "\\b") +                                  nil t) +      (replace-match (match-string 2))) ; replace with handle only +    (length (buffer-substring (point-min) (point-max))))) +  (defun mastodon-toot--save-toot-text (&rest _args)    "Save the current toot text in `mastodon-toot-current-toot-text'.  Added to `after-change-functions' in new toot buffers." @@ -1208,15 +1322,15 @@ Added to `after-change-functions' in new toot buffers."      (unless (string-empty-p text)        (setq mastodon-toot-current-toot-text text)))) -(defun mastodon-toot-open-draft-toot () +(defun mastodon-toot--open-draft-toot ()    "Prompt for a draft and compose a toot with it."    (interactive)    (if mastodon-toot-draft-toots-list        (let ((text (completing-read "Select draft toot: "                                     mastodon-toot-draft-toots-list                                     nil t))) -        (if (mastodon-toot-compose-buffer-p) -            (when (and (not (mastodon-toot-empty-p :text-only)) +        (if (mastodon-toot--compose-buffer-p) +            (when (and (not (mastodon-toot--empty-p :text-only))                         (y-or-n-p "Replace current text with draft?"))                (cl-pushnew mastodon-toot-current-toot-text                            mastodon-toot-draft-toots-list) @@ -1228,11 +1342,11 @@ Added to `after-change-functions' in new toot buffers."                ;; (delete-region (point) (point-max))                (insert text))            (mastodon-toot--compose-buffer nil nil nil text))) -    (unless (mastodon-toot-compose-buffer-p) +    (unless (mastodon-toot--compose-buffer-p)        (mastodon-toot--compose-buffer))      (message "No drafts available."))) -(defun mastodon-toot-delete-draft-toot () +(defun mastodon-toot--delete-draft-toot ()    "Prompt for a draft toot and delete it."    (interactive)    (if mastodon-toot-draft-toots-list @@ -1245,7 +1359,7 @@ Added to `after-change-functions' in new toot buffers."          (message "Draft deleted!"))      (message "No drafts to delete."))) -(defun mastodon-toot-delete-all-drafts () +(defun mastodon-toot--delete-all-drafts ()    "Delete all drafts."    (interactive)    (setq mastodon-toot-draft-toots-list nil) @@ -1254,7 +1368,7 @@ Added to `after-change-functions' in new toot buffers."  (defun mastodon-toot--propertize-tags-and-handles (&rest _args)    "Propertize tags and handles in toot compose buffer.  Added to `after-change-functions'." -  (when (mastodon-toot-compose-buffer-p) +  (when (mastodon-toot--compose-buffer-p)      (let ((header-region             (mastodon-tl--find-property-range 'toot-post-header                                               (point-min)))) @@ -1262,16 +1376,12 @@ Added to `after-change-functions'."        ;; stops all text after a handle or mention being propertized:        (set-text-properties (cdr header-region) (point-max) nil)        ;; TODO: confirm allowed hashtag/handle characters: -      (mastodon-toot--propertize-item "\\([\n\t ]\\|^\\)\\(?2:#[1-9a-zA-Z_]+\\)\\b" +      (mastodon-toot--propertize-item mastodon-toot-tag-regex                                        'success                                        (cdr header-region)) -      (mastodon-toot--propertize-item -       (concat "\\([\n\t ]\\|^\\)" ; preceding space or bol -               "\\(?2:@[1-9a-zA-Z._-]+" ; a handle -               "\\(@[1-9a-zA-Z._-]+\\)?\\)" ; with poss domain -               "\\b") ; boundary -       'mastodon-display-name-face -       (cdr header-region))))) +      (mastodon-toot--propertize-item mastodon-toot-handle-regex +                                      'mastodon-display-name-face +                                      (cdr header-region)))))  (defun mastodon-toot--propertize-item (regex face start)    "Propertize item matching REGEX with FACE starting from START." @@ -1282,7 +1392,7 @@ Added to `after-change-functions'."                                       (match-end 2)                                       `(face ,face))))) -(defun mastodon-toot-compose-buffer-p () +(defun mastodon-toot--compose-buffer-p ()    "Return t if compose buffer is current."    (equal (buffer-name (current-buffer)) "*new toot*")) @@ -1319,13 +1429,22 @@ a draft into the buffer."        ;; no need to fetch from `mastodon-profile-account-settings' as        ;; `mastodon-toot--max-toot-chars' is set when we set it        (mastodon-toot--get-max-toot-chars)) -    ;; set up company backends: -    (when (require 'company nil :noerror) -      (when mastodon-toot--enable-completion +    ;; set up completion: +    (when mastodon-toot--enable-completion +      (set ; (setq-local +       (make-local-variable 'completion-at-point-functions) +       (add-to-list +        'completion-at-point-functions +        #'mastodon-toot--mentions-capf)) +      (add-to-list +       'completion-at-point-functions +       #'mastodon-toot--tags-capf) +      ;; company +      (when mastodon-toot--use-company-for-completion          (set (make-local-variable 'company-backends) -             (add-to-list 'company-backends 'mastodon-toot-mentions)) -        (add-to-list 'company-backends 'mastodon-toot-tags)) -      (company-mode-on)) +             (add-to-list 'company-backends 'company-capf)) +        (company-mode-on))) +    ;; after-change:      (make-local-variable 'after-change-functions)      (push #'mastodon-toot--update-status-fields after-change-functions)      (mastodon-toot--refresh-attachments-display) @@ -1340,7 +1459,7 @@ a draft into the buffer."        (insert initial-text))))  ;;;###autoload -(add-hook 'mastodon-toot-mode-hook #'mastodon-profile-fetch-server-account-settings-maybe) +(add-hook 'mastodon-toot-mode-hook #'mastodon-profile--fetch-server-account-settings-maybe)  (define-minor-mode mastodon-toot-mode    "Minor mode to capture Mastodon toots." | 
