blob: c435dfdf8f3a51664aa92bf9125e4106a0c8e3d5 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html, css} from 'lit-element';
6
7import {SHARED_STYLES} from 'shared/shared-styles.js';
8import {FILE_DOWNLOAD_WARNING, ALLOWED_ATTACHMENT_EXTENSIONS,
9 ALLOWED_CONTENT_TYPE_PREFIXES} from 'shared/settings.js';
10import 'elements/chops/chops-button/chops-button.js';
11import {store, connectStore} from 'reducers/base.js';
12import * as issueV0 from 'reducers/issueV0.js';
13import {prpcClient} from 'prpc-client-instance.js';
14
15/**
16 * `<mr-attachment>`
17 *
18 * Display attachments for Monorail comments.
19 *
20 */
21export class MrAttachment extends connectStore(LitElement) {
22 /** @override */
23 static get properties() {
24 return {
25 attachment: {type: Object},
26 projectName: {type: String},
27 localId: {type: Number},
28 sequenceNum: {type: Number},
29 canDelete: {type: Boolean},
30 };
31 }
32
33 /** @override */
34 static get styles() {
35 return [
36 SHARED_STYLES,
37 css`
38 .attachment-view,
39 .attachment-download {
40 margin-left: 8px;
41 display: block;
42 }
43 .attachment-delete {
44 margin-left: 16px;
45 color: var(--chops-button-color);
46 background: var(--chops-button-bg);
47 border-color: transparent;
48 }
49 .comment-attachment {
50 min-width: 20%;
51 width: fit-content;
52 background: var(--chops-card-details-bg);
53 padding: 4px;
54 margin: 8px;
55 overflow: auto;
56 }
57 .comment-attachment-header {
58 display: flex;
59 flex-wrap: nowrap;
60 }
61 .filename {
62 margin-left: 8px;
63 display: flex;
64 justify-content: space-between;
65 align-items: center;
66 }
67 .filename-deleted {
68 margin-right: 4px;
69 }
70 .filesize {
71 margin-left: 8px;
72 white-space: nowrap;
73 }
74 .preview {
75 border: 2px solid #c3d9ff;
76 padding: 1px;
77 max-width: 98%;
78 }
79 .preview:hover {
80 border: 2px solid blue;
81 }
82 `];
83 }
84
85
86 /** @override */
87 render() {
88 return html`
89 <div class="comment-attachment">
90 <div class="filename">
91 ${this.attachment.isDeleted ? html`
92 <div class="filename-deleted">[Deleted]</div>
93 ` : ''}
94 <b>${this.attachment.filename}</b>
95 ${this.canDelete ? html`
96 <chops-button
97 class="attachment-delete"
98 @click=${this._deleteAttachment}>
99 ${this.attachment.isDeleted ? 'Undelete' : 'Delete'}
100 </chops-button>
101 ` : ''}
102 </div>
103 ${!this.attachment.isDeleted ? html`
104 <div class="comment-attachment-header">
105 <div class="filesize">${_bytesOrKbOrMb(this.attachment.size)}</div>
106 ${this.attachment.viewUrl ? html`
107 <a
108 class="attachment-view"
109 href=${this.attachment.viewUrl}
110 target="_blank"
111 >View</a>
112 `: ''}
113 <a
114 class="attachment-download"
115 href=${this.attachment.downloadUrl}
116 target="_blank"
117 ?hidden=${!this.attachment.downloadUrl}
118 @click=${this._warnOnDownload}
119 >Download</a>
120 </div>
121 ${this.attachment.thumbnailUrl ? html`
122 <a href=${this.attachment.viewUrl} target="_blank">
123 <img
124 class="preview" alt="attachment preview"
125 src=${this.attachment.thumbnailUrl}>
126 </a>
127 ` : ''}
128 ${_isVideo(this.attachment.contentType) ? html`
129 <video
130 src=${this.attachment.viewUrl}
131 class="preview"
132 controls
133 width="640"
134 preload="metadata"
135 ></video>
136 ` : ''}
137 ` : ''}
138 </div>
139 `;
140 }
141
142 /**
143 * Deletes a given attachment in a comment.
144 */
145 _deleteAttachment() {
146 const issueRef = {
147 projectName: this.projectName,
148 localId: this.localId,
149 };
150
151 const promise = prpcClient.call(
152 'monorail.Issues', 'DeleteAttachment',
153 {
154 issueRef,
155 sequenceNum: this.sequenceNum,
156 attachmentId: this.attachment.attachmentId,
157 delete: !this.attachment.isDeleted,
158 });
159
160 promise.then(() => {
161 store.dispatch(issueV0.fetchComments(issueRef));
162 }, (error) => {
163 console.log('Failed to (un)delete attachment', error);
164 });
165 }
166
167 /**
168 * Give the user a warning before they download files that Monorail thinks
169 * might have the potential to be unsafe.
170 * @param {MouseEvent} e
171 */
172 _warnOnDownload(e) {
173 const isAllowedType = ALLOWED_CONTENT_TYPE_PREFIXES.some((prefix) => {
174 return this.attachment.contentType.startsWith(prefix);
175 });
176 const isAllowedExtension = ALLOWED_ATTACHMENT_EXTENSIONS.some((ext) => {
177 return this.attachment.filename.toLowerCase().endsWith(ext);
178 });
179
180 if (isAllowedType || isAllowedExtension) return;
181 if (!window.confirm(FILE_DOWNLOAD_WARNING)) {
182 e.preventDefault();
183 }
184 }
185}
186
187function _isVideo(contentType) {
188 if (!contentType) return;
189 return contentType.startsWith('video/');
190}
191
192function _bytesOrKbOrMb(numBytes) {
193 if (numBytes < 1024) {
194 return `${numBytes} bytes`; // e.g., 128 bytes
195 } else if (numBytes < 99 * 1024) {
196 return `${(numBytes / 1024).toFixed(1)} KB`; // e.g. 23.4 KB
197 } else if (numBytes < 1024 * 1024) {
198 return `${(numBytes / 1024).toFixed(0)} KB`; // e.g., 219 KB
199 } else if (numBytes < 99 * 1024 * 1024) {
200 return `${(numBytes / 1024 / 1024).toFixed(1)} MB`; // e.g., 21.9 MB
201 } else {
202 return `${(numBytes / 1024 / 1024).toFixed(0)} MB`; // e.g., 100 MB
203 }
204}
205
206customElements.define('mr-attachment', MrAttachment);