[% END %]
[% BLOCK comment_body %]
+ [% IF comment.type == constants.CMT_ATTACHMENT_CREATED %]
+ [% att = comment.attachment; link = 'attachment.cgi?id=' _ att.id %]
+ <div id="att-[% att.id FILTER none %]"
+ class="attachment[% " patch" IF att.ispatch %][% " obsolete" IF att.isobsolete %]"
+ data-id="[% att.id FILTER none %]" itemscope itemtype="http://schema.org/MediaObject"
+ [% IF comment.collapsed +%] style="display:none"[% END ~%]>
+ <meta itemprop="contentSize" content="[% att.datasize FILTER none %]">
+ <meta itemprop="encodingFormat" content="[% att.mimetype FILTER html %]">
+ <div class="label">
+ Posted [% IF att.is_image %]image
+ [% ELSIF att.is_audio %]audio[% ELSIF att.is_video %]video
+ [% ELSIF att.ispatch %]patch[% ELSE %]file[% END %]
+ [% IF att.ispatch %]
+ <meta itemprop="contentUrl" content="[% link FILTER html %]">
+ <a class="link" href="[% link FILTER html %]&action=diff">
+ [% ELSE %]
+ <a class="link[% " lightbox" IF att.is_image %]" href="[% link FILTER html %]" itemprop="contentUrl">
+ [% END %]
+ <span id="att-[% att.id FILTER none %]-description" itemprop="name">[% att.description FILTER html %]</span></a>
+ [% " (obsolete)" IF att.isobsolete %]
+ — <a href="[% link FILTER html %]&action=edit" itemprop="url">Details</a>
+ [% IF att.ispatch && Param('splinter_base') %]
+ — <a href="[% Bugzilla.splinter_review_url(bug.bug_id, att.id) FILTER none %]">Splinter Review</a>
+ [% END %]
+ </div>
+ </div>
+ [% END %]
[% IF comment.is_markdown AND Param('use_markdown') %]
[% comment_tag = 'div' %]
[% ELSE %]
class="lightbox lightbox-icon [%= "markdown" IF comment_tag == 'div' %]"><img src="extensions/BugModal/web/image.png" width="16" height="16"></a>
[% END %]
[% END %]
- [%~ comment.body_full FILTER renderMarkdown(bug, comment) ~%]</[% comment_tag FILTER none %]>
+ [%~ comment.body_full({ exclude_attachment => 1 }) FILTER renderMarkdown(bug, comment) ~%]</[% comment_tag FILTER none %]>
[% END %]
[%
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 {
$('#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);
$('#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);
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);
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', `
+ <a href="${link}" class="outer lightbox"><img src="${link}" alt="${name}" itemprop="image"></a>`);
+
+ // 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', `
+ <span class="outer"><${media} src="${link}" controls itemprop="${media}"></span>`);
+ }
+ }
+
+ // 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', `
+ <a href="${link}" title="${name}" class="outer">
+ <pre class="language-${lang}" role="img" itemprop="text">${text}</pre></a>`);
+
+ if (Prism) {
+ Prism.highlightElement($att.querySelector('pre'));
+ }
+ } catch (ex) {}
+ }
+ }
+};
+
+document.addEventListener('DOMContentLoaded', () => new Bugzilla.BugModal.Comments(), { once: true });