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