From: Kohei Yoshino Date: Wed, 6 Mar 2019 18:35:33 +0000 (-0500) Subject: Bug 1472522 - Show image/text attachments inline X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=da1e18eb126478f457c13e0890f4a651f8afcbbd;p=thirdparty%2Fbugzilla.git Bug 1472522 - Show image/text attachments inline --- diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index 705a8396c..c13c32929 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -204,6 +204,12 @@ sub SETTINGS { default => 'on', category => 'User Interface' }, + { + name => 'inline_attachments', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' + }, ]; } diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm index bb1ba14c4..39e261356 100644 --- a/extensions/BugModal/lib/MonkeyPatches.pm +++ b/extensions/BugModal/lib/MonkeyPatches.pm @@ -54,4 +54,14 @@ sub is_image { return substr($self->contenttype, 0, 6) eq 'image/'; } +sub is_audio { + my ($self) = @_; + return substr($self->contenttype, 0, 6) eq 'audio/'; +} + +sub is_video { + my ($self) = @_; + return substr($self->contenttype, 0, 6) eq 'video/'; +} + 1; diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl index 710fa40d1..2661e256a 100644 --- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl @@ -255,6 +255,33 @@ [% END %] [% BLOCK comment_body %] + [% IF comment.type == constants.CMT_ATTACHMENT_CREATED %] + [% att = comment.attachment; link = 'attachment.cgi?id=' _ att.id %] + + [% END %] [% IF comment.is_markdown AND Param('use_markdown') %] [% comment_tag = 'div' %] [% ELSE %] @@ -273,7 +300,7 @@ class="lightbox lightbox-icon [%= "markdown" IF comment_tag == 'div' %]"> [% END %] [% END %] - [%~ comment.body_full FILTER renderMarkdown(bug, comment) ~%] + [%~ comment.body_full({ exclude_attachment => 1 }) FILTER renderMarkdown(bug, comment) ~%] [% END %] [% diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl index 92331f6cf..675082a50 100644 --- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl @@ -103,6 +103,7 @@ quote_replies: '[% user.settings.quote_replies.value FILTER js %]', zoom_textareas: [% user.settings.zoom_textareas.value == "on" ? "true" : "false" %], remember_collapsed: [% user.settings.ui_remember_collapsed.value == "on" ? "true" : "false" %], + inline_attachments: [% user.settings.inline_attachments.value == "on" ? "true" : "false" %], autosize_comments: [% user.settings.autosize_comments.value == "on" ? "true" : "false" %] } }; diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css index 1844c7f50..47bd806b5 100644 --- a/extensions/BugModal/web/bug_modal.css +++ b/extensions/BugModal/web/bug_modal.css @@ -745,6 +745,94 @@ h3.change-name { text-decoration: line-through; } +/* inline attachments */ + +.change-set .attachment { + border-top: 1px solid #ddd; + background-color: #FFF; +} + +.change-set .attachment { + background-color: #FFF; + padding: 8px; +} + +.change-set .attachment .label { + font-size: 14px; + font-style: italic; + color: grey; +} + +.change-set .attachment .label [itemprop="name"] { + font-weight: 600; +} + +.change-set .attachment .outer { + display: inline-block; + margin: 8px 0 0; + overflow: hidden; + border: 1px solid lightgrey; + border-radius: 4px; + vertical-align: top; + text-decoration: none; + color: #333; +} + +.change-set .attachment .lightbox { + cursor: zoom-in; +} + +.change-set .attachment .lightbox * { + pointer-events: none; +} + +.change-set .attachment img, +.change-set .attachment audio, +.change-set .attachment video { + margin: 0; + vertical-align: top; + max-width: 426px; +} + +.change-set .attachment [content="image/svg+xml"] ~ .outer img { + width: 426px; +} + +.change-set .attachment pre { + position: relative; + overflow: hidden; + box-sizing: border-box; + margin: 0; + padding: 8px; + width: 426px; + height: 240px; + font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace; + font-size: 12px; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + pointer-events: none; +} + +.change-set .attachment pre::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: linear-gradient(to bottom, transparent 75%, #FFF); + content: ''; +} + + +.change-set .attachment pre .token { + background-color: transparent !important; /* Override Prism.js */ +} + +.change-set .attachment ~ .comment-text { + padding-top: 0; +} + /* add comment */ #add-comment { diff --git a/extensions/BugModal/web/comments.js b/extensions/BugModal/web/comments.js index 4cc0560a4..3051fea2e 100644 --- a/extensions/BugModal/web/comments.js +++ b/extensions/BugModal/web/comments.js @@ -75,9 +75,7 @@ $(function() { $('#ct-' + id).hide(); if (BUGZILLA.user.id !== 0) $('#ctag-' + id).hide(); - $('#c' + id).find('.activity').hide(); - $('#c' + id).find('.comment-tags').hide(); - $('#c' + id).find('.comment-tags').hide(); + $('#c' + id).find('.activity, .attachment, .comment-tags').hide(); $('#c' + id).find('.gravatar').css('width', '16px').css('height', '16px'); $('#cr-' + id).hide(); update_spinner(realSpinner, false); @@ -90,9 +88,7 @@ $(function() { $('#ct-' + id).show(); if (BUGZILLA.user.id !== 0) $('#ctag-' + id).show(); - $('#c' + id).find('.activity').show(); - $('#c' + id).find('.comment-tags').show(); - $('#c' + id).find('.comment-tags').show(); + $('#c' + id).find('.activity, .attachment, .comment-tags').show(); $('#c' + id).find('.gravatar').css('width', '32px').css('height', '32px'); $('#cr-' + id).show(); update_spinner(realSpinner, true); @@ -100,6 +96,7 @@ $(function() { else { $('#ct-' + id).slideToggle('fast', function() { $('#c' + id).find('.activity').toggle(); + $('#c' + id).find('.attachment').slideToggle(); if ($('#ct-' + id + ':visible').length) { $('#c' + id).find('.comment-tags').show(); update_spinner(realSpinner, true); @@ -493,3 +490,125 @@ $(function() { updateTagsMenu(); }); + +/** + * Reference or define the Bugzilla app namespace. + * @namespace + */ +var Bugzilla = Bugzilla || {}; + +/** + * Reference or define the Review namespace. + * @namespace + */ +Bugzilla.BugModal = Bugzilla.BugModal || {}; + +/** + * Implement the modal bug view's comment-related functionality. + */ +Bugzilla.BugModal.Comments = class Comments { + /** + * Initiate a new Comments instance. + */ + constructor() { + this.prepare_inline_attachments(); + } + + /** + * Prepare to show image and text attachments inline if possible. For a better performance, this functionality uses + * the Intersection Observer API to show attachments when the associated comment goes into the viewport, when the page + * is scrolled down or the collapsed comment is expanded. This also utilizes the Network Information API to save + * bandwidth over cellular networks. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API + * @see https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API + */ + prepare_inline_attachments() { + // Check the user setting, API support and connectivity + if (!BUGZILLA.user.settings.inline_attachments || typeof IntersectionObserver !== 'function' || + (navigator.connection && navigator.connection.type === 'cellular')) { + return; + } + + const observer = new IntersectionObserver(entries => entries.forEach(entry => { + const $att = entry.target; + + if (entry.intersectionRatio > 0) { + observer.unobserve($att); + this.show_attachment($att); + } + }), { root: document.querySelector('#bugzilla-body') }); + + // Show only non-obsolete attachments + document.querySelectorAll('.change-set .attachment:not(.obsolete)').forEach($att => observer.observe($att)); + } + + /** + * Load and show an image, audio, video or text attachment. + * @param {HTMLElement} $att An attachment wrapper element. + */ + async show_attachment($att) { + const id = Number($att.dataset.id); + const link = $att.querySelector('.link').href; + const name = $att.querySelector('[itemprop="name"]').textContent; + const type = $att.querySelector('[itemprop="encodingFormat"]').content; + const size = Number($att.querySelector('[itemprop="contentSize"]').content); + const max_size = 2000000; + + // Show image smaller than 2 MB + if (type.match(/^image\/(?!vnd).+$/) && size < max_size) { + $att.insertAdjacentHTML('beforeend', ` + ${name}`); + + // Add lightbox support + $att.querySelector('.outer.lightbox').addEventListener('click', event => { + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) { + return; + } + + event.preventDefault(); + lb_show(event.target); + }); + } + + // Show audio and video + if (type.match(/^(?:audio|video)\/(?!vnd).+$/)) { + const media = type.split('/')[0]; + + if (document.createElement(media).canPlayType(type)) { + $att.insertAdjacentHTML('beforeend', ` + <${media} src="${link}" controls itemprop="${media}">`); + } + } + + // Detect text (code from attachment.js) + const is_patch = !!name.match(/\.(?:diff|patch)$/) || !!type.match(/^text\/x-(?:diff|patch)$/); + const is_markdown = !!name.match(/\.(?:md|mkdn?|mdown|markdown)$/); + const is_source = !!name.match(/\.(?:cpp|es|h|js|json|rs|rst|sh|toml|ts|tsx|xml|yaml|yml)$/); + const is_text = type.startsWith('text/') || is_patch || is_markdown || is_source; + + // Show text smaller than 2 MB + if (is_text && size < max_size) { + // Load text body + try { + const response = await fetch(`/attachment.cgi?id=${id}`, { credentials: 'same-origin' }); + + if (!response.ok) { + throw new Error(); + } + + const text = await response.text(); + const lang = is_patch ? 'diff' : type.match(/\w+$/)[0]; + + $att.insertAdjacentHTML('beforeend', ` + + `); + + if (Prism) { + Prism.highlightElement($att.querySelector('pre')); + } + } catch (ex) {} + } + } +}; + +document.addEventListener('DOMContentLoaded', () => new Bugzilla.BugModal.Comments(), { once: true }); diff --git a/template/en/default/bug/format_comment.txt.tmpl b/template/en/default/bug/format_comment.txt.tmpl index 9c1f1385f..1b0ebf579 100644 --- a/template/en/default/bug/format_comment.txt.tmpl +++ b/template/en/default/bug/format_comment.txt.tmpl @@ -40,12 +40,14 @@ X[% comment_body %] [% ELSIF comment.type == constants.CMT_HAS_DUPE %] *** [% terms.Bug %] [%+ comment.extra_data %] has been marked as a duplicate of this [% terms.bug %]. *** [% ELSIF comment.type == constants.CMT_ATTACHMENT_CREATED %] +[% UNLESS exclude_attachment %] Created attachment [% comment.extra_data %] [% IF is_bugmail %] --> [% urlbase _ "attachment.cgi?id=" _ comment.extra_data _ "&action=edit" %] [% END %] [%+ comment.attachment.description %] +[% END %] [%+ comment.body %] [% ELSIF comment.type == constants.CMT_ATTACHMENT_UPDATED %] Comment on attachment [% comment.extra_data %] diff --git a/template/en/default/global/setting-descs.none.tmpl b/template/en/default/global/setting-descs.none.tmpl index b6800d22d..7ed674b3b 100644 --- a/template/en/default/global/setting-descs.none.tmpl +++ b/template/en/default/global/setting-descs.none.tmpl @@ -43,6 +43,7 @@ "quote_replies" => "Quote the associated comment when you click on its reply link", "quoted_reply" => "Quote the full comment", "simple_reply" => "Reference the comment number only", + "inline_attachments" => "Show attachments inline", "autosize_comments" => "Expand the comment box dynamically", "comment_box_position" => "Position of the Additional Comments box", "before_comments" => "Before other comments",