Support for embedding HTML5 media links

Use markdown-it-html5-embed plugin so user can embed audio and
video using the markdown link syntax []() in the HTML5 way.
This commit is contained in:
cmrd Senya 2017-09-02 05:11:30 +03:00 committed by Benjamin Neff
parent 258b502cde
commit b32c844314
No known key found for this signature in database
GPG key ID: 971464C3F1A90194
14 changed files with 218 additions and 2 deletions

View file

@ -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"

View file

@ -368,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)
@ -816,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

View file

@ -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 "<table class=\"table table-striped\">\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);
};
})();

View file

@ -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 });

View file

@ -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

View file

@ -88,6 +88,7 @@
@import 'chat';
@import 'markdown-content';
@import 'oembed';
@import 'media-embed';
@import 'post-content';
// contacts

View file

@ -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%;
}
}

View file

@ -0,0 +1,19 @@
<div class="media-embed">
{{#if needsCover}}
<div class="thumb">
{{/if}}
<{{mediaType}} {{{attributes}}}>
<source type="{{mimetype}}" src="{{sourceURL}}" />
{{title}}
</{{mediaType}}>
{{#if needsCover}}
<div class="video-overlay">
<div class="video-info">
<div class="title">{{title}}</div>
</div>
</div>
</div>
{{/if}}
</div>

View file

@ -421,7 +421,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. Were 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. Were 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 youre in rather than the specific street address."
post_poll_q: "How do I add a poll to my post?"

View file

@ -0,0 +1,20 @@
@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

View file

@ -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

View file

@ -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)

View file

@ -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 =
'<p><a href="https://example.org/file.mp3" target="_blank" rel="noopener noreferrer">title</a></p>\n' +
'<div class="media-embed">\n' +
"\n" +
" <audio controls preload=none>\n" +
' <source type="audio/mpeg" src="https://example.org/file.mp3" />\n' +
" title\n" +
" </audio>\n" +
"\n" +
"</div>\n";
var content = "[title](https://example.org/file.mp3)";
var parsed = this.formatter(content);
expect(parsed).toContain(html);
});
it("embeds video", function() {
var html =
'<p><a href="https://example.org/file.mp4" target="_blank" rel="noopener noreferrer">title</a></p>\n' +
'<div class="media-embed">\n' +
' <div class="thumb">\n' +
"\n" +
" <video preload=none>\n" +
' <source type="video/mp4" src="https://example.org/file.mp4" />\n' +
" title\n" +
" </video>\n" +
"\n" +
' <div class="video-overlay">\n' +
' <div class="video-info">\n' +
' <div class="title">title</div>\n' +
" </div>\n" +
" </div>\n" +
" </div>\n" +
"</div>\n";
var content = "[title](https://example.org/file.mp4)";
var parsed = this.formatter(content);
expect(parsed).toContain(html);
});
});
});
context("real world examples", function(){

View file

@ -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 <video>.
if (/PhantomJS/.exec(navigator.userAgent) === null) {
describe("onVideoThumbClick", function() {
beforeEach(function() {
this.post = new app.models.StatusMessage({text: "[title](https://www.w3schools.com/html/mov_bbb.mp4)"});
this.view = new app.views.StatusMessage({model: this.post});
this.view.render();
});
afterEach(function() {
this.view.$("video").stop();
});
it("hides video overlay", function() {
expect(this.view.$(".video-overlay").length).toBe(1);
this.view.$(".media-embed .thumb").click();
expect(this.view.$(".video-overlay")).toHaveClass("hidden");
});
it("expands posts on click", function() {
this.view.$(".collapsible").height(500);
this.view.collapseOversized();
expect(this.view.$(".collapsed").length).toBe(1);
this.view.$(".media-embed .thumb").click();
expect(this.view.$(".opened").length).toBe(1);
});
it("plays video", function(done) {
this.view.$("video").on("playing", function() {
done();
});
this.view.$(".media-embed .thumb").click();
});
});
}
});