Introduce React, TypeScript, TSLint and React-StyleGuidist (#2219)

Quite a bit of change here.

First, the basics:

- New dependencies were added: react, typescript, tslint, and react-styleguidist
- A new npm script: transpile. It uses typescript to process .tsx files in js/react, putting .js files next to the original file. It's part of the watch functionality of grunt dev as well as the default task run with just grunt (used to build the app prior to release). A lighter-weight to get watch behavior when just working on React components is to run yarn transpile --watch.
- yarn run clean-transpile will remove generated .js files


Style guide via react-styleguidist. Example site: https://react-styleguidist.js.org/examples/basic/

- Start with yarn styleguide
- Component.md files right next to the .tsx file
- jsdoc-style comments are picked up and added to the generated part of the styleguide - the overall summary and a table listing methods and properties of the component
- It has hot-reloading!
- It uses webpack, which means that our app now pulls in webpack though we don't use it to generate anything for the production app.
- I did a bunch of work to enable the use of Backbone views in this context, which will allow us to move smoothly from the old world to the new. First, add all the permutations in the old way, and then slowly start to re-render those same views with React.

A bit of dependency cleanup to enable use in React components:

- moment was moved from our Bower dependencies to our npm dependencies, so it can be used in React components not running in a browser window.
- i18n was moved into the new commonjs format, so it can be used in React components even if window is not available.

Lastly, a bit of Gruntfile cleanup:

- Removal of Chrome App-era modifications of background.js
- Make jshint/jscs watch more targeted, since more and more we'll be using other tools
This commit is contained in:
Scott Nonnenberg 2018-04-06 08:13:00 -07:00 committed by GitHub
commit c6c3b65bbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 4611 additions and 12138 deletions

View file

@ -1,29 +0,0 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict';
// preload.js loads this, pulling it from main.js (where it was loaded from disk)
var messages = window.config.localeMessages;
var locale = window.config.locale;
window.i18n = function (message, substitutions) {
if (!messages[message]) {
return;
}
var s = messages[message].message;
if (substitutions instanceof Array) {
substitutions.forEach(function(sub) {
s = s.replace(/\$.+?\$/, sub);
});
} else if (substitutions) {
s = s.replace(/\$.+?\$/, substitutions);
}
return s;
};
i18n.getLocale = function() {
return locale;
};
})();

34
js/modules/i18n.js Normal file
View file

@ -0,0 +1,34 @@
/* eslint-env node */
exports.setup = (locale, messages) => {
if (!locale) {
throw new Error('i18n: locale parameter is required');
}
if (!messages) {
throw new Error('i18n: messages parameter is required');
}
function getMessage(key, substitutions) {
const entry = messages[key];
if (!entry) {
console.error(`i18n: Attempted to get translation for nonexistent key '${key}'`);
return '';
}
const { message } = entry;
if (Array.isArray(substitutions)) {
return substitutions.reduce(
(result, substitution) => result.replace(/\$.+?\$/, substitution),
message
);
} else if (substitutions) {
return message.replace(/\$.+?\$/, substitutions);
}
return message;
}
getMessage.getLocale = () => locale;
return getMessage;
};

View file

@ -0,0 +1,47 @@
/* global Backbone: false */
// Additional globals used:
// window.React
// window.ReactDOM
// window.i18n
// eslint-disable-next-line func-names
(function () {
'use strict';
window.Whisper = window.Whisper || {};
window.Whisper.ReactWrapper = Backbone.View.extend({
className: 'react-wrapper',
initialize(options) {
const { Component, props, onClose } = options;
this.render();
this.Component = Component;
this.onClose = onClose;
this.update(props);
},
update(props) {
const updatedProps = this.augmentProps(props);
const element = window.React.createElement(this.Component, updatedProps);
window.ReactDOM.render(element, this.el);
},
augmentProps(props) {
return Object.assign({}, props, {
close: () => {
if (this.onClose) {
this.onClose();
return;
}
this.remove();
},
i18n: window.i18n,
});
},
remove() {
window.ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
},
});
}());

View file

@ -5,15 +5,6 @@
'use strict';
window.Whisper = window.Whisper || {};
moment.updateLocale(i18n.getLocale(), {
relativeTime : {
s: i18n('timestamp_s') || 'now',
m: i18n('timestamp_m') || '1 minute',
h: i18n('timestamp_h') || '1 hour'
}
});
moment.locale(i18n.getLocale());
Whisper.TimestampView = Whisper.View.extend({
initialize: function(options) {
extension.windows.onClosed(this.clearTimeout.bind(this));