blob: 5db801712f5bee219690cab66a6fbe57c722bf85 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// 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);