From c11d75da8783f3ded3338b13238646b49cfdcc64 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 22 Apr 2021 11:28:35 -0400 Subject: [PATCH] FEATURE: Allow pausing animated images in posts (#12795) Co-authored-by: Jarek Radosz --- .../app/initializers/animated-images.js | 66 +++++++++++++++++++ .../stylesheets/common/base/topic-post.scss | 16 +++++ lib/cooked_post_processor.rb | 7 +- spec/components/cooked_post_processor_spec.rb | 3 +- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/initializers/animated-images.js diff --git a/app/assets/javascripts/discourse/app/initializers/animated-images.js b/app/assets/javascripts/discourse/app/initializers/animated-images.js new file mode 100644 index 00000000000..1d3a77e2fd4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/animated-images.js @@ -0,0 +1,66 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; + +let _gifClickHandlers = {}; + +export default { + name: "animated-images-pause-on-click", + + initialize() { + withPluginApi("0.8.7", (api) => { + function _cleanUp() { + Object.values(_gifClickHandlers || {}).forEach((handler) => + handler.removeEventListener("click", _handleClick) + ); + + _gifClickHandlers = {}; + } + + function _handleClick(event) { + const img = event.target; + if (img && !img.previousSibling) { + let canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height); + canvas.setAttribute("aria-hidden", "true"); + canvas.setAttribute("role", "presentation"); + + img.parentNode.classList.add("paused-animated-image"); + img.parentNode.insertBefore(canvas, img); + } else { + img.previousSibling.remove(); + img.parentNode.classList.remove("paused-animated-image"); + } + } + + function _attachCommands(post, helper) { + if (!helper) { + return; + } + + let images = post.querySelectorAll("img.animated"); + + images.forEach((img) => { + if (_gifClickHandlers[img.src]) { + _gifClickHandlers[img.src].removeEventListener( + "click", + _handleClick + ); + + delete _gifClickHandlers[img.src]; + } + + _gifClickHandlers[img.src] = img; + img.addEventListener("click", _handleClick, false); + }); + } + + api.decorateCookedElement(_attachCommands, { + onlyStream: true, + id: "animated-images-pause-on-click", + }); + + api.cleanupStream(_cleanUp); + }); + }, +}; diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 6fb2ec9a1b0..85dcb3d6253 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -1233,3 +1233,19 @@ a.mention-group { @include ellipsis; } } + +.paused-animated-image { + position: relative; + display: block; + > canvas { + position: absolute; + top: 0; + left: 0; + } + + img.animated { + // need to keep the image hidden but clickable + // so the user can resume animation + opacity: 0; + } +} diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 6197c434d63..ed57f8b067a 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -318,6 +318,12 @@ class CookedPostProcessor return end + upload = Upload.get_from_url(src) + + if upload.present? && upload.animated? + img.add_class("animated") + end + return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0 @@ -332,7 +338,6 @@ class CookedPostProcessor width, height = ImageSizer.resize(width, height) end - upload = Upload.get_from_url(src) if upload.present? upload.create_thumbnail!(width, height, crop: crop) diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 6117d88f231..7cffe18edff 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -973,7 +973,7 @@ describe CookedPostProcessor do expect(doc.css('img').first['srcset']).to_not eq(nil) end - it "does not optimize animated images" do + it "does not optimize animated images but adds a class so animated images can be identified" do upload.update!(animated: true) post = Fabricate(:post, raw: "![image|1024x768, 50%](#{upload.short_url})") @@ -984,6 +984,7 @@ describe CookedPostProcessor do expect(doc.css('.lightbox-wrapper').size).to eq(1) expect(doc.css('img').first['src']).to include(upload.url) expect(doc.css('img').first['srcset']).to eq(nil) + expect(doc.css('img.animated').size).to eq(1) end it "optimizes images in quotes" do