diff --git a/Changelog.md b/Changelog.md index 1220370b9..54c972751 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,7 @@ ## Bug fixes ## Features +* Add basic html5 audio/video embedding support [#6418](https://github.com/diaspora/diaspora/pull/6418) # 0.7.3.1 diff --git a/Gemfile b/Gemfile index c87f86b7b..b74d7524a 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem "json-schema", "2.8.0" # Authentication -gem "devise", "4.3.0" +gem "devise", "4.4.1" gem "devise_lastseenable", "0.0.6" # Captcha @@ -122,6 +122,8 @@ source "https://rails-assets.org" do gem "rails-assets-perfect-scrollbar", "0.6.16" end +gem "markdown-it-html5-embed", "1.0.0" + # Localization gem "http_accept_language", "2.1.1" @@ -186,7 +188,7 @@ gem "typhoeus", "1.3.0" # Views gem "gon", "6.1.0" -gem "hamlit", "2.8.4" +gem "hamlit", "2.8.6" gem "mobile-fu", "1.4.0" gem "rails-timeago", "2.16.0" gem "will_paginate", "3.1.6" diff --git a/Gemfile.lock b/Gemfile.lock index 054f1b156..1bfe6a95e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,7 +133,7 @@ GEM tins (~> 1.6) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.2) + crass (1.0.3) cucumber (2.4.0) builder (>= 2.1.2) cucumber-core (~> 1.5.0) @@ -156,7 +156,7 @@ GEM railties (>= 4, < 5.2) cucumber-wire (0.0.1) database_cleaner (1.6.1) - devise (4.3.0) + devise (4.4.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.2) @@ -187,7 +187,7 @@ GEM entypo-rails (3.0.0) railties (>= 4.1, < 6) equalizer (0.0.11) - erubi (1.6.1) + erubi (1.7.0) eslintrb (2.1.0) execjs multi_json (>= 1.3) @@ -273,7 +273,7 @@ GEM guard-rubocop (1.3.0) guard (~> 2.0) rubocop (~> 0.20) - haml (5.0.3) + haml (5.0.4) temple (>= 0.8.0) tilt haml_lint (0.26.0) @@ -282,7 +282,7 @@ GEM rake (>= 10, < 13) rubocop (>= 0.49.0) sysexits (~> 1.1) - hamlit (2.8.4) + hamlit (2.8.6) temple (>= 0.8.0) thor tilt @@ -306,7 +306,8 @@ GEM httparty (0.15.6) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (0.8.6) + i18n (0.9.5) + concurrent-ruby (~> 1.0) i18n-inflector (2.6.7) i18n (>= 0.4.1) i18n-inflector-rails (1.0.7) @@ -359,7 +360,7 @@ GEM multi_json (~> 1.10) logging-rails (0.6.0) logging (>= 1.8) - loofah (2.1.1) + loofah (2.2.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.0.12) @@ -367,6 +368,7 @@ GEM systemu (~> 2.6.2) mail (2.6.6) mime-types (>= 1.16, < 4) + markdown-it-html5-embed (1.0.0) markerb (1.1.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) @@ -377,7 +379,7 @@ GEM mini_magick (4.8.0) mini_mime (0.1.4) mini_portile2 (2.3.0) - minitest (5.10.3) + minitest (5.11.3) mobile-fu (1.4.0) rack-mobile-detect rails @@ -473,7 +475,7 @@ GEM byebug (~> 9.1) pry (~> 0.10) public_suffix (3.0.0) - rack (2.0.3) + rack (2.0.4) rack-cors (1.0.1) rack-google-analytics (1.2.0) actionpack @@ -492,7 +494,7 @@ GEM rack-rewrite (1.5.1) rack-ssl (1.4.1) rack - rack-test (0.7.0) + rack-test (0.8.2) rack (>= 1.0, < 3) rails (5.1.4) actioncable (= 5.1.4) @@ -576,7 +578,7 @@ GEM rainbow (2.2.2) rake raindrops (0.19.0) - rake (12.1.0) + rake (12.3.0) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -712,7 +714,7 @@ GEM unf (~> 0.1.0) typhoeus (1.3.0) ethon (>= 0.9.0) - tzinfo (1.2.3) + tzinfo (1.2.5) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) @@ -780,7 +782,7 @@ DEPENDENCIES cucumber-api-steps (= 0.13) cucumber-rails (= 1.5.0) database_cleaner (= 1.6.1) - devise (= 4.3.0) + devise (= 4.4.1) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) diaspora_federation-json_schema (= 0.2.3) @@ -801,7 +803,7 @@ DEPENDENCIES guard-rspec (= 4.7.3) guard-rubocop (= 1.3.0) haml_lint (= 0.26.0) - hamlit (= 2.8.4) + hamlit (= 2.8.6) handlebars_assets (= 0.23.2) http_accept_language (= 2.1.1) i18n-inflector-rails (= 1.0.7) @@ -815,6 +817,7 @@ DEPENDENCIES json-schema-rspec (= 0.0.4) leaflet-rails (= 1.2.0) logging-rails (= 0.6.0) + markdown-it-html5-embed (= 1.0.0) markerb (= 1.1.0) mini_magick (= 4.8.0) minitest diff --git a/app/assets/javascripts/app/helpers/text_formatter.js b/app/assets/javascripts/app/helpers/text_formatter.js index 3fc051404..753590664 100644 --- a/app/assets/javascripts/app/helpers/text_formatter.js +++ b/app/assets/javascripts/app/helpers/text_formatter.js @@ -1,6 +1,11 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later (function(){ + app.helpers.allowedEmbedsMime = function(mimetype) { + var v = document.createElement(mimetype[1]); + return v.canPlayType && v.canPlayType(mimetype[0]) !== ""; + }; + app.helpers.textFormatter = function(text, mentions) { mentions = mentions ? mentions : []; @@ -83,6 +88,30 @@ // Bootstrap table markup md.renderer.rules.table_open = function () { return "\n"; }; + + var html5medialPlugin = window.markdownitHTML5Embed; + md.use(html5medialPlugin, {html5embed: { + inline: false, + autoAppend: true, + renderFn: function handleBarsRenderFn(parsed, mediaAttributes) { + var attributes = mediaAttributes[parsed.mediaType]; + return HandlebarsTemplates["media-embed_tpl"]({ + mediaType: parsed.mediaType, + attributes: attributes, + mimetype: parsed.mimeType, + sourceURL: parsed.url, + title: parsed.title, + fallback: parsed.fallback, + needsCover: parsed.mediaType === "video" + }); + }, + attributes: { + "audio": "controls preload=none", + "video": "preload=none" + }, + isAllowedMimeType: app.helpers.allowedEmbedsMime + }}); + return md.render(text); }; })(); diff --git a/app/assets/javascripts/app/views/content_view.js b/app/assets/javascripts/app/views/content_view.js index 78367563b..a0a04b661 100644 --- a/app/assets/javascripts/app/views/content_view.js +++ b/app/assets/javascripts/app/views/content_view.js @@ -63,7 +63,29 @@ app.views.Content = app.views.Base.extend({ } }, + // This function is called when user clicks cover for HTML5 embedded video + onVideoThumbClick: function(evt) { + var clickedThumb; + if ($(evt.target).hasClass("thumb")) { + clickedThumb = $(evt.target); + } else { + clickedThumb = $(evt.target).parent(".thumb"); + } + clickedThumb.find(".video-overlay").addClass("hidden"); + clickedThumb.parents(".collapsed").children(".expander").click(); + var video = clickedThumb.find("video"); + video.attr("controls", ""); + video.get(0).load(); + video.get(0).play(); + clickedThumb.unbind("click"); + }, + + bindMediaEmbedThumbClickEvent: function() { + this.$(".media-embed .thumb").bind("click", this.onVideoThumbClick); + }, + postRenderTemplate : function(){ + this.bindMediaEmbedThumbClickEvent(); _.defer(_.bind(this.collapseOversized, this)); // run collapseOversized again after all contained images are loaded @@ -93,6 +115,8 @@ app.views.StatusMessage = app.views.Content.extend({ app.views.ExpandedStatusMessage = app.views.StatusMessage.extend({ postRenderTemplate : function(){ + this.bindMediaEmbedThumbClickEvent(); + var photoAttachments = this.$(".photo-attachments"); if(photoAttachments.length > 0) { new app.views.Gallery({ el: photoAttachments }); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index a59460b96..ee4fc5e7f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -26,6 +26,7 @@ //= require markdown-it-sanitizer //= require markdown-it-sub //= require markdown-it-sup +//= require markdown-it-html5-embed //= require highlightjs //= require clear-form //= require corejs-typeahead diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss index 169bf1e1c..983ea5151 100644 --- a/app/assets/stylesheets/_application.scss +++ b/app/assets/stylesheets/_application.scss @@ -88,6 +88,7 @@ @import 'chat'; @import 'markdown-content'; @import 'oembed'; +@import 'media-embed'; @import 'post-content'; // contacts diff --git a/app/assets/stylesheets/media-embed.scss b/app/assets/stylesheets/media-embed.scss new file mode 100644 index 000000000..1083cd358 --- /dev/null +++ b/app/assets/stylesheets/media-embed.scss @@ -0,0 +1,21 @@ +$stub-bg-color: #ddd; + +.media-embed { + margin-top: 5px; + + .thumb { + @include video-overlay; + + background-color: $stub-bg-color; + + video { + min-height: 60%; + vertical-align: middle; + width: 100%; + } + } + + audio { + width: 100%; + } +} diff --git a/app/assets/templates/media-embed_tpl.jst.hbs b/app/assets/templates/media-embed_tpl.jst.hbs new file mode 100644 index 000000000..4c9b3b83c --- /dev/null +++ b/app/assets/templates/media-embed_tpl.jst.hbs @@ -0,0 +1,19 @@ +
+ {{#if needsCover}} +
+ {{/if}} + + <{{mediaType}} {{{attributes}}}> + + {{title}} + + + {{#if needsCover}} +
+
+
{{title}}
+
+
+
+ {{/if}} +
diff --git a/config/cucumber.yml b/config/cucumber.yml index cace674ba..977c9d5ae 100644 --- a/config/cucumber.yml +++ b/config/cucumber.yml @@ -9,11 +9,11 @@ screenshot_opts = "--require features --format pretty" %> # 'normal' test runs -default: <%= std_opts %> -r features +default: <%= std_opts %> -r features --tags ~@nophantomjs wip: -r features --tags @wip:3 --wip features rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip --tags ~@screenshots # screenshot feature ref_screens: "<%= screenshot_opts %> --tags @reference-screenshots" cmp_screens: "<%= screenshot_opts %> --tags @comparison-screenshots" -all_screens: "<%= screenshot_opts %> --tags @screenshots" \ No newline at end of file +all_screens: "<%= screenshot_opts %> --tags @screenshots" diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 58393959b..6c6c3f394 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -436,7 +436,7 @@ en: size_of_images_q: "Can I customize the size of images in posts or comments?" size_of_images_a: "No. Images are resized automatically to fit the stream or single-post view. Markdown does not have a code for specifying the size of an image." embed_multimedia_q: "How do I embed a video, audio, or other multimedia content into a post?" - embed_multimedia_a: "You can usually just paste the URL (e.g. http://www.youtube.com/watch?v=nnnnnnnnnnn ) into your post and the video or audio will be embedded automatically. The sites supported include: YouTube, Vimeo, SoundCloud, Flickr and a few more. diaspora* uses oEmbed for this feature. We’re supporting more media sources all the time. Remember to always post simple, full links – no shortened links; no operators after the base URL – and give it a little time before you refresh the page after posting for seeing the preview." + embed_multimedia_a: "You can usually just paste the URL (e.g. http://www.youtube.com/watch?v=nnnnnnnnnnn ) into your post and the video or audio will be embedded automatically. The sites supported include: YouTube, Vimeo, SoundCloud, Flickr and a few more. diaspora* uses oEmbed for this feature. If you post a direct link to an audio or video file, diaspora* will embed it using standard HTML5 player. We’re supporting more media sources all the time. Remember to always post simple, full links – no shortened links; no operators after the base URL – and give it a little time before you refresh the page after posting for seeing the preview." post_location_q: "How do I add my location to a post?" post_location_a: "Click the pin icon next to the camera in the publisher. This will insert your location from OpenStreetMap. You can edit your location – you might only want to include the city you’re in rather than the specific street address." post_poll_q: "How do I add a poll to my post?" diff --git a/features/desktop/media-embed.feature b/features/desktop/media-embed.feature new file mode 100644 index 000000000..07efef81d --- /dev/null +++ b/features/desktop/media-embed.feature @@ -0,0 +1,22 @@ + # We can create a separate cucumber profile that will run these tests with Selenium +@nophantomjs +@javascript +Feature: oembed + In order to make videos easy accessible + As a user + I want the media links in my posts be replaced by an embedded player + + Background: + Given following user exists: + | username | email | + | Alice Smith | alice@alice.alice | + And I sign in as "alice@alice.alice" + + Scenario: Post a video link + When I click the publisher and post "[title](http://example.com/file.ogv)" + Then I should see a HTML5 video player + + Scenario: Post an audio link + When I click the publisher and post "[title](http://example.com/file.ogg)" + Then I should see a HTML5 audio player + diff --git a/features/step_definitions/media_embed_steps.rb b/features/step_definitions/media_embed_steps.rb new file mode 100644 index 000000000..356a2ed6d --- /dev/null +++ b/features/step_definitions/media_embed_steps.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +Then /^I should see a HTML5 (video|audio) player$/ do |type| + find(".post-content .media-embed") + find(".stream-container").should have_css(".post-content .media-embed #{type}") +end diff --git a/features/support/publishing_cuke_helpers.rb b/features/support/publishing_cuke_helpers.rb index 13ed9834c..3fe3e4b59 100644 --- a/features/support/publishing_cuke_helpers.rb +++ b/features/support/publishing_cuke_helpers.rb @@ -26,13 +26,17 @@ module PublishingCukeHelpers submit_publisher end + def visible_text_from_markdown(text) + CGI.unescapeHTML(ActionController::Base.helpers.strip_tags(Diaspora::MessageRenderer.new(text).markdownified.strip)) + end + def submit_publisher txt = find("#publisher #status_message_text").value find("#publisher .btn-primary").click # wait for the publisher to be closed expect(find("#publisher")["class"]).to include("closed") # wait for the content to appear - expect(find("#main-stream")).to have_content(txt) + expect(find("#main-stream")).to have_content(visible_text_from_markdown(txt)) end def click_and_post(text) diff --git a/spec/javascripts/app/helpers/text_formatter_spec.js b/spec/javascripts/app/helpers/text_formatter_spec.js index c2e8e7bcf..fdd73d305 100644 --- a/spec/javascripts/app/helpers/text_formatter_spec.js +++ b/spec/javascripts/app/helpers/text_formatter_spec.js @@ -347,6 +347,54 @@ describe("app.helpers.textFormatter", function(){ } }); }); + + context("media embed", function() { + beforeEach(function() { + spyOn(app.helpers, "allowedEmbedsMime").and.returnValue(true); + }); + + it("embeds audio", function() { + var html = + '

title

\n' + + '
\n' + + "\n" + + " \n" + + "\n" + + "
\n"; + var content = "[title](https://example.org/file.mp3)"; + var parsed = this.formatter(content); + + expect(parsed).toContain(html); + }); + + it("embeds video", function() { + var html = + '

title

\n' + + '
\n' + + '
\n' + + "\n" + + " \n" + + "\n" + + '
\n' + + '
\n' + + '
title
\n' + + "
\n" + + "
\n" + + "
\n" + + "
\n"; + + var content = "[title](https://example.org/file.mp4)"; + var parsed = this.formatter(content); + + expect(parsed).toContain(html); + }); + }); }); context("real world examples", function(){ diff --git a/spec/javascripts/app/views/content_view_spec.js b/spec/javascripts/app/views/content_view_spec.js index 2315b4473..a10a3cfc6 100644 --- a/spec/javascripts/app/views/content_view_spec.js +++ b/spec/javascripts/app/views/content_view_spec.js @@ -36,4 +36,43 @@ describe("app.views.Content", function(){ expect(this.view.presenter().location).toEqual(factory.location()); }); }); + + // These tests don't work in PhantomJS because it doesn't support HTML5