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:
commit
c6c3b65bbc
31 changed files with 4611 additions and 12138 deletions
|
@ -7,13 +7,15 @@ libtextsecure/**
|
|||
# these aren't ready yet, pulling files in one-by-one
|
||||
js/*.js
|
||||
js/models/**/*.js
|
||||
js/react/**/*.js
|
||||
js/views/**/*.js
|
||||
test/*.js
|
||||
test/models/*.js
|
||||
test/views/*.js
|
||||
/*.js
|
||||
|
||||
# typescript-generated files
|
||||
ts/**/*.js
|
||||
|
||||
# ES2015+ files
|
||||
!js/background.js
|
||||
!js/backup.js
|
||||
|
@ -22,6 +24,7 @@ test/views/*.js
|
|||
!js/models/conversations.js
|
||||
!js/views/attachment_view.js
|
||||
!js/views/conversation_search_view.js
|
||||
!js/views/backbone_wrapper_view.js
|
||||
!js/views/debug_log_view.js
|
||||
!js/views/file_input_view.js
|
||||
!js/views/inbox_view.js
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,3 +22,6 @@ js/libtextsecure.js
|
|||
stylesheets/*.css
|
||||
test/test.js
|
||||
libtextsecure/test/test.js
|
||||
|
||||
# React / TypeScript
|
||||
ts/**/*.js
|
||||
|
|
|
@ -13,7 +13,6 @@ assets
|
|||
|
||||
# examples
|
||||
example
|
||||
examples
|
||||
|
||||
# code coverage directories
|
||||
coverage
|
||||
|
|
61
Gruntfile.js
61
Gruntfile.js
|
@ -98,19 +98,19 @@ module.exports = function(grunt) {
|
|||
'Gruntfile.js',
|
||||
'js/**/*.js',
|
||||
'!js/background.js',
|
||||
'!js/jquery.js',
|
||||
'!js/libtextsecure.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/logging.js',
|
||||
'!js/backup.js',
|
||||
'!js/components.js',
|
||||
'!js/database.js',
|
||||
'!js/jquery.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/libtextsecure.js',
|
||||
'!js/logging.js',
|
||||
'!js/modules/**/*.js',
|
||||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/signal_protocol_store.js',
|
||||
'!js/views/conversation_search_view.js',
|
||||
'!js/views/debug_log_view.js',
|
||||
'!js/signal_protocol_store.js',
|
||||
'!js/database.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'_locales/**/*'
|
||||
],
|
||||
options: { jshintrc: '.jshintrc' },
|
||||
|
@ -149,25 +149,6 @@ module.exports = function(grunt) {
|
|||
},
|
||||
src: {
|
||||
files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }],
|
||||
options: {
|
||||
process: function(content, srcpath) {
|
||||
if (srcpath.match('background.js')) {
|
||||
return content.replace(
|
||||
/textsecure-service-staging.whispersystems.org/g,
|
||||
'textsecure-service-ca.whispersystems.org');
|
||||
} else if (srcpath.match('expire.js')) {
|
||||
var gitinfo = grunt.config.get('gitinfo');
|
||||
var commited = gitinfo.local.branch.current.lastCommitTime;
|
||||
var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
|
||||
return content.replace(
|
||||
/var BUILD_EXPIRATION = 0/,
|
||||
"var BUILD_EXPIRATION = " + time
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jscs: {
|
||||
|
@ -175,12 +156,12 @@ module.exports = function(grunt) {
|
|||
src: [
|
||||
'Gruntfile',
|
||||
'js/**/*.js',
|
||||
'!js/libtextsecure.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/libtextsecure.js',
|
||||
'!js/modules/**/*.js',
|
||||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'test/**/*.js',
|
||||
'!test/blanket_mocha.js',
|
||||
'!test/modules/**/*.js',
|
||||
|
@ -202,17 +183,24 @@ module.exports = function(grunt) {
|
|||
tasks: ['copy_dist']
|
||||
},
|
||||
scripts: {
|
||||
files: ['<%= jshint.files %>', './js/**/*.js'],
|
||||
files: ['<%= jshint.files %>'],
|
||||
tasks: ['jshint']
|
||||
},
|
||||
style: {
|
||||
files: ['<%= jscs.all.src %>', './js/**/*.js'],
|
||||
files: ['<%= jscs.all.src %>'],
|
||||
tasks: ['jscs']
|
||||
},
|
||||
transpile: {
|
||||
files: ['./ts/**/*.js'],
|
||||
tasks: ['exec:transpile']
|
||||
}
|
||||
},
|
||||
exec: {
|
||||
'tx-pull': {
|
||||
cmd: 'tx pull'
|
||||
},
|
||||
'transpile': {
|
||||
cmd: 'npm run transpile',
|
||||
}
|
||||
},
|
||||
'test-release': {
|
||||
|
@ -496,5 +484,8 @@ module.exports = function(grunt) {
|
|||
grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']);
|
||||
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
|
||||
grunt.registerTask('prep-release', ['gitinfo', 'clean-release', 'fetch-release']);
|
||||
grunt.registerTask('default', ['concat', 'copy:deps', 'sass', 'date']);
|
||||
grunt.registerTask(
|
||||
'default',
|
||||
['concat', 'copy:deps', 'sass', 'date', 'exec:transpile']
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
<title>Signal</title>
|
||||
<link href='images/icon_128.png' rel='shortcut icon'>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<!-- When making changes to these templates, be sure to update these two places:
|
||||
1) test/styleguide/legacy_templates.js
|
||||
2) test/index.html
|
||||
-->
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='app-migration-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/icon_128.png'>
|
||||
|
@ -909,8 +915,8 @@
|
|||
<script type='text/javascript' src='js/expire.js'></script>
|
||||
<script type='text/javascript' src='js/conversation_controller.js'></script>
|
||||
<script type='text/javascript' src='js/emoji_util.js'></script>
|
||||
<script type='text/javascript' src='js/i18n.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/views/backbone_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"protobuf": "~3.8.0",
|
||||
"mustache": "~0.8.2",
|
||||
"qrcode": "https://github.com/davidshimjs/qrcodejs.git#1c78ccd71",
|
||||
"moment": "~2.14.0",
|
||||
"indexeddb-backbonejs-adapter": "*",
|
||||
"intl-tel-input": "~4.0.1",
|
||||
"blueimp-load-image": "~1.13.0",
|
||||
|
@ -57,9 +56,6 @@
|
|||
"chai": [
|
||||
"chai.js"
|
||||
],
|
||||
"moment": [
|
||||
"min/moment-with-locales.js"
|
||||
],
|
||||
"indexeddb-backbonejs-adapter": [
|
||||
"backbone-indexeddb.js"
|
||||
],
|
||||
|
|
File diff suppressed because it is too large
Load diff
BIN
fixtures/incompetech-com-Agnus-Dei-X.mp3
Normal file
BIN
fixtures/incompetech-com-Agnus-Dei-X.mp3
Normal file
Binary file not shown.
29
js/i18n.js
29
js/i18n.js
|
@ -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
34
js/modules/i18n.js
Normal 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;
|
||||
};
|
47
js/views/backbone_wrapper_view.js
Normal file
47
js/views/backbone_wrapper_view.js
Normal 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);
|
||||
},
|
||||
});
|
||||
}());
|
|
@ -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));
|
||||
|
|
26
package.json
26
package.json
|
@ -12,7 +12,7 @@
|
|||
"main": "main.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||
"test": "yarn eslint && yarn test-server && grunt test && yarn test-app && yarn test-modules",
|
||||
"test": "yarn eslint && yarn tslint && yarn test-server && grunt test && yarn test-app && yarn test-modules",
|
||||
"lint": "grunt jshint",
|
||||
"start": "electron .",
|
||||
"asarl": "asar l release/mac/Signal.app/Contents/Resources/app.asar",
|
||||
|
@ -41,7 +41,11 @@
|
|||
"test-server": "mocha --recursive test/server",
|
||||
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
||||
"eslint": "eslint .",
|
||||
"open-coverage": "open coverage/lcov-report/index.html"
|
||||
"tslint": "tslint ts/**/*.{ts,tsx}",
|
||||
"transpile": "tsc",
|
||||
"clean-transpile": "rimraf ts/**/*.js ts/*.js",
|
||||
"open-coverage": "open coverage/lcov-report/index.html",
|
||||
"styleguide": "styleguidist server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^0.8.0",
|
||||
|
@ -50,6 +54,7 @@
|
|||
"blueimp-canvas-to-blob": "^3.14.0",
|
||||
"blueimp-load-image": "^2.18.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"classnames": "^2.2.5",
|
||||
"config": "^1.28.1",
|
||||
"decompress": "^4.2.0",
|
||||
"electron-config": "^1.0.0",
|
||||
|
@ -68,11 +73,14 @@
|
|||
"got": "^8.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.21.0",
|
||||
"node-fetch": "https://github.com/scottnonnenberg/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
|
||||
"node-notifier": "^5.1.2",
|
||||
"os-locale": "^2.1.0",
|
||||
"pify": "^3.0.0",
|
||||
"proxy-agent": "^2.1.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"read-last-lines": "^1.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
|
@ -83,6 +91,10 @@
|
|||
"websocket": "^1.0.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "^6.5.1",
|
||||
"@types/react": "^16.3.1",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"arraybuffer-loader": "^1.0.3",
|
||||
"asar": "^0.14.0",
|
||||
"bower": "^1.8.2",
|
||||
"chai": "^4.1.2",
|
||||
|
@ -111,8 +123,16 @@
|
|||
"node-sass-import-once": "^1.2.0",
|
||||
"nsp": "^3.2.1",
|
||||
"nyc": "^11.4.1",
|
||||
"qs": "^6.5.1",
|
||||
"react-docgen-typescript": "^1.2.6",
|
||||
"react-styleguidist": "^7.0.1",
|
||||
"sinon": "^4.4.2",
|
||||
"spectron": "^3.8.0"
|
||||
"spectron": "^3.8.0",
|
||||
"ts-loader": "^4.1.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-react": "^3.5.1",
|
||||
"typescript": "^2.8.1",
|
||||
"webpack": "^4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.2.1"
|
||||
|
|
26
preload.js
26
preload.js
|
@ -109,6 +109,30 @@ window.nodeFetch = require('node-fetch');
|
|||
window.nodeNotifier = require('node-notifier');
|
||||
window.ProxyAgent = require('proxy-agent');
|
||||
|
||||
// Note: when modifying this file, consider whether our React Components or Backbone Views
|
||||
// will need these things to render in the Style Guide. If so, go update one of these
|
||||
// two locations:
|
||||
//
|
||||
// 1) test/styleguide/legacy_bridge.js
|
||||
// 2) ts/test/StyleGuideUtil.js
|
||||
|
||||
window.React = require('react');
|
||||
window.ReactDOM = require('react-dom');
|
||||
window.moment = require('moment');
|
||||
|
||||
const { setup } = require('./js/modules/i18n');
|
||||
|
||||
const { locale, localeMessages } = window.config;
|
||||
window.i18n = setup(locale, localeMessages);
|
||||
window.moment.updateLocale(locale, {
|
||||
relativeTime: {
|
||||
s: window.i18n('timestamp_s'),
|
||||
m: window.i18n('timestamp_m'),
|
||||
h: window.i18n('timestamp_h'),
|
||||
},
|
||||
});
|
||||
window.moment.locale(locale);
|
||||
|
||||
// ES2015+ modules
|
||||
const attachmentsPath = Attachments.getPath(app.getPath('userData'));
|
||||
const deleteAttachmentData = Attachments.createDeleter(attachmentsPath);
|
||||
|
@ -136,6 +160,8 @@ window.Signal.Database = require('./js/modules/database');
|
|||
window.Signal.Debug = require('./js/modules/debug');
|
||||
window.Signal.Logs = require('./js/modules/logs');
|
||||
|
||||
window.Signal.Components = {};
|
||||
|
||||
window.Signal.Migrations = {};
|
||||
window.Signal.Migrations.deleteAttachmentData =
|
||||
Attachment.deleteData(deleteAttachmentData);
|
||||
|
|
163
styleguide.config.js
Normal file
163
styleguide.config.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const typescriptSupport = require('react-docgen-typescript');
|
||||
|
||||
|
||||
const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse;
|
||||
|
||||
module.exports = {
|
||||
sections: [
|
||||
{
|
||||
name: 'Conversation',
|
||||
description: 'Everything necessary to render a conversation',
|
||||
components: 'ts/components/conversation/*.tsx',
|
||||
},
|
||||
{
|
||||
name: 'Utility',
|
||||
description: 'Utility components used across the application',
|
||||
components: 'ts/components/utility/*.tsx',
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
description: 'Components only used for testing',
|
||||
components: 'ts/test/**/*.tsx',
|
||||
},
|
||||
],
|
||||
context: {
|
||||
// Exposes necessary utilities in the global scope for all readme code snippets
|
||||
util: 'ts/test/StyleGuideUtil',
|
||||
},
|
||||
// We don't want one long, single page
|
||||
pagePerSection: true,
|
||||
// Expose entire repository to the styleguidist server, primarily for stylesheets
|
||||
assetsDir: './',
|
||||
// Add top-level elements to the HTML:
|
||||
// docs: https://github.com/vxna/mini-html-webpack-template
|
||||
// https://react-styleguidist.js.org/docs/configuration.html#template
|
||||
template: {
|
||||
head: {
|
||||
links: [{
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: '/stylesheets/manifest.css',
|
||||
}],
|
||||
},
|
||||
body: {
|
||||
// Brings in all the necessary components to boostrap Backbone views
|
||||
// Mirrors the order used in background.js.
|
||||
scripts: [
|
||||
{
|
||||
src: 'test/styleguide/legacy_bridge.js',
|
||||
},
|
||||
{
|
||||
src: 'node_modules/moment/min/moment-with-locales.min.js',
|
||||
},
|
||||
{
|
||||
src: 'js/components.js',
|
||||
},
|
||||
{
|
||||
src: 'js/reliable_trigger.js',
|
||||
},
|
||||
{
|
||||
src: 'js/database.js',
|
||||
},
|
||||
{
|
||||
src: 'js/storage.js',
|
||||
},
|
||||
{
|
||||
src: 'js/signal_protocol_store.js',
|
||||
},
|
||||
{
|
||||
src: 'js/libtextsecure.js',
|
||||
},
|
||||
{
|
||||
src: 'js/focus_listener.js',
|
||||
},
|
||||
{
|
||||
src: 'js/notifications.js',
|
||||
},
|
||||
{
|
||||
src: 'js/delivery_receipts.js',
|
||||
},
|
||||
{
|
||||
src: 'js/read_receipts.js',
|
||||
},
|
||||
{
|
||||
src: 'js/read_syncs.js',
|
||||
},
|
||||
{
|
||||
src: 'js/libphonenumber-util.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/messages.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/conversations.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/blockedNumbers.js',
|
||||
},
|
||||
{
|
||||
src: 'js/expiring_messages.js',
|
||||
},
|
||||
|
||||
{
|
||||
src: 'js/chromium.js',
|
||||
},
|
||||
{
|
||||
src: 'js/registration.js',
|
||||
},
|
||||
{
|
||||
src: 'js/expire.js',
|
||||
},
|
||||
{
|
||||
src: 'js/conversation_controller.js',
|
||||
},
|
||||
{
|
||||
src: 'js/emoji_util.js',
|
||||
},
|
||||
// Select Backbone views
|
||||
{
|
||||
src: 'js/views/whisper_view.js',
|
||||
},
|
||||
{
|
||||
src: 'js/views/timestamp_view.js',
|
||||
},
|
||||
{
|
||||
src: 'js/views/message_view.js',
|
||||
},
|
||||
// Hacky way of including templates for Backbone components
|
||||
{
|
||||
src: 'test/styleguide/legacy_templates.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
propsParser,
|
||||
webpackConfig: {
|
||||
devtool: 'source-map',
|
||||
|
||||
resolve: {
|
||||
// Necessary to enable the absolute path used in the context option above
|
||||
modules: [
|
||||
__dirname,
|
||||
path.join(__dirname, 'node_modules'),
|
||||
],
|
||||
extensions: ['.tsx'],
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader'
|
||||
},
|
||||
{
|
||||
// To test handling of attachments, we need arraybuffers in memory
|
||||
test: /\.(gif|mp3|mp4|txt)$/,
|
||||
loader: 'arraybuffer-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
describe('i18n', function() {
|
||||
describe('i18n', function() {
|
||||
it('returns undefined for unknown string', function() {
|
||||
assert.strictEqual(i18n('random'), undefined);
|
||||
it('returns empty string for unknown string', function() {
|
||||
assert.strictEqual(i18n('random'), '');
|
||||
});
|
||||
it('returns message for given string', function() {
|
||||
assert.equal(i18n('reportIssue'), 'Report an issue');
|
||||
|
|
|
@ -562,8 +562,6 @@
|
|||
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
|
||||
<script type='text/javascript' src='../js/i18n.js'></script>
|
||||
|
||||
<script type='text/javascript' src='../js/registration.js' data-cover></script>
|
||||
<script type="text/javascript" src="../js/expire.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
|
@ -585,6 +583,7 @@
|
|||
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
|
||||
<script type='text/javascript' src='../js/views/backbone_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/debug_log_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
||||
|
|
45
test/styleguide/legacy_bridge.js
Normal file
45
test/styleguide/legacy_bridge.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* global window: false */
|
||||
|
||||
// Because we aren't hosting the Style Guide in Electron, we can't rely on preload.js
|
||||
// to set things up for us. This gives us the minimum bar shims for everything it
|
||||
// provdes.
|
||||
//
|
||||
// Remember, the idea here is just to enable visual testing, no full functionality. Most
|
||||
// of thise can be very simple.
|
||||
|
||||
window.PROTO_ROOT = '/protos';
|
||||
window.nodeSetImmediate = () => {};
|
||||
|
||||
window.Signal = {};
|
||||
window.Signal.Backup = {};
|
||||
window.Signal.Crypto = {};
|
||||
window.Signal.Logs = {};
|
||||
window.Signal.Migrations = {
|
||||
getPlaceholderMigrations: () => {},
|
||||
};
|
||||
|
||||
window.Signal.Components = {};
|
||||
|
||||
window.EmojiConvertor = function EmojiConvertor() {};
|
||||
window.EmojiConvertor.prototype.init_colons = () => {};
|
||||
window.EmojiConvertor.prototype.signalReplace = html => html;
|
||||
window.EmojiConvertor.prototype.replace_unified = string => string;
|
||||
window.EmojiConvertor.prototype.img_sets = {
|
||||
apple: {},
|
||||
};
|
||||
|
||||
window.i18n = () => '';
|
||||
|
||||
window.Signal.Migrations.V17 = {};
|
||||
window.Signal.OS = {};
|
||||
window.Signal.Types = {};
|
||||
window.Signal.Types.Attachment = {};
|
||||
window.Signal.Types.Errors = {};
|
||||
window.Signal.Types.Message = {
|
||||
initializeSchemaVersion: attributes => attributes,
|
||||
};
|
||||
window.Signal.Types.MIME = {};
|
||||
window.Signal.Types.Settings = {};
|
||||
window.Signal.Views = {};
|
||||
window.Signal.Views.Initialization = {};
|
||||
window.Signal.Workflow = {};
|
52
test/styleguide/legacy_templates.js
Normal file
52
test/styleguide/legacy_templates.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/* global window: false */
|
||||
|
||||
// Taken from background.html.
|
||||
// Templates are here solely to support the Backbone views rendered in the Style Guide.
|
||||
|
||||
// Note: Any change here must be reflected in background.html to be reflected in the app
|
||||
// and test/index.html to be reflected in the unit tests.
|
||||
|
||||
window.Whisper.View.Templates = {
|
||||
hasRetry: `
|
||||
{{ messageNotSent }} <span href='#' class='retry'>{{ resend }}</span>
|
||||
`,
|
||||
'some-failed': `
|
||||
{{ someFailed }}
|
||||
`,
|
||||
keychange: `
|
||||
<span class='content' dir='auto'>
|
||||
<span class='shield icon'></span> {{ content }}
|
||||
</span>
|
||||
`,
|
||||
'verified-change': `
|
||||
<span class='content' dir='auto'>
|
||||
<span class='{{ icon }} icon'></span> {{ content }}
|
||||
</span>
|
||||
`,
|
||||
message: `
|
||||
{{> avatar }}
|
||||
<div class='bubble {{ avatar.color }}'>
|
||||
<div class='sender' dir='auto'>
|
||||
{{ sender }}
|
||||
{{ #profileName }}
|
||||
<span class='profileName'>{{ profileName }} </span>
|
||||
{{ /profileName }}
|
||||
</div>
|
||||
<div class='attachments'></div>
|
||||
<p class='content' dir='auto'>
|
||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
||||
</p>
|
||||
<div class='meta'>
|
||||
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||
<span class='status hide'></span>
|
||||
<span class='timer'></span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
hourglass: `
|
||||
<span class='hourglass'><span class='sand'></span></span>
|
||||
`,
|
||||
expirationTimerUpdate: `
|
||||
<span class='content'><span class='icon clock'></span> {{ content }}</span>
|
||||
`,
|
||||
};
|
6
ts/components/conversation/Message.md
Normal file
6
ts/components/conversation/Message.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<Message />
|
||||
</util.ConversationContext>
|
||||
```
|
36
ts/components/conversation/Message.tsx
Normal file
36
ts/components/conversation/Message.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
/**
|
||||
* A placeholder Message component, giving the structure of a plain message with none of
|
||||
* the dynamic functionality. We can build off of this going forward.
|
||||
*/
|
||||
export class Message extends React.Component<{}, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<li className="entry outgoing sent delivered">
|
||||
<span className="avatar" />
|
||||
<div className="bubble">
|
||||
<div className="sender" dir="auto" />
|
||||
<div className="attachments" />
|
||||
<p className="content" dir="auto">
|
||||
<span className="body">
|
||||
Hi there. How are you doing? Feeling pretty good? Awesome.
|
||||
</span>
|
||||
</p>
|
||||
<div className="meta">
|
||||
<span
|
||||
className="timestamp"
|
||||
data-timestamp="1522800995425"
|
||||
title="Tue, Apr 3, 2018 5:16 PM"
|
||||
>
|
||||
1 minute ago
|
||||
</span>
|
||||
<span className="status hide" />
|
||||
<span className="timer" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
2
ts/components/conversation/Reply.md
Normal file
2
ts/components/conversation/Reply.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
This is Reply.md.
|
14
ts/components/conversation/Reply.tsx
Normal file
14
ts/components/conversation/Reply.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface Props { name: string; }
|
||||
|
||||
interface State { count: number; }
|
||||
|
||||
export class Reply extends React.Component<Props, State> {
|
||||
public render() {
|
||||
return (
|
||||
<div>Placeholder</div>
|
||||
);
|
||||
}
|
||||
}
|
20
ts/components/utility/BackboneWrapper.md
Normal file
20
ts/components/utility/BackboneWrapper.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
Rendering a real `Whisper.MessageView` using `<util.ConversationContext />` and
|
||||
`<util.BackboneWrapper />`.
|
||||
|
||||
```jsx
|
||||
const model = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: 'text',
|
||||
sent_at: Date.now() - 5000,
|
||||
})
|
||||
const View = Whisper.MessageView;
|
||||
const options = {
|
||||
model,
|
||||
};
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
View={View}
|
||||
options={options}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
69
ts/components/utility/BackboneWrapper.tsx
Normal file
69
ts/components/utility/BackboneWrapper.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
/** The View class, which will be instantiated then treated like a Backbone View */
|
||||
readonly View: BackboneViewConstructor;
|
||||
/** Options to be passed along to the view when constructed */
|
||||
readonly options: object;
|
||||
}
|
||||
|
||||
interface BackboneView {
|
||||
remove: () => void;
|
||||
render: () => void;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
interface BackboneViewConstructor {
|
||||
new (options: object): BackboneView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows Backbone Views to be rendered inside of React (primarily for the Style Guide)
|
||||
* while we slowly replace the internals of a given Backbone view with React.
|
||||
*/
|
||||
export class BackboneWrapper extends React.Component<Props, {}> {
|
||||
protected el: Element | null = null;
|
||||
protected view: BackboneView | null = null;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.teardown();
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
// we're handling all updates manually
|
||||
return false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div ref={this.setEl} />;
|
||||
}
|
||||
|
||||
protected setEl = (element: HTMLDivElement | null) => {
|
||||
this.el = element;
|
||||
this.setup();
|
||||
}
|
||||
|
||||
protected setup = () => {
|
||||
const { el } = this;
|
||||
const { View, options } = this.props;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
this.view = new View(options);
|
||||
this.view.render();
|
||||
|
||||
// It's important to let the view create its own root DOM element. This ensures that
|
||||
// its tagName property actually takes effect.
|
||||
el.appendChild(this.view.el);
|
||||
}
|
||||
|
||||
protected teardown() {
|
||||
if (!this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.remove();
|
||||
this.view = null;
|
||||
}
|
||||
}
|
8
ts/test/ConversationContext.md
Normal file
8
ts/test/ConversationContext.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
The simplest example of using the `<ConversationContext />` component:
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div>Just a plain bit of text</div>
|
||||
</util.ConversationContext>
|
||||
```
|
31
ts/test/ConversationContext.tsx
Normal file
31
ts/test/ConversationContext.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Corresponds to the theme setting in the app, and the class added to the root element.
|
||||
*/
|
||||
theme: 'ios' | 'android' | 'android-dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
|
||||
* apply (with no changes) to messages in the Style Guide.
|
||||
*/
|
||||
export class ConversationContext extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<div className="conversation">
|
||||
<div className="discussion-container" style={{padding: '0.5em'}}>
|
||||
<ul className="message-list">
|
||||
{this.props.children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
84
ts/test/StyleGuideUtil.ts
Normal file
84
ts/test/StyleGuideUtil.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import moment from 'moment';
|
||||
import qs from 'qs';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
|
||||
// Helper components used in the Style Guide, exposed at 'util' in the global scope via
|
||||
// the 'context' option in react-styleguidist.
|
||||
|
||||
export { ConversationContext } from './ConversationContext';
|
||||
export { BackboneWrapper } from '../components/utility/BackboneWrapper';
|
||||
|
||||
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
||||
|
||||
import { Message } from '../components/conversation/Message';
|
||||
import { Reply } from '../components/conversation/Reply';
|
||||
|
||||
|
||||
// TypeScript wants two things when you import:
|
||||
// 1) a normal typescript file
|
||||
// 2) a javascript file with type definitions
|
||||
// Anything else will raise an error, that it can't find the module. And so, we ignore...
|
||||
|
||||
// @ts-ignore
|
||||
import gif from '../../fixtures/giphy-GVNvOUpeYmI7e.gif';
|
||||
// @ts-ignore
|
||||
import mp3 from '../../fixtures/incompetech-com-Agnus-Dei-X.mp3';
|
||||
// @ts-ignore
|
||||
import txt from '../../fixtures/lorem-ipsum.txt';
|
||||
// @ts-ignore
|
||||
import mp4 from '../../fixtures/pixabay-Soap-Bubble-7141.mp4';
|
||||
|
||||
export {
|
||||
mp3,
|
||||
gif,
|
||||
mp4,
|
||||
txt,
|
||||
};
|
||||
|
||||
|
||||
// Required, or TypeScript complains about adding keys to window
|
||||
const parent = window as any;
|
||||
|
||||
const query = window.location.search.replace(/^\?/, '');
|
||||
const urlOptions = qs.parse(query);
|
||||
const theme = urlOptions.theme || 'android';
|
||||
const locale = urlOptions.locale || 'en';
|
||||
|
||||
// @ts-ignore
|
||||
import localeMessages from '../../_locales/en/messages.json';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup } from '../../js/modules/i18n';
|
||||
|
||||
const i18n = setup(locale, localeMessages);
|
||||
|
||||
export {
|
||||
theme,
|
||||
locale,
|
||||
i18n,
|
||||
};
|
||||
|
||||
|
||||
parent.i18n = i18n;
|
||||
parent.moment = moment;
|
||||
|
||||
parent.moment.updateLocale(locale, {
|
||||
relativeTime: {
|
||||
h: parent.i18n('timestamp_h'),
|
||||
m: parent.i18n('timestamp_m'),
|
||||
s: parent.i18n('timestamp_s'),
|
||||
},
|
||||
});
|
||||
parent.moment.locale(locale);
|
||||
|
||||
parent.React = React;
|
||||
parent.ReactDOM = ReactDOM;
|
||||
|
||||
parent.Signal.Components = {
|
||||
Message,
|
||||
Reply,
|
||||
};
|
||||
|
57
tsconfig.json
Normal file
57
tsconfig.json
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./ts", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
14
tslint.json
Normal file
14
tslint.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-react"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"quotemark": [true, "single", "jsx-double", "avoid-template", "avoid-escape"],
|
||||
"no-consecutive-blank-lines": [true, 2],
|
||||
"interface-name": [true, "never-prefix"]
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
Loading…
Add table
Reference in a new issue