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()}
+
+
+ {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