diff --git a/internal_packages/attachments/lib/attachment-component.cjsx b/internal_packages/attachments/lib/attachment-component.cjsx deleted file mode 100644 index 76781dc4d..000000000 --- a/internal_packages/attachments/lib/attachment-component.cjsx +++ /dev/null @@ -1,97 +0,0 @@ -_ = require 'underscore' -path = require 'path' -fs = require 'fs' -React = require 'react' -{RetinaImg, Flexbox} = require 'nylas-component-kit' -{Actions, Utils, FileDownloadStore} = require 'nylas-exports' - -class AttachmentComponent extends React.Component - @displayName: 'AttachmentComponent' - - @propTypes: - file: React.PropTypes.object.isRequired - download: React.PropTypes.object - removable: React.PropTypes.bool - targetPath: React.PropTypes.string - messageClientId: React.PropTypes.string - - constructor: (@props) -> - @state = progressPercent: 0 - - render: => -
- - - - - - - - {@props.file.displayName()} - {@_renderFileActions()} - -
- - _renderFileActions: => - if @props.removable -
- {@_renderRemoveIcon()} -
- else if @_isDownloading() and @_canAbortDownload() -
- {@_renderRemoveIcon()} -
- else -
- {@_renderDownloadButton()} -
- - _downloadProgressStyle: => - width: "#{@props.download?.percent ? 0}%" - - _canAbortDownload: -> true - - _canClickToView: => not @props.removable - - _isDownloading: => @props.download?.state is "downloading" - - _renderRemoveIcon: -> - - - _renderDownloadButton: -> - - - _onDragStart: (event) => - filePath = FileDownloadStore.pathForFile(@props.file) - if fs.existsSync(filePath) - # Note: From trial and error, it appears that the second param /MUST/ be the - # same as the last component of the filePath URL, or the download fails. - DownloadURL = "#{@props.file.contentType}:#{path.basename(filePath)}:file://#{filePath}" - event.dataTransfer.setData("DownloadURL", DownloadURL) - event.dataTransfer.setData("text/nylas-file-url", DownloadURL) - else - event.preventDefault() - return - - _onClickView: => - Actions.fetchAndOpenFile(@props.file) if @_canClickToView() - - _onClickRemove: (event) => - Actions.removeFile - file: @props.file - messageClientId: @props.messageClientId - event.stopPropagation() # Prevent 'onClickView' - - _onClickDownload: (event) => - Actions.fetchAndSaveFile(@props.file) - event.stopPropagation() # Prevent 'onClickView' - - _onClickAbort: (event) => - Actions.abortFetchFile(@props.file) - event.stopPropagation() # Prevent 'onClickView' - - -module.exports = AttachmentComponent diff --git a/internal_packages/attachments/lib/attachment-component.jsx b/internal_packages/attachments/lib/attachment-component.jsx new file mode 100644 index 000000000..164069ea8 --- /dev/null +++ b/internal_packages/attachments/lib/attachment-component.jsx @@ -0,0 +1,154 @@ +import fs from 'fs' +import path from 'path' +import React, {Component, PropTypes} from 'react' +import {RetinaImg, Flexbox} from 'nylas-component-kit' +import {Actions, FileDownloadStore} from 'nylas-exports' + + +class AttachmentComponent extends Component { + static displayName = 'AttachmentComponent'; + + static propTypes = { + file: PropTypes.object.isRequired, + download: PropTypes.object, + removable: PropTypes.bool, + targetPath: PropTypes.string, + messageClientId: PropTypes.string, + }; + + constructor() { + super() + this.state = {progressPercent: 0} + } + + static containerRequired = false; + + _isDownloading() { + const {download} = this.props + const state = download ? download.state : null + return state === 'downloading' + } + + _canClickToView() { + return !this.props.removable + } + + _canAbortDownload() { + return true + } + + _downloadProgressStyle() { + const {download} = this.props + const percent = download ? download.percent || 0 : 0; + return { + width: `${percent}%`, + } + } + + _onDragStart = (event) => { + const {file} = this.props + const filePath = FileDownloadStore.pathForFile(file) + if (fs.existsSync(filePath)) { + // Note: From trial and error, it appears that the second param /MUST/ be the + // same as the last component of the filePath URL, or the download fails. + const DownloadURL = `${file.contentType}:${path.basename(filePath)}:file://${filePath}` + event.dataTransfer.setData("DownloadURL", DownloadURL) + event.dataTransfer.setData("text/nylas-file-url", DownloadURL) + } else { + event.preventDefault() + } + }; + + _onClickView = () => { + if (this._canClickToView()) { + Actions.fetchAndOpenFile(this.props.file) + } + }; + + _onClickRemove = (event) => { + Actions.removeFile({ + file: this.props.file, + messageClientId: this.props.messageClientId, + }) + event.stopPropagation() // Prevent 'onClickView' + }; + + _onClickDownload = (event) => { + Actions.fetchAndSaveFile(this.props.file) + event.stopPropagation() // Prevent 'onClickView' + }; + + _onClickAbort = (event) => { + Actions.abortFetchFile(this.props.file) + event.stopPropagation() // Prevent 'onClickView' + }; + + _renderRemoveIcon() { + return ( + + ) + } + + _renderDownloadButton() { + return ( + + ) + } + + _renderFileActionIcon() { + if (this.props.removable) { + return ( +
+ {this._renderRemoveIcon()} +
+ ) + } else if (this._isDownloading() && this._canAbortDownload()) { + return ( +
+ {this._renderRemoveIcon()} +
+ ) + } + return ( +
+ {this._renderDownloadButton()} +
+ ) + } + + render() { + const {file, download} = this.props; + const downloadState = download ? download.state || "" : ""; + + return ( +
+ + + + + + +
+ + {file.displayName()} + {file.displayFileSize()} +
+ {this._renderFileActionIcon()} +
+
+ ) + } +} + +export default AttachmentComponent diff --git a/internal_packages/attachments/lib/image-attachment-component.cjsx b/internal_packages/attachments/lib/image-attachment-component.cjsx deleted file mode 100644 index 248cf92a6..000000000 --- a/internal_packages/attachments/lib/image-attachment-component.cjsx +++ /dev/null @@ -1,44 +0,0 @@ -path = require 'path' -React = require 'react' -AttachmentComponent = require './attachment-component' -{RetinaImg, Spinner, DraggableImg} = require 'nylas-component-kit' - -class ImageAttachmentComponent extends AttachmentComponent - @displayName: 'ImageAttachmentComponent' - - render: => -
- - - - - - {@_renderFileActions()} - -
-
-
{@props.file.displayName()}
-
- {@_imgOrLoader()} -
-
- - _canAbortDownload: -> false - - _renderRemoveIcon: -> - - - _renderDownloadButton: -> - - - _imgOrLoader: -> - if @props.download and @props.download.percent <= 5 -
- -
- else if @props.download and @props.download.percent < 100 - - else - - -module.exports = ImageAttachmentComponent diff --git a/internal_packages/attachments/lib/image-attachment-component.jsx b/internal_packages/attachments/lib/image-attachment-component.jsx new file mode 100644 index 000000000..6b10dc14b --- /dev/null +++ b/internal_packages/attachments/lib/image-attachment-component.jsx @@ -0,0 +1,77 @@ +import React, {PropTypes} from 'react' +import {RetinaImg, Spinner, DraggableImg} from 'nylas-component-kit' +import AttachmentComponent from './attachment-component' + + +class ImageAttachmentComponent extends AttachmentComponent { + static displayName = 'ImageAttachmentComponent'; + + static propTypes = { + file: PropTypes.object.isRequired, + download: PropTypes.object, + targetPath: PropTypes.string, + }; + + static containerRequired = false; + + _canAbortDownload() { + return false + } + + _imgOrLoader() { + const {download, targetPath} = this.props + if (download && download.percent <= 5) { + return ( +
+ +
+ ) + } else if (download && download.percent < 100) { + return ( + + ) + } + return + } + + _renderRemoveIcon() { + return ( + + ) + } + + _renderDownloadButton() { + return ( + + ) + } + + render() { + const {download, file} = this.props + const state = download ? download.state || "" : "" + const displayName = file.displayName() + return ( +
+ + + + + {this._renderFileActions()} +
+
+
{displayName}
+
+ {this._imgOrLoader()} +
+
+ ) + } +} + +export default ImageAttachmentComponent diff --git a/internal_packages/attachments/lib/main.cjsx b/internal_packages/attachments/lib/main.cjsx deleted file mode 100644 index cf736ad11..000000000 --- a/internal_packages/attachments/lib/main.cjsx +++ /dev/null @@ -1,18 +0,0 @@ -{ComponentRegistry} = require 'nylas-exports' - -AttachmentComponent = require "./attachment-component" -ImageAttachmentComponent = require "./image-attachment-component" - -module.exports = - activate: (@state={}) -> - ComponentRegistry.register AttachmentComponent, - role: 'Attachment' - - ComponentRegistry.register ImageAttachmentComponent, - role: 'Attachment:Image' - - deactivate: -> - ComponentRegistry.unregister(AttachmentComponent) - ComponentRegistry.unregister(ImageAttachmentComponent) - - serialize: -> @state diff --git a/internal_packages/attachments/lib/main.es6 b/internal_packages/attachments/lib/main.es6 new file mode 100644 index 000000000..94635cc29 --- /dev/null +++ b/internal_packages/attachments/lib/main.es6 @@ -0,0 +1,14 @@ +import {ComponentRegistry} from 'nylas-exports' +import AttachmentComponent from "./attachment-component" +import ImageAttachmentComponent from "./image-attachment-component" + + +export function activate() { + ComponentRegistry.register(AttachmentComponent, {role: 'Attachment'}) + ComponentRegistry.register(ImageAttachmentComponent, {role: 'Attachment:Image'}) +} + +export function deactivate() { + ComponentRegistry.unregister(AttachmentComponent) + ComponentRegistry.unregister(ImageAttachmentComponent) +} diff --git a/internal_packages/attachments/stylesheets/attachments.less b/internal_packages/attachments/stylesheets/attachments.less index 4e0235f45..362e6fdb3 100644 --- a/internal_packages/attachments/stylesheets/attachments.less +++ b/internal_packages/attachments/stylesheets/attachments.less @@ -12,12 +12,11 @@ -webkit-user-drag: element; .inner { - border-radius: 4px; + border-radius: 2px; color: @text-color; background: @background-off-primary; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.09); - padding: 0 @spacing-standard; - height:46px; + height: 37px; } &:hover { @@ -25,6 +24,9 @@ .inner { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18); } + .file-action-icon { + border-left: 1px solid rgba(0, 0, 0, 0.18); + } } &.file-upload { @@ -80,20 +82,46 @@ } } - .file-icon { - margin-right: 10px; - flex-shrink:0; - } - .file-name { - font-weight: @font-weight-medium; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .file-info-wrap { + display: flex; + align-items: center; + padding-left: @spacing-half + 1; + width: 100%; + + .file-icon { + margin-right: 10px; + flex-shrink:0; + } + .file-name { + font-weight: @font-weight-medium; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-size { + @file-size-color: #b8b8b8; + + margin-left: auto; + margin-right: @spacing-three-quarters; + color: @file-size-color; + } } + .file-action-icon { - margin-left: 10px; - flex-shrink:0; + @file-icon-color: #c7c7c7; + + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + padding-top: 1px; + height: 100%; + width: 37px; + border-left: 1px solid rgba(0, 0, 0, 0.09); + img { + background-color: @file-icon-color; + } } } @@ -123,6 +151,9 @@ .file-action-icon, .file-name-container, .file-name { display: block; } + .file-action-icon { + border-left: none; + } } .file-action-icon { @@ -132,6 +163,10 @@ top: -8px; width: 26px; border-radius: 0 0 0 3px; + border-left: none; + img { + background: none; + } } .file-preview { @@ -170,7 +205,7 @@ z-index: 1; max-width: 100%; background: url(../static/images/attachments/transparency-background.png) top left repeat; - background-size:8px; + background-size: 8px; } } diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index fa0904c35..29a030330 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -364,9 +364,8 @@ class ComposerView extends React.Component _renderQuotedTextControl: -> if QuotedHTMLTransformer.hasQuotedHTML(@state.body) - text = if @state.showQuotedText then "Hide" else "Show" - •••{text} previous + ••• else return [] diff --git a/internal_packages/composer/spec/quoted-text-spec.cjsx b/internal_packages/composer/spec/quoted-text-spec.cjsx index 9f4437a7d..0da6b840e 100644 --- a/internal_packages/composer/spec/quoted-text-spec.cjsx +++ b/internal_packages/composer/spec/quoted-text-spec.cjsx @@ -109,9 +109,6 @@ describe "Composer Quoted Text", -> it 'should be rendered', -> expect(@toggle).toBeDefined() - it 'prompts to hide the quote', -> - expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous" - describe 'when showQuotedText is false', -> beforeEach -> @composer.setState @@ -151,6 +148,3 @@ describe "Composer Quoted Text", -> it 'should be rendered', -> expect(@toggle).toBeDefined() - - it 'prompts to hide the quote', -> - expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous" diff --git a/internal_packages/message-list/lib/message-item-body.cjsx b/internal_packages/message-list/lib/message-item-body.cjsx index 21b6365fa..0895613df 100644 --- a/internal_packages/message-list/lib/message-item-body.cjsx +++ b/internal_packages/message-list/lib/message-item-body.cjsx @@ -68,9 +68,8 @@ class MessageItemBody extends React.Component _renderQuotedTextControl: => return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body) - text = if @state.showQuotedText then "Hide" else "Show" - •••{text} previous + ••• _toggleQuotedText: => diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx index e54592cfe..8e6f79d03 100644 --- a/internal_packages/message-list/lib/message-item.cjsx +++ b/internal_packages/message-list/lib/message-item.cjsx @@ -157,12 +157,38 @@ class MessageItem extends React.Component el = el.parentElement @_toggleCollapsed() + _onDownloadAll: => + Actions.fetchAndSaveAllFiles(@props.message.files) + + _renderDownloadAllButton: => +
+
+ + {@props.message.files.length} attachments +
+
-
+
+ + Download all +
+
+ + _renderAttachments: => attachments = @_attachmentComponents() if attachments.length > 0 -
{attachments}
+
+ {if attachments.length > 1 then @_renderDownloadAllButton()} +
{attachments}
+
else -
+
_renderHeaderSideItems: -> styles = diff --git a/internal_packages/message-list/spec/message-item-body-spec.cjsx b/internal_packages/message-list/spec/message-item-body-spec.cjsx index 9a45fbba2..74de8b1b4 100644 --- a/internal_packages/message-list/spec/message-item-body-spec.cjsx +++ b/internal_packages/message-list/spec/message-item-body-spec.cjsx @@ -196,9 +196,6 @@ describe "MessageItem", -> it 'should be rendered', -> expect(@toggle).toBeDefined() - it 'prompts to hide the quote', -> - expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous" - it "should be initialized to true if the message contains `Forwarded`...", -> @message.body = """ Hi guys, take a look at this. Very relevant. -mg @@ -249,9 +246,6 @@ describe "MessageItem", -> it 'should be rendered', -> expect(@toggle).toBeDefined() - it 'prompts to hide the quote', -> - expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous" - it "should pass the value into the EmailFrame", -> frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub) expect(frame.props.showQuotedText).toBe(true) diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less index 64f0448ab..ee14b3d23 100644 --- a/internal_packages/message-list/stylesheets/message-list.less +++ b/internal_packages/message-list/stylesheets/message-list.less @@ -532,8 +532,43 @@ body.platform-win32 { } } + +.download-all { + @download-btn-color: fadeout(#929292, 20%); + @download-hover-color: fadeout(@component-active-color, 20%); + + display: flex; + align-items: center; + color: @download-btn-color; + font-size: 0.9em; + cursor: default; + margin-top: @spacing-three-quarters; + + .separator { + margin: 0 5px; + } + + .attachment-number { + display: flex; + align-items: center; + } + + img { + vertical-align: middle; + margin-right: @spacing-half; + background-color: @download-btn-color; + } + + .download-all-action:hover { + color: @download-hover-color; + img { + background-color: @download-hover-color; + } + } +} + .attachments-area { - padding-top: @spacing-standard; + padding-top: @spacing-half + 2; // attachments are padded on both sides so that things like the remove "X" can // overhang them. To make the attachments line up with the body, we need to outdent diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 0e9a96d32..7dba11da0 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -475,6 +475,7 @@ class Actions @fetchAndOpenFile: ActionScopeWindow @fetchAndSaveFile: ActionScopeWindow + @fetchAndSaveAllFiles: ActionScopeWindow @fetchFile: ActionScopeWindow @abortFetchFile: ActionScopeWindow diff --git a/src/flux/models/file.coffee b/src/flux/models/file.coffee index 170ce05bf..f710548a9 100644 --- a/src/flux/models/file.coffee +++ b/src/flux/models/file.coffee @@ -67,6 +67,10 @@ class File extends Model else return "Unnamed Attachment" + safeDisplayName: -> + RegExpUtils = require '../../regexp-utils' + return @displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-') + # Public: Returns the file extension that should be used for this file. # Note that asking for the displayExtension is more accurate than trying to read # the extension directly off the filename. The returned extension may be based @@ -77,4 +81,20 @@ class File extends Model displayExtension: -> path.extname(@displayName().toLowerCase())[1..-1] + displayFileSize: (bytes = @size) -> + threshold = 1000000000 + units = ['B', 'KB', 'MB', 'GB'] + idx = units.length - 1 + + result = bytes / threshold + while result < 1 and idx >= 0 + threshold /= 1000 + result = bytes / threshold + idx-- + + # parseFloat will remove trailing zeros + decimalPoints = if idx >= 2 then 1 else 0 + rounded = parseFloat(result.toFixed(decimalPoints)) + return "#{rounded} #{units[idx]}" + module.exports = File diff --git a/src/flux/stores/file-download-store.coffee b/src/flux/stores/file-download-store.coffee index 2463e5e29..205cca76a 100644 --- a/src/flux/stores/file-download-store.coffee +++ b/src/flux/stores/file-download-store.coffee @@ -98,6 +98,7 @@ FileDownloadStore = Reflux.createStore @listenTo Actions.fetchFile, @_fetch @listenTo Actions.fetchAndOpenFile, @_fetchAndOpen @listenTo Actions.fetchAndSaveFile, @_fetchAndSave + @listenTo Actions.fetchAndSaveAllFiles, @_fetchAndSaveAll @listenTo Actions.abortFetchFile, @_abortFetchFile @listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived @@ -113,9 +114,7 @@ FileDownloadStore = Reflux.createStore # pathForFile: (file) -> return undefined unless file - - filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-') - path.join(@_downloadDirectory, file.id, filesafeName) + path.join(@_downloadDirectory, file.id, file.safeDisplayName()) downloadDataForFile: (fileId) -> @_downloads[fileId]?.data() @@ -202,6 +201,13 @@ FileDownloadStore = Reflux.createStore .catch => @_presentError(file) + _saveDownload: (download, savePath) => + return new Promise (resolve, reject) => + stream = fs.createReadStream(download.targetPath) + stream.pipe(fs.createWriteStream(savePath)) + stream.on 'error', (err) -> reject(err) + stream.on 'end', -> resolve() + _fetchAndSave: (file) -> defaultPath = @_defaultSavePath(file) defaultExtension = path.extname(defaultPath) @@ -215,12 +221,36 @@ FileDownloadStore = Reflux.createStore if didLoseExtension savePath = savePath + defaultExtension - defaultPath = NylasEnv.savedState.lastDownloadDirectory - @_runDownload(file).then (download) -> - stream = fs.createReadStream(download.targetPath) - stream.pipe(fs.createWriteStream(savePath)) - stream.on 'end', -> - shell.showItemInFolder(savePath) + @_runDownload(file) + .then (download) => @_saveDownload(download, savePath) + .then => shell.showItemInFolder(savePath) + .catch => + @_presentError(file) + + _fetchAndSaveAll: (files) -> + defaultPath = @_defaultSaveDir() + options = { + defaultPath, + properties: ['openDirectory'], + } + + NylasEnv.showOpenDialog options, (selected) => + return unless selected + dirPath = selected[0] + return unless dirPath + NylasEnv.savedState.lastDownloadDirectory = dirPath + + lastSavePath = null + savePromises = files.map (file) => + savePath = path.join(dirPath, file.safeDisplayName()) + @_runDownload(file) + .then (download) => @_saveDownload(download, savePath) + .then -> + lastSavePath = savePath + + Promise.all(savePromises) + .then => + shell.showItemInFolder(lastSavePath) if lastSavePath .catch => @_presentError(file) @@ -234,7 +264,7 @@ FileDownloadStore = Reflux.createStore fs.exists downloadPath, (exists) -> fs.unlink(downloadPath) if exists - _defaultSavePath: (file) -> + _defaultSaveDir: -> if process.platform is 'win32' home = process.env.USERPROFILE else @@ -248,8 +278,11 @@ FileDownloadStore = Reflux.createStore if fs.existsSync(NylasEnv.savedState.lastDownloadDirectory) downloadDir = NylasEnv.savedState.lastDownloadDirectory - filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-') - path.join(downloadDir, filesafeName) + return downloadDir + + _defaultSavePath: (file) -> + downloadDir = @_defaultSaveDir() + path.join(downloadDir, file.safeDisplayName()) _presentError: (file) -> remote.dialog.showMessageBox diff --git a/static/components/extra.less b/static/components/extra.less index 00d2fb865..477b7136f 100644 --- a/static/components/extra.less +++ b/static/components/extra.less @@ -7,7 +7,7 @@ border: 1px solid fade(@text-color-very-subtle, 15%); border-radius: 3px; line-height: 10px; - padding: 6px 10px; + padding: 1px 5px; font-weight: 600; font-size: @font-size-smaller; margin: 5px 0 3px 0; @@ -23,7 +23,6 @@ font-size: @font-size-smaller * 0.8; top:-1px; position:relative; - padding-right:8px; } } diff --git a/static/images/attachments/ic-attachments-all-clippy@1x.png b/static/images/attachments/ic-attachments-all-clippy@1x.png new file mode 100644 index 000000000..0845dcc2c Binary files /dev/null and b/static/images/attachments/ic-attachments-all-clippy@1x.png differ diff --git a/static/images/attachments/ic-attachments-all-clippy@2x.png b/static/images/attachments/ic-attachments-all-clippy@2x.png new file mode 100644 index 000000000..85ef5d54e Binary files /dev/null and b/static/images/attachments/ic-attachments-all-clippy@2x.png differ diff --git a/static/images/attachments/ic-attachments-download-all@1x.png b/static/images/attachments/ic-attachments-download-all@1x.png new file mode 100644 index 000000000..fb208ae19 Binary files /dev/null and b/static/images/attachments/ic-attachments-download-all@1x.png differ diff --git a/static/images/attachments/ic-attachments-download-all@2x.png b/static/images/attachments/ic-attachments-download-all@2x.png new file mode 100644 index 000000000..d6fce9d0f Binary files /dev/null and b/static/images/attachments/ic-attachments-download-all@2x.png differ diff --git a/static/images/attachments/icon-attachment-download@1x.png b/static/images/attachments/icon-attachment-download@1x.png index 19399477c..77d7cd482 100644 Binary files a/static/images/attachments/icon-attachment-download@1x.png and b/static/images/attachments/icon-attachment-download@1x.png differ diff --git a/static/images/attachments/icon-attachment-download@2x.png b/static/images/attachments/icon-attachment-download@2x.png index 1b45867cd..106e70025 100644 Binary files a/static/images/attachments/icon-attachment-download@2x.png and b/static/images/attachments/icon-attachment-download@2x.png differ diff --git a/static/images/attachments/remove-attachment@1x.png b/static/images/attachments/remove-attachment@1x.png index 6df89c951..4a17e8645 100644 Binary files a/static/images/attachments/remove-attachment@1x.png and b/static/images/attachments/remove-attachment@1x.png differ diff --git a/static/images/attachments/remove-attachment@2x.png b/static/images/attachments/remove-attachment@2x.png new file mode 100644 index 000000000..8c82207ef Binary files /dev/null and b/static/images/attachments/remove-attachment@2x.png differ