diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1e29067..006ae8663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,35 @@ # N1 Changelog -### 0.3.38 (1/8/16) +### 0.3.43 (1/12/16) - Features: + You can now enable and disable bundled plugins from Preferences > Plugins, and bundled plugin updates are delivered alongside N1 updates. + You can now adjust the interface zoom from the workspace preferences. +- Development: + + Packages can now list a relative `icon` path in their package.json. + +- Composer Improvements: + + You can now reply inline by outdenting (pressing delete) in quoted text. + + The Apple Mail keyboard shortcut for send is now correct. + + Keyboard shortcuts are shown in the shortcuts preferences. + + Clicking beneath the message body now positions your cursor correctly. + + Tabbing to the body positions the cursor at the end of the draft reliably. + + Tabbing to the subject highlights it correctly. + + Copy & paste now preserves line breaks reliably + + Inserting a template into a draft will no longer remove your signature. + - Fixes: - + The Apple Mail keyboard shortcut for send is not correct. - + Composer keyboard shortcuts are shown in the shortcuts preferences. + You can now unsubscribe from the N1 mailing list from the Account preferences. + The message actions dropdown is now left aligned. + Thread "Quick Actions" are now displayed in the correct order. + Account names can no longer overflow the preferences sidebar. + + On Windows, N1 restarts after installing an update. + + N1 now re-opens in fullscreen mode if it exited in fullscreen mode. + + Files with illegal filesystem characters can be dragged and dropped normally. + + Files with illegal filesystem characters now download and open correctly. + + The Event RSVP interface only appears if you are a participant on the event. ### 0.3.36 (1/5/16) diff --git a/README.md b/README.md index 15b0c3c37..80dcc585b 100644 --- a/README.md +++ b/README.md @@ -56,4 +56,4 @@ By default the N1 source points to our hosted version of the Nylas Sync Engine; Have an idea for a package, or a feature you'd love to see in N1? Check out our [public Trello board](https://trello.com/b/hxsqB6vx/n1-open-source-roadmap) -to contribute your thoughts and vote on existing ideas or see the [existing plugins and themes](http://github.com/nylas/n1-plugins). +to contribute your thoughts and vote on existing ideas. diff --git a/build/resources/linux/debian/lintian-overrides b/build/resources/linux/debian/lintian-overrides index 04a62fb95..0c946dfc0 100644 --- a/build/resources/linux/debian/lintian-overrides +++ b/build/resources/linux/debian/lintian-overrides @@ -2,6 +2,8 @@ nylas: arch-dependent-file-in-usr-share nylas: changelog-file-missing-in-native-package nylas: copyright-file-contains-full-apache-2-license nylas: copyright-should-refer-to-common-license-file-for-apache-2 +nylas: copyright-should-refer-to-common-license-file-for-lgpl nylas: embedded-library nylas: package-installs-python-bytecode nylas: unstripped-binary-or-object +nylas: extra-license-file diff --git a/build/resources/linux/nylas.desktop.in b/build/resources/linux/nylas.desktop.in index 611c25bfb..f8c4dc0d9 100644 --- a/build/resources/linux/nylas.desktop.in +++ b/build/resources/linux/nylas.desktop.in @@ -7,5 +7,5 @@ Icon=<%= iconName %> Type=Application StartupNotify=true StartupWMClass=Nylas N1 -Categories=GNOME;GTK;Utility;EmailClient;Development; +Categories=GNOME;GTK;Email;Utility;Development; MimeType=text/plain;x-scheme-handler/mailto;x-scheme-handler/nylas; diff --git a/build/tasks/publish-nylas-build-task.coffee b/build/tasks/publish-nylas-build-task.coffee index 3ea959626..27560a88d 100644 --- a/build/tasks/publish-nylas-build-task.coffee +++ b/build/tasks/publish-nylas-build-task.coffee @@ -62,21 +62,24 @@ module.exports = (grunt) -> return reject(err) if err resolve() - put = (localSource, destName) -> + put = (localSource, destName, options = {}) -> grunt.log.writeln ">> Uploading #{localSource} to S3…" write = grunt.log.writeln ext = path.extname(destName) lastPc = 0 + params = + Key: destName + ACL: "public-read" + Bucket: "edgehill" + + _.extend(params, options) + new Promise (resolve, reject) -> uploader = s3Client.uploadFile localFile: localSource - s3Params: - Key: destName - ACL: "public-read" - Bucket: "edgehill" - + s3Params: params uploader.on "error", (err) -> reject(err) uploader.on "progress", -> @@ -158,9 +161,11 @@ module.exports = (grunt) -> files = fs.readdirSync(buildDir) for file in files if path.extname(file) is '.deb' - uploadPromises.push uploadToS3(file, "#{fullVersion}/#{process.platform}-deb/#{process.arch}/N1.deb") + uploadPromises.push uploadToS3(file, "#{fullVersion}/#{process.platform}-deb/#{process.arch}/N1.deb", + {"ContentType": "application/x-deb"}) if path.extname(file) is '.rpm' - uploadPromises.push uploadToS3(file, "#{fullVersion}/#{process.platform}-rpm/#{process.arch}/N1.rpm") + uploadPromises.push uploadToS3(file, "#{fullVersion}/#{process.platform}-rpm/#{process.arch}/N1.rpm", + {"ContentType": "application/x-rpm"}) else grunt.fail.fatal "Unsupported platform: '#{process.platform}'" diff --git a/internal_packages/attachments/lib/attachment-component.cjsx b/internal_packages/attachments/lib/attachment-component.cjsx index 3b5f239b5..76781dc4d 100644 --- a/internal_packages/attachments/lib/attachment-component.cjsx +++ b/internal_packages/attachments/lib/attachment-component.cjsx @@ -65,9 +65,11 @@ class AttachmentComponent extends React.Component _onDragStart: (event) => - path = FileDownloadStore.pathForFile(@props.file) - if fs.existsSync(path) - DownloadURL = "#{@props.file.contentType}:#{@props.file.displayName()}:file://#{path}" + 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 diff --git a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.coffee b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.coffee index e8ba75750..a55b694b3 100644 --- a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.coffee +++ b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.coffee @@ -13,7 +13,7 @@ class SpellcheckComposerExtension extends ComposerExtension SpellcheckCache[word] @onContentChanged: ({editor}) => - @walkTree(editor) + @update(editor) @onShowContextMenu: ({editor, event, menu}) => selection = editor.currentSelection() @@ -39,87 +39,116 @@ class SpellcheckComposerExtension extends ComposerExtension @applyCorrection: (editor, range, selection, correction) => DOMUtils.Mutating.applyTextInRange(range, selection, correction) - @walkTree(editor) + @update(editor) @learnSpelling: (editor, word) => spellchecker.add(word) delete SpellcheckCache[word] - @walkTree(editor) + @update(editor) - @walkTree: (editor) => - # Remove all existing spellcheck nodes - spellingNodes = editor.rootNode.querySelectorAll('spelling') - for node in spellingNodes - editor.whilePreservingSelection => - DOMUtils.unwrapNode(node) - - # Normalize to make sure words aren't split across text nodes - editor.rootNode.normalize() + @update: (editor) => + @_unwrapWords(editor) + @_wrapMisspelledWords(editor) + # Creates a shallow copy of a selection object where anchorNode / focusNode + # can be changed, and provides it to the callback provided. After the callback + # runs, it applies the new selection if `snapshot.modified` has been set. + # + # Note: This is different from ExposedSelection because the nodes are not cloned. + # In the callback functions, we need to check whether the anchor/focus nodes + # are INSIDE the nodes we're adjusting. + # + @_whileApplyingSelectionChanges: (cb) => selection = document.getSelection() selectionSnapshot = anchorNode: selection.anchorNode anchorOffset: selection.anchorOffset focusNode: selection.focusNode focusOffset: selection.focusOffset - selectionImpacted = false + modified: false - treeWalker = document.createTreeWalker(editor.rootNode, NodeFilter.SHOW_TEXT) - nodeList = [] - nodeMisspellingsFound = 0 + cb(selectionSnapshot) - while (treeWalker.nextNode()) - nodeList.push(treeWalker.currentNode) - - # Note: As a performance optimization, we stop spellchecking after encountering - # 10 misspelled words. This keeps the runtime of this method bounded! - - while (node = nodeList.shift()) - break if nodeMisspellingsFound > 10 - str = node.textContent - - # https://regex101.com/r/bG5yC4/1 - wordRegexp = /(\w[\w'’-]*\w|\w)/g - - while ((match = wordRegexp.exec(str)) isnt null) - break if nodeMisspellingsFound > 10 - misspelled = @isMisspelled(match[0]) - - if misspelled - # The insertion point is currently at the end of this misspelled word. - # Do not mark it until the user types a space or leaves. - if selectionSnapshot.focusNode is node and selectionSnapshot.focusOffset is match.index + match[0].length - continue - - if match.index is 0 - matchNode = node - else - matchNode = node.splitText(match.index) - afterMatchNode = matchNode.splitText(match[0].length) - - spellingSpan = document.createElement('spelling') - spellingSpan.classList.add('misspelled') - spellingSpan.innerText = match[0] - matchNode.parentNode.replaceChild(spellingSpan, matchNode) - - for prop in ['anchor', 'focus'] - if selectionSnapshot["#{prop}Node"] is node - if selectionSnapshot["#{prop}Offset"] > match.index + match[0].length - selectionImpacted = true - selectionSnapshot["#{prop}Node"] = afterMatchNode - selectionSnapshot["#{prop}Offset"] -= match.index + match[0].length - else if selectionSnapshot["#{prop}Offset"] > match.index - selectionImpacted = true - selectionSnapshot["#{prop}Node"] = spellingSpan.childNodes[0] - selectionSnapshot["#{prop}Offset"] -= match.index - - nodeMisspellingsFound += 1 - nodeList.unshift(afterMatchNode) - break - - if selectionImpacted + if selectionSnapshot.modified selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset) + # Removes all of the nodes found in the provided `editor`. + # It normalizes the DOM after removing spelling nodes to ensure that words + # are not split between text nodes. (ie: doesn, 't => doesn't) + @_unwrapWords: (editor) => + @_whileApplyingSelectionChanges (selectionSnapshot) => + spellingNodes = editor.rootNode.querySelectorAll('spelling') + + for node in spellingNodes + if selectionSnapshot.anchorNode is node + selectionSnapshot.anchorNode = node.firstChild + if selectionSnapshot.focusNode is node + selectionSnapshot.focusNode = node.firstChild + + selectionSnapshot.modified = true + node.parentNode.insertBefore(node.firstChild, node) while (node.firstChild) + node.parentNode.removeChild(node) + + editor.rootNode.normalize() + + # Traverses all of the text nodes within the provided `editor`. If it finds a + # text node with a misspelled word, it splits it, wraps the misspelled word + # with a node and updates the selection to account for the change. + @_wrapMisspelledWords: (editor) => + @_whileApplyingSelectionChanges (selectionSnapshot) => + treeWalker = document.createTreeWalker(editor.rootNode, NodeFilter.SHOW_TEXT) + nodeList = [] + nodeMisspellingsFound = 0 + + while (treeWalker.nextNode()) + nodeList.push(treeWalker.currentNode) + + # Note: As a performance optimization, we stop spellchecking after encountering + # 30 misspelled words. This keeps the runtime of this method bounded! + + while (node = nodeList.shift()) + break if nodeMisspellingsFound > 30 + str = node.textContent + + # https://regex101.com/r/bG5yC4/1 + wordRegexp = /(\w[\w'’-]*\w|\w)/g + + while ((match = wordRegexp.exec(str)) isnt null) + break if nodeMisspellingsFound > 30 + misspelled = @isMisspelled(match[0]) + + if misspelled + # The insertion point is currently at the end of this misspelled word. + # Do not mark it until the user types a space or leaves. + if selectionSnapshot.focusNode is node and selectionSnapshot.focusOffset is match.index + match[0].length + continue + + if match.index is 0 + matchNode = node + else + matchNode = node.splitText(match.index) + afterMatchNode = matchNode.splitText(match[0].length) + + spellingSpan = document.createElement('spelling') + spellingSpan.classList.add('misspelled') + spellingSpan.innerText = match[0] + matchNode.parentNode.replaceChild(spellingSpan, matchNode) + + for prop in ['anchor', 'focus'] + if selectionSnapshot["#{prop}Node"] is node + if selectionSnapshot["#{prop}Offset"] > match.index + match[0].length + selectionSnapshot.modified = true + selectionSnapshot["#{prop}Node"] = afterMatchNode + selectionSnapshot["#{prop}Offset"] -= match.index + match[0].length + else if selectionSnapshot["#{prop}Offset"] > match.index + selectionSnapshot.modified = true + selectionSnapshot["#{prop}Node"] = spellingSpan.childNodes[0] + selectionSnapshot["#{prop}Offset"] -= match.index + + nodeMisspellingsFound += 1 + nodeList.unshift(afterMatchNode) + break + @finalizeSessionBeforeSending: ({session}) -> body = session.draft().body clean = body.replace(/<\/?spelling[^>]*>/g, '') diff --git a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.coffee b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.coffee index 11c6f2955..f2795aa5c 100644 --- a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.coffee +++ b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.coffee @@ -12,7 +12,7 @@ describe "SpellcheckComposerExtension", -> spyOn(SpellcheckComposerExtension, 'isMisspelled').andCallFake (word) -> spellings[word] - describe "walkTree", -> + describe "update", -> it "correctly walks a DOM tree and surrounds mispelled words", -> dom = document.createElement('div') dom.innerHTML = initialHTML @@ -21,7 +21,7 @@ describe "SpellcheckComposerExtension", -> rootNode: dom whilePreservingSelection: (cb) -> cb() - SpellcheckComposerExtension.walkTree(editor) + SpellcheckComposerExtension.update(editor) expect(dom.innerHTML).toEqual(expectedHTML) describe "finalizeSessionBeforeSending", -> diff --git a/internal_packages/composer-templates/assets/Welcome to Quick Replies.html b/internal_packages/composer-templates/assets/Welcome to Quick Replies.html new file mode 100644 index 000000000..c8700b156 --- /dev/null +++ b/internal_packages/composer-templates/assets/Welcome to Quick Replies.html @@ -0,0 +1,24 @@ +

+ Hi there {{First Name}}, +

+

+ Welcome to the Quick Replies plugin! Here you can create email templates with + {{variable regions}} that you can quickly fill in + before you send your email. +

+
+

+ Just wrap text in {{double brackets}} to create a variable! You + can add {{variables}} inside areas of formatted + text too. +

+
+

+ When you send your message, the highlighting is always removed so the recipient + never sees it. +

+ Enjoy! +

+

+ - Nylas Team +

diff --git a/internal_packages/composer-templates/assets/Welcome to Templates.html b/internal_packages/composer-templates/assets/Welcome to Templates.html deleted file mode 100644 index 3cb97efab..000000000 --- a/internal_packages/composer-templates/assets/Welcome to Templates.html +++ /dev/null @@ -1,20 +0,0 @@ -

- Hi there First Name, -

-

- Welcome to the templates package! Templates live in the ~/.nylas/templates - directory on your computer. Each template is an HTML file - the name of the - file is the name of the template, and it's contents are the default message body. -

-

- If you include HTML <code> tags in your template, you can create - regions that you can jump between and fill easily. Check out the source of - the template for a super awesome example! -

-

- Give <code> tags the `var` class to mark them as template regions. Add - the `empty` class to make them dark yellow. When you send your message, <code> - tags are always stripped so the recipient never sees any highlighting. -

- - Nylas Team -

diff --git a/internal_packages/composer-templates/lib/preferences-templates.cjsx b/internal_packages/composer-templates/lib/preferences-templates.cjsx index 0f7024434..236486646 100644 --- a/internal_packages/composer-templates/lib/preferences-templates.cjsx +++ b/internal_packages/composer-templates/lib/preferences-templates.cjsx @@ -13,7 +13,7 @@ class PreferencesTemplates extends React.Component {templates, selectedTemplate, selectedTemplateName} = @_getStateFromStores() @state = editAsHTML: false - editState: null + editState: if templates.length==0 then "new" else null templates: templates selectedTemplate: selectedTemplate selectedTemplateName: selectedTemplateName @@ -108,14 +108,14 @@ class PreferencesTemplates extends React.Component _renderEditableTemplate: -> _renderHTMLTemplate: ->