2016-05-07 03:40:01 +08:00
|
|
|
/* eslint global-require: "off" */
|
|
|
|
|
2016-03-01 10:47:22 +08:00
|
|
|
// // Translation Plugin
|
|
|
|
// Last Revised: Feb. 29, 2016 by Ben Gotow
|
|
|
|
|
|
|
|
// TranslateButton is a simple React component that allows you to select
|
|
|
|
// a language from a popup menu and translates draft text into that language.
|
|
|
|
|
|
|
|
import request from 'request'
|
|
|
|
|
|
|
|
import {
|
|
|
|
React,
|
2016-03-29 16:41:24 +08:00
|
|
|
ReactDOM,
|
2016-03-01 10:47:22 +08:00
|
|
|
ComponentRegistry,
|
|
|
|
QuotedHTMLTransformer,
|
2016-03-10 02:01:18 +08:00
|
|
|
Actions,
|
2016-03-01 10:47:22 +08:00
|
|
|
} from 'nylas-exports';
|
|
|
|
|
|
|
|
import {
|
|
|
|
Menu,
|
|
|
|
RetinaImg,
|
|
|
|
} from 'nylas-component-kit';
|
|
|
|
|
|
|
|
const YandexTranslationURL = 'https://translate.yandex.net/api/v1.5/tr.json/translate';
|
|
|
|
const YandexTranslationKey = 'trnsl.1.1.20150415T044616Z.24814c314120d022.0a339e2bc2d2337461a98d5ec9863fc46e42735e';
|
|
|
|
const YandexLanguages = {
|
2016-05-07 03:40:01 +08:00
|
|
|
English: 'en',
|
|
|
|
Spanish: 'es',
|
|
|
|
Russian: 'ru',
|
|
|
|
Chinese: 'zh',
|
|
|
|
French: 'fr',
|
|
|
|
German: 'de',
|
|
|
|
Italian: 'it',
|
|
|
|
Japanese: 'ja',
|
|
|
|
Portuguese: 'pt',
|
|
|
|
Korean: 'ko',
|
2016-03-01 10:47:22 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
class TranslateButton extends React.Component {
|
|
|
|
|
|
|
|
// Adding a `displayName` makes debugging React easier
|
2016-03-10 02:01:18 +08:00
|
|
|
static displayName = 'TranslateButton';
|
2016-03-01 10:47:22 +08:00
|
|
|
|
|
|
|
// Since our button is being injected into the Composer Footer,
|
|
|
|
// we receive the local id of the current draft as a `prop` (a read-only
|
|
|
|
// property). Since our code depends on this prop, we mark it as a requirement.
|
|
|
|
static propTypes = {
|
refactor(composer): Make session, draft available everywhere
Summary:
Up until now, we've been requiring that every plugin control in the composer take the draftClientId, retreive the session, listen to it, build state from the draft, etc. This is a huge pain and is hard to explain to newcomers becaus it frankly makes no sense.
In 0.3.45 we made it so that the ComposerView always has a non-null draft and session. (It isn't rendered until they're available). In this diff, I just pass those through to all the plugins and remove all the session retrieval cruft.
Almost none of the buttons have state of their own, which I think is appropriate.
They do render on every keystroke, but they were already running code (to recompute their state) on each keystroke and profiling suggests this has no impact.
Prepare for immutable
In preparation for Immutable models, make the draft store proxy returns a !== draft if any changes have been made. This means you can safely know that a draft has changed if `props.draft !== nextProps.draft`
Test Plan: Run tests
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2902
2016-04-20 07:05:15 +08:00
|
|
|
draft: React.PropTypes.object.isRequired,
|
|
|
|
session: React.PropTypes.object.isRequired,
|
2016-03-10 02:01:18 +08:00
|
|
|
};
|
2016-03-01 10:47:22 +08:00
|
|
|
|
refactor(composer): Make session, draft available everywhere
Summary:
Up until now, we've been requiring that every plugin control in the composer take the draftClientId, retreive the session, listen to it, build state from the draft, etc. This is a huge pain and is hard to explain to newcomers becaus it frankly makes no sense.
In 0.3.45 we made it so that the ComposerView always has a non-null draft and session. (It isn't rendered until they're available). In this diff, I just pass those through to all the plugins and remove all the session retrieval cruft.
Almost none of the buttons have state of their own, which I think is appropriate.
They do render on every keystroke, but they were already running code (to recompute their state) on each keystroke and profiling suggests this has no impact.
Prepare for immutable
In preparation for Immutable models, make the draft store proxy returns a !== draft if any changes have been made. This means you can safely know that a draft has changed if `props.draft !== nextProps.draft`
Test Plan: Run tests
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2902
2016-04-20 07:05:15 +08:00
|
|
|
shouldComponentUpdate(nextProps) {
|
|
|
|
// Our render method doesn't use the provided `draft`, and the draft changes
|
|
|
|
// constantly (on every keystroke!) `shouldComponentUpdate` helps keep N1 fast.
|
|
|
|
return nextProps.session !== this.props.session;
|
|
|
|
}
|
|
|
|
|
2016-03-01 10:47:22 +08:00
|
|
|
_onError(error) {
|
2016-03-10 02:01:18 +08:00
|
|
|
Actions.closePopover()
|
2016-04-14 06:35:01 +08:00
|
|
|
const dialog = require('electron').remote.dialog;
|
2016-03-01 10:47:22 +08:00
|
|
|
dialog.showErrorBox('Language Conversion Failed', error.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
_onTranslate = (lang) => {
|
2016-03-10 02:01:18 +08:00
|
|
|
Actions.closePopover()
|
2016-03-01 10:47:22 +08:00
|
|
|
|
|
|
|
// Obtain the session for the current draft. The draft session provides us
|
|
|
|
// the draft object and also manages saving changes to the local cache and
|
|
|
|
// Nilas API as multiple parts of the application touch the draft.
|
refactor(composer): Make session, draft available everywhere
Summary:
Up until now, we've been requiring that every plugin control in the composer take the draftClientId, retreive the session, listen to it, build state from the draft, etc. This is a huge pain and is hard to explain to newcomers becaus it frankly makes no sense.
In 0.3.45 we made it so that the ComposerView always has a non-null draft and session. (It isn't rendered until they're available). In this diff, I just pass those through to all the plugins and remove all the session retrieval cruft.
Almost none of the buttons have state of their own, which I think is appropriate.
They do render on every keystroke, but they were already running code (to recompute their state) on each keystroke and profiling suggests this has no impact.
Prepare for immutable
In preparation for Immutable models, make the draft store proxy returns a !== draft if any changes have been made. This means you can safely know that a draft has changed if `props.draft !== nextProps.draft`
Test Plan: Run tests
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2902
2016-04-20 07:05:15 +08:00
|
|
|
const draftHtml = this.props.draft.body;
|
|
|
|
const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml);
|
|
|
|
|
2016-06-08 03:53:05 +08:00
|
|
|
Actions.recordUserEvent("Email Translated", {
|
|
|
|
language: YandexLanguages[lang],
|
|
|
|
})
|
|
|
|
|
refactor(composer): Make session, draft available everywhere
Summary:
Up until now, we've been requiring that every plugin control in the composer take the draftClientId, retreive the session, listen to it, build state from the draft, etc. This is a huge pain and is hard to explain to newcomers becaus it frankly makes no sense.
In 0.3.45 we made it so that the ComposerView always has a non-null draft and session. (It isn't rendered until they're available). In this diff, I just pass those through to all the plugins and remove all the session retrieval cruft.
Almost none of the buttons have state of their own, which I think is appropriate.
They do render on every keystroke, but they were already running code (to recompute their state) on each keystroke and profiling suggests this has no impact.
Prepare for immutable
In preparation for Immutable models, make the draft store proxy returns a !== draft if any changes have been made. This means you can safely know that a draft has changed if `props.draft !== nextProps.draft`
Test Plan: Run tests
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2902
2016-04-20 07:05:15 +08:00
|
|
|
const query = {
|
|
|
|
key: YandexTranslationKey,
|
|
|
|
lang: YandexLanguages[lang],
|
|
|
|
text: text,
|
|
|
|
format: 'html',
|
|
|
|
};
|
|
|
|
|
|
|
|
// Use Node's `request` library to perform the translation using the Yandex API.
|
2016-05-06 13:30:34 +08:00
|
|
|
request({url: YandexTranslationURL, qs: query}, (error, resp, data) => {
|
refactor(composer): Make session, draft available everywhere
Summary:
Up until now, we've been requiring that every plugin control in the composer take the draftClientId, retreive the session, listen to it, build state from the draft, etc. This is a huge pain and is hard to explain to newcomers becaus it frankly makes no sense.
In 0.3.45 we made it so that the ComposerView always has a non-null draft and session. (It isn't rendered until they're available). In this diff, I just pass those through to all the plugins and remove all the session retrieval cruft.
Almost none of the buttons have state of their own, which I think is appropriate.
They do render on every keystroke, but they were already running code (to recompute their state) on each keystroke and profiling suggests this has no impact.
Prepare for immutable
In preparation for Immutable models, make the draft store proxy returns a !== draft if any changes have been made. This means you can safely know that a draft has changed if `props.draft !== nextProps.draft`
Test Plan: Run tests
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2902
2016-04-20 07:05:15 +08:00
|
|
|
if (resp.statusCode !== 200) {
|
|
|
|
this._onError(error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const json = JSON.parse(data);
|
|
|
|
let translated = json.text.join('');
|
|
|
|
|
|
|
|
// The new text of the draft is our translated response, plus any quoted text
|
|
|
|
// that we didn't process.
|
|
|
|
translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml);
|
|
|
|
|
|
|
|
// To update the draft, we add the new body to it's session. The session object
|
|
|
|
// automatically marshalls changes to the database and ensures that others accessing
|
|
|
|
// the same draft are notified of changes.
|
|
|
|
this.props.session.changes.add({body: translated});
|
|
|
|
this.props.session.changes.commit();
|
2016-03-01 10:47:22 +08:00
|
|
|
});
|
2016-03-10 02:01:18 +08:00
|
|
|
};
|
|
|
|
|
2016-05-06 13:30:34 +08:00
|
|
|
_onClickTranslateButton = () => {
|
2016-03-29 16:41:24 +08:00
|
|
|
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()
|
2016-03-10 02:01:18 +08:00
|
|
|
Actions.openPopover(
|
|
|
|
this._renderPopover(),
|
|
|
|
{originRect: buttonRect, direction: 'up'}
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
// Helper method that will render the contents of our popover.
|
|
|
|
_renderPopover() {
|
|
|
|
const headerComponents = [
|
|
|
|
<span>Translate:</span>,
|
|
|
|
];
|
|
|
|
return (
|
|
|
|
<Menu
|
|
|
|
className="translate-language-picker"
|
2016-05-07 07:24:40 +08:00
|
|
|
items={Object.keys(YandexLanguages)}
|
|
|
|
itemKey={(item) => item}
|
|
|
|
itemContent={(item) => item}
|
2016-03-10 02:01:18 +08:00
|
|
|
headerComponents={headerComponents}
|
|
|
|
defaultSelectedIndex={-1}
|
|
|
|
onSelect={this._onTranslate}
|
|
|
|
/>
|
|
|
|
)
|
2016-03-01 10:47:22 +08:00
|
|
|
}
|
|
|
|
|
2016-03-10 02:01:18 +08:00
|
|
|
// The `render` method returns a React Virtual DOM element. This code looks
|
|
|
|
// like HTML, but don't be fooled. The JSX preprocessor converts
|
|
|
|
// `<a href="http://facebook.github.io/react/">Hello!</a>`
|
|
|
|
// into Javascript objects which describe the HTML you want:
|
|
|
|
// `React.createElement('a', {href: 'http://facebook.github.io/react/'}, 'Hello!')`
|
|
|
|
|
|
|
|
// We're rendering a `Menu` inside our Popover, and using a `RetinaImg` for the button.
|
|
|
|
// These components are part of N1's standard `nylas-component-kit` library,
|
|
|
|
// and make it easy to build interfaces that match the rest of N1's UI.
|
|
|
|
//
|
|
|
|
// For example, using the `RetinaImg` component makes it easy to display an
|
|
|
|
// image from our package. `RetinaImg` will automatically chose the best image
|
|
|
|
// format for our display.
|
|
|
|
render() {
|
2016-03-01 10:47:22 +08:00
|
|
|
return (
|
|
|
|
<button
|
fix(focus): Remove focusedField in favor of imperative focus, break apart ComposerView
Summary:
- Removes controlled focus in the composer!
- No React components ever perfom focus in lifecycle methods. Never again.
- A new `Utils.schedule({action, after, timeout})` helper makes it easy to say "setState or load draft, etc. and then focus"
- The DraftStore issues a focusDraft action after creating a draft, which causes the MessageList to focus and scroll to the desired composer, which itself decides which field to focus.
- The MessageList never focuses anything automatically.
- Refactors ComposerView apart — ComposerHeader handles all top fields, DraftSessionContainer handles draft session initialization and exposes props to ComposerView
- ComposerHeader now uses a KeyCommandRegion (with focusIn and focusOut) to do the expanding and collapsing of the participants fields. May rename that container very soon.
- Removes all CommandRegistry handling of tab and shift-tab. Unless you preventDefault, the browser does it's thing.
- Removes all tabIndexes greater than 1. This is an anti-pattern—assigning everything a tabIndex of 0 tells the browser to move between them based on their order in the DOM, and is almost always what you want.
- Adds "TabGroupRegion" which allows you to create a tab/shift-tabbing group, (so tabbing does not leave the active composer). Can't believe this isn't a browser feature.
Todos:
- Occasionally, clicking out of the composer contenteditable requires two clicks. This is because atomicEdit is restoring selection within the contenteditable and breaking blur.
- Because the ComposerView does not render until it has a draft, we're back to it being white in popout composers for a brief moment. We will fix this another way - all the "return unless draft" statements were untenable.
- Clicking a row in the thread list no longer shifts focus to the message list and focuses the last draft. This will be restored soon.
Test Plan: Broken
Reviewers: juan, evan
Reviewed By: juan, evan
Differential Revision: https://phab.nylas.com/D2814
2016-04-05 06:22:01 +08:00
|
|
|
tabIndex={-1}
|
2016-09-01 00:56:09 +08:00
|
|
|
className="btn btn-toolbar btn-translate pull-right"
|
2016-03-10 02:01:18 +08:00
|
|
|
onClick={this._onClickTranslateButton}
|
2016-05-07 07:24:40 +08:00
|
|
|
title="Translate email body…"
|
|
|
|
>
|
2016-03-01 10:47:22 +08:00
|
|
|
<RetinaImg
|
|
|
|
mode={RetinaImg.Mode.ContentIsMask}
|
2016-05-07 07:24:40 +08:00
|
|
|
url="nylas://composer-translate/assets/icon-composer-translate@2x.png"
|
|
|
|
/>
|
2016-03-01 10:47:22 +08:00
|
|
|
|
|
|
|
<RetinaImg
|
|
|
|
name="icon-composer-dropdown.png"
|
2016-05-07 07:24:40 +08:00
|
|
|
mode={RetinaImg.Mode.ContentIsMask}
|
|
|
|
/>
|
2016-03-01 10:47:22 +08:00
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
All packages must export a basic object that has at least the following 3
|
|
|
|
methods:
|
|
|
|
|
|
|
|
1. `activate` - Actions to take once the package gets turned on.
|
|
|
|
Pre-enabled packages get activated on N1 bootup. They can also be
|
|
|
|
activated manually by a user.
|
|
|
|
|
|
|
|
2. `deactivate` - Actions to take when a package gets turned off. This can
|
|
|
|
happen when a user manually disables a package.
|
|
|
|
|
|
|
|
3. `serialize` - A simple serializable object that gets saved to disk
|
|
|
|
before N1 quits. This gets passed back into `activate` next time N1 boots
|
|
|
|
up or your package is manually activated.
|
|
|
|
*/
|
|
|
|
|
|
|
|
export function activate() {
|
|
|
|
ComponentRegistry.register(TranslateButton, {
|
|
|
|
role: 'Composer:ActionButton',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function serialize() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export function deactivate() {
|
|
|
|
ComponentRegistry.unregister(TranslateButton);
|
|
|
|
}
|