From 35886f7c19ef00f7c86c2e63ca335ae13ae7aa81 Mon Sep 17 00:00:00 2001 From: Holger Dürer Date: Fri, 5 May 2017 22:19:02 +0100 Subject: Show users' avatars plus other image work. - Shows users' avatars (makes only sense if Emacs is built with imagemagick) - Scales media attachement previews to a max size (if Emacs is built with imagemagick) - Enable cacheing of image fetches Known issues: - We should really cache the avatars to avoid having multiple identical images in memory. --- lisp/mastodon-tl.el | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) (limited to 'lisp/mastodon-tl.el') diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index e025a6e..1a5d9ae 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -104,8 +104,18 @@ Optionally start from POS." "Propertize author of TOOT." (let* ((account (cdr (assoc 'account toot))) (handle (cdr (assoc 'acct account))) - (name (cdr (assoc 'display_name account)))) + (name (cdr (assoc 'display_name account))) + (avatar-url (cdr (assoc 'avatar account)))) (concat + (when mastodon-media-show-avatars-p + ;; We use just an empty space as the textual representation. + ;; This is what a user will see on a non-graphical display + ;; where not showing an avatar at all is preferable. + (concat (propertize " " + 'media-url avatar-url + 'media-state 'needs-loading + 'media-type 'avatar) + " ")) (propertize name 'face 'warning) " (@" handle @@ -177,14 +187,16 @@ also render the html" (defun mastodon-tl--media (toot) "Retrieve a media attachment link for TOOT if one exists." - (let ((media (mastodon-tl--field 'media_attachments toot))) - (mapconcat - (lambda (media-preview) - (concat "Media_Link:: " - (mastodon-tl--set-face - (cdr (assoc 'preview_url media-preview)) - 'mouse-face nil))) - media "\n"))) + (let ((media-attachements (mastodon-tl--field 'media_attachments toot))) + (mapconcat + (lambda (media-attachement) + (let ((preview-url (cdr (assoc 'preview_url media-attachement)))) + (concat (propertize "[img]" + 'media-url preview-url + 'media-state 'needs-loading + 'media-type 'media-link) + " "))) + media-attachements ""))) (defun mastodon-tl--content (toot) "Retrieve text content from TOOT." -- cgit v1.2.3 From b20265eea37884bde663aa6d1d498c9180b89947 Mon Sep 17 00:00:00 2001 From: Holger Dürer Date: Mon, 8 May 2017 22:17:39 +0100 Subject: Move the rendering of images fully into mastodon-media.el and use default images. Having all the logic in one file reduces interdependencies. Having default images is more pleasing during the incremental loading. --- lisp/mastodon-media.el | 118 +++++++++++++++++++++++++++++++++++++++++++--- lisp/mastodon-tl.el | 15 +----- test/mastodon-tl-tests.el | 52 +++++++++++++++++--- 3 files changed, 160 insertions(+), 25 deletions(-) (limited to 'lisp/mastodon-tl.el') diff --git a/lisp/mastodon-media.el b/lisp/mastodon-media.el index 289637e..734e11f 100644 --- a/lisp/mastodon-media.el +++ b/lisp/mastodon-media.el @@ -32,7 +32,6 @@ ;;; Code: (require 'mastodon-http nil t) -(require 'mastodon) (defgroup mastodon-media nil "Inline Mastadon media." @@ -43,6 +42,84 @@ (image-type-available-p 'imagemagick) "A boolean value stating whether to show avatars in timelines.") +(defvar mastodon-media--generic-avatar-data + (base64-decode-string + "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") + "The PNG data for a generic 100x100 avatar") + +(defvar mastodon-media--generic-broken-image-data + (base64-decode-string + "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA +B3RJTUUH4QUIFQUVFt+0LQAAABZ0RVh0Q29tbWVudABHZW5lcmljIGF2YXRhcsyCnMsAAAdoSURB +VHja7d1NSFRrAIfx//iB6ZDSMJYVkWEk0ceYFUkkhhQlEUhEg0FlC1eBoRTUwlbRok0TgRQURZAE +FgpjJmFajpK4kggxpXHRQEGWUJZizpy7uPfC5eKiV+dD5zw/mN05jrxnnjnfcxyWZVkCMKc0SXI4 +HIwEMIcUhgAgEIBAAAIBCAQgEIBAAAIBCAQgEAAEAhAIQCAAgQAEAhAIQCAAgQA2kBaNP8Jt7ViM +onErOWsQgEAAAgEIBCAQgEAAAgEIBCAQgEAAEAhAIACBAAQCEAhAIACBAAQCEAhAIAAIBCAQgEAA +AgEIBCAQgEAAAgEIBACBAAQCEAhAIACBAAQCEAhAIACBAAQCgEAAAgEIBCAQgECAxSyNIYitz58/ +a3BwUIODgxoZGVEoFFIoFNK3b980NTWlX79+SZIyMzOVlZWlVatWae3atSooKJDH49HOnTvl8XiU +ksJ3WSI4LMuyHA7Hgv6IZVmM5D8mJyf1/PlzdXZ2qrOzU8FgcMF/0+126+DBg6qqqlJFRYXS0vhe ++6MP9wI/1wQSJeFwWH6/X01NTWpra9PU1FTM3isvL0/nz5/XuXPntHz5ciqIcSCy/v50L+hlV+Pj +49a1a9esdevWLXgMTV8ul8u6c+eOFYlELMwtKmNNIOa+fv1qXbp0yXI6nXEP4/+v0tJS6+PHj9RA +IIk3PT1tXb161crOzk54GP995ebmWt3d3RRBIInj9/utgoKCRRXGf18ZGRmW3++niigHwk56PHf4 +Yiw9PV0dHR0qLy9nD52jWAQylxUrVmhgYEAbN24kkCgsM84+JZmJiQmdPn1akUiEweBE4eL/NsrN +zVVZWZlKSkpUWFioTZs2yeVyKTs7W7Ozs5qYmNDExITev3+v/v5+9fX1qb+/f8FjevPmTdXW1rIG +IZDFN9gbNmyQ1+uV1+uVx+MxXlAjIyNqbGzU3bt39fPnz3n9vytXrlQwGJTT6SQQThQm/ohIamqq +VVlZaXV1dUXtPT98+GCVlZXNe7n4fD6OYnGYN7GDnZ6ebtXU1FhjY2Mxed9IJGLV19fPa7kUFRUR +CIEkZrAdDod15syZmIXxf7W1tfNaNqOjowSygBdHseZh7969GhgY0IMHD5Sfnx+X97xx44Z2795t +PF93dzcLjMO88TvHcP/+ffX19WnXrl3xXVApKbp9+7bxfSFv3rxhwRFI7B07dkxDQ0Oqrq5O2P9Q +XFysffv2Gc0zOjrKwiOQ2Hv69Kny8vIS/n8cP37caPqxsTEWHoHYa//HxPfv3xk0ArGP1atXG03/ +7z3vIBBbyM3NNZo+KyuLQSMQ+5icnDSaPicnh0EjEPsYHh42mp7L3gnEVnp6eoymLyoqYtAIxD4e +PXpkNP3+/fsZtAXgcvclpL29XUeOHPnj6Z1Op8bHx7Vs2TJ7fri5o9A+ZmZmdPHiRaN5vF6vbeNg +E8tmGhoaNDQ0ZPTteeHCBQaOQJLfkydPdP36daN5Tp48qc2bNzN47IMkt9evX+vw4cOanp7+43ly +cnI0PDy8KK4dYx8EMRMIBHT06FGjOCTJ5/PZPg42sZJce3u7Dh06pB8/fhjNV11dndBL8tnEYhMr +5lpaWuT1evX792+j+YqLixUIBLj+ik2s5NXc3KwTJ04Yx5Gfn69nz54RB5tYyaupqUlVVVWanZ01 +ms/tdqujo4P9DgJJXg8fPtSpU6cUDoeN43j58qUKCwsZRAJJTvfu3dPZs2eNf0/X7Xarq6tL27dv +ZxAJJDn5fD7V1NQYx7FmzRq9evVK27ZtYxAJJDk1NDSorq7O+ChgQUGBent7tWXLFgYxxniecILU +1dXJ5/MZz7d161a9ePHC+N50sAZZMq5cuTKvOEpKStTT00McccSJwji7devWvJ7bceDAAbW2ttr6 +cQbGH26eD7K0BAIBlZeXG5/nqKioUEtLizIyMhhEAklOX758kcfj0adPn4zXHG1tbcSRoEDYB4mT +y5cvG8exZ88etba2Egf7IMnt7du32rFjh9G5jvz8fA0MDBj/UBxYgyw5jY2NRnGkpqaqubmZOBYB +AomxmZkZPX782Gie+vr6uD9/BGxiJURvb69KS0v/ePrMzEyFQiG5XC4Gj02s5BcIBIymr6ysJA42 +sezj3bt3RtObPv8DBLKkBYNBo+m5r4NAbCUUChlNv379egaNQOzD9FdJ2P8gEFsxfQQaFyMuLhzm +jfUAG45tOBw2fhY6ojP2rEGWwiqdONjEAggEIBCAQAACAUAgAIEA0cIPx8UYJ1FZgwAEAhAIAAIB +CAQgEIBAAAIBFiNOFMaY6V1tnFhkDQIQCEAgAIEABAKAQAACAQgEIBCAQAACAQgEIBCAQABIXO4e +c1y+zhoEIBCAQAAQCEAgAIEABAIQCEAgAIEABAIQCEAgAAgEIBCAQAACAQgEIBCAQAACAQgEAIEA +BAIQCEAgAIEABAIsJVH58WqHw8FIgjUIQCAACAQgEIBAAAIBCAQgEIBAAAIBCAQgEAAEAhAIQCBA +fKRJkmVZjAQwh78A6vCRWJE8K+8AAAAASUVORK5CYII=") + "The PNG data for a generic 200x200 'broken image' view") + (defun mastodon-media--process-image-response (status-plist marker image-options region-length image-url) "Callback function processing the url retrieve response for URL. @@ -68,16 +145,18 @@ IMAGE-URL is the URL that was retrieved. (save-restriction (widen) (put-text-property marker (+ marker region-length) 'media-state 'loaded) - (put-text-property marker (+ marker region-length) - 'display (or - image - (format "Failed to load %s" image-url))) + (when image + ;; We only set the image to display if we could load + ;; it; we already have set a default image when we + ;; added the tag. + (put-text-property marker (+ marker region-length) + 'display image)) ;; We are done with the marker; release it: (set-marker marker nil))) (kill-buffer url-buffer))))) (defun mastodon-media--load-image-from-url (url media-type start region-length) - "Takes a URL and MEDIA-TYPE and return an image. + "Takes a URL and MEDIA-TYPE and load the image asynchronously. MEDIA-TYPE is a symbol and either 'avatar or 'media-link." ;; TODO: Cache the avatars @@ -134,5 +213,32 @@ not been returned." (put-text-property start end 'media-state 'loading) (mastodon-media--load-image-from-url image-url media-type start (- end start))))))) +(defun mastodon-media--get-avatar-rendering (avatar-url) + "Returns the string to be written that renders the avatar at AVATAR-URL." + ;; We use just an empty space as the textual representation. + ;; This is what a user will see on a non-graphical display + ;; where not showing an avatar at all is preferable. + (let ((image-options (when (image-type-available-p 'imagemagick) + `(:height ,mastodon-avatar-height)))) + (concat + (propertize " " + 'media-url avatar-url + 'media-state 'needs-loading + 'media-type 'avatar + 'display (apply #'create-image mastodon-media--generic-avatar-data + (when image-options 'imagemagick) + t image-options)) + " "))) + +(defun mastodon-media--get-media-link-rendering (media-url) + "Returns the string to be written that renders the image at MEDIA-URL." + (concat + (propertize "[img]" + 'media-url media-url + 'media-state 'needs-loading + 'media-type 'media-link + 'display (create-image mastodon-media--generic-broken-image-data nil t)) + " ")) + (provide 'mastodon-media) ;;; mastodon-media.el ends here diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index 1a5d9ae..b3550c6 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -108,14 +108,7 @@ Optionally start from POS." (avatar-url (cdr (assoc 'avatar account)))) (concat (when mastodon-media-show-avatars-p - ;; We use just an empty space as the textual representation. - ;; This is what a user will see on a non-graphical display - ;; where not showing an avatar at all is preferable. - (concat (propertize " " - 'media-url avatar-url - 'media-state 'needs-loading - 'media-type 'avatar) - " ")) + (mastodon-media--get-avatar-rendering avatar-url)) (propertize name 'face 'warning) " (@" handle @@ -191,11 +184,7 @@ also render the html" (mapconcat (lambda (media-attachement) (let ((preview-url (cdr (assoc 'preview_url media-attachement)))) - (concat (propertize "[img]" - 'media-url preview-url - 'media-state 'needs-loading - 'media-type 'media-link) - " "))) + (mastodon-media--get-media-link-rendering preview-url))) media-attachements ""))) (defun mastodon-tl--content (toot) diff --git a/test/mastodon-tl-tests.el b/test/mastodon-tl-tests.el index e89d313..0d0458d 100644 --- a/test/mastodon-tl-tests.el +++ b/test/mastodon-tl-tests.el @@ -105,7 +105,8 @@ (ert-deftest mastodon-tl--byline-regular () "Should format the regular toot correctly." - (let ((timestamp (cdr (assoc 'created_at mastodon-tl-test-base-toot)))) + (let ((mastodon-media-show-avatars-p nil) + (timestamp (cdr (assoc 'created_at mastodon-tl-test-base-toot)))) (with-mock (mock (date-to-time timestamp) => '(22782 21551)) (mock (format-time-string mastodon-toot-timestamp-format '(22782 21551)) => "2999-99-99 00:11:22") @@ -116,9 +117,24 @@ | Account 42 (@acct42@example.space) 2999-99-99 00:11:22 ------------"))))) +(ert-deftest mastodon-tl--byline-regular-with-avatar () + "Should format the regular toot correctly." + (let ((mastodon-media-show-avatars-p t) + (timestamp (cdr (assoc 'created_at mastodon-tl-test-base-toot)))) + (with-mock + (mock (date-to-time timestamp) => '(22782 21551)) + (mock (format-time-string mastodon-toot-timestamp-format '(22782 21551)) => "2999-99-99 00:11:22") + + (should (string= (substring-no-properties + (mastodon-tl--byline mastodon-tl-test-base-toot)) + " + | Account 42 (@acct42@example.space) 2999-99-99 00:11:22 + ------------"))))) + (ert-deftest mastodon-tl--byline-boosted () "Should format the boosted toot correctly." - (let* ((toot (cons '(reblogged . t) mastodon-tl-test-base-toot)) + (let* ((mastodon-media-show-avatars-p nil) + (toot (cons '(reblogged . t) mastodon-tl-test-base-toot)) (timestamp (cdr (assoc 'created_at toot)))) (with-mock (mock (date-to-time timestamp) => '(22782 21551)) @@ -131,7 +147,8 @@ (ert-deftest mastodon-tl--byline-favorited () "Should format the favourited toot correctly." - (let* ((toot (cons '(favourited . t) mastodon-tl-test-base-toot)) + (let* ((mastodon-media-show-avatars-p nil) + (toot (cons '(favourited . t) mastodon-tl-test-base-toot)) (timestamp (cdr (assoc 'created_at toot)))) (with-mock (mock (date-to-time timestamp) => '(22782 21551)) @@ -145,7 +162,8 @@ (ert-deftest mastodon-tl--byline-boosted/favorited () "Should format the boosted & favourited toot correctly." - (let* ((toot `((favourited . t) (reblogged . t) ,@mastodon-tl-test-base-toot)) + (let* ((mastodon-media-show-avatars-p nil) + (toot `((favourited . t) (reblogged . t) ,@mastodon-tl-test-base-toot)) (timestamp (cdr (assoc 'created_at toot)))) (with-mock (mock (date-to-time timestamp) => '(22782 21551)) @@ -158,7 +176,8 @@ (ert-deftest mastodon-tl--byline-reblogged () "Should format the reblogged toot correctly." - (let* ((toot mastodon-tl-test-base-boosted-toot) + (let* ((mastodon-media-show-avatars-p nil) + (toot mastodon-tl-test-base-boosted-toot) (original-toot (cdr (assoc 'reblog mastodon-tl-test-base-boosted-toot))) (timestamp (cdr (assoc 'created_at toot))) (original-timestamp (cdr (assoc 'created_at original-toot)))) @@ -175,9 +194,30 @@ | Account 42 (@acct42@example.space) Boosted Account 43 (@acct43@example.space) original time ------------"))))) +(ert-deftest mastodon-tl--byline-reblogged-with-avatars () + "Should format the reblogged toot correctly." + (let* ((mastodon-media-show-avatars-p t) + (toot mastodon-tl-test-base-boosted-toot) + (original-toot (cdr (assoc 'reblog mastodon-tl-test-base-boosted-toot))) + (timestamp (cdr (assoc 'created_at toot))) + (original-timestamp (cdr (assoc 'created_at original-toot)))) + (with-mock + ;; We don't expect to use the toot's timestamp but the timestamp of the + ;; reblogged toot: + (mock (date-to-time timestamp) => '(1 2)) + (mock (format-time-string mastodon-toot-timestamp-format '(1 2)) => "reblogging time") + (mock (date-to-time original-timestamp) => '(3 4)) + (mock (format-time-string mastodon-toot-timestamp-format '(3 4)) => "original time") + + (should (string= (substring-no-properties (mastodon-tl--byline toot)) + " + | Account 42 (@acct42@example.space) Boosted Account 43 (@acct43@example.space) original time + ------------"))))) + (ert-deftest mastodon-tl--byline-reblogged-boosted/favorited () "Should format the reblogged toot that was also boosted & favoritedcorrectly." - (let* ((toot `((favourited . t) (reblogged . t) ,@mastodon-tl-test-base-boosted-toot)) + (let* ((mastodon-media-show-avatars-p nil) + (toot `((favourited . t) (reblogged . t) ,@mastodon-tl-test-base-boosted-toot)) (original-toot (cdr (assoc 'reblog mastodon-tl-test-base-boosted-toot))) (timestamp (cdr (assoc 'created_at toot))) (original-timestamp (cdr (assoc 'created_at original-toot)))) -- cgit v1.2.3