feat(download-all): Adds download-all button + style updates, more ES6
Summary: - Adds initial version of download all button - Converts attachments plugin to ES6 and adds updated styling - Updates quoted text button - #905, #1712 Test Plan: - Unit + manual Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2769
|
@ -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: =>
|
|
||||||
<div className="inner" onDoubleClick={@_onClickView} onDragStart={@_onDragStart} draggable="true">
|
|
||||||
<span className={"progress-bar-wrap state-#{@props.download?.state ? ""}"}>
|
|
||||||
<span className="progress-background"></span>
|
|
||||||
<span className="progress-foreground" style={@_downloadProgressStyle()}></span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Flexbox direction="row" style={alignItems: 'center'}>
|
|
||||||
<RetinaImg className="file-icon"
|
|
||||||
fallback="file-fallback.png"
|
|
||||||
mode={RetinaImg.Mode.ContentPreserve}
|
|
||||||
name="file-#{@props.file.displayExtension()}.png"/>
|
|
||||||
<span className="file-name">{@props.file.displayName()}</span>
|
|
||||||
{@_renderFileActions()}
|
|
||||||
</Flexbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_renderFileActions: =>
|
|
||||||
if @props.removable
|
|
||||||
<div className="file-action-icon" onClick={@_onClickRemove}>
|
|
||||||
{@_renderRemoveIcon()}
|
|
||||||
</div>
|
|
||||||
else if @_isDownloading() and @_canAbortDownload()
|
|
||||||
<div className="file-action-icon" onClick={@_onClickAbort}>
|
|
||||||
{@_renderRemoveIcon()}
|
|
||||||
</div>
|
|
||||||
else
|
|
||||||
<div className="file-action-icon" onClick={@_onClickDownload}>
|
|
||||||
{@_renderDownloadButton()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_downloadProgressStyle: =>
|
|
||||||
width: "#{@props.download?.percent ? 0}%"
|
|
||||||
|
|
||||||
_canAbortDownload: -> true
|
|
||||||
|
|
||||||
_canClickToView: => not @props.removable
|
|
||||||
|
|
||||||
_isDownloading: => @props.download?.state is "downloading"
|
|
||||||
|
|
||||||
_renderRemoveIcon: ->
|
|
||||||
<RetinaImg name="remove-attachment.png" mode={RetinaImg.Mode.ContentPreserve} />
|
|
||||||
|
|
||||||
_renderDownloadButton: ->
|
|
||||||
<RetinaImg name="icon-attachment-download.png" mode={RetinaImg.Mode.ContentPreserve} />
|
|
||||||
|
|
||||||
_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
|
|
154
internal_packages/attachments/lib/attachment-component.jsx
Normal file
|
@ -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 (
|
||||||
|
<RetinaImg
|
||||||
|
name="remove-attachment.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDownloadButton() {
|
||||||
|
return (
|
||||||
|
<RetinaImg
|
||||||
|
name="icon-attachment-download.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFileActionIcon() {
|
||||||
|
if (this.props.removable) {
|
||||||
|
return (
|
||||||
|
<div className="file-action-icon" onClick={this._onClickRemove}>
|
||||||
|
{this._renderRemoveIcon()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (this._isDownloading() && this._canAbortDownload()) {
|
||||||
|
return (
|
||||||
|
<div className="file-action-icon" onClick={this._onClickAbort}>
|
||||||
|
{this._renderRemoveIcon()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="file-action-icon" onClick={this._onClickDownload}>
|
||||||
|
{this._renderDownloadButton()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {file, download} = this.props;
|
||||||
|
const downloadState = download ? download.state || "" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inner" onDoubleClick={this._onClickView} onDragStart={this._onDragStart} draggable="true">
|
||||||
|
<span className={`progress-bar-wrap state-${downloadState}`}>
|
||||||
|
<span className="progress-background" />
|
||||||
|
<span className="progress-foreground" style={this._downloadProgressStyle()} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Flexbox direction="row" style={{alignItems: 'center'}}>
|
||||||
|
<div className="file-info-wrap">
|
||||||
|
<RetinaImg
|
||||||
|
className="file-icon"
|
||||||
|
fallback="file-fallback.png"
|
||||||
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
|
name={`file-${file.displayExtension()}.png`}
|
||||||
|
/>
|
||||||
|
<span className="file-name">{file.displayName()}</span>
|
||||||
|
<span className="file-size">{file.displayFileSize()}</span>
|
||||||
|
</div>
|
||||||
|
{this._renderFileActionIcon()}
|
||||||
|
</Flexbox>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentComponent
|
|
@ -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: =>
|
|
||||||
<div>
|
|
||||||
<span className={"progress-bar-wrap state-#{@props.download?.state ? ""}"}>
|
|
||||||
<span className="progress-background"></span>
|
|
||||||
<span className="progress-foreground" style={@_downloadProgressStyle()}></span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{@_renderFileActions()}
|
|
||||||
|
|
||||||
<div className="file-preview" onDoubleClick={@_onClickView}>
|
|
||||||
<div className="file-name-container">
|
|
||||||
<div className="file-name">{@props.file.displayName()}</div>
|
|
||||||
</div>
|
|
||||||
{@_imgOrLoader()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_canAbortDownload: -> false
|
|
||||||
|
|
||||||
_renderRemoveIcon: ->
|
|
||||||
<RetinaImg name="image-cancel-button.png" mode={RetinaImg.Mode.ContentPreserve} />
|
|
||||||
|
|
||||||
_renderDownloadButton: ->
|
|
||||||
<RetinaImg name="image-download-button.png" mode={RetinaImg.Mode.ContentPreserve} />
|
|
||||||
|
|
||||||
_imgOrLoader: ->
|
|
||||||
if @props.download and @props.download.percent <= 5
|
|
||||||
<div style={width: "100%", height: "100px"}>
|
|
||||||
<Spinner visible={true} />
|
|
||||||
</div>
|
|
||||||
else if @props.download and @props.download.percent < 100
|
|
||||||
<DraggableImg src={"#{@props.targetPath}?percent=#{@props.download.percent}"} />
|
|
||||||
else
|
|
||||||
<DraggableImg src={@props.targetPath} />
|
|
||||||
|
|
||||||
module.exports = ImageAttachmentComponent
|
|
|
@ -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 (
|
||||||
|
<div style={{width: "100%", height: "100px"}}>
|
||||||
|
<Spinner visible />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (download && download.percent < 100) {
|
||||||
|
return (
|
||||||
|
<DraggableImg src={`${targetPath}?percent=${download.percent}`} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <DraggableImg src={targetPath} />
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderRemoveIcon() {
|
||||||
|
return (
|
||||||
|
<RetinaImg
|
||||||
|
name="image-cancel-button.png"
|
||||||
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDownloadButton() {
|
||||||
|
return (
|
||||||
|
<RetinaImg
|
||||||
|
name="image-download-button.png"
|
||||||
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {download, file} = this.props
|
||||||
|
const state = download ? download.state || "" : ""
|
||||||
|
const displayName = file.displayName()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className={`progress-bar-wrap state-${state}`}>
|
||||||
|
<span className="progress-background"></span>
|
||||||
|
<span className="progress-foreground" style={this._downloadProgressStyle()}></span>
|
||||||
|
</span>
|
||||||
|
{this._renderFileActions()}
|
||||||
|
<div className="file-preview" onDoubleClick={this._onClickView}>
|
||||||
|
<div className="file-name-container">
|
||||||
|
<div className="file-name">{displayName}</div>
|
||||||
|
</div>
|
||||||
|
{this._imgOrLoader()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageAttachmentComponent
|
|
@ -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
|
|
14
internal_packages/attachments/lib/main.es6
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -12,12 +12,11 @@
|
||||||
-webkit-user-drag: element;
|
-webkit-user-drag: element;
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
border-radius: 4px;
|
border-radius: 2px;
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
background: @background-off-primary;
|
background: @background-off-primary;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.09);
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.09);
|
||||||
padding: 0 @spacing-standard;
|
height: 37px;
|
||||||
height:46px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -25,6 +24,9 @@
|
||||||
.inner {
|
.inner {
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18);
|
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 {
|
&.file-upload {
|
||||||
|
@ -80,20 +82,46 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-icon {
|
.file-info-wrap {
|
||||||
margin-right: 10px;
|
display: flex;
|
||||||
flex-shrink:0;
|
align-items: center;
|
||||||
}
|
padding-left: @spacing-half + 1;
|
||||||
.file-name {
|
width: 100%;
|
||||||
font-weight: @font-weight-medium;
|
|
||||||
flex: 1;
|
.file-icon {
|
||||||
overflow: hidden;
|
margin-right: 10px;
|
||||||
text-overflow: ellipsis;
|
flex-shrink:0;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
.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 {
|
.file-action-icon {
|
||||||
margin-left: 10px;
|
@file-icon-color: #c7c7c7;
|
||||||
flex-shrink:0;
|
|
||||||
|
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 {
|
.file-action-icon, .file-name-container, .file-name {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.file-action-icon {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-action-icon {
|
.file-action-icon {
|
||||||
|
@ -132,6 +163,10 @@
|
||||||
top: -8px;
|
top: -8px;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
border-radius: 0 0 0 3px;
|
border-radius: 0 0 0 3px;
|
||||||
|
border-left: none;
|
||||||
|
img {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-preview {
|
.file-preview {
|
||||||
|
@ -170,7 +205,7 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background: url(../static/images/attachments/transparency-background.png) top left repeat;
|
background: url(../static/images/attachments/transparency-background.png) top left repeat;
|
||||||
background-size:8px;
|
background-size: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -364,9 +364,8 @@ class ComposerView extends React.Component
|
||||||
|
|
||||||
_renderQuotedTextControl: ->
|
_renderQuotedTextControl: ->
|
||||||
if QuotedHTMLTransformer.hasQuotedHTML(@state.body)
|
if QuotedHTMLTransformer.hasQuotedHTML(@state.body)
|
||||||
text = if @state.showQuotedText then "Hide" else "Show"
|
|
||||||
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
|
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
|
||||||
<span className="dots">•••</span>{text} previous
|
<span className="dots">•••</span>
|
||||||
</a>
|
</a>
|
||||||
else return []
|
else return []
|
||||||
|
|
||||||
|
|
|
@ -109,9 +109,6 @@ describe "Composer Quoted Text", ->
|
||||||
it 'should be rendered', ->
|
it 'should be rendered', ->
|
||||||
expect(@toggle).toBeDefined()
|
expect(@toggle).toBeDefined()
|
||||||
|
|
||||||
it 'prompts to hide the quote', ->
|
|
||||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"
|
|
||||||
|
|
||||||
describe 'when showQuotedText is false', ->
|
describe 'when showQuotedText is false', ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@composer.setState
|
@composer.setState
|
||||||
|
@ -151,6 +148,3 @@ describe "Composer Quoted Text", ->
|
||||||
|
|
||||||
it 'should be rendered', ->
|
it 'should be rendered', ->
|
||||||
expect(@toggle).toBeDefined()
|
expect(@toggle).toBeDefined()
|
||||||
|
|
||||||
it 'prompts to hide the quote', ->
|
|
||||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
|
|
||||||
|
|
|
@ -68,9 +68,8 @@ class MessageItemBody extends React.Component
|
||||||
|
|
||||||
_renderQuotedTextControl: =>
|
_renderQuotedTextControl: =>
|
||||||
return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body)
|
return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body)
|
||||||
text = if @state.showQuotedText then "Hide" else "Show"
|
|
||||||
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
|
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
|
||||||
<span className="dots">•••</span>{text} previous
|
<span className="dots">•••</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
_toggleQuotedText: =>
|
_toggleQuotedText: =>
|
||||||
|
|
|
@ -157,12 +157,38 @@ class MessageItem extends React.Component
|
||||||
el = el.parentElement
|
el = el.parentElement
|
||||||
@_toggleCollapsed()
|
@_toggleCollapsed()
|
||||||
|
|
||||||
|
_onDownloadAll: =>
|
||||||
|
Actions.fetchAndSaveAllFiles(@props.message.files)
|
||||||
|
|
||||||
|
_renderDownloadAllButton: =>
|
||||||
|
<div className="download-all" onClick={@_onDownloadAll}>
|
||||||
|
<div className="attachment-number">
|
||||||
|
<RetinaImg
|
||||||
|
name="ic-attachments-all-clippy.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
|
/>
|
||||||
|
<span>{@props.message.files.length} attachments</span>
|
||||||
|
</div>
|
||||||
|
<div className="separator">-</div>
|
||||||
|
<div className="download-all-action">
|
||||||
|
<RetinaImg
|
||||||
|
name="ic-attachments-download-all.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
|
/>
|
||||||
|
<span>Download all</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
_renderAttachments: =>
|
_renderAttachments: =>
|
||||||
attachments = @_attachmentComponents()
|
attachments = @_attachmentComponents()
|
||||||
if attachments.length > 0
|
if attachments.length > 0
|
||||||
<div className="attachments-area">{attachments}</div>
|
<div>
|
||||||
|
{if attachments.length > 1 then @_renderDownloadAllButton()}
|
||||||
|
<div className="attachments-area">{attachments}</div>
|
||||||
|
</div>
|
||||||
else
|
else
|
||||||
<div></div>
|
<div />
|
||||||
|
|
||||||
_renderHeaderSideItems: ->
|
_renderHeaderSideItems: ->
|
||||||
styles =
|
styles =
|
||||||
|
|
|
@ -196,9 +196,6 @@ describe "MessageItem", ->
|
||||||
it 'should be rendered', ->
|
it 'should be rendered', ->
|
||||||
expect(@toggle).toBeDefined()
|
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`...", ->
|
it "should be initialized to true if the message contains `Forwarded`...", ->
|
||||||
@message.body = """
|
@message.body = """
|
||||||
Hi guys, take a look at this. Very relevant. -mg
|
Hi guys, take a look at this. Very relevant. -mg
|
||||||
|
@ -249,9 +246,6 @@ describe "MessageItem", ->
|
||||||
it 'should be rendered', ->
|
it 'should be rendered', ->
|
||||||
expect(@toggle).toBeDefined()
|
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", ->
|
it "should pass the value into the EmailFrame", ->
|
||||||
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
||||||
expect(frame.props.showQuotedText).toBe(true)
|
expect(frame.props.showQuotedText).toBe(true)
|
||||||
|
|
|
@ -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 {
|
.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
|
// 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
|
// overhang them. To make the attachments line up with the body, we need to outdent
|
||||||
|
|
|
@ -475,6 +475,7 @@ class Actions
|
||||||
|
|
||||||
@fetchAndOpenFile: ActionScopeWindow
|
@fetchAndOpenFile: ActionScopeWindow
|
||||||
@fetchAndSaveFile: ActionScopeWindow
|
@fetchAndSaveFile: ActionScopeWindow
|
||||||
|
@fetchAndSaveAllFiles: ActionScopeWindow
|
||||||
@fetchFile: ActionScopeWindow
|
@fetchFile: ActionScopeWindow
|
||||||
@abortFetchFile: ActionScopeWindow
|
@abortFetchFile: ActionScopeWindow
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,10 @@ class File extends Model
|
||||||
else
|
else
|
||||||
return "Unnamed Attachment"
|
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.
|
# 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
|
# 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
|
# the extension directly off the filename. The returned extension may be based
|
||||||
|
@ -77,4 +81,20 @@ class File extends Model
|
||||||
displayExtension: ->
|
displayExtension: ->
|
||||||
path.extname(@displayName().toLowerCase())[1..-1]
|
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
|
module.exports = File
|
||||||
|
|
|
@ -98,6 +98,7 @@ FileDownloadStore = Reflux.createStore
|
||||||
@listenTo Actions.fetchFile, @_fetch
|
@listenTo Actions.fetchFile, @_fetch
|
||||||
@listenTo Actions.fetchAndOpenFile, @_fetchAndOpen
|
@listenTo Actions.fetchAndOpenFile, @_fetchAndOpen
|
||||||
@listenTo Actions.fetchAndSaveFile, @_fetchAndSave
|
@listenTo Actions.fetchAndSaveFile, @_fetchAndSave
|
||||||
|
@listenTo Actions.fetchAndSaveAllFiles, @_fetchAndSaveAll
|
||||||
@listenTo Actions.abortFetchFile, @_abortFetchFile
|
@listenTo Actions.abortFetchFile, @_abortFetchFile
|
||||||
@listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived
|
@listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived
|
||||||
|
|
||||||
|
@ -113,9 +114,7 @@ FileDownloadStore = Reflux.createStore
|
||||||
#
|
#
|
||||||
pathForFile: (file) ->
|
pathForFile: (file) ->
|
||||||
return undefined unless file
|
return undefined unless file
|
||||||
|
path.join(@_downloadDirectory, file.id, file.safeDisplayName())
|
||||||
filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-')
|
|
||||||
path.join(@_downloadDirectory, file.id, filesafeName)
|
|
||||||
|
|
||||||
downloadDataForFile: (fileId) ->
|
downloadDataForFile: (fileId) ->
|
||||||
@_downloads[fileId]?.data()
|
@_downloads[fileId]?.data()
|
||||||
|
@ -202,6 +201,13 @@ FileDownloadStore = Reflux.createStore
|
||||||
.catch =>
|
.catch =>
|
||||||
@_presentError(file)
|
@_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) ->
|
_fetchAndSave: (file) ->
|
||||||
defaultPath = @_defaultSavePath(file)
|
defaultPath = @_defaultSavePath(file)
|
||||||
defaultExtension = path.extname(defaultPath)
|
defaultExtension = path.extname(defaultPath)
|
||||||
|
@ -215,12 +221,36 @@ FileDownloadStore = Reflux.createStore
|
||||||
if didLoseExtension
|
if didLoseExtension
|
||||||
savePath = savePath + defaultExtension
|
savePath = savePath + defaultExtension
|
||||||
|
|
||||||
defaultPath = NylasEnv.savedState.lastDownloadDirectory
|
@_runDownload(file)
|
||||||
@_runDownload(file).then (download) ->
|
.then (download) => @_saveDownload(download, savePath)
|
||||||
stream = fs.createReadStream(download.targetPath)
|
.then => shell.showItemInFolder(savePath)
|
||||||
stream.pipe(fs.createWriteStream(savePath))
|
.catch =>
|
||||||
stream.on 'end', ->
|
@_presentError(file)
|
||||||
shell.showItemInFolder(savePath)
|
|
||||||
|
_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 =>
|
.catch =>
|
||||||
@_presentError(file)
|
@_presentError(file)
|
||||||
|
|
||||||
|
@ -234,7 +264,7 @@ FileDownloadStore = Reflux.createStore
|
||||||
fs.exists downloadPath, (exists) ->
|
fs.exists downloadPath, (exists) ->
|
||||||
fs.unlink(downloadPath) if exists
|
fs.unlink(downloadPath) if exists
|
||||||
|
|
||||||
_defaultSavePath: (file) ->
|
_defaultSaveDir: ->
|
||||||
if process.platform is 'win32'
|
if process.platform is 'win32'
|
||||||
home = process.env.USERPROFILE
|
home = process.env.USERPROFILE
|
||||||
else
|
else
|
||||||
|
@ -248,8 +278,11 @@ FileDownloadStore = Reflux.createStore
|
||||||
if fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)
|
if fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)
|
||||||
downloadDir = NylasEnv.savedState.lastDownloadDirectory
|
downloadDir = NylasEnv.savedState.lastDownloadDirectory
|
||||||
|
|
||||||
filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-')
|
return downloadDir
|
||||||
path.join(downloadDir, filesafeName)
|
|
||||||
|
_defaultSavePath: (file) ->
|
||||||
|
downloadDir = @_defaultSaveDir()
|
||||||
|
path.join(downloadDir, file.safeDisplayName())
|
||||||
|
|
||||||
_presentError: (file) ->
|
_presentError: (file) ->
|
||||||
remote.dialog.showMessageBox
|
remote.dialog.showMessageBox
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
border: 1px solid fade(@text-color-very-subtle, 15%);
|
border: 1px solid fade(@text-color-very-subtle, 15%);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
padding: 6px 10px;
|
padding: 1px 5px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: @font-size-smaller;
|
font-size: @font-size-smaller;
|
||||||
margin: 5px 0 3px 0;
|
margin: 5px 0 3px 0;
|
||||||
|
@ -23,7 +23,6 @@
|
||||||
font-size: @font-size-smaller * 0.8;
|
font-size: @font-size-smaller * 0.8;
|
||||||
top:-1px;
|
top:-1px;
|
||||||
position:relative;
|
position:relative;
|
||||||
padding-right:8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
static/images/attachments/ic-attachments-all-clippy@1x.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
static/images/attachments/ic-attachments-all-clippy@2x.png
Normal file
After Width: | Height: | Size: 731 B |
BIN
static/images/attachments/ic-attachments-download-all@1x.png
Normal file
After Width: | Height: | Size: 355 B |
BIN
static/images/attachments/ic-attachments-download-all@2x.png
Normal file
After Width: | Height: | Size: 570 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 463 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 407 B After Width: | Height: | Size: 15 KiB |
BIN
static/images/attachments/remove-attachment@2x.png
Normal file
After Width: | Height: | Size: 15 KiB |