Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/framework/mr-comment-content/mr-attachment.js b/static_src/elements/framework/mr-comment-content/mr-attachment.js
new file mode 100644
index 0000000..c435dfd
--- /dev/null
+++ b/static_src/elements/framework/mr-comment-content/mr-attachment.js
@@ -0,0 +1,206 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+import {FILE_DOWNLOAD_WARNING, ALLOWED_ATTACHMENT_EXTENSIONS,
+ ALLOWED_CONTENT_TYPE_PREFIXES} from 'shared/settings.js';
+import 'elements/chops/chops-button/chops-button.js';
+import {store, connectStore} from 'reducers/base.js';
+import * as issueV0 from 'reducers/issueV0.js';
+import {prpcClient} from 'prpc-client-instance.js';
+
+/**
+ * `<mr-attachment>`
+ *
+ * Display attachments for Monorail comments.
+ *
+ */
+export class MrAttachment extends connectStore(LitElement) {
+ /** @override */
+ static get properties() {
+ return {
+ attachment: {type: Object},
+ projectName: {type: String},
+ localId: {type: Number},
+ sequenceNum: {type: Number},
+ canDelete: {type: Boolean},
+ };
+ }
+
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ css`
+ .attachment-view,
+ .attachment-download {
+ margin-left: 8px;
+ display: block;
+ }
+ .attachment-delete {
+ margin-left: 16px;
+ color: var(--chops-button-color);
+ background: var(--chops-button-bg);
+ border-color: transparent;
+ }
+ .comment-attachment {
+ min-width: 20%;
+ width: fit-content;
+ background: var(--chops-card-details-bg);
+ padding: 4px;
+ margin: 8px;
+ overflow: auto;
+ }
+ .comment-attachment-header {
+ display: flex;
+ flex-wrap: nowrap;
+ }
+ .filename {
+ margin-left: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .filename-deleted {
+ margin-right: 4px;
+ }
+ .filesize {
+ margin-left: 8px;
+ white-space: nowrap;
+ }
+ .preview {
+ border: 2px solid #c3d9ff;
+ padding: 1px;
+ max-width: 98%;
+ }
+ .preview:hover {
+ border: 2px solid blue;
+ }
+ `];
+ }
+
+
+ /** @override */
+ render() {
+ return html`
+ <div class="comment-attachment">
+ <div class="filename">
+ ${this.attachment.isDeleted ? html`
+ <div class="filename-deleted">[Deleted]</div>
+ ` : ''}
+ <b>${this.attachment.filename}</b>
+ ${this.canDelete ? html`
+ <chops-button
+ class="attachment-delete"
+ @click=${this._deleteAttachment}>
+ ${this.attachment.isDeleted ? 'Undelete' : 'Delete'}
+ </chops-button>
+ ` : ''}
+ </div>
+ ${!this.attachment.isDeleted ? html`
+ <div class="comment-attachment-header">
+ <div class="filesize">${_bytesOrKbOrMb(this.attachment.size)}</div>
+ ${this.attachment.viewUrl ? html`
+ <a
+ class="attachment-view"
+ href=${this.attachment.viewUrl}
+ target="_blank"
+ >View</a>
+ `: ''}
+ <a
+ class="attachment-download"
+ href=${this.attachment.downloadUrl}
+ target="_blank"
+ ?hidden=${!this.attachment.downloadUrl}
+ @click=${this._warnOnDownload}
+ >Download</a>
+ </div>
+ ${this.attachment.thumbnailUrl ? html`
+ <a href=${this.attachment.viewUrl} target="_blank">
+ <img
+ class="preview" alt="attachment preview"
+ src=${this.attachment.thumbnailUrl}>
+ </a>
+ ` : ''}
+ ${_isVideo(this.attachment.contentType) ? html`
+ <video
+ src=${this.attachment.viewUrl}
+ class="preview"
+ controls
+ width="640"
+ preload="metadata"
+ ></video>
+ ` : ''}
+ ` : ''}
+ </div>
+ `;
+ }
+
+ /**
+ * Deletes a given attachment in a comment.
+ */
+ _deleteAttachment() {
+ const issueRef = {
+ projectName: this.projectName,
+ localId: this.localId,
+ };
+
+ const promise = prpcClient.call(
+ 'monorail.Issues', 'DeleteAttachment',
+ {
+ issueRef,
+ sequenceNum: this.sequenceNum,
+ attachmentId: this.attachment.attachmentId,
+ delete: !this.attachment.isDeleted,
+ });
+
+ promise.then(() => {
+ store.dispatch(issueV0.fetchComments(issueRef));
+ }, (error) => {
+ console.log('Failed to (un)delete attachment', error);
+ });
+ }
+
+ /**
+ * Give the user a warning before they download files that Monorail thinks
+ * might have the potential to be unsafe.
+ * @param {MouseEvent} e
+ */
+ _warnOnDownload(e) {
+ const isAllowedType = ALLOWED_CONTENT_TYPE_PREFIXES.some((prefix) => {
+ return this.attachment.contentType.startsWith(prefix);
+ });
+ const isAllowedExtension = ALLOWED_ATTACHMENT_EXTENSIONS.some((ext) => {
+ return this.attachment.filename.toLowerCase().endsWith(ext);
+ });
+
+ if (isAllowedType || isAllowedExtension) return;
+ if (!window.confirm(FILE_DOWNLOAD_WARNING)) {
+ e.preventDefault();
+ }
+ }
+}
+
+function _isVideo(contentType) {
+ if (!contentType) return;
+ return contentType.startsWith('video/');
+}
+
+function _bytesOrKbOrMb(numBytes) {
+ if (numBytes < 1024) {
+ return `${numBytes} bytes`; // e.g., 128 bytes
+ } else if (numBytes < 99 * 1024) {
+ return `${(numBytes / 1024).toFixed(1)} KB`; // e.g. 23.4 KB
+ } else if (numBytes < 1024 * 1024) {
+ return `${(numBytes / 1024).toFixed(0)} KB`; // e.g., 219 KB
+ } else if (numBytes < 99 * 1024 * 1024) {
+ return `${(numBytes / 1024 / 1024).toFixed(1)} MB`; // e.g., 21.9 MB
+ } else {
+ return `${(numBytes / 1024 / 1024).toFixed(0)} MB`; // e.g., 100 MB
+ }
+}
+
+customElements.define('mr-attachment', MrAttachment);