Remove jshint - move everything over to eslint

Also removed all hints of previous linters
This commit is contained in:
Scott Nonnenberg 2018-07-06 17:48:14 -07:00
parent dc11db92f9
commit 43a44793c5
71 changed files with 1837 additions and 2030 deletions

View file

@ -1,4 +0,0 @@
{
"directory": "components/",
"analytics": false
}

View file

@ -5,13 +5,9 @@ dist/**
# these aren't ready yet, pulling files in one-by-one # these aren't ready yet, pulling files in one-by-one
libtextsecure/** libtextsecure/**
js/*.js
js/models/**/*.js
js/views/**/*.js
test/*.js test/*.js
test/models/*.js test/models/*.js
test/views/*.js test/views/*.js
/*.js
# Generated files # Generated files
js/components.js js/components.js
@ -24,35 +20,12 @@ test/test.js
# Third-party files # Third-party files
js/Mp3LameEncoder.min.js js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js js/WebAudioRecorderMp3.js
js/libphonenumber-util.js
js/libsignal-protocol-worker.js
libtextsecure/libsignal-protocol.js
libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js
# TypeScript generated files # TypeScript generated files
ts/**/*.js ts/**/*.js
# ES2015+ files
!libtextsecure/api.js
!js/background.js
!js/backup.js
!js/database.js
!js/logging.js
!js/models/conversations.js
!js/models/messages.js
!js/notifications.js
!js/expiring_messages.js
!js/views/attachment_view.js
!js/views/backbone_wrapper_view.js
!js/views/clear_data_view.js
!js/views/contact_list_view.js
!js/views/conversation_search_view.js
!js/views/conversation_view.js
!js/views/debug_log_view.js
!js/views/file_input_view.js
!js/views/inbox_view.js
!js/views/message_view.js
!js/views/settings_view.js
!js/views/timestamp_view.js
!test/backup_test.js
!test/views/attachment_view_test.js
!libtextsecure/message_receiver.js
!main.js
!preload.js
!prepare_build.js

33
.jscsrc
View file

@ -1,33 +0,0 @@
{
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"disallowNewlineBeforeBlockStatements": true,
"requireCommaBeforeLineBreak": true,
"requireSemicolons": true,
"requireSpaceBeforeBlockStatements": true,
"disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"requireSpacesInNamedFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"requireCurlyBraces": [
"if",
"else",
"for",
"while",
"do",
"try",
"catch"
],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"case",
"try",
"typeof",
"return"
]
}

View file

@ -1,73 +0,0 @@
{
"maxerr" : 50,
"bitwise" : false,
"camelcase" : false,
"curly" : false,
"eqeqeq" : false,
"forin" : false,
"freeze" : false,
"immed" : false,
"indent" : 4,
"latedef" : false,
"newcap" : false,
"noarg" : false,
"noempty" : false,
"nonbsp" : false,
"nonew" : false,
"plusplus" : false,
"quotmark" : false,
"undef" : false,
"unused" : false,
"strict" : false,
"maxparams" : false,
"maxdepth" : false,
"maxstatements" : false,
"maxcomplexity" : false,
"maxlen" : false,
"asi" : false,
"boss" : false,
"debug" : false,
"eqnull" : false,
"es5" : false,
"esversion" : 6,
"esnext" : false,
"moz" : false,
"evil" : false,
"expr" : false,
"funcscope" : false,
"globalstrict" : false,
"iterator" : false,
"lastsemic" : false,
"laxbreak" : true,
"laxcomma" : false,
"loopfunc" : false,
"multistr" : false,
"noyield" : false,
"notypeof" : false,
"proto" : false,
"scripturl" : false,
"shadow" : false,
"sub" : false,
"supernew" : false,
"validthis" : false,
"browser" : false,
"browserify" : false,
"couch" : false,
"devel" : false,
"dojo" : false,
"jasmine" : false,
"jquery" : false,
"mocha" : false,
"mootools" : false,
"node" : false,
"nonstandard" : false,
"prototypejs" : false,
"qunit" : false,
"rhino" : false,
"shelljs" : false,
"worker" : false,
"wsh" : false,
"yui" : false,
"globals" : {},
"-W100" : true
}

View file

@ -6,7 +6,6 @@ config/local-*.json
config/local.json config/local.json
dist/** dist/**
js/components.js js/components.js
js/libsignal-protocol-worker.js
js/libtextsecure.js js/libtextsecure.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js
@ -21,6 +20,7 @@ stylesheets/manifest.css
components/** components/**
js/Mp3LameEncoder.min.js js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js js/WebAudioRecorderMp3.js
js/libsignal-protocol-worker.js
libtextsecure/libsignal-protocol.js libtextsecure/libsignal-protocol.js
libtextsecure/test/blanket_mocha.js libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js test/blanket_mocha.js

View file

@ -1,21 +1,29 @@
var path = require('path'); const path = require('path');
var packageJson = require('./package.json'); const packageJson = require('./package.json');
const importOnce = require('node-sass-import-once');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const spectron = require('spectron');
const asar = require('asar');
const fs = require('fs');
const assert = require('assert');
module.exports = function(grunt) { /* eslint-disable more/no-then */
'use strict';
var bower = grunt.file.readJSON('bower.json'); module.exports = grunt => {
var components = []; const bower = grunt.file.readJSON('bower.json');
for (var i in bower.concat.app) { const components = [];
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const i in bower.concat.app) {
components.push(bower.concat.app[i]); components.push(bower.concat.app[i]);
} }
var libtextsecurecomponents = []; const libtextsecurecomponents = [];
for (i in bower.concat.libtextsecure) { // eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const i in bower.concat.libtextsecure) {
libtextsecurecomponents.push(bower.concat.libtextsecure[i]); libtextsecurecomponents.push(bower.concat.libtextsecure[i]);
} }
var importOnce = require('node-sass-import-once');
grunt.loadNpmTasks('grunt-sass'); grunt.loadNpmTasks('grunt-sass');
grunt.initConfig({ grunt.initConfig({
@ -37,7 +45,7 @@ module.exports = function(grunt) {
], ],
dest: 'test/test.js', dest: 'test/test.js',
}, },
//TODO: Move errors back down? // TODO: Move errors back down?
libtextsecure: { libtextsecure: {
options: { options: {
banner: ';(function() {\n', banner: ';(function() {\n',
@ -91,39 +99,6 @@ module.exports = function(grunt) {
}, },
}, },
}, },
jshint: {
files: [
'Gruntfile.js',
'js/**/*.js',
'!js/background.js',
'!js/backup.js',
'!js/components.js',
'!js/database.js',
'!js/libsignal-protocol-worker.js',
'!js/libtextsecure.js',
'!js/logging.js',
'!js/expiring_messages.js',
'!js/modules/**/*.js',
'!js/Mp3LameEncoder.min.js',
'!js/settings_start.js',
'!js/signal_protocol_store.js',
'!js/views/clear_data_view.js',
'!js/views/conversation_search_view.js',
'!js/views/conversation_view.js',
'!js/views/debug_log_view.js',
'!js/views/file_input_view.js',
'!js/views/timestamp_view.js',
'!js/views/message_view.js',
'!js/views/settings_view.js',
'!js/views/contact_list_view.js',
'!js/models/conversations.js',
'!js/models/messages.js',
'!js/WebAudioRecorderMp3.js',
'!libtextsecure/message_receiver.js',
'_locales/**/*',
],
options: { jshintrc: '.jshintrc' },
},
copy: { copy: {
deps: { deps: {
files: [ files: [
@ -151,10 +126,6 @@ module.exports = function(grunt) {
files: ['./stylesheets/*.scss'], files: ['./stylesheets/*.scss'],
tasks: ['sass'], tasks: ['sass'],
}, },
scripts: {
files: ['<%= jshint.files %>'],
tasks: ['jshint'],
},
transpile: { transpile: {
files: ['./ts/**/*.ts'], files: ['./ts/**/*.ts'],
tasks: ['exec:transpile'], tasks: ['exec:transpile'],
@ -173,41 +144,37 @@ module.exports = function(grunt) {
}, },
'test-release': { 'test-release': {
osx: { osx: {
archive: archive: `mac/${
'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar', packageJson.productName
appUpdateYML: }.app/Contents/Resources/app.asar`,
'mac/' + appUpdateYML: `mac/${
packageJson.productName + packageJson.productName
'.app/Contents/Resources/app-update.yml', }.app/Contents/Resources/app-update.yml`,
exe: exe: `mac/${packageJson.productName}.app/Contents/MacOS/${
'mac/' + packageJson.productName
packageJson.productName + }`,
'.app/Contents/MacOS/' +
packageJson.productName,
}, },
mas: { mas: {
archive: 'mas/Signal.app/Contents/Resources/app.asar', archive: 'mas/Signal.app/Contents/Resources/app.asar',
appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml', appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml',
exe: exe: `mas/${packageJson.productName}.app/Contents/MacOS/${
'mas/' + packageJson.productName
packageJson.productName + }`,
'.app/Contents/MacOS/' +
packageJson.productName,
}, },
linux: { linux: {
archive: 'linux-unpacked/resources/app.asar', archive: 'linux-unpacked/resources/app.asar',
exe: 'linux-unpacked/' + packageJson.name, exe: `linux-unpacked/${packageJson.name}`,
}, },
win: { win: {
archive: 'win-unpacked/resources/app.asar', archive: 'win-unpacked/resources/app.asar',
appUpdateYML: 'win-unpacked/resources/app-update.yml', appUpdateYML: 'win-unpacked/resources/app-update.yml',
exe: 'win-unpacked/' + packageJson.productName + '.exe', exe: `win-unpacked/${packageJson.productName}.exe`,
}, },
}, },
gitinfo: {}, // to be populated by grunt gitinfo gitinfo: {}, // to be populated by grunt gitinfo
}); });
Object.keys(grunt.config.get('pkg').devDependencies).forEach(function(key) { Object.keys(grunt.config.get('pkg').devDependencies).forEach(key => {
if (/^grunt(?!(-cli)?$)/.test(key)) { if (/^grunt(?!(-cli)?$)/.test(key)) {
// ignore grunt and grunt-cli // ignore grunt and grunt-cli
grunt.loadNpmTasks(key); grunt.loadNpmTasks(key);
@ -216,20 +183,16 @@ module.exports = function(grunt) {
// Transifex does not understand placeholders, so this task patches all non-en // Transifex does not understand placeholders, so this task patches all non-en
// locales with missing placeholders // locales with missing placeholders
grunt.registerTask('locale-patch', function() { grunt.registerTask('locale-patch', () => {
var en = grunt.file.readJSON('_locales/en/messages.json'); const en = grunt.file.readJSON('_locales/en/messages.json');
grunt.file.recurse('_locales', function( grunt.file.recurse('_locales', (abspath, rootdir, subdir, filename) => {
abspath,
rootdir,
subdir,
filename
) {
if (subdir === 'en' || filename !== 'messages.json') { if (subdir === 'en' || filename !== 'messages.json') {
return; return;
} }
var messages = grunt.file.readJSON(abspath); const messages = grunt.file.readJSON(abspath);
for (var key in messages) { // eslint-disable-next-line no-restricted-syntax
for (const key in messages) {
if (en[key] !== undefined && messages[key] !== undefined) { if (en[key] !== undefined && messages[key] !== undefined) {
if ( if (
en[key].placeholders !== undefined && en[key].placeholders !== undefined &&
@ -240,32 +203,32 @@ module.exports = function(grunt) {
} }
} }
grunt.file.write(abspath, JSON.stringify(messages, null, 4) + '\n'); grunt.file.write(abspath, `${JSON.stringify(messages, null, 4)}\n`);
}); });
}); });
grunt.registerTask('getExpireTime', function() { grunt.registerTask('getExpireTime', () => {
grunt.task.requires('gitinfo'); grunt.task.requires('gitinfo');
var gitinfo = grunt.config.get('gitinfo'); const gitinfo = grunt.config.get('gitinfo');
var commited = gitinfo.local.branch.current.lastCommitTime; const commited = gitinfo.local.branch.current.lastCommitTime;
var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90; const time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
grunt.file.write( grunt.file.write(
'config/local-production.json', 'config/local-production.json',
JSON.stringify({ buildExpiration: time }) + '\n' `${JSON.stringify({ buildExpiration: time })}\n`
); );
}); });
grunt.registerTask('clean-release', function() { grunt.registerTask('clean-release', () => {
require('rimraf').sync('release'); rimraf.sync('release');
require('mkdirp').sync('release'); mkdirp.sync('release');
}); });
function runTests(environment, cb) { function runTests(environment, cb) {
var failure; let failure;
var Application = require('spectron').Application; const { Application } = spectron;
var electronBinary = const electronBinary =
process.platform === 'win32' ? 'electron.cmd' : 'electron'; process.platform === 'win32' ? 'electron.cmd' : 'electron';
var app = new Application({ const app = new Application({
path: path.join(__dirname, 'node_modules', '.bin', electronBinary), path: path.join(__dirname, 'node_modules', '.bin', electronBinary),
args: [path.join(__dirname, 'main.js')], args: [path.join(__dirname, 'main.js')],
env: { env: {
@ -275,78 +238,71 @@ module.exports = function(grunt) {
}); });
function getMochaResults() { function getMochaResults() {
// eslint-disable-next-line no-undef
return window.mochaResults; return window.mochaResults;
} }
app app
.start() .start()
.then(function() { .then(() =>
return app.client.waitUntil( app.client.waitUntil(
function() { () =>
return app.client.execute(getMochaResults).then(function(data) { app.client
return Boolean(data.value); .execute(getMochaResults)
}); .then(data => Boolean(data.value)),
},
10000, 10000,
'Expected to find window.mochaResults set!' 'Expected to find window.mochaResults set!'
); )
}) )
.then(function() { .then(() => app.client.execute(getMochaResults))
return app.client.execute(getMochaResults); .then(data => {
}) const results = data.value;
.then(function(data) {
var results = data.value;
if (results.failures > 0) { if (results.failures > 0) {
console.error(results.reports); console.error(results.reports);
failure = function() { failure = () =>
grunt.fail.fatal( grunt.fail.fatal(`Found ${results.failures} failing unit tests.`);
'Found ' + results.failures + ' failing unit tests.'
);
};
return app.client.log('browser'); return app.client.log('browser');
} else {
grunt.log.ok(results.passes + ' tests passed.');
} }
grunt.log.ok(`${results.passes} tests passed.`);
return null;
}) })
.then(function(logs) { .then(logs => {
if (logs) { if (logs) {
console.error(); console.error();
console.error('Because tests failed, printing browser logs:'); console.error('Because tests failed, printing browser logs:');
console.error(logs); console.error(logs);
} }
}) })
.catch(function(error) { .catch(error => {
failure = function() { failure = () =>
grunt.fail.fatal( grunt.fail.fatal(
'Something went wrong: ' + error.message + ' ' + error.stack `Something went wrong: ${error.message} ${error.stack}`
); );
};
}) })
.then(function() { .then(() => {
// We need to use the failure variable and this early stop to clean up before // We need to use the failure variable and this early stop to clean up before
// shutting down. Grunt's fail methods are the only way to set the return value, // shutting down. Grunt's fail methods are the only way to set the return value,
// but they shut the process down immediately! // but they shut the process down immediately!
if (failure) { if (failure) {
console.log(); console.log();
console.log('Main process logs:'); console.log('Main process logs:');
return app.client.getMainProcessLogs().then(function(logs) { return app.client.getMainProcessLogs().then(logs => {
logs.forEach(function(log) { logs.forEach(log => {
console.log(log); console.log(log);
}); });
return app.stop(); return app.stop();
}); });
} else {
return app.stop();
} }
return app.stop();
}) })
.then(function() { .then(() => {
if (failure) { if (failure) {
failure(); failure();
} }
cb(); cb();
}) })
.catch(function(error) { .catch(error => {
console.error('Second-level error:', error.message, error.stack); console.error('Second-level error:', error.message, error.stack);
if (failure) { if (failure) {
failure(); failure();
@ -355,9 +311,9 @@ module.exports = function(grunt) {
}); });
} }
grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() { grunt.registerTask('unit-tests', 'Run unit tests w/Electron', () => {
var environment = grunt.option('env') || 'test'; const environment = grunt.option('env') || 'test';
var done = this.async(); const done = this.async();
runTests(environment, done); runTests(environment, done);
}); });
@ -365,102 +321,93 @@ module.exports = function(grunt) {
grunt.registerTask( grunt.registerTask(
'lib-unit-tests', 'lib-unit-tests',
'Run libtextsecure unit tests w/Electron', 'Run libtextsecure unit tests w/Electron',
function() { () => {
var environment = grunt.option('env') || 'test-lib'; const environment = grunt.option('env') || 'test-lib';
var done = this.async(); const done = this.async();
runTests(environment, done); runTests(environment, done);
} }
); );
grunt.registerMultiTask('test-release', 'Test packaged releases', function() { grunt.registerMultiTask('test-release', 'Test packaged releases', () => {
var dir = grunt.option('dir') || 'dist'; const dir = grunt.option('dir') || 'dist';
var environment = grunt.option('env') || 'production'; const environment = grunt.option('env') || 'production';
var asar = require('asar'); const config = this.data;
var config = this.data; const archive = [dir, config.archive].join('/');
var archive = [dir, config.archive].join('/'); const files = [
var files = [
'config/default.json', 'config/default.json',
'config/' + environment + '.json', `config/${environment}.json`,
'config/local-' + environment + '.json', `config/local-${environment}.json`,
]; ];
console.log(this.target, archive); console.log(this.target, archive);
var releaseFiles = files.concat(config.files || []); const releaseFiles = files.concat(config.files || []);
releaseFiles.forEach(function(fileName) { releaseFiles.forEach(fileName => {
console.log(fileName); console.log(fileName);
try { try {
asar.statFile(archive, fileName); asar.statFile(archive, fileName);
return true; return true;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
throw new Error('Missing file ' + fileName); throw new Error(`Missing file ${fileName}`);
} }
}); });
if (config.appUpdateYML) { if (config.appUpdateYML) {
var appUpdateYML = [dir, config.appUpdateYML].join('/'); const appUpdateYML = [dir, config.appUpdateYML].join('/');
if (require('fs').existsSync(appUpdateYML)) { if (fs.existsSync(appUpdateYML)) {
console.log('auto update ok'); console.log('auto update ok');
} else { } else {
throw new Error('Missing auto update config ' + appUpdateYML); throw new Error(`Missing auto update config ${appUpdateYML}`);
} }
} }
var done = this.async(); const done = this.async();
// A simple test to verify a visible window is opened with a title // A simple test to verify a visible window is opened with a title
var Application = require('spectron').Application; const { Application } = spectron;
var assert = require('assert');
var app = new Application({ const app = new Application({
path: [dir, config.exe].join('/'), path: [dir, config.exe].join('/'),
requireName: 'unused', requireName: 'unused',
}); });
app app
.start() .start()
.then(function() { .then(() => app.client.getWindowCount())
return app.client.getWindowCount(); .then(count => {
})
.then(function(count) {
assert.equal(count, 1); assert.equal(count, 1);
console.log('window opened'); console.log('window opened');
}) })
.then(function() { .then(() =>
// Get the window's title // Get the window's title
return app.client.getTitle(); app.client.getTitle()
}) )
.then(function(title) { .then(title => {
// Verify the window's title // Verify the window's title
assert.equal(title, packageJson.productName); assert.equal(title, packageJson.productName);
console.log('title ok'); console.log('title ok');
}) })
.then(function() { .then(() => {
assert( assert(
app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1 app.chromeDriver.logLines.indexOf(`NODE_ENV ${environment}`) > -1
); );
console.log('environment ok'); console.log('environment ok');
}) })
.then( .then(
function() { () =>
// Successfully completed test // Successfully completed test
return app.stop(); app.stop(),
}, error =>
function(error) {
// Test failed! // Test failed!
return app.stop().then(function() { app.stop().then(() => {
grunt.fail.fatal( grunt.fail.fatal(`Test failed: ${error.message} ${error.stack}`);
'Test failed: ' + error.message + ' ' + error.stack })
);
});
}
) )
.then(done); .then(done);
}); });
grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']); grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']);
grunt.registerTask('dev', ['default', 'watch']); grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('lint', ['jshint']);
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('default', [ grunt.registerTask('default', [

View file

@ -1,3 +1,5 @@
/* global window */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const i18n = require('./js/modules/i18n'); const i18n = require('./js/modules/i18n');

View file

@ -471,9 +471,6 @@
<div class='contacts'></div> <div class='contacts'></div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='generic-error'>
<p>{{ message }}</p>
</script>
<script type='text/x-tmpl-mustache' id='error-icon'> <script type='text/x-tmpl-mustache' id='error-icon'>
<span class='error-icon'> <span class='error-icon'>
</span> </span>
@ -805,7 +802,6 @@
<script type='text/javascript' src='js/views/contact_list_view.js'></script> <script type='text/javascript' src='js/views/contact_list_view.js'></script>
<script type='text/javascript' src='js/views/new_group_update_view.js'></script> <script type='text/javascript' src='js/views/new_group_update_view.js'></script>
<script type='text/javascript' src='js/views/attachment_view.js'></script> <script type='text/javascript' src='js/views/attachment_view.js'></script>
<script type='text/javascript' src='js/views/error_view.js'></script>
<script type='text/javascript' src='js/views/timestamp_view.js'></script> <script type='text/javascript' src='js/views/timestamp_view.js'></script>
<script type='text/javascript' src='js/views/message_view.js'></script> <script type='text/javascript' src='js/views/message_view.js'></script>
<script type='text/javascript' src='js/views/key_verification_view.js'></script> <script type='text/javascript' src='js/views/key_verification_view.js'></script>

View file

@ -1,3 +1,5 @@
/* global window */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const i18n = require('./js/modules/i18n'); const i18n = require('./js/modules/i18n');

View file

@ -1,3 +1,5 @@
/* global $: false */
// Add version // Add version
$('.version').text(`v${window.getVersion()}`); $('.version').text(`v${window.getVersion()}`);
@ -14,7 +16,9 @@ if (window.getAppInstance()) {
$('.environment').text(states.join(' - ')); $('.environment').text(states.join(' - '));
// Install the 'dismiss with escape key' handler // Install the 'dismiss with escape key' handler
$(document).on('keyup', function(e) { $(document).on('keyup', e => {
'use strict';
if (e.keyCode === 27) { if (e.keyCode === 27) {
window.closeAbout(); window.closeAbout();
} }

View file

@ -1,10 +1,14 @@
/* global extension: false */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
// Browser specific functions for Chrom* // Browser specific functions for Chrom*
window.extension = window.extension || {}; window.extension = window.extension || {};
extension.windows = { extension.windows = {
onClosed: function(callback) { onClosed(callback) {
window.addEventListener('beforeunload', callback); window.addEventListener('beforeunload', callback);
}, },
}; };

View file

@ -1,20 +1,20 @@
/*global $, Whisper, Backbone, textsecure, extension*/ /* global _, Whisper, Backbone, storage */
// This script should only be included in background.html /* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var conversations = new Whisper.ConversationCollection(); const conversations = new Whisper.ConversationCollection();
var inboxCollection = new (Backbone.Collection.extend({ const inboxCollection = new (Backbone.Collection.extend({
initialize: function() { initialize() {
this.on('change:timestamp change:name change:number', this.sort); this.on('change:timestamp change:name change:number', this.sort);
this.listenTo(conversations, 'add change:active_at', this.addActive); this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', function() { this.listenTo(conversations, 'reset', () => this.reset([]));
this.reset([]);
});
this.on( this.on(
'add remove change:unreadCount', 'add remove change:unreadCount',
@ -24,9 +24,9 @@
this.collator = new Intl.Collator(); this.collator = new Intl.Collator();
}, },
comparator: function(m1, m2) { comparator(m1, m2) {
var timestamp1 = m1.get('timestamp'); const timestamp1 = m1.get('timestamp');
var timestamp2 = m2.get('timestamp'); const timestamp2 = m2.get('timestamp');
if (timestamp1 && !timestamp2) { if (timestamp1 && !timestamp2) {
return -1; return -1;
} }
@ -37,57 +37,48 @@
return timestamp2 - timestamp1; return timestamp2 - timestamp1;
} }
var title1 = m1.getTitle().toLowerCase(); const title1 = m1.getTitle().toLowerCase();
var title2 = m2.getTitle().toLowerCase(); const title2 = m2.getTitle().toLowerCase();
return this.collator.compare(title1, title2); return this.collator.compare(title1, title2);
}, },
addActive: function(model) { addActive(model) {
if (model.get('active_at')) { if (model.get('active_at')) {
this.add(model); this.add(model);
} else { } else {
this.remove(model); this.remove(model);
} }
}, },
updateUnreadCount: function() { updateUnreadCount() {
var newUnreadCount = _.reduce( const newUnreadCount = _.reduce(
this.map(function(m) { this.map(m => m.get('unreadCount')),
return m.get('unreadCount'); (item, memo) => item + memo,
}),
function(item, memo) {
return item + memo;
},
0 0
); );
storage.put('unreadCount', newUnreadCount); storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) { if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount); window.setBadgeCount(newUnreadCount);
window.document.title = window.getTitle() + ' (' + newUnreadCount + ')'; window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else { } else {
window.setBadgeCount(0); window.setBadgeCount(0);
window.document.title = window.getTitle(); window.document.title = window.getTitle();
} }
window.updateTrayIcon(newUnreadCount); window.updateTrayIcon(newUnreadCount);
}, },
startPruning: function() { startPruning() {
var halfHour = 30 * 60 * 1000; const halfHour = 30 * 60 * 1000;
this.interval = setInterval( this.interval = setInterval(() => {
function() { this.forEach(conversation => {
this.forEach(function(conversation) { conversation.trigger('prune');
conversation.trigger('prune'); });
}); }, halfHour);
}.bind(this),
halfHour
);
}, },
}))(); }))();
window.getInboxCollection = function() { window.getInboxCollection = () => inboxCollection;
return inboxCollection;
};
window.ConversationController = { window.ConversationController = {
get: function(id) { get(id) {
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error( throw new Error(
'ConversationController.get() needs complete initial fetch' 'ConversationController.get() needs complete initial fetch'
@ -97,13 +88,13 @@
return conversations.get(id); return conversations.get(id);
}, },
// Needed for some model setup which happens during the initial fetch() call below // Needed for some model setup which happens during the initial fetch() call below
getUnsafe: function(id) { getUnsafe(id) {
return conversations.get(id); return conversations.get(id);
}, },
dangerouslyCreateAndAdd: function(attributes) { dangerouslyCreateAndAdd(attributes) {
return conversations.add(attributes); return conversations.add(attributes);
}, },
getOrCreate: function(id, type) { getOrCreate(id, type) {
if (typeof id !== 'string') { if (typeof id !== 'string') {
throw new TypeError("'id' must be a string"); throw new TypeError("'id' must be a string");
} }
@ -120,18 +111,18 @@
); );
} }
var conversation = conversations.get(id); let conversation = conversations.get(id);
if (conversation) { if (conversation) {
return conversation; return conversation;
} }
conversation = conversations.add({ conversation = conversations.add({
id: id, id,
type: type, type,
}); });
conversation.initialPromise = new Promise(function(resolve, reject) { conversation.initialPromise = new Promise((resolve, reject) => {
if (!conversation.isValid()) { if (!conversation.isValid()) {
var validationError = conversation.validationError || {}; const validationError = conversation.validationError || {};
console.log( console.log(
'Contact is not valid. Not saving, but adding to collection:', 'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(), conversation.idForLogging(),
@ -141,72 +132,64 @@
return resolve(conversation); return resolve(conversation);
} }
var deferred = conversation.save(); const deferred = conversation.save();
if (!deferred) { if (!deferred) {
console.log('Conversation save failed! ', id, type); console.log('Conversation save failed! ', id, type);
return reject(new Error('getOrCreate: Conversation save failed')); return reject(new Error('getOrCreate: Conversation save failed'));
} }
deferred.then(function() { return deferred.then(() => {
resolve(conversation); resolve(conversation);
}, reject); }, reject);
}); });
return conversation; return conversation;
}, },
getOrCreateAndWait: function(id, type) { getOrCreateAndWait(id, type) {
return this._initialPromise.then( return this._initialPromise.then(() => {
function() { const conversation = this.getOrCreate(id, type);
var conversation = this.getOrCreate(id, type);
if (conversation) { if (conversation) {
return conversation.initialPromise.then(function() { return conversation.initialPromise.then(() => conversation);
return conversation; }
});
}
return Promise.reject( return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation') new Error('getOrCreateAndWait: did not get conversation')
); );
}.bind(this)
);
},
getAllGroupsInvolvingId: function(id) {
var groups = new Whisper.GroupCollection();
return groups.fetchGroups(id).then(function() {
return groups.map(function(group) {
return conversations.add(group);
});
}); });
}, },
loadPromise: function() { getAllGroupsInvolvingId(id) {
const groups = new Whisper.GroupCollection();
return groups
.fetchGroups(id)
.then(() => groups.map(group => conversations.add(group)));
},
loadPromise() {
return this._initialPromise; return this._initialPromise;
}, },
reset: function() { reset() {
this._initialPromise = Promise.resolve(); this._initialPromise = Promise.resolve();
conversations.reset([]); conversations.reset([]);
}, },
load: function() { load() {
console.log('ConversationController: starting initial fetch'); console.log('ConversationController: starting initial fetch');
this._initialPromise = new Promise( this._initialPromise = new Promise((resolve, reject) => {
function(resolve, reject) { conversations.fetch().then(
conversations.fetch().then( () => {
function() { console.log('ConversationController: done with initial fetch');
console.log('ConversationController: done with initial fetch'); this._initialFetchComplete = true;
this._initialFetchComplete = true; resolve();
resolve(); },
}.bind(this), error => {
function(error) { console.log(
console.log( 'ConversationController: initial fetch failed',
'ConversationController: initial fetch failed', error && error.stack ? error.stack : error
error && error.stack ? error.stack : error );
); reject(error);
reject(error); }
} );
); });
}.bind(this)
);
return this._initialPromise; return this._initialPromise;
}, },

View file

@ -1,4 +1,9 @@
$(document).on('keyup', function(e) { /* global $: false */
/* global Whisper: false */
$(document).on('keyup', e => {
'use strict';
if (e.keyCode === 27) { if (e.keyCode === 27) {
window.closeDebugLog(); window.closeDebugLog();
} }

View file

@ -1,97 +1,95 @@
/* global Backbone: false */
/* global Whisper: false */
/* global ConversationController: false */
/* global _: false */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.DeliveryReceipts = new (Backbone.Collection.extend({ Whisper.DeliveryReceipts = new (Backbone.Collection.extend({
forMessage: function(conversation, message) { forMessage(conversation, message) {
var recipients; let recipients;
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
recipients = [conversation.id]; recipients = [conversation.id];
} else { } else {
recipients = conversation.get('members') || []; recipients = conversation.get('members') || [];
} }
var receipts = this.filter(function(receipt) { const receipts = this.filter(
return ( receipt =>
receipt.get('timestamp') === message.get('sent_at') && receipt.get('timestamp') === message.get('sent_at') &&
recipients.indexOf(receipt.get('source')) > -1 recipients.indexOf(receipt.get('source')) > -1
); );
});
this.remove(receipts); this.remove(receipts);
return receipts; return receipts;
}, },
onReceipt: function(receipt) { onReceipt(receipt) {
var messages = new Whisper.MessageCollection(); const messages = new Whisper.MessageCollection();
return messages return messages
.fetchSentAt(receipt.get('timestamp')) .fetchSentAt(receipt.get('timestamp'))
.then(function() { .then(() => {
if (messages.length === 0) { if (messages.length === 0) {
return; return null;
} }
var message = messages.find(function(message) { const message = messages.find(
return ( item =>
!message.isIncoming() && !item.isIncoming() &&
receipt.get('source') === message.get('conversationId') receipt.get('source') === item.get('conversationId')
); );
});
if (message) { if (message) {
return message; return message;
} }
var groups = new Whisper.GroupCollection(); const groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('source')).then(function() { return groups.fetchGroups(receipt.get('source')).then(() => {
var ids = groups.pluck('id'); const ids = groups.pluck('id');
ids.push(receipt.get('source')); ids.push(receipt.get('source'));
return messages.find(function(message) { return messages.find(
return ( item =>
!message.isIncoming() && !item.isIncoming() &&
_.contains(ids, message.get('conversationId')) _.contains(ids, item.get('conversationId'))
); );
});
}); });
}) })
.then( .then(message => {
function(message) { if (message) {
if (message) { const deliveries = message.get('delivered') || 0;
var deliveries = message.get('delivered') || 0; const deliveredTo = message.get('delivered_to') || [];
var delivered_to = message.get('delivered_to') || []; return new Promise((resolve, reject) => {
return new Promise( message
function(resolve, reject) { .save({
message delivered_to: _.union(deliveredTo, [receipt.get('source')]),
.save({ delivered: deliveries + 1,
delivered_to: _.union(delivered_to, [ })
receipt.get('source'), .then(() => {
]), // notify frontend listeners
delivered: deliveries + 1, const conversation = ConversationController.get(
}) message.get('conversationId')
.then( );
function() { if (conversation) {
// notify frontend listeners conversation.trigger('delivered', message);
var conversation = ConversationController.get( }
message.get('conversationId')
);
if (conversation) {
conversation.trigger('delivered', message);
}
this.remove(receipt); this.remove(receipt);
resolve(); resolve();
}.bind(this), }, reject);
reject });
); // TODO: consider keeping a list of numbers we've
}.bind(this) // successfully delivered to?
); }
// TODO: consider keeping a list of numbers we've console.log(
// successfully delivered to? 'No message for delivery receipt',
} else { receipt.get('source'),
console.log( receipt.get('timestamp')
'No message for delivery receipt', );
receipt.get('source'),
receipt.get('timestamp') return null;
); })
} .catch(error => {
}.bind(this)
)
.catch(function(error) {
console.log( console.log(
'DeliveryReceipts.onReceipt error:', 'DeliveryReceipts.onReceipt error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error

View file

@ -1,16 +1,19 @@
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
var BUILD_EXPIRATION = 0;
let BUILD_EXPIRATION = 0;
try { try {
BUILD_EXPIRATION = parseInt(window.getExpiration()); BUILD_EXPIRATION = parseInt(window.getExpiration(), 10);
if (BUILD_EXPIRATION) { if (BUILD_EXPIRATION) {
console.log('Build expires: ', new Date(BUILD_EXPIRATION).toISOString()); console.log('Build expires: ', new Date(BUILD_EXPIRATION).toISOString());
} }
} catch (e) {} } catch (e) {
// nothing
}
window.extension = window.extension || {}; window.extension = window.extension || {};
extension.expired = function() { window.extension.expired = () =>
return BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION; BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION;
};
})(); })();

View file

@ -1,15 +1,14 @@
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
var windowFocused = false; let windowFocused = false;
window.addEventListener('blur', function() { window.addEventListener('blur', () => {
windowFocused = false; windowFocused = false;
}); });
window.addEventListener('focus', function() { window.addEventListener('focus', () => {
windowFocused = true; windowFocused = true;
}); });
window.isFocused = function() { window.isFocused = () => windowFocused;
return windowFocused;
};
})(); })();

View file

@ -1,27 +1,31 @@
/* global Whisper, SignalProtocolStore, ConversationController, _ */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.KeyChangeListener = { Whisper.KeyChangeListener = {
init: function(signalProtocolStore) { init(signalProtocolStore) {
if (!(signalProtocolStore instanceof SignalProtocolStore)) { if (!(signalProtocolStore instanceof SignalProtocolStore)) {
throw new Error('KeyChangeListener requires a SignalProtocolStore'); throw new Error('KeyChangeListener requires a SignalProtocolStore');
} }
signalProtocolStore.on('keychange', function(id) { signalProtocolStore.on('keychange', id => {
ConversationController.getOrCreateAndWait(id, 'private').then(function( ConversationController.getOrCreateAndWait(id, 'private').then(
conversation conversation => {
) { conversation.addKeyChange(id);
conversation.addKeyChange(id);
ConversationController.getAllGroupsInvolvingId(id).then(function( ConversationController.getAllGroupsInvolvingId(id).then(groups => {
groups _.forEach(groups, group => {
) { group.addKeyChange(id);
_.forEach(groups, function(group) { });
group.addKeyChange(id);
}); });
}); }
}); );
}); });
}, },
}; };

View file

@ -1,12 +1,16 @@
/* global storage, _ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
storage.isBlocked = function(number) {
var numbers = storage.get('blocked', []); storage.isBlocked = number => {
const numbers = storage.get('blocked', []);
return _.include(numbers, number); return _.include(numbers, number);
}; };
storage.addBlockedNumber = function(number) { storage.addBlockedNumber = number => {
var numbers = storage.get('blocked', []); const numbers = storage.get('blocked', []);
if (_.include(numbers, number)) { if (_.include(numbers, number)) {
return; return;
} }
@ -14,8 +18,8 @@
console.log('adding', number, 'to blocked list'); console.log('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number)); storage.put('blocked', numbers.concat(number));
}; };
storage.removeBlockedNumber = function(number) { storage.removeBlockedNumber = number => {
var numbers = storage.get('blocked', []); const numbers = storage.get('blocked', []);
if (!_.include(numbers, number)) { if (!_.include(numbers, number)) {
return; return;
} }

View file

@ -1,4 +1,8 @@
$(document).on('keyup', function(e) { /* global $, Whisper, i18n */
$(document).on('keyup', e => {
'use strict';
if (e.keyCode === 27) { if (e.keyCode === 27) {
window.closePermissionsPopup(); window.closePermissionsPopup();
} }
@ -11,6 +15,8 @@ window.view = new Whisper.ConfirmationDialogView({
message: i18n('audioPermissionNeeded'), message: i18n('audioPermissionNeeded'),
okText: i18n('allowAccess'), okText: i18n('allowAccess'),
resolve: () => { resolve: () => {
'use strict';
window.setMediaPermissions(true); window.setMediaPermissions(true);
window.closePermissionsPopup(); window.closePermissionsPopup();
}, },

View file

@ -1,93 +1,89 @@
/* global Whisper, Backbone, _, ConversationController */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ReadReceipts = new (Backbone.Collection.extend({ Whisper.ReadReceipts = new (Backbone.Collection.extend({
forMessage: function(conversation, message) { forMessage(conversation, message) {
if (!message.isOutgoing()) { if (!message.isOutgoing()) {
return []; return [];
} }
var ids = []; let ids = [];
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
ids = [conversation.id]; ids = [conversation.id];
} else { } else {
ids = conversation.get('members'); ids = conversation.get('members');
} }
var receipts = this.filter(function(receipt) { const receipts = this.filter(
return ( receipt =>
receipt.get('timestamp') === message.get('sent_at') && receipt.get('timestamp') === message.get('sent_at') &&
_.contains(ids, receipt.get('reader')) _.contains(ids, receipt.get('reader'))
); );
});
if (receipts.length) { if (receipts.length) {
console.log('Found early read receipts for message'); console.log('Found early read receipts for message');
this.remove(receipts); this.remove(receipts);
} }
return receipts; return receipts;
}, },
onReceipt: function(receipt) { onReceipt(receipt) {
var messages = new Whisper.MessageCollection(); const messages = new Whisper.MessageCollection();
return messages return messages
.fetchSentAt(receipt.get('timestamp')) .fetchSentAt(receipt.get('timestamp'))
.then(function() { .then(() => {
if (messages.length === 0) { if (messages.length === 0) {
return; return null;
} }
var message = messages.find(function(message) { const message = messages.find(
return ( item =>
message.isOutgoing() && item.isOutgoing() &&
receipt.get('reader') === message.get('conversationId') receipt.get('reader') === item.get('conversationId')
); );
});
if (message) { if (message) {
return message; return message;
} }
var groups = new Whisper.GroupCollection(); const groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('reader')).then(function() { return groups.fetchGroups(receipt.get('reader')).then(() => {
var ids = groups.pluck('id'); const ids = groups.pluck('id');
ids.push(receipt.get('reader')); ids.push(receipt.get('reader'));
return messages.find(function(message) { return messages.find(
return ( item =>
message.isOutgoing() && item.isOutgoing() && _.contains(ids, item.get('conversationId'))
_.contains(ids, message.get('conversationId')) );
);
});
}); });
}) })
.then( .then(message => {
function(message) { if (message) {
if (message) { const readBy = message.get('read_by') || [];
var read_by = message.get('read_by') || []; readBy.push(receipt.get('reader'));
read_by.push(receipt.get('reader')); return new Promise((resolve, reject) => {
return new Promise( message.save({ read_by: readBy }).then(() => {
function(resolve, reject) { // notify frontend listeners
message.save({ read_by: read_by }).then( const conversation = ConversationController.get(
function() { message.get('conversationId')
// notify frontend listeners );
var conversation = ConversationController.get( if (conversation) {
message.get('conversationId') conversation.trigger('read', message);
); }
if (conversation) {
conversation.trigger('read', message);
}
this.remove(receipt); this.remove(receipt);
resolve(); resolve();
}.bind(this), }, reject);
reject });
); }
}.bind(this) console.log(
); 'No message for read receipt',
} else { receipt.get('reader'),
console.log( receipt.get('timestamp')
'No message for read receipt', );
receipt.get('reader'),
receipt.get('timestamp') return null;
); })
} .catch(error => {
}.bind(this)
)
.catch(function(error) {
console.log( console.log(
'ReadReceipts.onReceipt error:', 'ReadReceipts.onReceipt error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error

View file

@ -1,9 +1,15 @@
/* global Backbone, Whisper, ConversationController */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ReadSyncs = new (Backbone.Collection.extend({ Whisper.ReadSyncs = new (Backbone.Collection.extend({
forMessage: function(message) { forMessage(message) {
var receipt = this.findWhere({ const receipt = this.findWhere({
sender: message.get('source'), sender: message.get('source'),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
}); });
@ -12,52 +18,49 @@
this.remove(receipt); this.remove(receipt);
return receipt; return receipt;
} }
return null;
}, },
onReceipt: function(receipt) { onReceipt(receipt) {
var messages = new Whisper.MessageCollection(); const messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then( return messages.fetchSentAt(receipt.get('timestamp')).then(() => {
function() { const message = messages.find(
var message = messages.find(function(message) { item =>
return ( item.isIncoming() &&
message.isIncoming() && item.isUnread() &&
message.isUnread() && item.get('source') === receipt.get('sender')
message.get('source') === receipt.get('sender') );
); const notificationForMessage = message
}); ? Whisper.Notifications.findWhere({ messageId: message.id })
const notificationForMessage = message : null;
? Whisper.Notifications.findWhere({ messageId: message.id }) const removedNotification = Whisper.Notifications.remove(
: null; notificationForMessage
const removedNotification = Whisper.Notifications.remove( );
notificationForMessage const receiptSender = receipt.get('sender');
); const receiptTimestamp = receipt.get('timestamp');
const receiptSender = receipt.get('sender'); const wasMessageFound = Boolean(message);
const receiptTimestamp = receipt.get('timestamp'); const wasNotificationFound = Boolean(notificationForMessage);
const wasMessageFound = Boolean(message); const wasNotificationRemoved = Boolean(removedNotification);
const wasNotificationFound = Boolean(notificationForMessage); console.log('Receive read sync:', {
const wasNotificationRemoved = Boolean(removedNotification); receiptSender,
console.log('Receive read sync:', { receiptTimestamp,
receiptSender, wasMessageFound,
receiptTimestamp, wasNotificationFound,
wasMessageFound, wasNotificationRemoved,
wasNotificationFound, });
wasNotificationRemoved, return message
}); ? message.markRead(receipt.get('read_at')).then(() => {
return message // This notification may result in messages older than this one being
? message.markRead(receipt.get('read_at')).then( // marked read. We want those messages to have the same expire timer
function() { // start time as this one, so we pass the read_at value through.
// This notification may result in messages older than this one being this.notifyConversation(message, receipt.get('read_at'));
// marked read. We want those messages to have the same expire timer this.remove(receipt);
// start time as this one, so we pass the read_at value through. })
this.notifyConversation(message, receipt.get('read_at')); : Promise.resolve();
this.remove(receipt); });
}.bind(this)
)
: Promise.resolve();
}.bind(this)
);
}, },
notifyConversation: function(message, readAt) { notifyConversation(message, readAt) {
var conversation = ConversationController.get({ const conversation = ConversationController.get({
id: message.get('conversationId'), id: message.get('conversationId'),
}); });

View file

@ -1,23 +1,27 @@
/* global storage, Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
Whisper.Registration = { Whisper.Registration = {
markEverDone: function() { markEverDone() {
storage.put('chromiumRegistrationDoneEver', ''); storage.put('chromiumRegistrationDoneEver', '');
}, },
markDone: function() { markDone() {
this.markEverDone(); this.markEverDone();
storage.put('chromiumRegistrationDone', ''); storage.put('chromiumRegistrationDone', '');
}, },
isDone: function() { isDone() {
return storage.get('chromiumRegistrationDone') === ''; return storage.get('chromiumRegistrationDone') === '';
}, },
everDone: function() { everDone() {
return ( return (
storage.get('chromiumRegistrationDoneEver') === '' || storage.get('chromiumRegistrationDoneEver') === '' ||
storage.get('chromiumRegistrationDone') === '' storage.get('chromiumRegistrationDone') === ''
); );
}, },
remove: function() { remove() {
storage.remove('chromiumRegistrationDone'); storage.remove('chromiumRegistrationDone');
}, },
}; };

View file

@ -1,41 +1,50 @@
/* eslint-disable */
// This file was taken from Backbone and then modified. It does not conform to this
// project's standards.
(function() { (function() {
'use strict';
// Note: this is all the code required to customize Backbone's trigger() method to make // Note: this is all the code required to customize Backbone's trigger() method to make
// it resilient to exceptions thrown by event handlers. Indentation and code styles // it resilient to exceptions thrown by event handlers. Indentation and code styles
// were kept inline with the Backbone implementation for easier diffs. // were kept inline with the Backbone implementation for easier diffs.
// The changes are: // The changes are:
// 1. added 'name' parameter to triggerEvents to give it access to the current event name // 1. added 'name' parameter to triggerEvents to give it access to the
// 2. added try/catch handlers to triggerEvents with error logging inside every while loop // current event name
// 2. added try/catch handlers to triggerEvents with error logging inside
// every while loop
// And of course, we update the protoypes of Backbone.Model/Backbone.View as well as // And of course, we update the protoypes of Backbone.Model/Backbone.View as well as
// Backbone.Events itself // Backbone.Events itself
var arr = []; const arr = [];
var slice = arr.slice; const slice = arr.slice;
// Regular expression used to split event strings. // Regular expression used to split event strings.
var eventSplitter = /\s+/; const eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event // Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}` // names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API. // in terms of the existing API.
var eventsApi = function(obj, action, name, rest) { const eventsApi = function(obj, action, name, rest) {
if (!name) return true; if (!name) return true;
// Handle event maps. // Handle event maps.
if (typeof name === 'object') { if (typeof name === 'object') {
for (var key in name) { for (const key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest)); obj[action](...[key, name[key]].concat(rest));
} }
return false; return false;
} }
// Handle space separated event names. // Handle space separated event names.
if (eventSplitter.test(name)) { if (eventSplitter.test(name)) {
var names = name.split(eventSplitter); const names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) { for (let i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest)); obj[action](...[names[i]].concat(rest));
} }
return false; return false;
} }
@ -46,14 +55,14 @@
// A difficult-to-believe, but optimized internal dispatch function for // A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal // triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments). // Backbone events have 3 arguments).
var triggerEvents = function(events, name, args) { const triggerEvents = function(events, name, args) {
var ev, let ev,
i = -1, i = -1,
l = events.length, l = events.length,
a1 = args[0], a1 = args[0],
a2 = args[1], a2 = args[1],
a3 = args[2]; a3 = args[2];
var logError = function(error) { const logError = function(error) {
console.log( console.log(
'Model caught error triggering', 'Model caught error triggering',
name, name,
@ -106,7 +115,6 @@
logError(error); logError(error);
} }
} }
return;
} }
}; };
@ -116,10 +124,10 @@
// receive the true name of the event as the first argument). // receive the true name of the event as the first argument).
function trigger(name) { function trigger(name) {
if (!this._events) return this; if (!this._events) return this;
var args = slice.call(arguments, 1); const args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this; if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name]; const events = this._events[name];
var allEvents = this._events.all; const allEvents = this._events.all;
if (events) triggerEvents(events, name, args); if (events) triggerEvents(events, name, args);
if (allEvents) triggerEvents(allEvents, name, arguments); if (allEvents) triggerEvents(allEvents, name, arguments);
return this; return this;

View file

@ -1,13 +1,17 @@
/* global Whisper, storage, getAccountManager */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; const ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
var timeout; let timeout;
var scheduledTime; let scheduledTime;
function scheduleNextRotation() { function scheduleNextRotation() {
var now = Date.now(); const now = Date.now();
var nextTime = now + ROTATION_INTERVAL; const nextTime = now + ROTATION_INTERVAL;
storage.put('nextSignedKeyRotationTime', nextTime); storage.put('nextSignedKeyRotationTime', nextTime);
} }
@ -15,7 +19,7 @@
console.log('Rotating signed prekey...'); console.log('Rotating signed prekey...');
getAccountManager() getAccountManager()
.rotateSignedPreKey() .rotateSignedPreKey()
.catch(function() { .catch(() => {
console.log( console.log(
'rotateSignedPrekey() failed. Trying again in five seconds' 'rotateSignedPrekey() failed. Trying again in five seconds'
); );
@ -32,7 +36,7 @@
console.log( console.log(
'We are offline; keys will be rotated when we are next online' 'We are offline; keys will be rotated when we are next online'
); );
var listener = function() { const listener = () => {
window.removeEventListener('online', listener); window.removeEventListener('online', listener);
run(); run();
}; };
@ -41,8 +45,8 @@
} }
function setTimeoutForNextRun() { function setTimeoutForNextRun() {
var now = Date.now(); const now = Date.now();
var time = storage.get('nextSignedKeyRotationTime', now); const time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) { if (scheduledTime !== time || !timeout) {
console.log( console.log(
@ -52,7 +56,7 @@
} }
scheduledTime = time; scheduledTime = time;
var waitTime = time - now; let waitTime = time - now;
if (waitTime < 0) { if (waitTime < 0) {
waitTime = 0; waitTime = 0;
} }
@ -61,9 +65,9 @@
timeout = setTimeout(runWhenOnline, waitTime); timeout = setTimeout(runWhenOnline, waitTime);
} }
var initComplete; let initComplete;
Whisper.RotateSignedPreKeyListener = { Whisper.RotateSignedPreKeyListener = {
init: function(events, newVersion) { init(events, newVersion) {
if (initComplete) { if (initComplete) {
console.log('Rotate signed prekey listener: Already initialized'); console.log('Rotate signed prekey listener: Already initialized');
return; return;
@ -76,7 +80,7 @@
setTimeoutForNextRun(); setTimeoutForNextRun();
} }
events.on('timetravel', function() { events.on('timetravel', () => {
if (Whisper.Registration.isDone()) { if (Whisper.Registration.isDone()) {
setTimeoutForNextRun(); setTimeoutForNextRun();
} }

View file

@ -1,4 +1,8 @@
$(document).on('keyup', function(e) { /* global $, Whisper */
$(document).on('keyup', e => {
'use strict';
if (e.keyCode === 27) { if (e.keyCode === 27) {
window.closeSettings(); window.closeSettings();
} }
@ -7,6 +11,7 @@ $(document).on('keyup', function(e) {
const $body = $(document.body); const $body = $(document.body);
$body.addClass(`${window.theme}-theme`); $body.addClass(`${window.theme}-theme`);
// eslint-disable-next-line strict
const getInitialData = async () => ({ const getInitialData = async () => ({
deviceName: await window.getDeviceName(), deviceName: await window.getDeviceName(),
@ -23,7 +28,11 @@ const getInitialData = async () => ({
}); });
window.initialRequest = getInitialData(); window.initialRequest = getInitialData();
// eslint-disable-next-line more/no-then
window.initialRequest.then(data => { window.initialRequest.then(data => {
'use strict';
window.initialData = data; window.initialData = data;
window.view = new Whisper.SettingsView(); window.view = new Whisper.SettingsView();
window.view.$el.appendTo($body); window.view.$el.appendTo($body);

File diff suppressed because it is too large Load diff

View file

@ -1,152 +1,153 @@
(function() { /* global require, process, _ */
var electron = require('electron');
var remote = electron.remote;
var app = remote.app;
var webFrame = electron.webFrame;
var path = require('path');
var osLocale = require('os-locale'); /* eslint-disable strict */
var os = require('os');
var semver = require('semver');
var spellchecker = require('spellchecker');
// `remote.require` since `Menu` is a main-process module. const electron = require('electron');
var buildEditorContextMenu = remote.require('electron-editor-context-menu');
var EN_VARIANT = /^en/; const osLocale = require('os-locale');
const os = require('os');
const semver = require('semver');
const spellchecker = require('spellchecker');
// Prevent the spellchecker from showing contractions as errors. const { remote, webFrame } = electron;
var ENGLISH_SKIP_WORDS = [
'ain',
'couldn',
'didn',
'doesn',
'hadn',
'hasn',
'mightn',
'mustn',
'needn',
'oughtn',
'shan',
'shouldn',
'wasn',
'weren',
'wouldn',
];
function setupLinux(locale) { // `remote.require` since `Menu` is a main-process module.
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') { const buildEditorContextMenu = remote.require('electron-editor-context-menu');
// apt-get install hunspell-<locale> can be run for easy access to other dictionaries
var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
console.log( const EN_VARIANT = /^en/;
'Detected Linux. Setting up spell check with locale',
locale,
'and dictionary location',
location
);
spellchecker.setDictionary(locale, location);
} else {
console.log('Detected Linux. Using default en_US spell check dictionary');
}
}
function setupWin7AndEarlier(locale) { // Prevent the spellchecker from showing contractions as errors.
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') { const ENGLISH_SKIP_WORDS = [
var location = process.env.HUNSPELL_DICTIONARIES; 'ain',
'couldn',
'didn',
'doesn',
'hadn',
'hasn',
'mightn',
'mustn',
'needn',
'oughtn',
'shan',
'shouldn',
'wasn',
'weren',
'wouldn',
];
console.log( function setupLinux(locale) {
'Detected Windows 7 or below. Setting up spell-check with locale', if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
locale, // apt-get install hunspell-<locale> can be run for easy access
'and dictionary location', // to other dictionaries
location const location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
);
spellchecker.setDictionary(locale, location);
} else {
console.log(
'Detected Windows 7 or below. Using default en_US spell check dictionary'
);
}
}
// We load locale this way and not via app.getLocale() because this call returns console.log(
// 'es_ES' and not just 'es.' And hunspell requires the fully-qualified locale. 'Detected Linux. Setting up spell check with locale',
var locale = osLocale.sync().replace('-', '_'); locale,
'and dictionary location',
// The LANG environment variable is how node spellchecker finds its default language: location
// https://github.com/atom/node-spellchecker/blob/59d2d5eee5785c4b34e9669cd5d987181d17c098/lib/spellchecker.js#L29 );
if (!process.env.LANG) { spellchecker.setDictionary(locale, location);
process.env.LANG = locale;
}
if (process.platform === 'linux') {
setupLinux(locale);
} else if (
process.platform === 'windows' &&
semver.lt(os.release(), '8.0.0')
) {
setupWin7AndEarlier(locale);
} else { } else {
// OSX and Windows 8+ have OS-level spellcheck APIs console.log('Detected Linux. Using default en_US spell check dictionary');
console.log('Using OS-level spell check API with locale', process.env.LANG);
} }
}
var simpleChecker = (window.spellChecker = { function setupWin7AndEarlier(locale) {
spellCheck: function(text) { if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
return !this.isMisspelled(text); const location = process.env.HUNSPELL_DICTIONARIES;
},
isMisspelled: function(text) {
var misspelled = spellchecker.isMisspelled(text);
// The idea is to make this as fast as possible. For the many, many calls which console.log(
// don't result in the red squiggly, we minimize the number of checks. 'Detected Windows 7 or below. Setting up spell-check with locale',
if (!misspelled) { locale,
return false; 'and dictionary location',
} location
);
spellchecker.setDictionary(locale, location);
} else {
console.log(
'Detected Windows 7 or below. Using default en_US spell check dictionary'
);
}
}
// Only if we think we've found an error do we check the locale and skip list. // We load locale this way and not via app.getLocale() because this call returns
if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, text)) { // 'es_ES' and not just 'es.' And hunspell requires the fully-qualified locale.
return false; const locale = osLocale.sync().replace('-', '_');
}
return true; // The LANG environment variable is how node spellchecker finds its default language:
}, // https://github.com/atom/node-spellchecker/blob/59d2d5eee5785c4b34e9669cd5d987181d17c098/lib/spellchecker.js#L29
getSuggestions: function(text) { if (!process.env.LANG) {
return spellchecker.getCorrectionsForMisspelling(text); process.env.LANG = locale;
}, }
add: function(text) {
spellchecker.add(text);
},
});
webFrame.setSpellCheckProvider( if (process.platform === 'linux') {
'en-US', setupLinux(locale);
// Not sure what this parameter (`autoCorrectWord`) does: https://github.com/atom/electron/issues/4371 } else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) {
// The documentation for `webFrame.setSpellCheckProvider` passes `true` so we do too. setupWin7AndEarlier(locale);
true, } else {
simpleChecker // OSX and Windows 8+ have OS-level spellcheck APIs
); console.log('Using OS-level spell check API with locale', process.env.LANG);
}
window.addEventListener('contextmenu', function(e) { const simpleChecker = {
// Only show the context menu in text editors. spellCheck(text) {
if (!e.target.closest('textarea, input, [contenteditable="true"]')) { return !this.isMisspelled(text);
return; },
isMisspelled(text) {
const misspelled = spellchecker.isMisspelled(text);
// The idea is to make this as fast as possible. For the many, many calls which
// don't result in the red squiggly, we minimize the number of checks.
if (!misspelled) {
return false;
} }
var selectedText = window.getSelection().toString(); // Only if we think we've found an error do we check the locale and skip list.
var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText); if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, text)) {
var spellingSuggestions = return false;
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5); }
var menu = buildEditorContextMenu({
isMisspelled: isMisspelled,
spellingSuggestions: spellingSuggestions,
});
// The 'contextmenu' event is emitted after 'selectionchange' has fired but possibly before the return true;
// visible selection has changed. Try to wait to show the menu until after that, otherwise the },
// visible selection will update after the menu dismisses and look weird. getSuggestions(text) {
setTimeout(function() { return spellchecker.getCorrectionsForMisspelling(text);
menu.popup(remote.getCurrentWindow()); },
}, 30); add(text) {
spellchecker.add(text);
},
};
window.spellChecker = simpleChecker;
webFrame.setSpellCheckProvider(
'en-US',
// Not sure what this parameter (`autoCorrectWord`) does: https://github.com/atom/electron/issues/4371
// The documentation for `webFrame.setSpellCheckProvider` passes `true` so we do too.
true,
simpleChecker
);
window.addEventListener('contextmenu', e => {
// Only show the context menu in text editors.
if (!e.target.closest('textarea, input, [contenteditable="true"]')) {
return;
}
const selectedText = window.getSelection().toString();
const isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
const spellingSuggestions =
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
const menu = buildEditorContextMenu({
isMisspelled,
spellingSuggestions,
}); });
})();
// The 'contextmenu' event is emitted after 'selectionchange' has fired
// but possibly before the visible selection has changed. Try to wait
// to show the menu until after that, otherwise the visible selection
// will update after the menu dismisses and look weird.
setTimeout(() => {
menu.popup(remote.getCurrentWindow());
}, 30);
});

View file

@ -1,58 +1,64 @@
/* global Backbone, Whisper */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var Item = Backbone.Model.extend({ const Item = Backbone.Model.extend({
database: Whisper.Database, database: Whisper.Database,
storeName: 'items', storeName: 'items',
}); });
var ItemCollection = Backbone.Collection.extend({ const ItemCollection = Backbone.Collection.extend({
model: Item, model: Item,
storeName: 'items', storeName: 'items',
database: Whisper.Database, database: Whisper.Database,
}); });
var ready = false; let ready = false;
var items = new ItemCollection(); const items = new ItemCollection();
items.on('reset', function() { items.on('reset', () => {
ready = true; ready = true;
}); });
window.storage = { window.storage = {
/***************************** /** ***************************
*** Base Storage Routines *** *** Base Storage Routines ***
*****************************/ **************************** */
put: function(key, value) { put(key, value) {
if (value === undefined) { if (value === undefined) {
throw new Error('Tried to store undefined'); throw new Error('Tried to store undefined');
} }
if (!ready) { if (!ready) {
console.log('Called storage.put before storage is ready. key:', key); console.log('Called storage.put before storage is ready. key:', key);
} }
var item = items.add({ id: key, value: value }, { merge: true }); const item = items.add({ id: key, value }, { merge: true });
return new Promise(function(resolve, reject) { return new Promise((resolve, reject) => {
item.save().then(resolve, reject); item.save().then(resolve, reject);
}); });
}, },
get: function(key, defaultValue) { get(key, defaultValue) {
var item = items.get('' + key); const item = items.get(`${key}`);
if (!item) { if (!item) {
return defaultValue; return defaultValue;
} }
return item.get('value'); return item.get('value');
}, },
remove: function(key) { remove(key) {
var item = items.get('' + key); const item = items.get(`${key}`);
if (item) { if (item) {
items.remove(item); items.remove(item);
return new Promise(function(resolve, reject) { return new Promise((resolve, reject) => {
item.destroy().then(resolve, reject); item.destroy().then(resolve, reject);
}); });
} }
return Promise.resolve(); return Promise.resolve();
}, },
onready: function(callback) { onready(callback) {
if (ready) { if (ready) {
callback(); callback();
} else { } else {
@ -60,7 +66,7 @@
} }
}, },
fetch: function() { fetch() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
items items
.fetch({ reset: true }) .fetch({ reset: true })
@ -76,7 +82,7 @@
}); });
}, },
reset: function() { reset() {
items.reset(); items.reset();
}, },
}; };

View file

@ -1,10 +1,15 @@
/* global Backbone, Whisper, storage, _, ConversationController, $ */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.AppView = Backbone.View.extend({ Whisper.AppView = Backbone.View.extend({
initialize: function(options) { initialize() {
this.inboxView = null; this.inboxView = null;
this.installView = null; this.installView = null;
@ -15,38 +20,41 @@
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
openInbox: 'openInbox', openInbox: 'openInbox',
}, },
applyTheme: function() { applyTheme() {
var theme = storage.get('theme-setting') || 'light'; const theme = storage.get('theme-setting') || 'light';
this.$el this.$el
.removeClass('light-theme') .removeClass('light-theme')
.removeClass('dark-theme') .removeClass('dark-theme')
.addClass(`${theme}-theme`); .addClass(`${theme}-theme`);
}, },
applyHideMenu: function() { applyHideMenu() {
var hideMenuBar = storage.get('hide-menu-bar', false); const hideMenuBar = storage.get('hide-menu-bar', false);
window.setAutoHideMenuBar(hideMenuBar); window.setAutoHideMenuBar(hideMenuBar);
window.setMenuBarVisibility(!hideMenuBar); window.setMenuBarVisibility(!hideMenuBar);
}, },
openView: function(view) { openView(view) {
this.el.innerHTML = ''; this.el.innerHTML = '';
this.el.append(view.el); this.el.append(view.el);
this.delegateEvents(); this.delegateEvents();
}, },
openDebugLog: function() { openDebugLog() {
this.closeDebugLog(); this.closeDebugLog();
this.debugLogView = new Whisper.DebugLogView(); this.debugLogView = new Whisper.DebugLogView();
this.debugLogView.$el.appendTo(this.el); this.debugLogView.$el.appendTo(this.el);
}, },
closeDebugLog: function() { closeDebugLog() {
if (this.debugLogView) { if (this.debugLogView) {
this.debugLogView.remove(); this.debugLogView.remove();
this.debugLogView = null; this.debugLogView = null;
} }
}, },
openImporter: function() { openImporter() {
window.addSetupMenuItems(); window.addSetupMenuItems();
this.resetViews(); this.resetViews();
var importView = (this.importView = new Whisper.ImportView());
const importView = new Whisper.ImportView();
this.importView = importView;
this.listenTo( this.listenTo(
importView, importView,
'light-import', 'light-import',
@ -54,21 +62,19 @@
); );
this.openView(this.importView); this.openView(this.importView);
}, },
finishLightImport: function() { finishLightImport() {
var options = { const options = {
hasExistingData: true, hasExistingData: true,
}; };
this.openInstaller(options); this.openInstaller(options);
}, },
closeImporter: function() { closeImporter() {
if (this.importView) { if (this.importView) {
this.importView.remove(); this.importView.remove();
this.importView = null; this.importView = null;
} }
}, },
openInstaller: function(options) { openInstaller(options = {}) {
options = options || {};
// If we're in the middle of import, we don't want to show the menu options // If we're in the middle of import, we don't want to show the menu options
// allowing the user to switch to other ways to set up the app. If they // allowing the user to switch to other ways to set up the app. If they
// switched back and forth in the middle of a light import, they'd lose all // switched back and forth in the middle of a light import, they'd lose all
@ -78,16 +84,18 @@
} }
this.resetViews(); this.resetViews();
var installView = (this.installView = new Whisper.InstallView(options)); const installView = new Whisper.InstallView(options);
this.installView = installView;
this.openView(this.installView); this.openView(this.installView);
}, },
closeInstaller: function() { closeInstaller() {
if (this.installView) { if (this.installView) {
this.installView.remove(); this.installView.remove();
this.installView = null; this.installView = null;
} }
}, },
openStandalone: function() { openStandalone() {
if (window.getEnvironment() !== 'production') { if (window.getEnvironment() !== 'production') {
window.addSetupMenuItems(); window.addSetupMenuItems();
this.resetViews(); this.resetViews();
@ -95,19 +103,18 @@
this.openView(this.standaloneView); this.openView(this.standaloneView);
} }
}, },
closeStandalone: function() { closeStandalone() {
if (this.standaloneView) { if (this.standaloneView) {
this.standaloneView.remove(); this.standaloneView.remove();
this.standaloneView = null; this.standaloneView = null;
} }
}, },
resetViews: function() { resetViews() {
this.closeInstaller(); this.closeInstaller();
this.closeImporter(); this.closeImporter();
this.closeStandalone(); this.closeStandalone();
}, },
openInbox: function(options) { openInbox(options = {}) {
options = options || {};
// The inbox can be created before the 'empty' event fires or afterwards. If // The inbox can be created before the 'empty' event fires or afterwards. If
// before, it's straightforward: the onEmpty() handler below updates the // before, it's straightforward: the onEmpty() handler below updates the
// view directly, and we're in good shape. If we create the inbox late, we // view directly, and we're in good shape. If we create the inbox late, we
@ -130,44 +137,38 @@
// this.initialLoadComplete between the start of this method and the // this.initialLoadComplete between the start of this method and the
// creation of inboxView. // creation of inboxView.
this.inboxView = new Whisper.InboxView({ this.inboxView = new Whisper.InboxView({
model: self, window,
window: window,
initialLoadComplete: options.initialLoadComplete, initialLoadComplete: options.initialLoadComplete,
}); });
return ConversationController.loadPromise().then( return ConversationController.loadPromise().then(() => {
function() {
this.openView(this.inboxView);
}.bind(this)
);
} else {
if (!$.contains(this.el, this.inboxView.el)) {
this.openView(this.inboxView); this.openView(this.inboxView);
} });
window.focus(); // FIXME
return Promise.resolve();
} }
if (!$.contains(this.el, this.inboxView.el)) {
this.openView(this.inboxView);
}
window.focus(); // FIXME
return Promise.resolve();
}, },
onEmpty: function() { onEmpty() {
var view = this.inboxView; const view = this.inboxView;
this.initialLoadComplete = true; this.initialLoadComplete = true;
if (view) { if (view) {
view.onEmpty(); view.onEmpty();
} }
}, },
onProgress: function(count) { onProgress(count) {
var view = this.inboxView; const view = this.inboxView;
if (view) { if (view) {
view.onProgress(count); view.onProgress(count);
} }
}, },
openConversation: function(conversation) { openConversation(conversation) {
if (conversation) { if (conversation) {
this.openInbox().then( this.openInbox().then(() => {
function() { this.inboxView.openConversation(null, conversation);
this.inboxView.openConversation(null, conversation); });
}.bind(this)
);
} }
}, },
}); });

View file

@ -1,11 +1,15 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.AttachmentPreviewView = Whisper.View.extend({ Whisper.AttachmentPreviewView = Whisper.View.extend({
className: 'attachment-preview', className: 'attachment-preview',
templateName: 'attachment-preview', templateName: 'attachment-preview',
render_attributes: function() { render_attributes() {
return { source: this.src }; return { source: this.src };
}, },
}); });

View file

@ -1,5 +1,9 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.BannerView = Whisper.View.extend({ Whisper.BannerView = Whisper.View.extend({
@ -9,7 +13,7 @@
'click .dismiss': 'onDismiss', 'click .dismiss': 'onDismiss',
'click .body': 'onClick', 'click .body': 'onClick',
}, },
initialize: function(options) { initialize(options) {
this.message = options.message; this.message = options.message;
this.callbacks = { this.callbacks = {
onDismiss: options.onDismiss, onDismiss: options.onDismiss,
@ -17,16 +21,16 @@
}; };
this.render(); this.render();
}, },
render_attributes: function() { render_attributes() {
return { return {
message: this.message, message: this.message,
}; };
}, },
onDismiss: function(e) { onDismiss(e) {
this.callbacks.onDismiss(); this.callbacks.onDismiss();
e.stopPropagation(); e.stopPropagation();
}, },
onClick: function() { onClick() {
this.callbacks.onClick(); this.callbacks.onClick();
}, },
}); });

View file

@ -1,11 +1,15 @@
/* global Whisper, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ConfirmationDialogView = Whisper.View.extend({ Whisper.ConfirmationDialogView = Whisper.View.extend({
className: 'confirmation-dialog modal', className: 'confirmation-dialog modal',
templateName: 'confirmation-dialog', templateName: 'confirmation-dialog',
initialize: function(options) { initialize(options) {
this.message = options.message; this.message = options.message;
this.hideCancel = options.hideCancel; this.hideCancel = options.hideCancel;
@ -22,7 +26,7 @@
'click .ok': 'ok', 'click .ok': 'ok',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
render_attributes: function() { render_attributes() {
return { return {
message: this.message, message: this.message,
showCancel: !this.hideCancel, showCancel: !this.hideCancel,
@ -30,24 +34,24 @@
ok: this.okText, ok: this.okText,
}; };
}, },
ok: function() { ok() {
this.remove(); this.remove();
if (this.resolve) { if (this.resolve) {
this.resolve(); this.resolve();
} }
}, },
cancel: function() { cancel() {
this.remove(); this.remove();
if (this.reject) { if (this.reject) {
this.reject(); this.reject();
} }
}, },
onKeyup: function(event) { onKeyup(event) {
if (event.key === 'Escape' || event.key === 'Esc') { if (event.key === 'Escape' || event.key === 'Esc') {
this.cancel(); this.cancel();
} }
}, },
focusCancel: function() { focusCancel() {
this.$('.cancel').focus(); this.$('.cancel').focus();
}, },
}); });

View file

@ -1,5 +1,4 @@
/* global Whisper: false */ /* global Whisper: false */
/* global i18n: false */
/* global textsecure: false */ /* global textsecure: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names

View file

@ -1,18 +1,22 @@
/* global Whisper, _, extension, Backbone, Mustache */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
// list of conversations, showing user/group and last message sent // list of conversations, showing user/group and last message sent
Whisper.ConversationListItemView = Whisper.View.extend({ Whisper.ConversationListItemView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: function() { className() {
return 'conversation-list-item contact ' + this.model.cid; return `conversation-list-item contact ${this.model.cid}`;
}, },
templateName: 'conversation-preview', templateName: 'conversation-preview',
events: { events: {
click: 'select', click: 'select',
}, },
initialize: function() { initialize() {
// auto update // auto update
this.listenTo( this.listenTo(
this.model, this.model,
@ -22,7 +26,7 @@
this.listenTo(this.model, 'destroy', this.remove); // auto update this.listenTo(this.model, 'destroy', this.remove); // auto update
this.listenTo(this.model, 'opened', this.markSelected); // auto update this.listenTo(this.model, 'opened', this.markSelected); // auto update
var updateLastMessage = _.debounce( const updateLastMessage = _.debounce(
this.model.updateLastMessage.bind(this.model), this.model.updateLastMessage.bind(this.model),
1000 1000
); );
@ -33,23 +37,21 @@
); );
this.listenTo(this.model, 'newmessage', updateLastMessage); this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed( extension.windows.onClosed(() => {
function() { this.stopListening();
this.stopListening(); });
}.bind(this)
);
this.timeStampView = new Whisper.TimestampView({ brief: true }); this.timeStampView = new Whisper.TimestampView({ brief: true });
this.model.updateLastMessage(); this.model.updateLastMessage();
}, },
markSelected: function() { markSelected() {
this.$el this.$el
.addClass('selected') .addClass('selected')
.siblings('.selected') .siblings('.selected')
.removeClass('selected'); .removeClass('selected');
}, },
select: function(e) { select() {
this.markSelected(); this.markSelected();
this.$el.trigger('select', this.model); this.$el.trigger('select', this.model);
}, },
@ -66,7 +68,7 @@
Backbone.View.prototype.remove.call(this); Backbone.View.prototype.remove.call(this);
}, },
render: function() { render() {
const lastMessage = this.model.get('lastMessage'); const lastMessage = this.model.get('lastMessage');
this.$el.html( this.$el.html(
@ -117,7 +119,7 @@
this.$('.last-message').append(this.bodyView.el); this.$('.last-message').append(this.bodyView.el);
} }
var unread = this.model.get('unreadCount'); const unread = this.model.get('unreadCount');
if (unread > 0) { if (unread > 0) {
this.$el.addClass('unread'); this.$el.addClass('unread');
} else { } else {

View file

@ -1,12 +1,16 @@
/* global Whisper, getInboxCollection */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ConversationListView = Whisper.ListView.extend({ Whisper.ConversationListView = Whisper.ListView.extend({
tagName: 'div', tagName: 'div',
itemView: Whisper.ConversationListItemView, itemView: Whisper.ConversationListItemView,
updateLocation: function(conversation) { updateLocation(conversation) {
var $el = this.$('.' + conversation.cid); const $el = this.$(`.${conversation.cid}`);
if (!$el || !$el.length) { if (!$el || !$el.length) {
console.log( console.log(
@ -23,11 +27,11 @@
return; return;
} }
var $allConversations = this.$('.conversation-list-item'); const $allConversations = this.$('.conversation-list-item');
var inboxCollection = getInboxCollection(); const inboxCollection = getInboxCollection();
var index = inboxCollection.indexOf(conversation); const index = inboxCollection.indexOf(conversation);
var elIndex = $allConversations.index($el); const elIndex = $allConversations.index($el);
if (elIndex < 0) { if (elIndex < 0) {
console.log( console.log(
'updateLocation: did not find index for conversation', 'updateLocation: did not find index for conversation',
@ -43,8 +47,8 @@
} else if (index === this.collection.length - 1) { } else if (index === this.collection.length - 1) {
this.$el.append($el); this.$el.append($el);
} else { } else {
var targetConversation = inboxCollection.at(index - 1); const targetConversation = inboxCollection.at(index - 1);
var target = this.$('.' + targetConversation.cid); const target = this.$(`.${targetConversation.cid}`);
$el.insertAfter(target); $el.insertAfter(target);
} }
@ -54,8 +58,8 @@
}); });
} }
}, },
removeItem: function(conversation) { removeItem(conversation) {
var $el = this.$('.' + conversation.cid); const $el = this.$(`.${conversation.cid}`);
if ($el && $el.length > 0) { if ($el && $el.length > 0) {
$el.remove(); $el.remove();
} }

View file

@ -1,13 +0,0 @@
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var ErrorView = Whisper.View.extend({
className: 'error',
templateName: 'generic-error',
render_attributes: function() {
return this.model;
},
});
})();

View file

@ -1,12 +1,16 @@
/* global Whisper, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
// TODO: take a title string which could replace the 'members' header // TODO: take a title string which could replace the 'members' header
Whisper.GroupMemberList = Whisper.View.extend({ Whisper.GroupMemberList = Whisper.View.extend({
className: 'group-member-list panel', className: 'group-member-list panel',
templateName: 'group-member-list', templateName: 'group-member-list',
initialize: function(options) { initialize(options) {
this.needVerify = options.needVerify; this.needVerify = options.needVerify;
this.render(); this.render();
@ -22,15 +26,15 @@
this.$('.container').append(this.member_list_view.el); this.$('.container').append(this.member_list_view.el);
}, },
render_attributes: function() { render_attributes() {
var summary; let summary;
if (this.needVerify) { if (this.needVerify) {
summary = i18n('membersNeedingVerification'); summary = i18n('membersNeedingVerification');
} }
return { return {
members: i18n('groupMembers'), members: i18n('groupMembers'),
summary: summary, summary,
}; };
}, },
}); });

View file

@ -1,3 +1,6 @@
/* global Backbone, Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
@ -6,19 +9,19 @@
Whisper.GroupUpdateView = Backbone.View.extend({ Whisper.GroupUpdateView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'group-update', className: 'group-update',
render: function() { render() {
//TODO l10n // TODO l10n
if (this.model.left) { if (this.model.left) {
this.$el.text(this.model.left + ' left the group'); this.$el.text(`${this.model.left} left the group`);
return this; return this;
} }
var messages = ['Updated the group.']; const messages = ['Updated the group.'];
if (this.model.name) { if (this.model.name) {
messages.push("Title is now '" + this.model.name + "'."); messages.push(`Title is now '${this.model.name}'.`);
} }
if (this.model.joined) { if (this.model.joined) {
messages.push(this.model.joined.join(', ') + ' joined the group'); messages.push(`${this.model.joined.join(', ')} joined the group`);
} }
this.$el.text(messages.join(' ')); this.$el.text(messages.join(' '));

View file

@ -1,13 +1,17 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.HintView = Whisper.View.extend({ Whisper.HintView = Whisper.View.extend({
templateName: 'hint', templateName: 'hint',
initialize: function(options) { initialize(options) {
this.content = options.content; this.content = options.content;
}, },
render_attributes: function() { render_attributes() {
return { content: this.content }; return { content: this.content };
}, },
}); });

View file

@ -1,5 +1,9 @@
/* global Whisper, loadImage */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
/* /*
@ -7,26 +11,26 @@
*/ */
Whisper.IdenticonSVGView = Whisper.View.extend({ Whisper.IdenticonSVGView = Whisper.View.extend({
templateName: 'identicon-svg', templateName: 'identicon-svg',
initialize: function(options) { initialize(options) {
this.render_attributes = options; this.render_attributes = options;
this.render_attributes.color = COLORS[this.render_attributes.color]; this.render_attributes.color = COLORS[this.render_attributes.color];
}, },
getSVGUrl: function() { getSVGUrl() {
var html = this.render().$el.html(); const html = this.render().$el.html();
var svg = new Blob([html], { type: 'image/svg+xml;charset=utf-8' }); const svg = new Blob([html], { type: 'image/svg+xml;charset=utf-8' });
return URL.createObjectURL(svg); return URL.createObjectURL(svg);
}, },
getDataUrl: function() { getDataUrl() {
var svgurl = this.getSVGUrl(); const svgurl = this.getSVGUrl();
return new Promise(function(resolve) { return new Promise(resolve => {
var img = document.createElement('img'); const img = document.createElement('img');
img.onload = function() { img.onload = () => {
var canvas = loadImage.scale(img, { const canvas = loadImage.scale(img, {
canvas: true, canvas: true,
maxWidth: 100, maxWidth: 100,
maxHeight: 100, maxHeight: 100,
}); });
var ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(svgurl); URL.revokeObjectURL(svgurl);
resolve(canvas.toDataURL('image/png')); resolve(canvas.toDataURL('image/png'));
@ -37,7 +41,7 @@
}, },
}); });
var COLORS = { const COLORS = {
red: '#EF5350', red: '#EF5350',
pink: '#EC407A', pink: '#EC407A',
purple: '#AB47BC', purple: '#AB47BC',

View file

@ -1,11 +1,15 @@
/* global Whisper, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({ Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({
className: 'identity-key-send-error panel', className: 'identity-key-send-error panel',
templateName: 'identity-key-send-error', templateName: 'identity-key-send-error',
initialize: function(options) { initialize(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
@ -17,31 +21,31 @@
'click .send-anyway': 'sendAnyway', 'click .send-anyway': 'sendAnyway',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
showSafetyNumber: function() { showSafetyNumber() {
var view = new Whisper.KeyVerificationPanelView({ const view = new Whisper.KeyVerificationPanelView({
model: this.model, model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
}, },
sendAnyway: function() { sendAnyway() {
this.resetPanel(); this.resetPanel();
this.trigger('send-anyway'); this.trigger('send-anyway');
}, },
cancel: function() { cancel() {
this.resetPanel(); this.resetPanel();
}, },
render_attributes: function() { render_attributes() {
var send = i18n('sendAnyway'); let send = i18n('sendAnyway');
if (this.wasUnverified && !this.model.isUnverified()) { if (this.wasUnverified && !this.model.isUnverified()) {
send = i18n('resend'); send = i18n('resend');
} }
var errorExplanation = i18n('identityKeyErrorOnSend', [ const errorExplanation = i18n('identityKeyErrorOnSend', [
this.model.getTitle(), this.model.getTitle(),
this.model.getTitle(), this.model.getTitle(),
]); ]);
return { return {
errorExplanation: errorExplanation, errorExplanation,
showSafetyNumber: i18n('showSafetyNumber'), showSafetyNumber: i18n('showSafetyNumber'),
sendAnyway: send, sendAnyway: send,
cancel: i18n('cancel'), cancel: i18n('cancel'),

View file

@ -1,37 +1,43 @@
/* global Whisper, storage, i18n, ConversationController */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var State = { const State = {
IMPORTING: 1, IMPORTING: 1,
COMPLETE: 2, COMPLETE: 2,
LIGHT_COMPLETE: 3, LIGHT_COMPLETE: 3,
}; };
var IMPORT_STARTED = 'importStarted'; const IMPORT_STARTED = 'importStarted';
var IMPORT_COMPLETE = 'importComplete'; const IMPORT_COMPLETE = 'importComplete';
var IMPORT_LOCATION = 'importLocation'; const IMPORT_LOCATION = 'importLocation';
Whisper.Import = { Whisper.Import = {
isStarted: function() { isStarted() {
return Boolean(storage.get(IMPORT_STARTED)); return Boolean(storage.get(IMPORT_STARTED));
}, },
isComplete: function() { isComplete() {
return Boolean(storage.get(IMPORT_COMPLETE)); return Boolean(storage.get(IMPORT_COMPLETE));
}, },
isIncomplete: function() { isIncomplete() {
return this.isStarted() && !this.isComplete(); return this.isStarted() && !this.isComplete();
}, },
start: function() { start() {
return storage.put(IMPORT_STARTED, true); return storage.put(IMPORT_STARTED, true);
}, },
complete: function() { complete() {
return storage.put(IMPORT_COMPLETE, true); return storage.put(IMPORT_COMPLETE, true);
}, },
saveLocation: function(location) { saveLocation(location) {
return storage.put(IMPORT_LOCATION, location); return storage.put(IMPORT_LOCATION, location);
}, },
reset: function() { reset() {
return Whisper.Database.clear(); return Whisper.Database.clear();
}, },
}; };
@ -45,7 +51,7 @@
'click .cancel': 'onCancel', 'click .cancel': 'onCancel',
'click .register': 'onRegister', 'click .register': 'onRegister',
}, },
initialize: function() { initialize() {
if (Whisper.Import.isIncomplete()) { if (Whisper.Import.isIncomplete()) {
this.error = true; this.error = true;
} }
@ -53,7 +59,7 @@
this.render(); this.render();
this.pending = Promise.resolve(); this.pending = Promise.resolve();
}, },
render_attributes: function() { render_attributes() {
if (this.error) { if (this.error) {
return { return {
isError: true, isError: true,
@ -64,9 +70,9 @@
}; };
} }
var restartButton = i18n('importCompleteStartButton'); let restartButton = i18n('importCompleteStartButton');
var registerButton = i18n('importCompleteLinkButton'); let registerButton = i18n('importCompleteLinkButton');
var step = 'step2'; let step = 'step2';
if (this.state === State.IMPORTING) { if (this.state === State.IMPORTING) {
step = 'step3'; step = 'step3';
@ -89,22 +95,22 @@
isStep4: step === 'step4', isStep4: step === 'step4',
completeHeader: i18n('importCompleteHeader'), completeHeader: i18n('importCompleteHeader'),
restartButton: restartButton, restartButton,
registerButton: registerButton, registerButton,
}; };
}, },
onRestart: function() { onRestart() {
return window.restart(); return window.restart();
}, },
onCancel: function() { onCancel() {
this.trigger('cancel'); this.trigger('cancel');
}, },
onImport: function() { onImport() {
window.Signal.Backup.getDirectoryForImport().then( window.Signal.Backup.getDirectoryForImport().then(
function(directory) { directory => {
this.doImport(directory); this.doImport(directory);
}.bind(this), },
function(error) { error => {
if (error.name !== 'ChooseError') { if (error.name !== 'ChooseError') {
console.log( console.log(
'Error choosing directory:', 'Error choosing directory:',
@ -114,13 +120,13 @@
} }
); );
}, },
onRegister: function() { onRegister() {
// AppView listens for this, and opens up InstallView to the QR code step to // AppView listens for this, and opens up InstallView to the QR code step to
// finish setting this device up. // finish setting this device up.
this.trigger('light-import'); this.trigger('light-import');
}, },
doImport: function(directory) { doImport(directory) {
window.removeSetupMenuItems(); window.removeSetupMenuItems();
this.error = null; this.error = null;
@ -129,70 +135,64 @@
// Wait for prior database interaction to complete // Wait for prior database interaction to complete
this.pending = this.pending this.pending = this.pending
.then(function() { .then(() =>
// For resilience to interruption, clear database both before and on failure // For resilience to interruption, clear database both before and on failure
return Whisper.Import.reset(); Whisper.Import.reset()
}) )
.then(function() { .then(() =>
return Promise.all([ Promise.all([
Whisper.Import.start(), Whisper.Import.start(),
window.Signal.Backup.importFromDirectory(directory), window.Signal.Backup.importFromDirectory(directory),
]); ])
})
.then(
function(results) {
var importResult = results[1];
// A full import changes so much we need a restart of the app
if (importResult.fullImport) {
return this.finishFullImport(directory);
}
// A light import just brings in contacts, groups, and messages. And we need a
// normal link to finish the process.
return this.finishLightImport(directory);
}.bind(this)
) )
.catch( .then(results => {
function(error) { const importResult = results[1];
console.log(
'Error importing:',
error && error.stack ? error.stack : error
);
this.error = error || new Error('Something went wrong!'); // A full import changes so much we need a restart of the app
this.state = null; if (importResult.fullImport) {
this.render(); return this.finishFullImport(directory);
}
return Whisper.Import.reset(); // A light import just brings in contacts, groups, and messages. And we need a
}.bind(this) // normal link to finish the process.
); return this.finishLightImport(directory);
})
.catch(error => {
console.log(
'Error importing:',
error && error.stack ? error.stack : error
);
this.error = error || new Error('Something went wrong!');
this.state = null;
this.render();
return Whisper.Import.reset();
});
}, },
finishLightImport: function(directory) { finishLightImport(directory) {
ConversationController.reset(); ConversationController.reset();
return ConversationController.load() return ConversationController.load()
.then(function() { .then(() =>
return Promise.all([ Promise.all([
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete(), Whisper.Import.complete(),
]); ])
}) )
.then( .then(() => {
function() { this.state = State.LIGHT_COMPLETE;
this.state = State.LIGHT_COMPLETE; this.render();
this.render(); });
}.bind(this)
);
}, },
finishFullImport: function(directory) { finishFullImport(directory) {
// Catching in-memory cache up with what's in indexeddb now... // Catching in-memory cache up with what's in indexeddb now...
// NOTE: this fires storage.onready, listened to across the app. We'll restart // NOTE: this fires storage.onready, listened to across the app. We'll restart
// to complete the install to start up cleanly with everything now in the DB. // to complete the install to start up cleanly with everything now in the DB.
return storage return storage
.fetch() .fetch()
.then(function() { .then(() =>
return Promise.all([ Promise.all([
// Clearing any migration-related state inherited from the Chrome App // Clearing any migration-related state inherited from the Chrome App
storage.remove('migrationState'), storage.remove('migrationState'),
storage.remove('migrationEnabled'), storage.remove('migrationEnabled'),
@ -201,14 +201,12 @@
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete(), Whisper.Import.complete(),
]); ])
}) )
.then( .then(() => {
function() { this.state = State.COMPLETE;
this.state = State.COMPLETE; this.render();
this.render(); });
}.bind(this)
);
}, },
}); });
})(); })();

View file

@ -1,8 +1,14 @@
/* global Whisper, i18n, getAccountManager, $, textsecure, QRCode */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var Steps = { const Steps = {
INSTALL_SIGNAL: 2, INSTALL_SIGNAL: 2,
SCAN_QR_CODE: 3, SCAN_QR_CODE: 3,
ENTER_NAME: 4, ENTER_NAME: 4,
@ -11,9 +17,9 @@
NETWORK_ERROR: 'NetworkError', NETWORK_ERROR: 'NetworkError',
}; };
var DEVICE_NAME_SELECTOR = 'input.device-name'; const DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1; const CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411; const TOO_MANY_DEVICES = 411;
Whisper.InstallView = Whisper.View.extend({ Whisper.InstallView = Whisper.View.extend({
templateName: 'link-flow-template', templateName: 'link-flow-template',
@ -23,9 +29,7 @@
'click .finish': 'finishLinking', 'click .finish': 'finishLinking',
// the actual next step happens in confirmNumber() on submit form #link-phone // the actual next step happens in confirmNumber() on submit form #link-phone
}, },
initialize: function(options) { initialize(options = {}) {
options = options || {};
this.selectStep(Steps.SCAN_QR_CODE); this.selectStep(Steps.SCAN_QR_CODE);
this.connect(); this.connect();
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
@ -34,18 +38,18 @@
this.shouldRetainData = this.shouldRetainData =
Whisper.Registration.everDone() || options.hasExistingData; Whisper.Registration.everDone() || options.hasExistingData;
}, },
render_attributes: function() { render_attributes() {
var errorMessage; let errorMessage;
if (this.error) { if (this.error) {
if ( if (
this.error.name === 'HTTPError' && this.error.name === 'HTTPError' &&
this.error.code == TOO_MANY_DEVICES this.error.code === TOO_MANY_DEVICES
) { ) {
errorMessage = i18n('installTooManyDevices'); errorMessage = i18n('installTooManyDevices');
} else if ( } else if (
this.error.name === 'HTTPError' && this.error.name === 'HTTPError' &&
this.error.code == CONNECTION_ERROR this.error.code === CONNECTION_ERROR
) { ) {
errorMessage = i18n('installConnectionFailed'); errorMessage = i18n('installConnectionFailed');
} else if (this.error.message === 'websocket closed') { } else if (this.error.message === 'websocket closed') {
@ -78,11 +82,11 @@
syncing: i18n('initialSync'), syncing: i18n('initialSync'),
}; };
}, },
selectStep: function(step) { selectStep(step) {
this.step = step; this.step = step;
this.render(); this.render();
}, },
connect: function() { connect() {
this.error = null; this.error = null;
this.selectStep(Steps.SCAN_QR_CODE); this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR(); this.clearQR();
@ -91,7 +95,7 @@
this.timeout = null; this.timeout = null;
} }
var accountManager = getAccountManager(); const accountManager = getAccountManager();
accountManager accountManager
.registerSecondDevice( .registerSecondDevice(
@ -100,7 +104,7 @@
) )
.catch(this.handleDisconnect.bind(this)); .catch(this.handleDisconnect.bind(this));
}, },
handleDisconnect: function(e) { handleDisconnect(e) {
console.log('provisioning failed', e.stack); console.log('provisioning failed', e.stack);
this.error = e; this.error = e;
@ -115,20 +119,20 @@
throw e; throw e;
} }
}, },
reconnect: function() { reconnect() {
if (this.timeout) { if (this.timeout) {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.timeout = null; this.timeout = null;
} }
this.timeout = setTimeout(this.connect.bind(this), 10000); this.timeout = setTimeout(this.connect.bind(this), 10000);
}, },
clearQR: function() { clearQR() {
this.$('#qr img').remove(); this.$('#qr img').remove();
this.$('#qr canvas').remove(); this.$('#qr canvas').remove();
this.$('#qr .container').show(); this.$('#qr .container').show();
this.$('#qr').removeClass('ready'); this.$('#qr').removeClass('ready');
}, },
setProvisioningUrl: function(url) { setProvisioningUrl(url) {
if ($('#qr').length === 0) { if ($('#qr').length === 0) {
console.log('Did not find #qr element in the DOM!'); console.log('Did not find #qr element in the DOM!');
return; return;
@ -139,63 +143,57 @@
this.$('#qr').removeAttr('title'); this.$('#qr').removeAttr('title');
this.$('#qr').addClass('ready'); this.$('#qr').addClass('ready');
}, },
setDeviceNameDefault: function() { setDeviceNameDefault() {
var deviceName = textsecure.storage.user.getDeviceName(); const deviceName = textsecure.storage.user.getDeviceName();
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName()); this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());
this.$(DEVICE_NAME_SELECTOR).focus(); this.$(DEVICE_NAME_SELECTOR).focus();
}, },
finishLinking: function() { finishLinking() {
// We use a form so we get submit-on-enter behavior // We use a form so we get submit-on-enter behavior
this.$('#link-phone').submit(); this.$('#link-phone').submit();
}, },
confirmNumber: function(number) { confirmNumber() {
var tsp = textsecure.storage.protocol; const tsp = textsecure.storage.protocol;
window.removeSetupMenuItems(); window.removeSetupMenuItems();
this.selectStep(Steps.ENTER_NAME); this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault(); this.setDeviceNameDefault();
return new Promise( return new Promise(resolve => {
function(resolve, reject) { this.$('#link-phone').submit(e => {
this.$('#link-phone').submit( e.stopPropagation();
function(e) { e.preventDefault();
e.stopPropagation();
e.preventDefault();
var name = this.$(DEVICE_NAME_SELECTOR).val(); let name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g, ''); // strip unicode null name = name.replace(/\0/g, ''); // strip unicode null
if (name.trim().length === 0) { if (name.trim().length === 0) {
this.$(DEVICE_NAME_SELECTOR).focus(); this.$(DEVICE_NAME_SELECTOR).focus();
return; return null;
} }
this.selectStep(Steps.PROGRESS_BAR); this.selectStep(Steps.PROGRESS_BAR);
var finish = function() { const finish = () => resolve(name);
resolve(name);
};
// Delete all data from database unless we're in the middle // Delete all data from database unless we're in the middle
// of a re-link, or we are finishing a light import. Without this, // of a re-link, or we are finishing a light import. Without this,
// app restarts at certain times can cause weird things to happen, // app restarts at certain times can cause weird things to happen,
// like data from a previous incomplete light import showing up // like data from a previous incomplete light import showing up
// after a new install. // after a new install.
if (this.shouldRetainData) { if (this.shouldRetainData) {
return finish(); return finish();
} }
tsp.removeAllData().then(finish, function(error) { return tsp.removeAllData().then(finish, error => {
console.log( console.log(
'confirmNumber: error clearing database', 'confirmNumber: error clearing database',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
finish(); finish();
}); });
}.bind(this) });
); });
}.bind(this)
);
}, },
}); });
})(); })();

View file

@ -1,5 +1,11 @@
/* global Whisper, textsecure, QRCode, dcodeIO, libsignal, i18n, _ */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.KeyVerificationPanelView = Whisper.View.extend({ Whisper.KeyVerificationPanelView = Whisper.View.extend({
@ -8,58 +14,54 @@
events: { events: {
'click button.verify': 'toggleVerified', 'click button.verify': 'toggleVerified',
}, },
initialize: function(options) { initialize(options) {
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
if (options.newKey) { if (options.newKey) {
this.theirKey = options.newKey; this.theirKey = options.newKey;
} }
this.loadKeys().then( this.loadKeys().then(() => {
function() { this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change', this.render); });
}.bind(this)
);
}, },
loadKeys: function() { loadKeys() {
return Promise.all([this.loadTheirKey(), this.loadOurKey()]) return Promise.all([this.loadTheirKey(), this.loadOurKey()])
.then(this.generateSecurityNumber.bind(this)) .then(this.generateSecurityNumber.bind(this))
.then(this.render.bind(this)); .then(this.render.bind(this));
//.then(this.makeQRCode.bind(this)); // .then(this.makeQRCode.bind(this));
}, },
makeQRCode: function() { makeQRCode() {
// Per Lilia: We can't turn this on until it generates a Latin1 string, as is // Per Lilia: We can't turn this on until it generates a Latin1 string, as is
// required by the mobile clients. // required by the mobile clients.
new QRCode(this.$('.qr')[0]).makeCode( new QRCode(this.$('.qr')[0]).makeCode(
dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64') dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')
); );
}, },
loadTheirKey: function() { loadTheirKey() {
return textsecure.storage.protocol.loadIdentityKey(this.model.id).then( return textsecure.storage.protocol
function(theirKey) { .loadIdentityKey(this.model.id)
.then(theirKey => {
this.theirKey = theirKey; this.theirKey = theirKey;
}.bind(this) });
);
}, },
loadOurKey: function() { loadOurKey() {
return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then( return textsecure.storage.protocol
function(ourKey) { .loadIdentityKey(this.ourNumber)
.then(ourKey => {
this.ourKey = ourKey; this.ourKey = ourKey;
}.bind(this) });
);
}, },
generateSecurityNumber: function() { generateSecurityNumber() {
return new libsignal.FingerprintGenerator(5200) return new libsignal.FingerprintGenerator(5200)
.createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey) .createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
.then( .then(securityNumber => {
function(securityNumber) { this.securityNumber = securityNumber;
this.securityNumber = securityNumber; });
}.bind(this)
);
}, },
onSafetyNumberChanged: function() { onSafetyNumberChanged() {
this.model.getProfiles().then(this.loadKeys.bind(this)); this.model.getProfiles().then(this.loadKeys.bind(this));
var dialog = new Whisper.ConfirmationDialogView({ const dialog = new Whisper.ConfirmationDialogView({
message: i18n('changedRightAfterVerify', [ message: i18n('changedRightAfterVerify', [
this.model.getTitle(), this.model.getTitle(),
this.model.getTitle(), this.model.getTitle(),
@ -70,65 +72,62 @@
dialog.$el.insertBefore(this.el); dialog.$el.insertBefore(this.el);
dialog.focusCancel(); dialog.focusCancel();
}, },
toggleVerified: function() { toggleVerified() {
this.$('button.verify').attr('disabled', true); this.$('button.verify').attr('disabled', true);
this.model this.model
.toggleVerified() .toggleVerified()
.catch( .catch(result => {
function(result) { if (result instanceof Error) {
if (result instanceof Error) { if (result.name === 'OutgoingIdentityKeyError') {
if (result.name === 'OutgoingIdentityKeyError') { this.onSafetyNumberChanged();
this.onSafetyNumberChanged();
} else {
console.log('failed to toggle verified:', result.stack);
}
} else { } else {
var keyError = _.some(result.errors, function(error) { console.log('failed to toggle verified:', result.stack);
return error.name === 'OutgoingIdentityKeyError';
});
if (keyError) {
this.onSafetyNumberChanged();
} else {
_.forEach(result.errors, function(error) {
console.log('failed to toggle verified:', error.stack);
});
}
} }
}.bind(this) } else {
) const keyError = _.some(
.then( result.errors,
function() { error => error.name === 'OutgoingIdentityKeyError'
this.$('button.verify').removeAttr('disabled'); );
}.bind(this) if (keyError) {
); this.onSafetyNumberChanged();
} else {
_.forEach(result.errors, error => {
console.log('failed to toggle verified:', error.stack);
});
}
}
})
.then(() => {
this.$('button.verify').removeAttr('disabled');
});
}, },
render_attributes: function() { render_attributes() {
var s = this.securityNumber; const s = this.securityNumber;
var chunks = []; const chunks = [];
for (var i = 0; i < s.length; i += 5) { for (let i = 0; i < s.length; i += 5) {
chunks.push(s.substring(i, i + 5)); chunks.push(s.substring(i, i + 5));
} }
var name = this.model.getTitle(); const name = this.model.getTitle();
var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name); const yourSafetyNumberWith = i18n(
var isVerified = this.model.isVerified(); 'yourSafetyNumberWith',
var verifyButton = isVerified ? i18n('unverify') : i18n('verify'); this.model.getTitle()
var verifiedStatus = isVerified );
const isVerified = this.model.isVerified();
const verifyButton = isVerified ? i18n('unverify') : i18n('verify');
const verifiedStatus = isVerified
? i18n('isVerified', name) ? i18n('isVerified', name)
: i18n('isNotVerified', name); : i18n('isNotVerified', name);
return { return {
learnMore: i18n('learnMore'), learnMore: i18n('learnMore'),
theirKeyUnknown: i18n('theirIdentityUnknown'), theirKeyUnknown: i18n('theirIdentityUnknown'),
yourSafetyNumberWith: i18n( yourSafetyNumberWith,
'yourSafetyNumberWith',
this.model.getTitle()
),
verifyHelp: i18n('verifyHelp', this.model.getTitle()), verifyHelp: i18n('verifyHelp', this.model.getTitle()),
verifyButton: verifyButton, verifyButton,
hasTheirKey: this.theirKey !== undefined, hasTheirKey: this.theirKey !== undefined,
chunks: chunks, chunks,
isVerified: isVerified, isVerified,
verifiedStatus: verifiedStatus, verifiedStatus,
}; };
}, },
}); });

View file

@ -1,34 +1,35 @@
/* global Whisper, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {};
var FIVE_SECONDS = 5 * 1000; window.Whisper = window.Whisper || {};
Whisper.LastSeenIndicatorView = Whisper.View.extend({ Whisper.LastSeenIndicatorView = Whisper.View.extend({
className: 'last-seen-indicator-view', className: 'last-seen-indicator-view',
templateName: 'last-seen-indicator-view', templateName: 'last-seen-indicator-view',
initialize: function(options) { initialize(options = {}) {
options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment(count) {
this.count += count; this.count += count;
this.render(); this.render();
}, },
getCount: function() { getCount() {
return this.count; return this.count;
}, },
render_attributes: function() { render_attributes() {
var unreadMessages = const unreadMessages =
this.count === 1 this.count === 1
? i18n('unreadMessage') ? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]); : i18n('unreadMessages', [this.count]);
return { return {
unreadMessages: unreadMessages, unreadMessages,
}; };
}, },
}); });

View file

@ -1,5 +1,9 @@
/* global Backbone, Whisper, _ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
/* /*
@ -9,27 +13,28 @@
Whisper.ListView = Backbone.View.extend({ Whisper.ListView = Backbone.View.extend({
tagName: 'ul', tagName: 'ul',
itemView: Backbone.View, itemView: Backbone.View,
initialize: function(options) { initialize(options) {
this.options = options || {}; this.options = options || {};
this.listenTo(this.collection, 'add', this.addOne); this.listenTo(this.collection, 'add', this.addOne);
this.listenTo(this.collection, 'reset', this.addAll); this.listenTo(this.collection, 'reset', this.addAll);
}, },
addOne: function(model) { addOne(model) {
if (this.itemView) { if (this.itemView) {
var options = _.extend({}, this.options.toInclude, { model: model }); const options = _.extend({}, this.options.toInclude, { model });
var view = new this.itemView(options); // eslint-disable-next-line new-cap
const view = new this.itemView(options);
this.$el.append(view.render().el); this.$el.append(view.render().el);
this.$el.trigger('add'); this.$el.trigger('add');
} }
}, },
addAll: function() { addAll() {
this.$el.html(''); this.$el.html('');
this.collection.each(this.addOne, this); this.collection.each(this.addOne, this);
}, },
render: function() { render() {
this.addAll(); this.addAll();
return this; return this;
}, },

View file

@ -1,32 +1,40 @@
/* global Whisper, i18n, _, ConversationController, Mustache, moment */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ContactView = Whisper.View.extend({ const ContactView = Whisper.View.extend({
className: 'contact-detail', className: 'contact-detail',
templateName: 'contact-detail', templateName: 'contact-detail',
initialize: function(options) { initialize(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
this.message = options.message; this.message = options.message;
var newIdentity = i18n('newIdentity'); const newIdentity = i18n('newIdentity');
this.errors = _.map(options.errors, function(error) { this.errors = _.map(options.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') { if (error.name === 'OutgoingIdentityKeyError') {
// eslint-disable-next-line no-param-reassign
error.message = newIdentity; error.message = newIdentity;
} }
return error; return error;
}); });
this.outgoingKeyError = _.find(this.errors, function(error) { this.outgoingKeyError = _.find(
return error.name === 'OutgoingIdentityKeyError'; this.errors,
}); error => error.name === 'OutgoingIdentityKeyError'
);
}, },
events: { events: {
click: 'onClick', click: 'onClick',
}, },
onClick: function() { onClick() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({ const view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model, model: this.model,
listenBack: this.listenBack, listenBack: this.listenBack,
resetPanel: this.resetPanel, resetPanel: this.resetPanel,
@ -40,41 +48,33 @@
view.$('.cancel').focus(); view.$('.cancel').focus();
} }
}, },
forceSend: function() { forceSend() {
this.model this.model
.updateVerified() .updateVerified()
.then( .then(() => {
function() { if (this.model.isUnverified()) {
if (this.model.isUnverified()) { return this.model.setVerifiedDefault();
return this.model.setVerifiedDefault(); }
} return null;
}.bind(this) })
) .then(() => this.model.isUntrusted())
.then( .then(untrusted => {
function() { if (untrusted) {
return this.model.isUntrusted(); return this.model.setApproved();
}.bind(this) }
) return null;
.then( })
function(untrusted) { .then(() => {
if (untrusted) { this.message.resend(this.outgoingKeyError.number);
return this.model.setApproved(); });
}
}.bind(this)
)
.then(
function() {
this.message.resend(this.outgoingKeyError.number);
}.bind(this)
);
}, },
onSendAnyway: function() { onSendAnyway() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
this.forceSend(); this.forceSend();
} }
}, },
render_attributes: function() { render_attributes() {
var showButton = Boolean(this.outgoingKeyError); const showButton = Boolean(this.outgoingKeyError);
return { return {
status: this.message.getStatus(this.model.id), status: this.message.getStatus(this.model.id),
@ -90,7 +90,7 @@
Whisper.MessageDetailView = Whisper.View.extend({ Whisper.MessageDetailView = Whisper.View.extend({
className: 'message-detail panel', className: 'message-detail panel',
templateName: 'message-detail', templateName: 'message-detail',
initialize: function(options) { initialize(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
@ -103,22 +103,22 @@
events: { events: {
'click button.delete': 'onDelete', 'click button.delete': 'onDelete',
}, },
onDelete: function() { onDelete() {
var dialog = new Whisper.ConfirmationDialogView({ const dialog = new Whisper.ConfirmationDialogView({
message: i18n('deleteWarning'), message: i18n('deleteWarning'),
okText: i18n('delete'), okText: i18n('delete'),
resolve: function() { resolve: () => {
this.model.destroy(); this.model.destroy();
this.resetPanel(); this.resetPanel();
}.bind(this), },
}); });
this.$el.prepend(dialog.el); this.$el.prepend(dialog.el);
dialog.focusCancel(); dialog.focusCancel();
}, },
getContacts: function() { getContacts() {
// Return the set of models to be rendered in this view // Return the set of models to be rendered in this view
var ids; let ids;
if (this.model.isIncoming()) { if (this.model.isIncoming()) {
ids = [this.model.get('source')]; ids = [this.model.get('source')];
} else if (this.model.isOutgoing()) { } else if (this.model.isOutgoing()) {
@ -130,13 +130,13 @@
} }
} }
return Promise.all( return Promise.all(
ids.map(function(number) { ids.map(number =>
return ConversationController.getOrCreateAndWait(number, 'private'); ConversationController.getOrCreateAndWait(number, 'private')
}) )
); );
}, },
renderContact: function(contact) { renderContact(contact) {
var view = new ContactView({ const view = new ContactView({
model: contact, model: contact,
errors: this.grouped[contact.id], errors: this.grouped[contact.id],
listenBack: this.listenBack, listenBack: this.listenBack,
@ -145,12 +145,10 @@
}).render(); }).render();
this.$('.contacts').append(view.el); this.$('.contacts').append(view.el);
}, },
render: function() { render() {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function( const errorsWithoutNumber = _.reject(this.model.get('errors'), error =>
error Boolean(error.number)
) { );
return Boolean(error.number);
});
this.$el.html( this.$el.html(
Mustache.render(_.result(this, 'template', ''), { Mustache.render(_.result(this, 'template', ''), {
@ -171,19 +169,14 @@
this.grouped = _.groupBy(this.model.get('errors'), 'number'); this.grouped = _.groupBy(this.model.get('errors'), 'number');
this.getContacts().then( this.getContacts().then(contacts => {
function(contacts) { _.sortBy(contacts, c => {
_.sortBy( const prefix = this.grouped[c.id] ? '0' : '1';
contacts, // this prefix ensures that contacts with errors are listed first;
function(c) { // otherwise it's alphabetical
var prefix = this.grouped[c.id] ? '0' : '1'; return prefix + c.getTitle();
// this prefix ensures that contacts with errors are listed first; }).forEach(this.renderContact.bind(this));
// otherwise it's alphabetical });
return prefix + c.getTitle();
}.bind(this)
).forEach(this.renderContact.bind(this));
}.bind(this)
);
}, },
}); });
})(); })();

View file

@ -1,5 +1,9 @@
/* global Whisper, _ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.MessageListView = Whisper.ListView.extend({ Whisper.MessageListView = Whisper.ListView.extend({
@ -9,17 +13,14 @@
events: { events: {
scroll: 'onScroll', scroll: 'onScroll',
}, },
initialize: function() { initialize() {
Whisper.ListView.prototype.initialize.call(this); Whisper.ListView.prototype.initialize.call(this);
this.triggerLazyScroll = _.debounce( this.triggerLazyScroll = _.debounce(() => {
function() { this.$el.trigger('lazyScroll');
this.$el.trigger('lazyScroll'); }, 500);
}.bind(this),
500
);
}, },
onScroll: function() { onScroll() {
this.measureScrollPosition(); this.measureScrollPosition();
if (this.$el.scrollTop() === 0) { if (this.$el.scrollTop() === 0) {
this.$el.trigger('loadMore'); this.$el.trigger('loadMore');
@ -32,10 +33,10 @@
this.triggerLazyScroll(); this.triggerLazyScroll();
}, },
atBottom: function() { atBottom() {
return this.bottomOffset < 30; return this.bottomOffset < 30;
}, },
measureScrollPosition: function() { measureScrollPosition() {
if (this.el.scrollHeight === 0) { if (this.el.scrollHeight === 0) {
// hidden // hidden
return; return;
@ -45,10 +46,10 @@
this.scrollHeight = this.el.scrollHeight; this.scrollHeight = this.el.scrollHeight;
this.bottomOffset = this.scrollHeight - this.scrollPosition; this.bottomOffset = this.scrollHeight - this.scrollPosition;
}, },
resetScrollPosition: function() { resetScrollPosition() {
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight()); this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
}, },
scrollToBottomIfNeeded: function() { scrollToBottomIfNeeded() {
// This is counter-intuitive. Our current bottomOffset is reflective of what // This is counter-intuitive. Our current bottomOffset is reflective of what
// we last measured, not necessarily the current state. And this is called // we last measured, not necessarily the current state. And this is called
// after we just made a change to the DOM: inserting a message, or an image // after we just made a change to the DOM: inserting a message, or an image
@ -58,25 +59,26 @@
this.scrollToBottom(); this.scrollToBottom();
} }
}, },
scrollToBottom: function() { scrollToBottom() {
this.$el.scrollTop(this.el.scrollHeight); this.$el.scrollTop(this.el.scrollHeight);
this.measureScrollPosition(); this.measureScrollPosition();
}, },
addOne: function(model) { addOne(model) {
var view; let view;
if (model.isExpirationTimerUpdate()) { if (model.isExpirationTimerUpdate()) {
view = new Whisper.ExpirationTimerUpdateView({ model: model }).render(); view = new Whisper.ExpirationTimerUpdateView({ model }).render();
} else if (model.get('type') === 'keychange') { } else if (model.get('type') === 'keychange') {
view = new Whisper.KeyChangeView({ model: model }).render(); view = new Whisper.KeyChangeView({ model }).render();
} else if (model.get('type') === 'verified-change') { } else if (model.get('type') === 'verified-change') {
view = new Whisper.VerifiedChangeView({ model: model }).render(); view = new Whisper.VerifiedChangeView({ model }).render();
} else { } else {
view = new this.itemView({ model: model }).render(); // eslint-disable-next-line new-cap
view = new this.itemView({ model }).render();
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition); this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded); this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
} }
var index = this.collection.indexOf(model); const index = this.collection.indexOf(model);
this.measureScrollPosition(); this.measureScrollPosition();
if (model.get('unread') && !this.atBottom()) { if (model.get('unread') && !this.atBottom()) {
@ -91,20 +93,20 @@
this.$el.prepend(view.el); this.$el.prepend(view.el);
} else { } else {
// insert // insert
var next = this.$('#' + this.collection.at(index + 1).id); const next = this.$(`#${this.collection.at(index + 1).id}`);
var prev = this.$('#' + this.collection.at(index - 1).id); const prev = this.$(`#${this.collection.at(index - 1).id}`);
if (next.length > 0) { if (next.length > 0) {
view.$el.insertBefore(next); view.$el.insertBefore(next);
} else if (prev.length > 0) { } else if (prev.length > 0) {
view.$el.insertAfter(prev); view.$el.insertAfter(prev);
} else { } else {
// scan for the right spot // scan for the right spot
var elements = this.$el.children(); const elements = this.$el.children();
if (elements.length > 0) { if (elements.length > 0) {
for (var i = 0; i < elements.length; ++i) { for (let i = 0; i < elements.length; i += 1) {
var m = this.collection.get(elements[i].id); const m = this.collection.get(elements[i].id);
var m_index = this.collection.indexOf(m); const mIndex = this.collection.indexOf(m);
if (m_index > index) { if (mIndex > index) {
view.$el.insertBefore(elements[i]); view.$el.insertBefore(elements[i]);
break; break;
} }

View file

@ -1,3 +1,6 @@
/* global Whisper, extension, Backbone, moment, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
@ -6,15 +9,13 @@
Whisper.NetworkStatusView = Whisper.View.extend({ Whisper.NetworkStatusView = Whisper.View.extend({
className: 'network-status', className: 'network-status',
templateName: 'networkStatus', templateName: 'networkStatus',
initialize: function() { initialize() {
this.$el.hide(); this.$el.hide();
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000); this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
extension.windows.onClosed( extension.windows.onClosed(() => {
function() { clearInterval(this.renderIntervalHandle);
clearInterval(this.renderIntervalHandle); });
}.bind(this)
);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
@ -27,29 +28,29 @@
this.model = new Backbone.Model(); this.model = new Backbone.Model();
this.listenTo(this.model, 'change', this.onChange); this.listenTo(this.model, 'change', this.onChange);
}, },
onReconnectTimer: function() { onReconnectTimer() {
this.setSocketReconnectInterval(60000); this.setSocketReconnectInterval(60000);
}, },
finishConnectingGracePeriod: function() { finishConnectingGracePeriod() {
this.withinConnectingGracePeriod = false; this.withinConnectingGracePeriod = false;
}, },
setSocketReconnectInterval: function(millis) { setSocketReconnectInterval(millis) {
this.socketReconnectWaitDuration = moment.duration(millis); this.socketReconnectWaitDuration = moment.duration(millis);
}, },
navigatorOnLine: function() { navigatorOnLine() {
return navigator.onLine; return navigator.onLine;
}, },
getSocketStatus: function() { getSocketStatus() {
return window.getSocketStatus(); return window.getSocketStatus();
}, },
getNetworkStatus: function() { getNetworkStatus() {
var message = ''; let message = '';
var instructions = ''; let instructions = '';
var hasInterruption = false; let hasInterruption = false;
var action = null; let action = null;
var buttonClass = null; let buttonClass = null;
var socketStatus = this.getSocketStatus(); const socketStatus = this.getSocketStatus();
switch (socketStatus) { switch (socketStatus) {
case WebSocket.CONNECTING: case WebSocket.CONNECTING:
message = i18n('connecting'); message = i18n('connecting');
@ -58,12 +59,13 @@
case WebSocket.OPEN: case WebSocket.OPEN:
this.setSocketReconnectInterval(null); this.setSocketReconnectInterval(null);
break; break;
case WebSocket.CLOSING: case WebSocket.CLOSED:
message = i18n('disconnected'); message = i18n('disconnected');
instructions = i18n('checkNetworkConnection'); instructions = i18n('checkNetworkConnection');
hasInterruption = true; hasInterruption = true;
break; break;
case WebSocket.CLOSED: case WebSocket.CLOSING:
default:
message = i18n('disconnected'); message = i18n('disconnected');
instructions = i18n('checkNetworkConnection'); instructions = i18n('checkNetworkConnection');
hasInterruption = true; hasInterruption = true;
@ -71,7 +73,7 @@
} }
if ( if (
socketStatus == WebSocket.CONNECTING && socketStatus === WebSocket.CONNECTING &&
!this.withinConnectingGracePeriod !this.withinConnectingGracePeriod
) { ) {
hasInterruption = true; hasInterruption = true;
@ -94,21 +96,21 @@
} }
return { return {
message: message, message,
instructions: instructions, instructions,
hasInterruption: hasInterruption, hasInterruption,
action: action, action,
buttonClass: buttonClass, buttonClass,
}; };
}, },
update: function() { update() {
var status = this.getNetworkStatus(); const status = this.getNetworkStatus();
this.model.set(status); this.model.set(status);
}, },
render_attributes: function() { render_attributes() {
return this.model.attributes; return this.model.attributes;
}, },
onChange: function() { onChange() {
this.render(); this.render();
if (this.model.attributes.hasInterruption) { if (this.model.attributes.hasInterruption) {
this.$el.slideDown(); this.$el.slideDown();

View file

@ -1,12 +1,18 @@
/* global Whisper, _ */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.NewGroupUpdateView = Whisper.View.extend({ Whisper.NewGroupUpdateView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: 'new-group-update', className: 'new-group-update',
templateName: 'new-group-update', templateName: 'new-group-update',
initialize: function(options) { initialize(options) {
this.render(); this.render();
this.avatarInput = new Whisper.FileInputView({ this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'), el: this.$('.group-avatar'),
@ -14,15 +20,13 @@
}); });
this.recipients_view = new Whisper.RecipientsInputView(); this.recipients_view = new Whisper.RecipientsInputView();
this.listenTo(this.recipients_view.typeahead, 'sync', function() { this.listenTo(this.recipients_view.typeahead, 'sync', () =>
this.model.contactCollection.models.forEach( this.model.contactCollection.models.forEach(model => {
function(model) { if (this.recipients_view.typeahead.get(model)) {
if (this.recipients_view.typeahead.get(model)) { this.recipients_view.typeahead.remove(model);
this.recipients_view.typeahead.remove(model); }
} })
}.bind(this) );
);
});
this.recipients_view.$el.insertBefore(this.$('.container')); this.recipients_view.$el.insertBefore(this.$('.container'));
this.member_list_view = new Whisper.ContactListView({ this.member_list_view = new Whisper.ContactListView({
@ -38,49 +42,47 @@
'focusin input.search': 'showResults', 'focusin input.search': 'showResults',
'focusout input.search': 'hideResults', 'focusout input.search': 'hideResults',
}, },
hideResults: function() { hideResults() {
this.$('.results').hide(); this.$('.results').hide();
}, },
showResults: function() { showResults() {
this.$('.results').show(); this.$('.results').show();
}, },
goBack: function() { goBack() {
this.trigger('back'); this.trigger('back');
}, },
render_attributes: function() { render_attributes() {
return { return {
name: this.model.getTitle(), name: this.model.getTitle(),
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
}; };
}, },
send: function() { send() {
return this.avatarInput.getThumbnail().then( return this.avatarInput.getThumbnail().then(avatarFile => {
function(avatarFile) { const now = Date.now();
var now = Date.now(); const attrs = {
var attrs = { timestamp: now,
timestamp: now, active_at: now,
active_at: now, name: this.$('.name').val(),
name: this.$('.name').val(), members: _.union(
members: _.union( this.model.get('members'),
this.model.get('members'), this.recipients_view.recipients.pluck('id')
this.recipients_view.recipients.pluck('id') ),
), };
}; if (avatarFile) {
if (avatarFile) { attrs.avatar = avatarFile;
attrs.avatar = avatarFile; }
} this.model.set(attrs);
this.model.set(attrs); const groupUpdate = this.model.changed;
var group_update = this.model.changed; this.model.save();
this.model.save();
if (group_update.avatar) { if (groupUpdate.avatar) {
this.model.trigger('change:avatar'); this.model.trigger('change:avatar');
} }
this.model.updateGroup(group_update); this.model.updateGroup(groupUpdate);
this.goBack(); this.goBack();
}.bind(this) });
);
}, },
}); });
})(); })();

View file

@ -1,26 +1,30 @@
/* global libphonenumber, Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.PhoneInputView = Whisper.View.extend({ Whisper.PhoneInputView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: 'phone-input', className: 'phone-input',
templateName: 'phone-number', templateName: 'phone-number',
initialize: function() { initialize() {
this.$('input.number').intlTelInput(); this.$('input.number').intlTelInput();
}, },
events: { events: {
change: 'validateNumber', change: 'validateNumber',
keyup: 'validateNumber', keyup: 'validateNumber',
}, },
validateNumber: function() { validateNumber() {
var input = this.$('input.number'); const input = this.$('input.number');
var regionCode = this.$('li.active') const regionCode = this.$('li.active')
.attr('data-country-code') .attr('data-country-code')
.toUpperCase(); .toUpperCase();
var number = input.val(); const number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); const parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
if (parsedNumber.isValidNumber) { if (parsedNumber.isValidNumber) {
this.$('.number-container').removeClass('invalid'); this.$('.number-container').removeClass('invalid');
this.$('.number-container').addClass('valid'); this.$('.number-container').addClass('valid');

View file

@ -1,8 +1,12 @@
/* global Whisper, Backbone, ConversationController */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ContactsTypeahead = Backbone.TypeaheadCollection.extend({ const ContactsTypeahead = Backbone.TypeaheadCollection.extend({
typeaheadAttributes: [ typeaheadAttributes: [
'name', 'name',
'e164_number', 'e164_number',
@ -12,7 +16,7 @@
database: Whisper.Database, database: Whisper.Database,
storeName: 'conversations', storeName: 'conversations',
model: Whisper.Conversation, model: Whisper.Conversation,
fetchContacts: function() { fetchContacts() {
return this.fetch({ reset: true, conditions: { type: 'private' } }); return this.fetch({ reset: true, conditions: { type: 'private' } });
}, },
}); });
@ -24,17 +28,17 @@
'click .remove': 'removeModel', 'click .remove': 'removeModel',
}, },
templateName: 'contact_pill', templateName: 'contact_pill',
initialize: function() { initialize() {
var error = this.model.validate(this.model.attributes); const error = this.model.validate(this.model.attributes);
if (error) { if (error) {
this.$el.addClass('error'); this.$el.addClass('error');
} }
}, },
removeModel: function() { removeModel() {
this.$el.trigger('remove', { modelId: this.model.id }); this.$el.trigger('remove', { modelId: this.model.id });
this.remove(); this.remove();
}, },
render_attributes: function() { render_attributes() {
return { name: this.model.getTitle() }; return { name: this.model.getTitle() };
}, },
}); });
@ -55,7 +59,7 @@
Whisper.RecipientsInputView = Whisper.View.extend({ Whisper.RecipientsInputView = Whisper.View.extend({
className: 'recipients-input', className: 'recipients-input',
templateName: 'recipients-input', templateName: 'recipients-input',
initialize: function(options) { initialize(options) {
if (options) { if (options) {
this.placeholder = options.placeholder; this.placeholder = options.placeholder;
} }
@ -81,7 +85,7 @@
// View to display the matched contacts from typeahead // View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.SuggestionListView({ this.typeahead_view = new Whisper.SuggestionListView({
collection: new Whisper.ConversationCollection([], { collection: new Whisper.ConversationCollection([], {
comparator: function(m) { comparator(m) {
return m.getTitle().toLowerCase(); return m.getTitle().toLowerCase();
}, },
}), }),
@ -91,7 +95,7 @@
this.listenTo(this.typeahead, 'reset', this.filterContacts); this.listenTo(this.typeahead, 'reset', this.filterContacts);
}, },
render_attributes: function() { render_attributes() {
return { placeholder: this.placeholder || 'name or phone number' }; return { placeholder: this.placeholder || 'name or phone number' };
}, },
@ -102,8 +106,8 @@
'remove .recipient': 'removeRecipient', 'remove .recipient': 'removeRecipient',
}, },
filterContacts: function(e) { filterContacts() {
var query = this.$input.val(); const query = this.$input.val();
if (query.length) { if (query.length) {
if (this.maybeNumber(query)) { if (this.maybeNumber(query)) {
this.new_contact_view.model.set('id', query); this.new_contact_view.model.set('id', query);
@ -117,7 +121,7 @@
} }
}, },
initNewContact: function() { initNewContact() {
if (this.new_contact_view) { if (this.new_contact_view) {
this.new_contact_view.undelegateEvents(); this.new_contact_view.undelegateEvents();
this.new_contact_view.$el.hide(); this.new_contact_view.$el.hide();
@ -132,47 +136,45 @@
}).render(); }).render();
}, },
addNewRecipient: function() { addNewRecipient() {
this.recipients.add(this.new_contact_view.model); this.recipients.add(this.new_contact_view.model);
this.initNewContact(); this.initNewContact();
this.resetTypeahead(); this.resetTypeahead();
}, },
addRecipient: function(e, conversation) { addRecipient(e, conversation) {
this.recipients.add(this.typeahead.remove(conversation.id)); this.recipients.add(this.typeahead.remove(conversation.id));
this.resetTypeahead(); this.resetTypeahead();
}, },
removeRecipient: function(e, data) { removeRecipient(e, data) {
var model = this.recipients.remove(data.modelId); const model = this.recipients.remove(data.modelId);
if (!model.get('newContact')) { if (!model.get('newContact')) {
this.typeahead.add(model); this.typeahead.add(model);
} }
this.filterContacts(); this.filterContacts();
}, },
reset: function() { reset() {
this.delegateEvents(); this.delegateEvents();
this.typeahead_view.delegateEvents(); this.typeahead_view.delegateEvents();
this.recipients_view.delegateEvents(); this.recipients_view.delegateEvents();
this.new_contact_view.delegateEvents(); this.new_contact_view.delegateEvents();
this.typeahead.add( this.typeahead.add(
this.recipients.filter(function(model) { this.recipients.filter(model => !model.get('newContact'))
return !model.get('newContact');
})
); );
this.recipients.reset([]); this.recipients.reset([]);
this.resetTypeahead(); this.resetTypeahead();
this.typeahead.fetchContacts(); this.typeahead.fetchContacts();
}, },
resetTypeahead: function() { resetTypeahead() {
this.new_contact_view.$el.hide(); this.new_contact_view.$el.hide();
this.$input.val('').focus(); this.$input.val('').focus();
this.typeahead_view.collection.reset([]); this.typeahead_view.collection.reset([]);
}, },
maybeNumber: function(number) { maybeNumber(number) {
return number.match(/^\+?[0-9]*$/); return number.match(/^\+?[0-9]*$/);
}, },
}); });

View file

@ -1,11 +1,17 @@
/* global Whisper, moment, WebAudioRecorder */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.RecorderView = Whisper.View.extend({ Whisper.RecorderView = Whisper.View.extend({
className: 'recorder clearfix', className: 'recorder clearfix',
templateName: 'recorder', templateName: 'recorder',
initialize: function() { initialize() {
this.startTime = Date.now(); this.startTime = Date.now();
this.interval = setInterval(this.updateTime.bind(this), 1000); this.interval = setInterval(this.updateTime.bind(this), 1000);
this.start(); this.start();
@ -15,16 +21,16 @@
'click .finish': 'finish', 'click .finish': 'finish',
close: 'close', close: 'close',
}, },
updateTime: function() { updateTime() {
var duration = moment.duration(Date.now() - this.startTime, 'ms'); const duration = moment.duration(Date.now() - this.startTime, 'ms');
var minutes = '' + Math.trunc(duration.asMinutes()); const minutes = `${Math.trunc(duration.asMinutes())}`;
var seconds = '' + duration.seconds(); let seconds = `${duration.seconds()}`;
if (seconds.length < 2) { if (seconds.length < 2) {
seconds = '0' + seconds; seconds = `0${seconds}`;
} }
this.$('.time').text(minutes + ':' + seconds); this.$('.time').text(`${minutes}:${seconds}`);
}, },
close: function() { close() {
// Note: the 'close' event can be triggered by InboxView, when the user clicks // Note: the 'close' event can be triggered by InboxView, when the user clicks
// anywhere outside the recording pane. // anywhere outside the recording pane.
@ -44,7 +50,7 @@
this.source = null; this.source = null;
if (this.context) { if (this.context) {
this.context.close().then(function() { this.context.close().then(() => {
console.log('audio context closed'); console.log('audio context closed');
}); });
} }
@ -53,16 +59,16 @@
this.remove(); this.remove();
this.trigger('closed'); this.trigger('closed');
}, },
finish: function() { finish() {
this.recorder.finishRecording(); this.recorder.finishRecording();
this.close(); this.close();
}, },
handleBlob: function(recorder, blob) { handleBlob(recorder, blob) {
if (blob) { if (blob) {
this.trigger('send', blob); this.trigger('send', blob);
} }
}, },
start: function() { start() {
this.context = new AudioContext(); this.context = new AudioContext();
this.input = this.context.createGain(); this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, { this.recorder = new WebAudioRecorder(this.input, {
@ -73,15 +79,15 @@
this.recorder.onError = this.onError; this.recorder.onError = this.onError;
navigator.webkitGetUserMedia( navigator.webkitGetUserMedia(
{ audio: true }, { audio: true },
function(stream) { stream => {
this.source = this.context.createMediaStreamSource(stream); this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input); this.source.connect(this.input);
}.bind(this), },
this.onError.bind(this) this.onError.bind(this)
); );
this.recorder.startRecording(); this.recorder.startRecording();
}, },
onError: function(error) { onError(error) {
// Protect against out-of-band errors, which can happen if the user revokes media // Protect against out-of-band errors, which can happen if the user revokes media
// permissions after successfully accessing the microphone. // permissions after successfully accessing the microphone.
if (!this.recorder) { if (!this.recorder) {

View file

@ -1,26 +1,28 @@
/* global Whisper, i18n */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ScrollDownButtonView = Whisper.View.extend({ Whisper.ScrollDownButtonView = Whisper.View.extend({
className: 'scroll-down-button-view', className: 'scroll-down-button-view',
templateName: 'scroll-down-button-view', templateName: 'scroll-down-button-view',
initialize: function(options) { initialize(options = {}) {
options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment(count = 0) {
count = count || 0;
this.count += count; this.count += count;
this.render(); this.render();
}, },
render_attributes: function() { render_attributes() {
var cssClass = this.count > 0 ? 'new-messages' : ''; const cssClass = this.count > 0 ? 'new-messages' : '';
var moreBelow = i18n('scrollDown'); let moreBelow = i18n('scrollDown');
if (this.count > 1) { if (this.count > 1) {
moreBelow = i18n('messagesBelow'); moreBelow = i18n('messagesBelow');
} else if (this.count === 1) { } else if (this.count === 1) {
@ -28,8 +30,8 @@
} }
return { return {
cssClass: cssClass, cssClass,
moreBelow: moreBelow, moreBelow,
}; };
}, },
}); });

View file

@ -1,16 +1,22 @@
/* global Whisper, $, getAccountManager, textsecure */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.StandaloneRegistrationView = Whisper.View.extend({ Whisper.StandaloneRegistrationView = Whisper.View.extend({
templateName: 'standalone', templateName: 'standalone',
className: 'full-screen-flow', className: 'full-screen-flow',
initialize: function() { initialize() {
this.accountManager = getAccountManager(); this.accountManager = getAccountManager();
this.render(); this.render();
var number = textsecure.storage.user.getNumber(); const number = textsecure.storage.user.getNumber();
if (number) { if (number) {
this.$('input.number').val(number); this.$('input.number').val(number);
} }
@ -26,58 +32,59 @@
'change #code': 'onChangeCode', 'change #code': 'onChangeCode',
'click #verifyCode': 'verifyCode', 'click #verifyCode': 'verifyCode',
}, },
verifyCode: function(e) { verifyCode() {
var number = this.phoneView.validateNumber(); const number = this.phoneView.validateNumber();
var verificationCode = $('#code') const verificationCode = $('#code')
.val() .val()
.replace(/\D+/g, ''); .replace(/\D+/g, '');
this.accountManager this.accountManager
.registerSingleDevice(number, verificationCode) .registerSingleDevice(number, verificationCode)
.then( .then(() => {
function() { this.$el.trigger('openInbox');
this.$el.trigger('openInbox'); })
}.bind(this)
)
.catch(this.log.bind(this)); .catch(this.log.bind(this));
}, },
log: function(s) { log(s) {
console.log(s); console.log(s);
this.$('#status').text(s); this.$('#status').text(s);
}, },
validateCode: function() { validateCode() {
var verificationCode = $('#code') const verificationCode = $('#code')
.val() .val()
.replace(/\D/g, ''); .replace(/\D/g, '');
if (verificationCode.length == 6) {
if (verificationCode.length === 6) {
return verificationCode; return verificationCode;
} }
return null;
}, },
displayError: function(error) { displayError(error) {
this.$('#error') this.$('#error')
.hide() .hide()
.text(error) .text(error)
.addClass('in') .addClass('in')
.fadeIn(); .fadeIn();
}, },
onValidation: function() { onValidation() {
if (this.$('#number-container').hasClass('valid')) { if (this.$('#number-container').hasClass('valid')) {
this.$('#request-sms, #request-voice').removeAttr('disabled'); this.$('#request-sms, #request-voice').removeAttr('disabled');
} else { } else {
this.$('#request-sms, #request-voice').prop('disabled', 'disabled'); this.$('#request-sms, #request-voice').prop('disabled', 'disabled');
} }
}, },
onChangeCode: function() { onChangeCode() {
if (!this.validateCode()) { if (!this.validateCode()) {
this.$('#code').addClass('invalid'); this.$('#code').addClass('invalid');
} else { } else {
this.$('#code').removeClass('invalid'); this.$('#code').removeClass('invalid');
} }
}, },
requestVoice: function() { requestVoice() {
window.removeSetupMenuItems(); window.removeSetupMenuItems();
this.$('#error').hide(); this.$('#error').hide();
var number = this.phoneView.validateNumber(); const number = this.phoneView.validateNumber();
if (number) { if (number) {
this.accountManager this.accountManager
.requestVoiceVerification(number) .requestVoiceVerification(number)
@ -89,10 +96,10 @@
this.$('#number-container').addClass('invalid'); this.$('#number-container').addClass('invalid');
} }
}, },
requestSMSVerification: function() { requestSMSVerification() {
window.removeSetupMenuItems(); window.removeSetupMenuItems();
$('#error').hide(); $('#error').hide();
var number = this.phoneView.validateNumber(); const number = this.phoneView.validateNumber();
if (number) { if (number) {
this.accountManager this.accountManager
.requestSMSVerification(number) .requestSMSVerification(number)

View file

@ -1,19 +1,23 @@
/* global Whisper, Mustache, _ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ToastView = Whisper.View.extend({ Whisper.ToastView = Whisper.View.extend({
className: 'toast', className: 'toast',
templateName: 'toast', templateName: 'toast',
initialize: function() { initialize() {
this.$el.hide(); this.$el.hide();
}, },
close: function() { close() {
this.$el.fadeOut(this.remove.bind(this)); this.$el.fadeOut(this.remove.bind(this));
}, },
render: function() { render() {
this.$el.html( this.$el.html(
Mustache.render( Mustache.render(
_.result(this, 'template', ''), _.result(this, 'template', ''),

View file

@ -1,3 +1,5 @@
/* global Whisper, Backbone, Mustache, _, $ */
/* /*
* Whisper.View * Whisper.View
* *
@ -17,64 +19,57 @@
* 4. Provides some common functionality, e.g. confirmation dialog * 4. Provides some common functionality, e.g. confirmation dialog
* *
*/ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.View = Backbone.View.extend( Whisper.View = Backbone.View.extend(
{ {
constructor: function() { constructor(...params) {
Backbone.View.apply(this, arguments); Backbone.View.call(this, ...params);
Mustache.parse(_.result(this, 'template')); Mustache.parse(_.result(this, 'template'));
}, },
render_attributes: function() { render_attributes() {
return _.result(this.model, 'attributes', {}); return _.result(this.model, 'attributes', {});
}, },
render_partials: function() { render_partials() {
return Whisper.View.Templates; return Whisper.View.Templates;
}, },
template: function() { template() {
if (this.templateName) { if (this.templateName) {
return Whisper.View.Templates[this.templateName]; return Whisper.View.Templates[this.templateName];
} }
return ''; return '';
}, },
render: function() { render() {
var attrs = _.result(this, 'render_attributes', {}); const attrs = _.result(this, 'render_attributes', {});
var template = _.result(this, 'template', ''); const template = _.result(this, 'template', '');
var partials = _.result(this, 'render_partials', ''); const partials = _.result(this, 'render_partials', '');
this.$el.html(Mustache.render(template, attrs, partials)); this.$el.html(Mustache.render(template, attrs, partials));
return this; return this;
}, },
confirm: function(message, okText) { confirm(message, okText) {
return new Promise( return new Promise((resolve, reject) => {
function(resolve, reject) { const dialog = new Whisper.ConfirmationDialogView({
var dialog = new Whisper.ConfirmationDialogView({ message,
message: message, okText,
okText: okText, resolve,
resolve: resolve, reject,
reject: reject, });
}); this.$el.append(dialog.el);
this.$el.append(dialog.el); });
}.bind(this)
);
},
i18n_with_links: function() {
var args = Array.prototype.slice.call(arguments);
for (var i = 1; i < args.length; ++i) {
args[i] =
'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
}
return i18n(args[0], args.slice(1));
}, },
}, },
{ {
// Class attributes // Class attributes
Templates: (function() { Templates: (() => {
var templates = {}; const templates = {};
$('script[type="text/x-tmpl-mustache"]').each(function(i, el) { $('script[type="text/x-tmpl-mustache"]').each((i, el) => {
var $el = $(el); const $el = $(el);
var id = $el.attr('id'); const id = $el.attr('id');
templates[id] = $el.html(); templates[id] = $el.html();
}); });
return templates; return templates;

View file

@ -1,12 +1,16 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var lastTime; let lastTime;
var interval = 1000; const interval = 1000;
var events; let events;
function checkTime() { function checkTime() {
var currentTime = Date.now(); const currentTime = Date.now();
if (currentTime > lastTime + interval * 2) { if (currentTime > lastTime + interval * 2) {
events.trigger('timetravel'); events.trigger('timetravel');
} }
@ -14,7 +18,7 @@
} }
Whisper.WallClockListener = { Whisper.WallClockListener = {
init: function(_events) { init(_events) {
events = _events; events = _events;
lastTime = Date.now(); lastTime = Date.now();
setInterval(checkTime, interval); setInterval(checkTime, interval);

View file

@ -30,9 +30,8 @@
"test-node": "mocha --recursive test/app test/modules ts/test", "test-node": "mocha --recursive test/app test/modules ts/test",
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test",
"eslint": "eslint .", "eslint": "eslint .",
"jshint": "yarn grunt jshint",
"lint": "yarn format --list-different && yarn lint-windows", "lint": "yarn format --list-different && yarn lint-windows",
"lint-windows": "yarn eslint && yarn grunt lint && yarn tslint", "lint-windows": "yarn eslint && yarn tslint",
"tslint": "tslint --format stylish --project .", "tslint": "tslint --format stylish --project .",
"format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"", "format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"",
"transpile": "tsc", "transpile": "tsc",
@ -122,7 +121,6 @@
"grunt-cli": "^1.2.0", "grunt-cli": "^1.2.0",
"grunt-contrib-concat": "^1.0.1", "grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0", "grunt-contrib-copy": "^1.0.0",
"grunt-contrib-jshint": "^1.1.0",
"grunt-contrib-watch": "^1.0.0", "grunt-contrib-watch": "^1.0.0",
"grunt-exec": "^3.0.0", "grunt-exec": "^3.0.0",
"grunt-gitinfo": "^0.1.7", "grunt-gitinfo": "^0.1.7",

View file

@ -1,3 +1,5 @@
/* global window */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const i18n = require('./js/modules/i18n'); const i18n = require('./js/modules/i18n');

View file

@ -41,11 +41,15 @@ _.set(defaultConfig, IMPORT_PATH, IMPORT_END_VALUE);
console.log('prepare_import_build: updating package.json'); console.log('prepare_import_build: updating package.json');
const MAC_ASSET_PATH = 'build.mac.artifactName'; const MAC_ASSET_PATH = 'build.mac.artifactName';
// eslint-disable-next-line no-template-curly-in-string
const MAC_ASSET_START_VALUE = '${name}-mac-${version}.${ext}'; const MAC_ASSET_START_VALUE = '${name}-mac-${version}.${ext}';
// eslint-disable-next-line no-template-curly-in-string
const MAC_ASSET_END_VALUE = '${name}-mac-${version}-import.${ext}'; const MAC_ASSET_END_VALUE = '${name}-mac-${version}-import.${ext}';
const WIN_ASSET_PATH = 'build.win.artifactName'; const WIN_ASSET_PATH = 'build.win.artifactName';
// eslint-disable-next-line no-template-curly-in-string
const WIN_ASSET_START_VALUE = '${name}-win-${version}.${ext}'; const WIN_ASSET_START_VALUE = '${name}-win-${version}.${ext}';
// eslint-disable-next-line no-template-curly-in-string
const WIN_ASSET_END_VALUE = '${name}-win-${version}-import.${ext}'; const WIN_ASSET_END_VALUE = '${name}-win-${version}-import.${ext}';
checkValue(packageJson, MAC_ASSET_PATH, MAC_ASSET_START_VALUE); checkValue(packageJson, MAC_ASSET_PATH, MAC_ASSET_START_VALUE);

View file

@ -1,3 +1,5 @@
/* global window */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const i18n = require('./js/modules/i18n'); const i18n = require('./js/modules/i18n');

View file

@ -1,5 +1,5 @@
const webpack = require('webpack');
const path = require('path'); const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
const typescriptSupport = require('react-docgen-typescript'); const typescriptSupport = require('react-docgen-typescript');
const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse; const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse;

View file

@ -511,7 +511,6 @@
<script type='text/javascript' src='../js/views/new_group_update_view.js' data-cover></script> <script type='text/javascript' src='../js/views/new_group_update_view.js' data-cover></script>
<script type="text/javascript" src="../js/views/group_update_view.js"></script> <script type="text/javascript" src="../js/views/group_update_view.js"></script>
<script type='text/javascript' src='../js/views/attachment_view.js' data-cover></script> <script type='text/javascript' src='../js/views/attachment_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/error_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script> <script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script> <script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script> <script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
@ -37,7 +37,7 @@ export class ContactListItem extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-contact-list-item__avatar-default', 'module-contact-list-item__avatar-default',
`module-contact-list-item__avatar-default--${color}` `module-contact-list-item__avatar-default--${color}`
)} )}
@ -77,7 +77,7 @@ export class ContactListItem extends React.Component<Props> {
<div <div
role="button" role="button"
onClick={onClick} onClick={onClick}
className={classnames( className={classNames(
'module-contact-list-item', 'module-contact-list-item',
onClick ? 'module-contact-list-item--with-click-handler' : null onClick ? 'module-contact-list-item--with-click-handler' : null
)} )}

View file

@ -93,6 +93,7 @@ export class ContactDetail extends React.Component<Props> {
<div <div
className="module-contact-detail__send-message" className="module-contact-detail__send-message"
role="button" role="button"
// tslint:disable-next-line react-this-binding-issue
onClick={onClick} onClick={onClick}
> >
<button className="module-contact-detail__send-message__inner"> <button className="module-contact-detail__send-message__inner">

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
import { Contact, getName } from '../../types/Contact'; import { Contact, getName } from '../../types/Contact';
@ -30,7 +30,7 @@ export class EmbeddedContact extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-embedded-contact', 'module-embedded-contact',
withContentAbove withContentAbove
? 'module-embedded-contact--with-content-above' ? 'module-embedded-contact--with-content-above'
@ -102,7 +102,7 @@ export function renderName({
}) { }) {
return ( return (
<div <div
className={classnames( className={classNames(
`module-${module}__contact-name`, `module-${module}__contact-name`,
isIncoming ? `module-${module}__contact-name--incoming` : null isIncoming ? `module-${module}__contact-name--incoming` : null
)} )}
@ -127,7 +127,7 @@ export function renderContactShorthand({
return ( return (
<div <div
className={classnames( className={classNames(
`module-${module}__contact-method`, `module-${module}__contact-method`,
isIncoming ? `module-${module}__contact-method--incoming` : null isIncoming ? `module-${module}__contact-method--incoming` : null
)} )}

View file

@ -1,9 +1,13 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import { padStart } from 'lodash'; import { padStart } from 'lodash';
import { formatRelativeTime } from '../../util/formatRelativeTime'; import { formatRelativeTime } from '../../util/formatRelativeTime';
import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import { MessageBody } from './MessageBody'; import { MessageBody } from './MessageBody';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
@ -69,15 +73,18 @@ interface Props {
} }
function isImage(attachment?: Attachment) { function isImage(attachment?: Attachment) {
// TODO: exclude svg and tiff here
return ( return (
attachment && attachment.contentType && MIME.isImage(attachment.contentType) attachment &&
attachment.contentType &&
isImageTypeSupported(attachment.contentType)
); );
} }
function isVideo(attachment?: Attachment) { function isVideo(attachment?: Attachment) {
return ( return (
attachment && attachment.contentType && MIME.isVideo(attachment.contentType) attachment &&
attachment.contentType &&
isVideoTypeSupported(attachment.contentType)
); );
} }
@ -143,7 +150,7 @@ export class Message extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-message__metadata__timer', 'module-message__metadata__timer',
`module-message__metadata__timer--${bucket}`, `module-message__metadata__timer--${bucket}`,
`module-message__metadata__timer--${direction}`, `module-message__metadata__timer--${direction}`,
@ -179,7 +186,7 @@ export class Message extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-message__metadata', 'module-message__metadata',
withImageNoCaption withImageNoCaption
? 'module-message__metadata--with-image-no-caption' ? 'module-message__metadata--with-image-no-caption'
@ -187,7 +194,7 @@ export class Message extends React.Component<Props> {
)} )}
> >
<span <span
className={classnames( className={classNames(
'module-message__metadata__date', 'module-message__metadata__date',
`module-message__metadata__date--${direction}`, `module-message__metadata__date--${direction}`,
withImageNoCaption withImageNoCaption
@ -202,7 +209,7 @@ export class Message extends React.Component<Props> {
<span className="module-message__metadata__spacer" /> <span className="module-message__metadata__spacer" />
{direction === 'outgoing' ? ( {direction === 'outgoing' ? (
<div <div
className={classnames( className={classNames(
'module-message__metadata__status-icon', 'module-message__metadata__status-icon',
`module-message__metadata__status-icon-${status}`, `module-message__metadata__status-icon-${status}`,
status === 'read' status === 'read'
@ -251,6 +258,7 @@ export class Message extends React.Component<Props> {
); );
} }
// tslint:disable-next-line max-func-body-length
public renderAttachment() { public renderAttachment() {
const { const {
i18n, i18n,
@ -277,7 +285,7 @@ export class Message extends React.Component<Props> {
return ( return (
<div className="module-message__attachment-container"> <div className="module-message__attachment-container">
<img <img
className={classnames( className={classNames(
'module-message__img-attachment', 'module-message__img-attachment',
withCaption withCaption
? 'module-message__img-attachment--with-content-below' ? 'module-message__img-attachment--with-content-below'
@ -299,7 +307,7 @@ export class Message extends React.Component<Props> {
return ( return (
<video <video
controls={true} controls={true}
className={classnames( className={classNames(
'module-message__img-attachment', 'module-message__img-attachment',
withCaption withCaption
? 'module-message__img-attachment--with-content-below' ? 'module-message__img-attachment--with-content-below'
@ -316,7 +324,7 @@ export class Message extends React.Component<Props> {
return ( return (
<audio <audio
controls={true} controls={true}
className={classnames( className={classNames(
'module-message__audio-attachment', 'module-message__audio-attachment',
withContentBelow withContentBelow
? 'module-message__audio-attachment--with-content-below' ? 'module-message__audio-attachment--with-content-below'
@ -335,7 +343,7 @@ export class Message extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-message__generic-attachment', 'module-message__generic-attachment',
withContentBelow withContentBelow
? 'module-message__generic-attachment--with-content-below' ? 'module-message__generic-attachment--with-content-below'
@ -354,7 +362,7 @@ export class Message extends React.Component<Props> {
</div> </div>
<div className="module-message__generic-attachment__text"> <div className="module-message__generic-attachment__text">
<div <div
className={classnames( className={classNames(
'module-message__generic-attachment__file-name', 'module-message__generic-attachment__file-name',
`module-message__generic-attachment__file-name--${direction}` `module-message__generic-attachment__file-name--${direction}`
)} )}
@ -362,7 +370,7 @@ export class Message extends React.Component<Props> {
{fileName} {fileName}
</div> </div>
<div <div
className={classnames( className={classNames(
'module-message__generic-attachment__file-size', 'module-message__generic-attachment__file-size',
`module-message__generic-attachment__file-size--${direction}` `module-message__generic-attachment__file-size--${direction}`
)} )}
@ -503,7 +511,7 @@ export class Message extends React.Component<Props> {
if (!authorAvatarPath) { if (!authorAvatarPath) {
return ( return (
<div <div
className={classnames( className={classNames(
'module-message__author-default-avatar', 'module-message__author-default-avatar',
`module-message__author-default-avatar--${color}` `module-message__author-default-avatar--${color}`
)} )}
@ -529,7 +537,7 @@ export class Message extends React.Component<Props> {
return ( return (
<div <div
className={classnames( className={classNames(
'module-message__text', 'module-message__text',
`module-message__text--${direction}` `module-message__text--${direction}`
)} )}
@ -557,7 +565,7 @@ export class Message extends React.Component<Props> {
<li> <li>
<div <div
id={id} id={id}
className={classnames( className={classNames(
'module-message', 'module-message',
`module-message--${direction}`, `module-message--${direction}`,
imageAndNothingElse ? 'module-message--with-image-only' : null, imageAndNothingElse ? 'module-message--with-image-only' : null,

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
type: string; type: string;
@ -20,7 +20,7 @@ export class Notification extends React.Component<Props> {
<div <div
role="button" role="button"
onClick={onClick} onClick={onClick}
className={classnames( className={classNames(
'module-notification', 'module-notification',
onClick ? 'module-notification--with-click-handler' : null onClick ? 'module-notification--with-click-handler' : null
)} )}

View file

@ -1,7 +1,7 @@
// tslint:disable:react-this-binding-issue // tslint:disable:react-this-binding-issue
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
import * as MIME from '../../../ts/types/MIME'; import * as MIME from '../../../ts/types/MIME';
import * as GoogleChrome from '../../../ts/util/GoogleChrome'; import * as GoogleChrome from '../../../ts/util/GoogleChrome';
@ -89,7 +89,7 @@ export class Quote extends React.Component<Props> {
<div className="module-quote__icon-container__inner"> <div className="module-quote__icon-container__inner">
<div className="module-quote__icon-container__circle-background"> <div className="module-quote__icon-container__circle-background">
<div <div
className={classnames( className={classNames(
'module-quote__icon-container__icon', 'module-quote__icon-container__icon',
`module-quote__icon-container__icon--${icon}` `module-quote__icon-container__icon--${icon}`
)} )}
@ -112,7 +112,7 @@ export class Quote extends React.Component<Props> {
<div className="module-quote__icon-container__inner"> <div className="module-quote__icon-container__inner">
<div className="module-quote__icon-container__circle-background"> <div className="module-quote__icon-container__circle-background">
<div <div
className={classnames( className={classNames(
'module-quote__icon-container__icon', 'module-quote__icon-container__icon',
`module-quote__icon-container__icon--${icon}` `module-quote__icon-container__icon--${icon}`
)} )}
@ -263,7 +263,7 @@ export class Quote extends React.Component<Props> {
<div <div
onClick={onClick} onClick={onClick}
role="button" role="button"
className={classnames( className={classNames(
'module-quote', 'module-quote',
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing', isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
!isIncoming ? `module-quote--outgoing-${color}` : null, !isIncoming ? `module-quote--outgoing-${color}` : null,

110
yarn.lock
View file

@ -1401,13 +1401,6 @@ cli-width@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
cli@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14"
dependencies:
exit "0.1.2"
glob "^7.1.1"
clipboard-copy@^2.0.0: clipboard-copy@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/clipboard-copy/-/clipboard-copy-2.0.0.tgz#663abcd8be9c641de6e92a2eb9afef6e0afa727e" resolved "https://registry.yarnpkg.com/clipboard-copy/-/clipboard-copy-2.0.0.tgz#663abcd8be9c641de6e92a2eb9afef6e0afa727e"
@ -1678,7 +1671,7 @@ connect-history-api-fallback@^1.3.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
console-browserify@1.1.x, console-browserify@^1.1.0: console-browserify@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
dependencies: dependencies:
@ -2294,13 +2287,6 @@ doctrine@^2.0.2:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
dependencies:
domelementtype "~1.1.1"
entities "~1.1.1"
dom-walk@^0.1.0: dom-walk@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
@ -2309,27 +2295,6 @@ domain-browser@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
domelementtype@1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
domelementtype@~1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
domhandler@2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
dependencies:
domelementtype "1"
domutils@1.5:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
dependencies:
dom-serializer "0"
domelementtype "1"
dot-prop@^4.1.0: dot-prop@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1"
@ -2640,14 +2605,6 @@ enhanced-resolve@^4.0.0:
memory-fs "^0.4.0" memory-fs "^0.4.0"
tapable "^1.0.0" tapable "^1.0.0"
entities@1.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
env-paths@^1.0.0: env-paths@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@ -2931,7 +2888,7 @@ exif-parser@^0.1.9:
version "0.1.9" version "0.1.9"
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.9.tgz#1d087e05fd2b079e3a8eaf8ff249978cb5f6fba7" resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.9.tgz#1d087e05fd2b079e3a8eaf8ff249978cb5f6fba7"
exit@0.1.2, exit@0.1.x, exit@~0.1.1: exit@~0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -3774,14 +3731,6 @@ grunt-contrib-copy@^1.0.0:
chalk "^1.1.1" chalk "^1.1.1"
file-sync-cmp "^0.1.0" file-sync-cmp "^0.1.0"
grunt-contrib-jshint@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz#369d909b2593c40e8be79940b21340850c7939ac"
dependencies:
chalk "^1.1.1"
hooker "^0.2.3"
jshint "~2.9.4"
grunt-contrib-watch@^1.0.0: grunt-contrib-watch@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz#84a1a7a1d6abd26ed568413496c73133e990018f" resolved "https://registry.yarnpkg.com/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz#84a1a7a1d6abd26ed568413496c73133e990018f"
@ -4051,7 +4000,7 @@ homedir-polyfill@^1.0.1:
dependencies: dependencies:
parse-passwd "^1.0.0" parse-passwd "^1.0.0"
hooker@^0.2.3, hooker@~0.2.3: hooker@~0.2.3:
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959"
@ -4080,16 +4029,6 @@ html-entities@^1.2.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
htmlparser2@3.8.x:
version "3.8.3"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
dependencies:
domelementtype "1"
domhandler "2.3"
domutils "1.5"
entities "1.0"
readable-stream "1.1"
http-cache-semantics@3.8.1: http-cache-semantics@3.8.1:
version "3.8.1" version "3.8.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
@ -4845,19 +4784,6 @@ jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
jshint@~2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.4.tgz#5e3ba97848d5290273db514aee47fe24cf592934"
dependencies:
cli "~1.0.0"
console-browserify "1.1.x"
exit "0.1.x"
htmlparser2 "3.8.x"
lodash "3.7.x"
minimatch "~3.0.2"
shelljs "0.3.x"
strip-json-comments "1.0.x"
json-buffer@3.0.0: json-buffer@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@ -5170,10 +5096,6 @@ lodash.uniq@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@3.7.x:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45"
lodash@^3.10.1, lodash@~3.10.1: lodash@^3.10.1, lodash@~3.10.1:
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@ -7240,15 +7162,6 @@ read-pkg@^2.0.0:
string_decoder "~1.0.3" string_decoder "~1.0.3"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
readable-stream@1.1, readable-stream@^1.1.8, readable-stream@~1.1.9:
version "1.1.13"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@1.1.x: readable-stream@1.1.x:
version "1.1.14" version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@ -7270,6 +7183,15 @@ readable-stream@2, readable-stream@^2.2.2:
string_decoder "~1.0.3" string_decoder "~1.0.3"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
readable-stream@^1.1.8, readable-stream@~1.1.9:
version "1.1.13"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@~2.0.0: readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@~2.0.0:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@ -7859,10 +7781,6 @@ shell-quote@1.6.1:
array-reduce "~0.0.0" array-reduce "~0.0.0"
jsonify "~0.0.0" jsonify "~0.0.0"
shelljs@0.3.x:
version "0.3.0"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1"
signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2: signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@ -8338,10 +8256,6 @@ strip-indent@^1.0.1:
dependencies: dependencies:
get-stdin "^4.0.1" get-stdin "^4.0.1"
strip-json-comments@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
strip-json-comments@~2.0.1: strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"