Format all source code using Prettier

This commit is contained in:
Daniel Gasienica 2018-04-27 17:25:04 -04:00
parent b4dee3f30b
commit 1dd87ad197
149 changed files with 17847 additions and 15439 deletions

View file

@ -2,29 +2,24 @@
module.exports = { module.exports = {
settings: { settings: {
'import/core-modules': [ 'import/core-modules': ['electron'],
'electron'
]
}, },
extends: [ extends: ['airbnb-base', 'prettier'],
'airbnb-base',
'prettier',
],
plugins: [ plugins: ['mocha', 'more'],
'mocha',
'more',
],
rules: { rules: {
'comma-dangle': ['error', { 'comma-dangle': [
'error',
{
arrays: 'always-multiline', arrays: 'always-multiline',
objects: 'always-multiline', objects: 'always-multiline',
imports: 'always-multiline', imports: 'always-multiline',
exports: 'always-multiline', exports: 'always-multiline',
functions: 'never', functions: 'never',
}], },
],
// prevents us from accidentally checking in exclusive tests (`.only`): // prevents us from accidentally checking in exclusive tests (`.only`):
'mocha/no-exclusive-tests': 'error', 'mocha/no-exclusive-tests': 'error',
@ -44,7 +39,11 @@ module.exports = {
// consistently place operators at end of line except ternaries // consistently place operators at end of line except ternaries
'operator-linebreak': 'error', 'operator-linebreak': 'error',
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], quotes: [
'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false },
],
// Prettier overrides: // Prettier overrides:
'arrow-parens': 'off', 'arrow-parens': 'off',

View file

@ -13,11 +13,13 @@ module.exports = function(grunt) {
var libtextsecurecomponents = []; var libtextsecurecomponents = [];
for (i in bower.concat.libtextsecure) { for (i in bower.concat.libtextsecure) {
libtextsecurecomponents.push('components/' + bower.concat.libtextsecure[i] + '/**/*.js'); libtextsecurecomponents.push(
'components/' + bower.concat.libtextsecure[i] + '/**/*.js'
);
} }
var importOnce = require("node-sass-import-once"); var importOnce = require('node-sass-import-once');
grunt.loadNpmTasks("grunt-sass"); grunt.loadNpmTasks('grunt-sass');
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
@ -34,15 +36,15 @@ module.exports = function(grunt) {
src: [ src: [
'components/mocha/mocha.js', 'components/mocha/mocha.js',
'components/chai/chai.js', 'components/chai/chai.js',
'test/_test.js' 'test/_test.js',
], ],
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',
footer: "})();\n", footer: '})();\n',
}, },
src: [ src: [
'libtextsecure/errors.js', 'libtextsecure/errors.js',
@ -77,21 +79,21 @@ module.exports = function(grunt) {
'components/mock-socket/dist/mock-socket.js', 'components/mock-socket/dist/mock-socket.js',
'components/mocha/mocha.js', 'components/mocha/mocha.js',
'components/chai/chai.js', 'components/chai/chai.js',
'libtextsecure/test/_test.js' 'libtextsecure/test/_test.js',
], ],
dest: 'libtextsecure/test/test.js', dest: 'libtextsecure/test/test.js',
} },
}, },
sass: { sass: {
options: { options: {
sourceMap: true, sourceMap: true,
importer: importOnce importer: importOnce,
}, },
dev: { dev: {
files: { files: {
"stylesheets/manifest.css": "stylesheets/manifest.scss" 'stylesheets/manifest.css': 'stylesheets/manifest.scss',
} },
} },
}, },
jshint: { jshint: {
files: [ files: [
@ -117,7 +119,7 @@ module.exports = function(grunt) {
'!js/models/messages.js', '!js/models/messages.js',
'!js/WebAudioRecorderMp3.js', '!js/WebAudioRecorderMp3.js',
'!libtextsecure/message_receiver.js', '!libtextsecure/message_receiver.js',
'_locales/**/*' '_locales/**/*',
], ],
options: { jshintrc: '.jshintrc' }, options: { jshintrc: '.jshintrc' },
}, },
@ -130,32 +132,33 @@ module.exports = function(grunt) {
'protos/*', 'protos/*',
'js/**', 'js/**',
'stylesheets/*.css', 'stylesheets/*.css',
'!js/register.js' '!js/register.js',
], ],
res: [ res: ['images/**/*', 'fonts/*'],
'images/**/*',
'fonts/*',
]
}, },
copy: { copy: {
deps: { deps: {
files: [{ files: [
{
src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js', src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js',
dest: 'js/Mp3LameEncoder.min.js' dest: 'js/Mp3LameEncoder.min.js',
}, { },
{
src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js', src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js',
dest: 'js/WebAudioRecorderMp3.js' dest: 'js/WebAudioRecorderMp3.js',
}, { },
{
src: 'components/jquery/dist/jquery.js', src: 'components/jquery/dist/jquery.js',
dest: 'js/jquery.js' dest: 'js/jquery.js',
}], },
],
}, },
res: { res: {
files: [{ expand: true, dest: 'dist/', src: ['<%= dist.res %>'] }], files: [{ expand: true, dest: 'dist/', src: ['<%= dist.res %>'] }],
}, },
src: { src: {
files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }], files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }],
} },
}, },
jscs: { jscs: {
all: { all: {
@ -179,69 +182,82 @@ module.exports = function(grunt) {
'!test/blanket_mocha.js', '!test/blanket_mocha.js',
'!test/modules/**/*.js', '!test/modules/**/*.js',
'!test/test.js', '!test/test.js',
] ],
} },
}, },
watch: { watch: {
sass: { sass: {
files: ['./stylesheets/*.scss'], files: ['./stylesheets/*.scss'],
tasks: ['sass'] tasks: ['sass'],
}, },
libtextsecure: { libtextsecure: {
files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'], files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
tasks: ['concat:libtextsecure'] tasks: ['concat:libtextsecure'],
}, },
dist: { dist: {
files: ['<%= dist.src %>', '<%= dist.res %>'], files: ['<%= dist.src %>', '<%= dist.res %>'],
tasks: ['copy_dist'] tasks: ['copy_dist'],
}, },
scripts: { scripts: {
files: ['<%= jshint.files %>'], files: ['<%= jshint.files %>'],
tasks: ['jshint'] tasks: ['jshint'],
}, },
style: { style: {
files: ['<%= jscs.all.src %>'], files: ['<%= jscs.all.src %>'],
tasks: ['jscs'] tasks: ['jscs'],
}, },
transpile: { transpile: {
files: ['./ts/**/*.ts'], files: ['./ts/**/*.ts'],
tasks: ['exec:transpile'] tasks: ['exec:transpile'],
} },
}, },
exec: { exec: {
'tx-pull': { 'tx-pull': {
cmd: 'tx pull' cmd: 'tx pull',
}, },
'transpile': { transpile: {
cmd: 'npm run transpile', cmd: 'npm run transpile',
} },
}, },
'test-release': { 'test-release': {
osx: { osx: {
archive: 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar', archive:
appUpdateYML: 'mac/' + packageJson.productName + '.app/Contents/Resources/app-update.yml', 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar',
exe: 'mac/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName appUpdateYML:
'mac/' +
packageJson.productName +
'.app/Contents/Resources/app-update.yml',
exe:
'mac/' +
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: 'mas/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName exe:
'mas/' +
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(function(key) {
if (/^grunt(?!(-cli)?$)/.test(key)) { // ignore grunt and grunt-cli if (/^grunt(?!(-cli)?$)/.test(key)) {
// ignore grunt and grunt-cli
grunt.loadNpmTasks(key); grunt.loadNpmTasks(key);
} }
}); });
@ -250,7 +266,12 @@ module.exports = function(grunt) {
// locales with missing placeholders // locales with missing placeholders
grunt.registerTask('locale-patch', function() { grunt.registerTask('locale-patch', function() {
var en = grunt.file.readJSON('_locales/en/messages.json'); var en = grunt.file.readJSON('_locales/en/messages.json');
grunt.file.recurse('_locales', function(abspath, rootdir, subdir, filename){ grunt.file.recurse('_locales', function(
abspath,
rootdir,
subdir,
filename
) {
if (subdir === 'en' || filename !== 'messages.json') { if (subdir === 'en' || filename !== 'messages.json') {
return; return;
} }
@ -258,7 +279,10 @@ module.exports = function(grunt) {
for (var key in messages) { for (var key in messages) {
if (en[key] !== undefined && messages[key] !== undefined) { if (en[key] !== undefined && messages[key] !== undefined) {
if (en[key].placeholders !== undefined && messages[key].placeholders === undefined){ if (
en[key].placeholders !== undefined &&
messages[key].placeholders === undefined
) {
messages[key].placeholders = en[key].placeholders; messages[key].placeholders = en[key].placeholders;
} }
} }
@ -273,8 +297,10 @@ module.exports = function(grunt) {
var gitinfo = grunt.config.get('gitinfo'); var gitinfo = grunt.config.get('gitinfo');
var commited = gitinfo.local.branch.current.lastCommitTime; var commited = gitinfo.local.branch.current.lastCommitTime;
var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90; var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
grunt.file.write('config/local-production.json', grunt.file.write(
JSON.stringify({ buildExpiration: time }) + '\n'); 'config/local-production.json',
JSON.stringify({ buildExpiration: time }) + '\n'
);
}); });
grunt.registerTask('clean-release', function() { grunt.registerTask('clean-release', function() {
@ -290,35 +316,45 @@ module.exports = function(grunt) {
var gitinfo = grunt.config.get('gitinfo'); var gitinfo = grunt.config.get('gitinfo');
var https = require('https'); var https = require('https');
var urlBase = "https://s3-us-west-1.amazonaws.com/signal-desktop-builds"; var urlBase = 'https://s3-us-west-1.amazonaws.com/signal-desktop-builds';
var keyBase = 'signalapp/Signal-Desktop'; var keyBase = 'signalapp/Signal-Desktop';
var sha = gitinfo.local.branch.current.SHA; var sha = gitinfo.local.branch.current.SHA;
var files = [{ var files = [
{
zip: packageJson.name + '-' + packageJson.version + '.zip', zip: packageJson.name + '-' + packageJson.version + '.zip',
extractedTo: 'linux' extractedTo: 'linux',
}]; },
];
var extract = require('extract-zip'); var extract = require('extract-zip');
var download = function(url, dest, extractedTo, cb) { var download = function(url, dest, extractedTo, cb) {
var file = fs.createWriteStream(dest); var file = fs.createWriteStream(dest);
var request = https.get(url, function(response) { var request = https
.get(url, function(response) {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
cb(response.statusCode); cb(response.statusCode);
} else { } else {
response.pipe(file); response.pipe(file);
file.on('finish', function() { file.on('finish', function() {
file.close(function() { file.close(function() {
extract(dest, {dir: path.join(__dirname, 'release', extractedTo)}, cb); extract(
dest,
{ dir: path.join(__dirname, 'release', extractedTo) },
cb
);
}); });
}); });
} }
}).on('error', function(err) { // Handle errors })
.on('error', function(err) {
// Handle errors
fs.unlink(dest); // Delete the file async. (But we don't check the result) fs.unlink(dest); // Delete the file async. (But we don't check the result)
if (cb) cb(err.message); if (cb) cb(err.message);
}); });
}; };
Promise.all(files.map(function(item) { Promise.all(
files.map(function(item) {
var key = [keyBase, sha, 'dist', item.zip].join('/'); var key = [keyBase, sha, 'dist', item.zip].join('/');
var url = [urlBase, key].join('/'); var url = [urlBase, key].join('/');
var dest = 'release/' + item.zip; var dest = 'release/' + item.zip;
@ -334,7 +370,8 @@ module.exports = function(grunt) {
} }
}); });
}); });
})).then(function(results) { })
).then(function(results) {
results.forEach(function(error) { results.forEach(function(error) {
if (error) { if (error) {
grunt.fail.warn('Failed to fetch some release artifacts'); grunt.fail.warn('Failed to fetch some release artifacts');
@ -347,59 +384,77 @@ module.exports = function(grunt) {
function runTests(environment, cb) { function runTests(environment, cb) {
var failure; var failure;
var Application = require('spectron').Application; var Application = require('spectron').Application;
var electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron'; var electronBinary =
process.platform === 'win32' ? 'electron.cmd' : 'electron';
var app = new Application({ var 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: {
NODE_ENV: environment NODE_ENV: environment,
} },
}); });
function getMochaResults() { function getMochaResults() {
return window.mochaResults; return window.mochaResults;
} }
app.start().then(function() { app
return app.client.waitUntil(function() { .start()
.then(function() {
return app.client.waitUntil(
function() {
return app.client.execute(getMochaResults).then(function(data) { return app.client.execute(getMochaResults).then(function(data) {
return Boolean(data.value); return Boolean(data.value);
}); });
}, 10000, 'Expected to find window.mochaResults set!'); },
}).then(function() { 10000,
'Expected to find window.mochaResults set!'
);
})
.then(function() {
return app.client.execute(getMochaResults); return app.client.execute(getMochaResults);
}).then(function(data) { })
.then(function(data) {
var results = data.value; var results = data.value;
if (results.failures > 0) { if (results.failures > 0) {
console.error(results.reports); console.error(results.reports);
failure = function() { failure = function() {
grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.'); grunt.fail.fatal(
'Found ' + results.failures + ' failing unit tests.'
);
}; };
return app.client.log('browser'); return app.client.log('browser');
} else { } else {
grunt.log.ok(results.passes + ' tests passed.'); grunt.log.ok(results.passes + ' tests passed.');
} }
}).then(function(logs) { })
.then(function(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(function(error) {
failure = function() { failure = function() {
grunt.fail.fatal('Something went wrong: ' + error.message + ' ' + error.stack); grunt.fail.fatal(
'Something went wrong: ' + error.message + ' ' + error.stack
);
}; };
}).then(function () { })
.then(function() {
// 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!
return app.stop(); return app.stop();
}).then(function() { })
.then(function() {
if (failure) { if (failure) {
failure(); failure();
} }
cb(); cb();
}).catch(function (error) { })
.catch(function(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();
@ -415,12 +470,16 @@ module.exports = function(grunt) {
runTests(environment, done); runTests(environment, done);
}); });
grunt.registerTask('lib-unit-tests', 'Run libtextsecure unit tests w/Electron', function() { grunt.registerTask(
'lib-unit-tests',
'Run libtextsecure unit tests w/Electron',
function() {
var environment = grunt.option('env') || 'test-lib'; var environment = grunt.option('env') || 'test-lib';
var done = this.async(); var done = this.async();
runTests(environment, done); runTests(environment, done);
}); }
);
grunt.registerMultiTask('test-release', 'Test packaged releases', function() { grunt.registerMultiTask('test-release', 'Test packaged releases', function() {
var dir = grunt.option('dir') || 'dist'; var dir = grunt.option('dir') || 'dist';
@ -431,7 +490,7 @@ module.exports = function(grunt) {
var 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);
@ -443,16 +502,16 @@ module.exports = function(grunt) {
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('/'); var appUpdateYML = [dir, config.appUpdateYML].join('/');
if (require('fs').existsSync(appUpdateYML)) { if (require('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);
} }
} }
@ -462,33 +521,48 @@ module.exports = function(grunt) {
var assert = require('assert'); var assert = require('assert');
var app = new Application({ var app = new Application({
path: [dir, config.exe].join('/') path: [dir, config.exe].join('/'),
}); });
app.start().then(function () { app
.start()
.then(function() {
return app.client.getWindowCount(); return app.client.getWindowCount();
}).then(function (count) { })
.then(function(count) {
assert.equal(count, 1); assert.equal(count, 1);
console.log('window opened'); console.log('window opened');
}).then(function () { })
.then(function() {
// Get the window's title // Get the window's title
return app.client.getTitle(); return app.client.getTitle();
}).then(function (title) { })
.then(function(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 () { })
assert(app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1); .then(function() {
assert(
app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1
);
console.log('environment ok'); console.log('environment ok');
}).then(function () { })
.then(
function() {
// Successfully completed test // Successfully completed test
return app.stop(); return app.stop();
}, function (error) { },
function(error) {
// Test failed! // Test failed!
return app.stop().then(function() { return app.stop().then(function() {
grunt.fail.fatal('Test failed: ' + error.message + ' ' + error.stack); grunt.fail.fatal(
'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']);
@ -497,9 +571,16 @@ module.exports = function(grunt) {
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']); grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('prep-release', ['gitinfo', 'clean-release', 'fetch-release']); grunt.registerTask('prep-release', [
grunt.registerTask( 'gitinfo',
'default', 'clean-release',
['concat', 'copy:deps', 'sass', 'date', 'exec:transpile'] 'fetch-release',
); ]);
grunt.registerTask('default', [
'concat',
'copy:deps',
'sass',
'date',
'exec:transpile',
]);
}; };

View file

@ -11,7 +11,7 @@
/* global Whisper: false */ /* global Whisper: false */
/* global wrapDeferred: false */ /* global wrapDeferred: false */
;(async function() { (async function() {
'use strict'; 'use strict';
const { IdleDetector, MessageDataMigrator } = Signal.Workflow; const { IdleDetector, MessageDataMigrator } = Signal.Workflow;
@ -65,7 +65,9 @@
var USERNAME = storage.get('number_id'); var USERNAME = storage.get('number_id');
var PASSWORD = storage.get('password'); var PASSWORD = storage.get('password');
accountManager = new textsecure.AccountManager( accountManager = new textsecure.AccountManager(
SERVER_URL, USERNAME, PASSWORD SERVER_URL,
USERNAME,
PASSWORD
); );
accountManager.addEventListener('registration', function() { accountManager.addEventListener('registration', function() {
Whisper.Registration.markDone(); Whisper.Registration.markDone();
@ -105,18 +107,20 @@
if (!isMigrationWithoutIndexComplete) { if (!isMigrationWithoutIndexComplete) {
const database = Migrations0DatabaseWithAttachmentData.getDatabase(); const database = Migrations0DatabaseWithAttachmentData.getDatabase();
const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex({ const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex(
{
databaseName: database.name, databaseName: database.name,
minDatabaseVersion: database.version, minDatabaseVersion: database.version,
numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, numMessagesPerBatch: NUM_MESSAGES_PER_BATCH,
upgradeMessageSchema, upgradeMessageSchema,
}); }
);
console.log('Upgrade message schema (without index):', batchWithoutIndex); console.log('Upgrade message schema (without index):', batchWithoutIndex);
isMigrationWithoutIndexComplete = batchWithoutIndex.done; isMigrationWithoutIndexComplete = batchWithoutIndex.done;
} }
const areAllMigrationsComplete = isMigrationWithIndexComplete && const areAllMigrationsComplete =
isMigrationWithoutIndexComplete; isMigrationWithIndexComplete && isMigrationWithoutIndexComplete;
if (areAllMigrationsComplete) { if (areAllMigrationsComplete) {
idleDetector.stop(); idleDetector.stop();
} }
@ -188,7 +192,9 @@
}); });
cancelInitializationMessage(); cancelInitializationMessage();
var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')}); var appView = (window.owsDesktopApp.appView = new Whisper.AppView({
el: $('body'),
}));
Whisper.WallClockListener.init(Whisper.events); Whisper.WallClockListener.init(Whisper.events);
Whisper.ExpiringMessagesListener.init(Whisper.events); Whisper.ExpiringMessagesListener.init(Whisper.events);
@ -200,7 +206,7 @@
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
connect(); connect();
appView.openInbox({ appView.openInbox({
initialLoadComplete: initialLoadComplete initialLoadComplete: initialLoadComplete,
}); });
} else if (window.config.importMode) { } else if (window.config.importMode) {
appView.openImporter(); appView.openImporter();
@ -214,7 +220,7 @@
Whisper.events.on('showSettings', () => { Whisper.events.on('showSettings', () => {
if (!appView || !appView.inboxView) { if (!appView || !appView.inboxView) {
console.log( console.log(
'background: Event: \'showSettings\':' + "background: Event: 'showSettings':" +
' Expected `appView.inboxView` to exist.' ' Expected `appView.inboxView` to exist.'
); );
return; return;
@ -238,7 +244,7 @@
appView.openConversation(conversation); appView.openConversation(conversation);
} else { } else {
appView.openInbox({ appView.openInbox({
initialLoadComplete: initialLoadComplete initialLoadComplete: initialLoadComplete,
}); });
} }
}); });
@ -258,7 +264,6 @@
} }
}); });
var disconnectTimer = null; var disconnectTimer = null;
function onOffline() { function onOffline() {
console.log('offline'); console.log('offline');
@ -294,7 +299,9 @@
function isSocketOnline() { function isSocketOnline() {
var socketStatus = window.getSocketStatus(); var socketStatus = window.getSocketStatus();
return socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN; return (
socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN
);
} }
function disconnect() { function disconnect() {
@ -317,14 +324,20 @@
window.addEventListener('offline', onOffline); window.addEventListener('offline', onOffline);
} }
if (connectCount === 0 && !navigator.onLine) { if (connectCount === 0 && !navigator.onLine) {
console.log('Starting up offline; will connect when we have network access'); console.log(
'Starting up offline; will connect when we have network access'
);
window.addEventListener('online', onOnline); window.addEventListener('online', onOnline);
onEmpty(); // this ensures that the loading screen is dismissed onEmpty(); // this ensures that the loading screen is dismissed
return; return;
} }
if (!Whisper.Registration.everDone()) { return; } if (!Whisper.Registration.everDone()) {
if (Whisper.Import.isIncomplete()) { return; } return;
}
if (Whisper.Import.isIncomplete()) {
return;
}
if (messageReceiver) { if (messageReceiver) {
messageReceiver.close(); messageReceiver.close();
@ -343,7 +356,11 @@
// initialize the socket and start listening for messages // initialize the socket and start listening for messages
messageReceiver = new textsecure.MessageReceiver( messageReceiver = new textsecure.MessageReceiver(
SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options SERVER_URL,
USERNAME,
PASSWORD,
mySignalingKey,
options
); );
messageReceiver.addEventListener('message', onMessageReceived); messageReceiver.addEventListener('message', onMessageReceived);
messageReceiver.addEventListener('delivery', onDeliveryReceipt); messageReceiver.addEventListener('delivery', onDeliveryReceipt);
@ -359,7 +376,10 @@
messageReceiver.addEventListener('configuration', onConfiguration); messageReceiver.addEventListener('configuration', onConfiguration);
window.textsecure.messaging = new textsecure.MessageSender( window.textsecure.messaging = new textsecure.MessageSender(
SERVER_URL, USERNAME, PASSWORD, CDN_URL SERVER_URL,
USERNAME,
PASSWORD,
CDN_URL
); );
// Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces // Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces
@ -406,7 +426,7 @@
}); });
if (Whisper.Import.isComplete()) { if (Whisper.Import.isComplete()) {
textsecure.messaging.sendRequestConfigurationSyncMessage().catch((e) => { textsecure.messaging.sendRequestConfigurationSyncMessage().catch(e => {
console.log(e); console.log(e);
}); });
} }
@ -416,8 +436,10 @@
const shouldSkipAttachmentMigrationForNewUsers = firstRun === true; const shouldSkipAttachmentMigrationForNewUsers = firstRun === true;
if (shouldSkipAttachmentMigrationForNewUsers) { if (shouldSkipAttachmentMigrationForNewUsers) {
const database = Migrations0DatabaseWithAttachmentData.getDatabase(); const database = Migrations0DatabaseWithAttachmentData.getDatabase();
const connection = const connection = await Signal.Database.open(
await Signal.Database.open(database.name, database.version); database.name,
database.version
);
await Signal.Settings.markAttachmentMigrationComplete(connection); await Signal.Settings.markAttachmentMigrationComplete(connection);
} }
idleDetector.start(); idleDetector.start();
@ -471,7 +493,7 @@
} }
var c = new Whisper.Conversation({ var c = new Whisper.Conversation({
id: id id: id,
}); });
var error = c.validateNumber(); var error = c.validateNumber();
if (error) { if (error) {
@ -501,18 +523,21 @@
} }
} }
return wrapDeferred(conversation.save({ return wrapDeferred(
conversation.save({
name: details.name, name: details.name,
avatar: details.avatar, avatar: details.avatar,
color: details.color, color: details.color,
active_at: activeAt, active_at: activeAt,
})).then(function() { })
).then(function() {
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) { if (!isValidExpireTimer) {
console.log( console.log(
'Ignore invalid expire timer.', 'Ignore invalid expire timer.',
'Expected numeric `expireTimer`, got:', expireTimer 'Expected numeric `expireTimer`, got:',
expireTimer
); );
return; return;
} }
@ -542,10 +567,7 @@
}) })
.then(ev.confirm) .then(ev.confirm)
.catch(function(error) { .catch(function(error) {
console.log( console.log('onContactReceived error:', Errors.toLogFormat(error));
'onContactReceived error:',
Errors.toLogFormat(error)
);
}); });
} }
@ -553,7 +575,9 @@
var details = ev.groupDetails; var details = ev.groupDetails;
var id = details.id; var id = details.id;
return ConversationController.getOrCreateAndWait(id, 'group').then(function(conversation) { return ConversationController.getOrCreateAndWait(id, 'group').then(function(
conversation
) {
var updates = { var updates = {
name: details.name, name: details.name,
members: details.members, members: details.members,
@ -573,13 +597,15 @@
updates.left = true; updates.left = true;
} }
return wrapDeferred(conversation.save(updates)).then(function() { return wrapDeferred(conversation.save(updates))
.then(function() {
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) { if (!isValidExpireTimer) {
console.log( console.log(
'Ignore invalid expire timer.', 'Ignore invalid expire timer.',
'Expected numeric `expireTimer`, got:', expireTimer 'Expected numeric `expireTimer`, got:',
expireTimer
); );
return; return;
} }
@ -592,7 +618,8 @@
receivedAt, receivedAt,
{ fromSync: true } { fromSync: true }
); );
}).then(ev.confirm); })
.then(ev.confirm);
}); });
} }
@ -605,25 +632,23 @@
}); });
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`: // Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
const getDescriptorForSent = ({ message, destination }) => ( const getDescriptorForSent = ({ message, destination }) =>
message.group message.group
? getGroupDescriptor(message.group) ? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: destination } : { type: Message.PRIVATE, id: destination };
);
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`: // Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
const getDescriptorForReceived = ({ message, source }) => ( const getDescriptorForReceived = ({ message, source }) =>
message.group message.group
? getGroupDescriptor(message.group) ? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: source } : { type: Message.PRIVATE, id: source };
);
function createMessageHandler({ function createMessageHandler({
createMessage, createMessage,
getMessageDescriptor, getMessageDescriptor,
handleProfileUpdate, handleProfileUpdate,
}) { }) {
return async (event) => { return async event => {
const { data, confirm } = event; const { data, confirm } = event;
const messageDescriptor = getMessageDescriptor(data); const messageDescriptor = getMessageDescriptor(data);
@ -647,11 +672,9 @@
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
return message.handleDataMessage( return message.handleDataMessage(upgradedMessage, event.confirm, {
upgradedMessage, initialLoadComplete,
event.confirm, });
{ initialLoadComplete }
);
}; };
} }
@ -677,7 +700,10 @@
}); });
// Sent: // Sent:
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) { async function handleMessageSentProfileUpdate({
confirm,
messageDescriptor,
}) {
const conversation = await ConversationController.getOrCreateAndWait( const conversation = await ConversationController.getOrCreateAndWait(
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
@ -716,9 +742,9 @@
value: [ value: [
message.get('source'), message.get('source'),
message.get('sourceDevice'), message.get('sourceDevice'),
message.get('sent_at') message.get('sent_at'),
] ],
} },
}; };
fetcher.fetch(options).always(function() { fetcher.fetch(options).always(function() {
@ -742,7 +768,7 @@
received_at: data.receivedAt || Date.now(), received_at: data.receivedAt || Date.now(),
conversationId: data.source, conversationId: data.source,
type: 'incoming', type: 'incoming',
unread : 1 unread: 1,
}); });
return message; return message;
@ -752,25 +778,33 @@
var error = ev.error; var error = ev.error;
console.log('background onError:', Errors.toLogFormat(error)); console.log('background onError:', Errors.toLogFormat(error));
if (error.name === 'HTTPError' && (error.code == 401 || error.code == 403)) { if (
error.name === 'HTTPError' &&
(error.code == 401 || error.code == 403)
) {
Whisper.events.trigger('unauthorized'); Whisper.events.trigger('unauthorized');
console.log('Client is no longer authorized; deleting local configuration'); console.log(
'Client is no longer authorized; deleting local configuration'
);
Whisper.Registration.remove(); Whisper.Registration.remove();
var previousNumberId = textsecure.storage.get('number_id'); var previousNumberId = textsecure.storage.get('number_id');
textsecure.storage.protocol.removeAllConfiguration().then(function() { textsecure.storage.protocol.removeAllConfiguration().then(
function() {
// These two bits of data are important to ensure that the app loads up // These two bits of data are important to ensure that the app loads up
// the conversation list, instead of showing just the QR code screen. // the conversation list, instead of showing just the QR code screen.
Whisper.Registration.markEverDone(); Whisper.Registration.markEverDone();
textsecure.storage.put('number_id', previousNumberId); textsecure.storage.put('number_id', previousNumberId);
console.log('Successfully cleared local configuration'); console.log('Successfully cleared local configuration');
}, function(error) { },
function(error) {
console.log( console.log(
'Something went wrong clearing local configuration', 'Something went wrong clearing local configuration',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
}); }
);
return; return;
} }
@ -800,15 +834,19 @@
return message.saveErrors(error).then(function() { return message.saveErrors(error).then(function() {
var id = message.get('conversationId'); var id = message.get('conversationId');
return ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { return ConversationController.getOrCreateAndWait(id, 'private').then(
function(conversation) {
conversation.set({ conversation.set({
active_at: Date.now(), active_at: Date.now(),
unreadCount: conversation.get('unreadCount') + 1 unreadCount: conversation.get('unreadCount') + 1,
}); });
var conversation_timestamp = conversation.get('timestamp'); var conversation_timestamp = conversation.get('timestamp');
var message_timestamp = message.get('timestamp'); var message_timestamp = message.get('timestamp');
if (!conversation_timestamp || message_timestamp > conversation_timestamp) { if (
!conversation_timestamp ||
message_timestamp > conversation_timestamp
) {
conversation.set({ timestamp: message.get('sent_at') }); conversation.set({ timestamp: message.get('sent_at') });
} }
@ -822,7 +860,8 @@
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
conversation.save().then(resolve, reject); conversation.save().then(resolve, reject);
}); });
}); }
);
}); });
} }
@ -860,7 +899,7 @@
var receipt = Whisper.ReadSyncs.add({ var receipt = Whisper.ReadSyncs.add({
sender: sender, sender: sender,
timestamp: timestamp, timestamp: timestamp,
read_at : read_at read_at: read_at,
}); });
receipt.on('remove', ev.confirm); receipt.on('remove', ev.confirm);
@ -875,14 +914,11 @@
var state; var state;
var c = new Whisper.Conversation({ var c = new Whisper.Conversation({
id: number id: number,
}); });
var error = c.validateNumber(); var error = c.validateNumber();
if (error) { if (error) {
console.log( console.log('Invalid verified sync received:', Errors.toLogFormat(error));
'Invalid verified sync received:',
Errors.toLogFormat(error)
);
return; return;
} }
@ -898,14 +934,19 @@
break; break;
} }
console.log('got verified sync for', number, state, console.log(
ev.viaContactSync ? 'via contact sync' : ''); 'got verified sync for',
number,
state,
ev.viaContactSync ? 'via contact sync' : ''
);
return ConversationController.getOrCreateAndWait(number, 'private').then(function(contact) { return ConversationController.getOrCreateAndWait(number, 'private').then(
function(contact) {
var options = { var options = {
viaSyncMessage: true, viaSyncMessage: true,
viaContactSync: ev.viaContactSync, viaContactSync: ev.viaContactSync,
key: key key: key,
}; };
if (state === 'VERIFIED') { if (state === 'VERIFIED') {
@ -915,7 +956,8 @@
} else { } else {
return contact.setUnverified(options).then(ev.confirm); return contact.setUnverified(options).then(ev.confirm);
} }
}); }
);
} }
function onDeliveryReceipt(ev) { function onDeliveryReceipt(ev) {
@ -928,7 +970,7 @@
var receipt = Whisper.DeliveryReceipts.add({ var receipt = Whisper.DeliveryReceipts.add({
timestamp: deliveryReceipt.timestamp, timestamp: deliveryReceipt.timestamp,
source: deliveryReceipt.source source: deliveryReceipt.source,
}); });
ev.confirm(); ev.confirm();

View file

@ -9,6 +9,6 @@
extension.windows = { extension.windows = {
onClosed: function(callback) { onClosed: function(callback) {
window.addEventListener('beforeunload', callback); window.addEventListener('beforeunload', callback);
} },
}; };
}()); })();

View file

@ -19,7 +19,8 @@
this.reset([]); this.reset([]);
}); });
this.on('add remove change:unreadCount', this.on(
'add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000) _.debounce(this.updateUnreadCount.bind(this), 1000)
); );
this.startPruning(); this.startPruning();
@ -52,17 +53,20 @@
}, },
updateUnreadCount: function() { updateUnreadCount: function() {
var newUnreadCount = _.reduce( var newUnreadCount = _.reduce(
this.map(function(m) { return m.get('unreadCount'); }), this.map(function(m) {
return m.get('unreadCount');
}),
function(item, memo) { function(item, memo) {
return 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.config.title + " (" + newUnreadCount + ")"; window.document.title =
window.config.title + ' (' + newUnreadCount + ')';
} else { } else {
window.setBadgeCount(0); window.setBadgeCount(0);
window.document.title = window.config.title; window.document.title = window.config.title;
@ -71,12 +75,15 @@
}, },
startPruning: function() { startPruning: function() {
var halfHour = 30 * 60 * 1000; var halfHour = 30 * 60 * 1000;
this.interval = setInterval(function() { this.interval = setInterval(
function() {
this.forEach(function(conversation) { this.forEach(function(conversation) {
conversation.trigger('prune'); conversation.trigger('prune');
}); });
}.bind(this), halfHour); }.bind(this),
} halfHour
);
},
}))(); }))();
window.getInboxCollection = function() { window.getInboxCollection = function() {
@ -86,7 +93,9 @@
window.ConversationController = { window.ConversationController = {
get: function(id) { get: function(id) {
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch'); throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
return conversations.get(id); return conversations.get(id);
@ -104,11 +113,15 @@
} }
if (type !== 'private' && type !== 'group') { if (type !== 'private' && type !== 'group') {
throw new TypeError(`'type' must be 'private' or 'group'; got: '${type}'`); throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
} }
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch'); throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
var conversation = conversations.get(id); var conversation = conversations.get(id);
@ -118,7 +131,7 @@
conversation = conversations.add({ conversation = conversations.add({
id: id, id: id,
type: type type: type,
}); });
conversation.initialPromise = new Promise(function(resolve, reject) { conversation.initialPromise = new Promise(function(resolve, reject) {
if (!conversation.isValid()) { if (!conversation.isValid()) {
@ -146,7 +159,8 @@
return conversation; return conversation;
}, },
getOrCreateAndWait: function(id, type) { getOrCreateAndWait: function(id, type) {
return this._initialPromise.then(function() { return this._initialPromise.then(
function() {
var conversation = this.getOrCreate(id, type); var conversation = this.getOrCreate(id, type);
if (conversation) { if (conversation) {
@ -158,7 +172,8 @@
return Promise.reject( return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation') new Error('getOrCreateAndWait: did not get conversation')
); );
}.bind(this)); }.bind(this)
);
}, },
getAllGroupsInvolvingId: function(id) { getAllGroupsInvolvingId: function(id) {
var groups = new Whisper.GroupCollection(); var groups = new Whisper.GroupCollection();
@ -178,21 +193,26 @@
load: function() { load: function() {
console.log('ConversationController: starting initial fetch'); console.log('ConversationController: starting initial fetch');
this._initialPromise = new Promise(function(resolve, reject) { this._initialPromise = new Promise(
conversations.fetch().then(function() { function(resolve, reject) {
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), function(error) { }.bind(this),
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)); );
}.bind(this)
);
return this._initialPromise; return this._initialPromise;
} },
}; };
})(); })();

View file

@ -24,13 +24,13 @@
}; };
function clearStores(db, names) { function clearStores(db, names) {
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const storeNames = names || db.objectStoreNames; const storeNames = names || db.objectStoreNames;
console.log('Clearing these indexeddb stores:', storeNames); console.log('Clearing these indexeddb stores:', storeNames);
const transaction = db.transaction(storeNames, 'readwrite'); const transaction = db.transaction(storeNames, 'readwrite');
let finished = false; let finished = false;
const finish = (via) => { const finish = via => {
console.log('clearing all stores done via', via); console.log('clearing all stores done via', via);
if (finished) { if (finished) {
resolve(); resolve();
@ -50,7 +50,7 @@
let count = 0; let count = 0;
// can't use built-in .forEach because db.objectStoreNames is not a plain array // can't use built-in .forEach because db.objectStoreNames is not a plain array
_.forEach(storeNames, (storeName) => { _.forEach(storeNames, storeName => {
const store = transaction.objectStore(storeName); const store = transaction.objectStore(storeName);
const request = store.clear(); const request = store.clear();
@ -72,7 +72,7 @@
); );
}; };
}); });
})); });
} }
Whisper.Database.open = () => { Whisper.Database.open = () => {
@ -80,7 +80,7 @@
const { version } = migrations[migrations.length - 1]; const { version } = migrations[migrations.length - 1];
const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version); const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version);
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
// these two event handlers act on the IDBDatabase object, // these two event handlers act on the IDBDatabase object,
// when the database is opened successfully, or not // when the database is opened successfully, or not
DBOpenRequest.onerror = reject; DBOpenRequest.onerror = reject;
@ -91,7 +91,7 @@
// been created before, or a new version number has been // been created before, or a new version number has been
// submitted via the window.indexedDB.open line above // submitted via the window.indexedDB.open line above
DBOpenRequest.onupgradeneeded = reject; DBOpenRequest.onupgradeneeded = reject;
})); });
}; };
Whisper.Database.clear = async () => { Whisper.Database.clear = async () => {
@ -99,7 +99,7 @@
return clearStores(db); return clearStores(db);
}; };
Whisper.Database.clearStores = async (storeNames) => { Whisper.Database.clearStores = async storeNames => {
const db = await Whisper.Database.open(); const db = await Whisper.Database.open();
return clearStores(db, storeNames); return clearStores(db, storeNames);
}; };
@ -107,7 +107,7 @@
Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall')); Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall'));
Whisper.Database.drop = () => Whisper.Database.drop = () =>
new Promise(((resolve, reject) => { new Promise((resolve, reject) => {
const request = window.indexedDB.deleteDatabase(Whisper.Database.id); const request = window.indexedDB.deleteDatabase(Whisper.Database.id);
request.onblocked = () => { request.onblocked = () => {
@ -121,7 +121,7 @@
}; };
request.onsuccess = resolve; request.onsuccess = resolve;
})); });
Whisper.Database.migrations = getPlaceholderMigrations(); Whisper.Database.migrations = getPlaceholderMigrations();
}()); })();

View file

@ -1,7 +1,7 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -14,39 +14,60 @@
recipients = conversation.get('members') || []; recipients = conversation.get('members') || [];
} }
var receipts = this.filter(function(receipt) { var receipts = this.filter(function(receipt) {
return (receipt.get('timestamp') === message.get('sent_at')) && return (
(recipients.indexOf(receipt.get('source')) > -1); receipt.get('timestamp') === message.get('sent_at') &&
recipients.indexOf(receipt.get('source')) > -1
);
}); });
this.remove(receipts); this.remove(receipts);
return receipts; return receipts;
}, },
onReceipt: function(receipt) { onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection(); var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { return messages
if (messages.length === 0) { return; } .fetchSentAt(receipt.get('timestamp'))
.then(function() {
if (messages.length === 0) {
return;
}
var message = messages.find(function(message) { var message = messages.find(function(message) {
return (!message.isIncoming() && receipt.get('source') === message.get('conversationId')); return (
!message.isIncoming() &&
receipt.get('source') === message.get('conversationId')
);
}); });
if (message) { return message; } if (message) {
return message;
}
var groups = new Whisper.GroupCollection(); var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('source')).then(function() { return groups.fetchGroups(receipt.get('source')).then(function() {
var ids = groups.pluck('id'); var ids = groups.pluck('id');
ids.push(receipt.get('source')); ids.push(receipt.get('source'));
return messages.find(function(message) { return messages.find(function(message) {
return (!message.isIncoming() && return (
_.contains(ids, message.get('conversationId'))); !message.isIncoming() &&
_.contains(ids, message.get('conversationId'))
);
}); });
}); });
}).then(function(message) { })
.then(
function(message) {
if (message) { if (message) {
var deliveries = message.get('delivered') || 0; var deliveries = message.get('delivered') || 0;
var delivered_to = message.get('delivered_to') || []; var delivered_to = message.get('delivered_to') || [];
return new Promise(function(resolve, reject) { return new Promise(
message.save({ function(resolve, reject) {
delivered_to: _.union(delivered_to, [receipt.get('source')]), message
delivered: deliveries + 1 .save({
}).then(function() { delivered_to: _.union(delivered_to, [
receipt.get('source'),
]),
delivered: deliveries + 1,
})
.then(
function() {
// notify frontend listeners // notify frontend listeners
var conversation = ConversationController.get( var conversation = ConversationController.get(
message.get('conversationId') message.get('conversationId')
@ -57,8 +78,11 @@
this.remove(receipt); this.remove(receipt);
resolve(); resolve();
}.bind(this), reject); }.bind(this),
}.bind(this)); reject
);
}.bind(this)
);
// TODO: consider keeping a list of numbers we've // TODO: consider keeping a list of numbers we've
// successfully delivered to? // successfully delivered to?
} else { } else {
@ -68,12 +92,14 @@
receipt.get('timestamp') receipt.get('timestamp')
); );
} }
}.bind(this)).catch(function(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

@ -2,7 +2,7 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
'use strict'; 'use strict';
window.emoji_util = window.emoji_util || {}; window.emoji_util = window.emoji_util || {};
@ -39,17 +39,13 @@
var emojiCount = self.getCountOfAllMatches(str, self.rx_unified); var emojiCount = self.getCountOfAllMatches(str, self.rx_unified);
if (emojiCount > 8) { if (emojiCount > 8) {
return ''; return '';
} } else if (emojiCount > 6) {
else if (emojiCount > 6) {
return 'small'; return 'small';
} } else if (emojiCount > 4) {
else if (emojiCount > 4) {
return 'medium'; return 'medium';
} } else if (emojiCount > 2) {
else if (emojiCount > 2) {
return 'large'; return 'large';
} } else {
else {
return 'jumbo'; return 'jumbo';
} }
}; };
@ -83,7 +79,8 @@
window.emoji = new EmojiConvertor(); window.emoji = new EmojiConvertor();
emoji.init_colons(); emoji.init_colons();
emoji.img_sets.apple.path = 'node_modules/emoji-datasource-apple/img/apple/64/'; emoji.img_sets.apple.path =
'node_modules/emoji-datasource-apple/img/apple/64/';
emoji.include_title = true; emoji.include_title = true;
emoji.replace_mode = 'img'; emoji.replace_mode = 'img';
emoji.supports_css = false; // needed to avoid spans with background-image emoji.supports_css = false; // needed to avoid spans with background-image
@ -95,5 +92,4 @@
$el.html(emoji.signalReplace($el.html())); $el.html(emoji.signalReplace($el.html()));
}; };
})(); })();

View file

@ -1,16 +1,16 @@
;(function() { (function() {
'use strict'; 'use strict';
var BUILD_EXPIRATION = 0; var BUILD_EXPIRATION = 0;
try { try {
BUILD_EXPIRATION = parseInt(window.config.buildExpiration); BUILD_EXPIRATION = parseInt(window.config.buildExpiration);
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) {}
window.extension = window.extension || {}; window.extension = window.extension || {};
extension.expired = function() { extension.expired = function() {
return (BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION); return BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION;
}; };
})(); })();

View file

@ -1,8 +1,7 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -36,10 +35,14 @@
var wait = expires_at - Date.now(); var wait = expires_at - Date.now();
// In the past // In the past
if (wait < 0) { wait = 0; } if (wait < 0) {
wait = 0;
}
// Too far in the future, since it's limited to a 32-bit value // Too far in the future, since it's limited to a 32-bit value
if (wait > 2147483647) { wait = 2147483647; } if (wait > 2147483647) {
wait = 2147483647;
}
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait); timeout = setTimeout(destroyExpiredMessages, wait);
@ -53,20 +56,23 @@
checkExpiringMessages(); checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages); events.on('timetravel', throttledCheckExpiringMessages);
}, },
update: throttledCheckExpiringMessages update: throttledCheckExpiringMessages,
}; };
var TimerOption = Backbone.Model.extend({ var TimerOption = Backbone.Model.extend({
getName: function() { getName: function() {
return i18n([ return (
'timerOption', this.get('time'), this.get('unit'), i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize(); moment.duration(this.get('time'), this.get('unit')).humanize()
);
}, },
getAbbreviated: function() { getAbbreviated: function() {
return i18n([ return i18n(
'timerOption', this.get('time'), this.get('unit'), 'abbreviated' ['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
].join('_')); '_'
} )
);
},
}); });
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption, model: TimerOption,
@ -75,8 +81,9 @@
seconds = 0; seconds = 0;
} }
var o = this.findWhere({ seconds: seconds }); var o = this.findWhere({ seconds: seconds });
if (o) { return o.getName(); } if (o) {
else { return o.getName();
} else {
return [seconds, 'seconds'].join(' '); return [seconds, 'seconds'].join(' ');
} }
}, },
@ -85,12 +92,14 @@
seconds = 0; seconds = 0;
} }
var o = this.findWhere({ seconds: seconds }); var o = this.findWhere({ seconds: seconds });
if (o) { return o.getAbbreviated(); } if (o) {
else { return o.getAbbreviated();
} else {
return [seconds, 's'].join(''); return [seconds, 's'].join('');
} }
} },
}))([ }))(
[
[0, 'seconds'], [0, 'seconds'],
[5, 'seconds'], [5, 'seconds'],
[10, 'seconds'], [10, 'seconds'],
@ -108,8 +117,8 @@
return { return {
time: o[0], time: o[0],
unit: o[1], unit: o[1],
seconds: duration.asSeconds() seconds: duration.asSeconds(),
}; };
})); })
);
})(); })();

View file

@ -2,7 +2,7 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -13,16 +13,20 @@
} }
signalProtocolStore.on('keychange', function(id) { signalProtocolStore.on('keychange', function(id) {
ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { ConversationController.getOrCreateAndWait(id, 'private').then(function(
conversation
) {
conversation.addKeyChange(id); conversation.addKeyChange(id);
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) { ConversationController.getAllGroupsInvolvingId(id).then(function(
groups
) {
_.forEach(groups, function(group) { _.forEach(groups, function(group) {
group.addKeyChange(id); group.addKeyChange(id);
}); });
}); });
}); });
}); });
} },
}; };
}()); })();

View file

@ -1,8 +1,8 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
"use strict"; 'use strict';
/* /*
* This file extends the libphonenumber object with a set of phonenumbery * This file extends the libphonenumber object with a set of phonenumbery
@ -17,7 +17,7 @@
var parsedNumber = libphonenumber.parse(number); var parsedNumber = libphonenumber.parse(number);
return libphonenumber.getRegionCodeForNumber(parsedNumber); return libphonenumber.getRegionCodeForNumber(parsedNumber);
} catch (e) { } catch (e) {
return "ZZ"; return 'ZZ';
} }
}, },
@ -25,13 +25,13 @@
var parsedNumber = libphonenumber.parse(number); var parsedNumber = libphonenumber.parse(number);
return { return {
country_code: parsedNumber.values_[1], country_code: parsedNumber.values_[1],
national_number: parsedNumber.values_[2] national_number: parsedNumber.values_[2],
}; };
}, },
getCountryCode: function(regionCode) { getCountryCode: function(regionCode) {
var cc = libphonenumber.getCountryCodeForRegion(regionCode); var cc = libphonenumber.getCountryCodeForRegion(regionCode);
return (cc !== 0) ? cc : ""; return cc !== 0 ? cc : '';
}, },
parseNumber: function(number, defaultRegionCode) { parseNumber: function(number, defaultRegionCode) {
@ -43,7 +43,10 @@
regionCode: libphonenumber.getRegionCodeForNumber(parsedNumber), regionCode: libphonenumber.getRegionCodeForNumber(parsedNumber),
countryCode: '' + parsedNumber.getCountryCode(), countryCode: '' + parsedNumber.getCountryCode(),
nationalNumber: '' + parsedNumber.getNationalNumber(), nationalNumber: '' + parsedNumber.getNationalNumber(),
e164: libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.E164) e164: libphonenumber.format(
parsedNumber,
libphonenumber.PhoneNumberFormat.E164
),
}; };
} catch (ex) { } catch (ex) {
return { error: ex, isValidNumber: false }; return { error: ex, isValidNumber: false };
@ -52,244 +55,244 @@
getAllRegionCodes: function() { getAllRegionCodes: function() {
return { return {
"AD":"Andorra", AD: 'Andorra',
"AE":"United Arab Emirates", AE: 'United Arab Emirates',
"AF":"Afghanistan", AF: 'Afghanistan',
"AG":"Antigua and Barbuda", AG: 'Antigua and Barbuda',
"AI":"Anguilla", AI: 'Anguilla',
"AL":"Albania", AL: 'Albania',
"AM":"Armenia", AM: 'Armenia',
"AO":"Angola", AO: 'Angola',
"AR":"Argentina", AR: 'Argentina',
"AS":"AmericanSamoa", AS: 'AmericanSamoa',
"AT":"Austria", AT: 'Austria',
"AU":"Australia", AU: 'Australia',
"AW":"Aruba", AW: 'Aruba',
"AX":"Åland Islands", AX: 'Åland Islands',
"AZ":"Azerbaijan", AZ: 'Azerbaijan',
"BA":"Bosnia and Herzegovina", BA: 'Bosnia and Herzegovina',
"BB":"Barbados", BB: 'Barbados',
"BD":"Bangladesh", BD: 'Bangladesh',
"BE":"Belgium", BE: 'Belgium',
"BF":"Burkina Faso", BF: 'Burkina Faso',
"BG":"Bulgaria", BG: 'Bulgaria',
"BH":"Bahrain", BH: 'Bahrain',
"BI":"Burundi", BI: 'Burundi',
"BJ":"Benin", BJ: 'Benin',
"BL":"Saint Barthélemy", BL: 'Saint Barthélemy',
"BM":"Bermuda", BM: 'Bermuda',
"BN":"Brunei Darussalam", BN: 'Brunei Darussalam',
"BO":"Bolivia, Plurinational State of", BO: 'Bolivia, Plurinational State of',
"BR":"Brazil", BR: 'Brazil',
"BS":"Bahamas", BS: 'Bahamas',
"BT":"Bhutan", BT: 'Bhutan',
"BW":"Botswana", BW: 'Botswana',
"BY":"Belarus", BY: 'Belarus',
"BZ":"Belize", BZ: 'Belize',
"CA":"Canada", CA: 'Canada',
"CC":"Cocos (Keeling) Islands", CC: 'Cocos (Keeling) Islands',
"CD":"Congo, The Democratic Republic of the", CD: 'Congo, The Democratic Republic of the',
"CF":"Central African Republic", CF: 'Central African Republic',
"CG":"Congo", CG: 'Congo',
"CH":"Switzerland", CH: 'Switzerland',
"CI":"Cote d'Ivoire", CI: "Cote d'Ivoire",
"CK":"Cook Islands", CK: 'Cook Islands',
"CL":"Chile", CL: 'Chile',
"CM":"Cameroon", CM: 'Cameroon',
"CN":"China", CN: 'China',
"CO":"Colombia", CO: 'Colombia',
"CR":"Costa Rica", CR: 'Costa Rica',
"CU":"Cuba", CU: 'Cuba',
"CV":"Cape Verde", CV: 'Cape Verde',
"CX":"Christmas Island", CX: 'Christmas Island',
"CY":"Cyprus", CY: 'Cyprus',
"CZ":"Czech Republic", CZ: 'Czech Republic',
"DE":"Germany", DE: 'Germany',
"DJ":"Djibouti", DJ: 'Djibouti',
"DK":"Denmark", DK: 'Denmark',
"DM":"Dominica", DM: 'Dominica',
"DO":"Dominican Republic", DO: 'Dominican Republic',
"DZ":"Algeria", DZ: 'Algeria',
"EC":"Ecuador", EC: 'Ecuador',
"EE":"Estonia", EE: 'Estonia',
"EG":"Egypt", EG: 'Egypt',
"ER":"Eritrea", ER: 'Eritrea',
"ES":"Spain", ES: 'Spain',
"ET":"Ethiopia", ET: 'Ethiopia',
"FI":"Finland", FI: 'Finland',
"FJ":"Fiji", FJ: 'Fiji',
"FK":"Falkland Islands (Malvinas)", FK: 'Falkland Islands (Malvinas)',
"FM":"Micronesia, Federated States of", FM: 'Micronesia, Federated States of',
"FO":"Faroe Islands", FO: 'Faroe Islands',
"FR":"France", FR: 'France',
"GA":"Gabon", GA: 'Gabon',
"GB":"United Kingdom", GB: 'United Kingdom',
"GD":"Grenada", GD: 'Grenada',
"GE":"Georgia", GE: 'Georgia',
"GF":"French Guiana", GF: 'French Guiana',
"GG":"Guernsey", GG: 'Guernsey',
"GH":"Ghana", GH: 'Ghana',
"GI":"Gibraltar", GI: 'Gibraltar',
"GL":"Greenland", GL: 'Greenland',
"GM":"Gambia", GM: 'Gambia',
"GN":"Guinea", GN: 'Guinea',
"GP":"Guadeloupe", GP: 'Guadeloupe',
"GQ":"Equatorial Guinea", GQ: 'Equatorial Guinea',
"GR":"Ελλάδα", GR: 'Ελλάδα',
"GT":"Guatemala", GT: 'Guatemala',
"GU":"Guam", GU: 'Guam',
"GW":"Guinea-Bissau", GW: 'Guinea-Bissau',
"GY":"Guyana", GY: 'Guyana',
"HK":"Hong Kong", HK: 'Hong Kong',
"HN":"Honduras", HN: 'Honduras',
"HR":"Croatia", HR: 'Croatia',
"HT":"Haiti", HT: 'Haiti',
"HU":"Magyarország", HU: 'Magyarország',
"ID":"Indonesia", ID: 'Indonesia',
"IE":"Ireland", IE: 'Ireland',
"IL":"Israel", IL: 'Israel',
"IM":"Isle of Man", IM: 'Isle of Man',
"IN":"India", IN: 'India',
"IO":"British Indian Ocean Territory", IO: 'British Indian Ocean Territory',
"IQ":"Iraq", IQ: 'Iraq',
"IR":"Iran, Islamic Republic of", IR: 'Iran, Islamic Republic of',
"IS":"Iceland", IS: 'Iceland',
"IT":"Italy", IT: 'Italy',
"JE":"Jersey", JE: 'Jersey',
"JM":"Jamaica", JM: 'Jamaica',
"JO":"Jordan", JO: 'Jordan',
"JP":"Japan", JP: 'Japan',
"KE":"Kenya", KE: 'Kenya',
"KG":"Kyrgyzstan", KG: 'Kyrgyzstan',
"KH":"Cambodia", KH: 'Cambodia',
"KI":"Kiribati", KI: 'Kiribati',
"KM":"Comoros", KM: 'Comoros',
"KN":"Saint Kitts and Nevis", KN: 'Saint Kitts and Nevis',
"KP":"Korea, Democratic People's Republic of", KP: "Korea, Democratic People's Republic of",
"KR":"Korea, Republic of", KR: 'Korea, Republic of',
"KW":"Kuwait", KW: 'Kuwait',
"KY":"Cayman Islands", KY: 'Cayman Islands',
"KZ":"Kazakhstan", KZ: 'Kazakhstan',
"LA":"Lao People's Democratic Republic", LA: "Lao People's Democratic Republic",
"LB":"Lebanon", LB: 'Lebanon',
"LC":"Saint Lucia", LC: 'Saint Lucia',
"LI":"Liechtenstein", LI: 'Liechtenstein',
"LK":"Sri Lanka", LK: 'Sri Lanka',
"LR":"Liberia", LR: 'Liberia',
"LS":"Lesotho", LS: 'Lesotho',
"LT":"Lithuania", LT: 'Lithuania',
"LU":"Luxembourg", LU: 'Luxembourg',
"LV":"Latvia", LV: 'Latvia',
"LY":"Libyan Arab Jamahiriya", LY: 'Libyan Arab Jamahiriya',
"MA":"Morocco", MA: 'Morocco',
"MC":"Monaco", MC: 'Monaco',
"MD":"Moldova, Republic of", MD: 'Moldova, Republic of',
"ME":"Црна Гора", ME: 'Црна Гора',
"MF":"Saint Martin", MF: 'Saint Martin',
"MG":"Madagascar", MG: 'Madagascar',
"MH":"Marshall Islands", MH: 'Marshall Islands',
"MK":"Macedonia, The Former Yugoslav Republic of", MK: 'Macedonia, The Former Yugoslav Republic of',
"ML":"Mali", ML: 'Mali',
"MM":"Myanmar", MM: 'Myanmar',
"MN":"Mongolia", MN: 'Mongolia',
"MO":"Macao", MO: 'Macao',
"MP":"Northern Mariana Islands", MP: 'Northern Mariana Islands',
"MQ":"Martinique", MQ: 'Martinique',
"MR":"Mauritania", MR: 'Mauritania',
"MS":"Montserrat", MS: 'Montserrat',
"MT":"Malta", MT: 'Malta',
"MU":"Mauritius", MU: 'Mauritius',
"MV":"Maldives", MV: 'Maldives',
"MW":"Malawi", MW: 'Malawi',
"MX":"Mexico", MX: 'Mexico',
"MY":"Malaysia", MY: 'Malaysia',
"MZ":"Mozambique", MZ: 'Mozambique',
"NA":"Namibia", NA: 'Namibia',
"NC":"New Caledonia", NC: 'New Caledonia',
"NE":"Niger", NE: 'Niger',
"NF":"Norfolk Island", NF: 'Norfolk Island',
"NG":"Nigeria", NG: 'Nigeria',
"NI":"Nicaragua", NI: 'Nicaragua',
"NL":"Netherlands", NL: 'Netherlands',
"NO":"Norway", NO: 'Norway',
"NP":"Nepal", NP: 'Nepal',
"NR":"Nauru", NR: 'Nauru',
"NU":"Niue", NU: 'Niue',
"NZ":"New Zealand", NZ: 'New Zealand',
"OM":"Oman", OM: 'Oman',
"PA":"Panama", PA: 'Panama',
"PE":"Peru", PE: 'Peru',
"PF":"French Polynesia", PF: 'French Polynesia',
"PG":"Papua New Guinea", PG: 'Papua New Guinea',
"PH":"Philippines", PH: 'Philippines',
"PK":"Pakistan", PK: 'Pakistan',
"PL":"Polska", PL: 'Polska',
"PM":"Saint Pierre and Miquelon", PM: 'Saint Pierre and Miquelon',
"PR":"Puerto Rico", PR: 'Puerto Rico',
"PS":"Palestinian Territory, Occupied", PS: 'Palestinian Territory, Occupied',
"PT":"Portugal", PT: 'Portugal',
"PW":"Palau", PW: 'Palau',
"PY":"Paraguay", PY: 'Paraguay',
"QA":"Qatar", QA: 'Qatar',
"RE":"Réunion", RE: 'Réunion',
"RO":"Romania", RO: 'Romania',
"RS":"Србија", RS: 'Србија',
"RU":"Russia", RU: 'Russia',
"RW":"Rwanda", RW: 'Rwanda',
"SA":"Saudi Arabia", SA: 'Saudi Arabia',
"SB":"Solomon Islands", SB: 'Solomon Islands',
"SC":"Seychelles", SC: 'Seychelles',
"SD":"Sudan", SD: 'Sudan',
"SE":"Sweden", SE: 'Sweden',
"SG":"Singapore", SG: 'Singapore',
"SH":"Saint Helena, Ascension and Tristan Da Cunha", SH: 'Saint Helena, Ascension and Tristan Da Cunha',
"SI":"Slovenia", SI: 'Slovenia',
"SJ":"Svalbard and Jan Mayen", SJ: 'Svalbard and Jan Mayen',
"SK":"Slovakia", SK: 'Slovakia',
"SL":"Sierra Leone", SL: 'Sierra Leone',
"SM":"San Marino", SM: 'San Marino',
"SN":"Senegal", SN: 'Senegal',
"SO":"Somalia", SO: 'Somalia',
"SR":"Suriname", SR: 'Suriname',
"ST":"Sao Tome and Principe", ST: 'Sao Tome and Principe',
"SV":"El Salvador", SV: 'El Salvador',
"SY":"Syrian Arab Republic", SY: 'Syrian Arab Republic',
"SZ":"Swaziland", SZ: 'Swaziland',
"TC":"Turks and Caicos Islands", TC: 'Turks and Caicos Islands',
"TD":"Chad", TD: 'Chad',
"TG":"Togo", TG: 'Togo',
"TH":"Thailand", TH: 'Thailand',
"TJ":"Tajikistan", TJ: 'Tajikistan',
"TK":"Tokelau", TK: 'Tokelau',
"TL":"Timor-Leste", TL: 'Timor-Leste',
"TM":"Turkmenistan", TM: 'Turkmenistan',
"TN":"Tunisia", TN: 'Tunisia',
"TO":"Tonga", TO: 'Tonga',
"TR":"Turkey", TR: 'Turkey',
"TT":"Trinidad and Tobago", TT: 'Trinidad and Tobago',
"TV":"Tuvalu", TV: 'Tuvalu',
"TW":"Taiwan, Province of China", TW: 'Taiwan, Province of China',
"TZ":"Tanzania, United Republic of", TZ: 'Tanzania, United Republic of',
"UA":"Ukraine", UA: 'Ukraine',
"UG":"Uganda", UG: 'Uganda',
"US":"United States", US: 'United States',
"UY":"Uruguay", UY: 'Uruguay',
"UZ":"Uzbekistan", UZ: 'Uzbekistan',
"VA":"Holy See (Vatican City State)", VA: 'Holy See (Vatican City State)',
"VC":"Saint Vincent and the Grenadines", VC: 'Saint Vincent and the Grenadines',
"VE":"Venezuela", VE: 'Venezuela',
"VG":"Virgin Islands, British", VG: 'Virgin Islands, British',
"VI":"Virgin Islands, U.S.", VI: 'Virgin Islands, U.S.',
"VN":"Viet Nam", VN: 'Viet Nam',
"VU":"Vanuatu", VU: 'Vanuatu',
"WF":"Wallis and Futuna", WF: 'Wallis and Futuna',
"WS":"Samoa", WS: 'Samoa',
"YE":"Yemen", YE: 'Yemen',
"YT":"Mayotte", YT: 'Mayotte',
"ZA":"South Africa", ZA: 'South Africa',
"ZM":"Zambia", ZM: 'Zambia',
"ZW":"Zimbabwe" ZW: 'Zimbabwe',
}; };
} // getAllRegionCodes }, // getAllRegionCodes
}; // libphonenumber.util }; // libphonenumber.util
})(); })();

View file

@ -34,7 +34,7 @@ function log(...args) {
console._log(...consoleArgs); console._log(...consoleArgs);
// To avoid [Object object] in our log since console.log handles non-strings smoothly // To avoid [Object object] in our log since console.log handles non-strings smoothly
const str = args.map((item) => { const str = args.map(item => {
if (typeof item !== 'string') { if (typeof item !== 'string') {
try { try {
return JSON.stringify(item); return JSON.stringify(item);
@ -55,7 +55,6 @@ if (window.console) {
console.log = log; console.log = log;
} }
// The mechanics of preparing a log for publish // The mechanics of preparing a log for publish
function getHeader() { function getHeader() {
@ -85,7 +84,7 @@ function format(entries) {
} }
function fetch() { function fetch() {
return new Promise((resolve) => { return new Promise(resolve => {
ipc.send('fetch-log'); ipc.send('fetch-log');
ipc.on('fetched-log', (event, text) => { ipc.on('fetched-log', (event, text) => {
@ -103,14 +102,16 @@ const publish = debuglogs.upload;
// Anyway, the default process.stdout stream goes to the command-line, not the devtools. // Anyway, the default process.stdout stream goes to the command-line, not the devtools.
const logger = bunyan.createLogger({ const logger = bunyan.createLogger({
name: 'log', name: 'log',
streams: [{ streams: [
{
level: 'debug', level: 'debug',
stream: { stream: {
write(entry) { write(entry) {
console._log(formatLine(JSON.parse(entry))); console._log(formatLine(JSON.parse(entry)));
}, },
}, },
}], },
],
}); });
// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api // The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api
@ -137,6 +138,8 @@ window.onerror = (message, script, line, col, error) => {
window.log.error(`Top-level unhandled error: ${errorInfo}`); window.log.error(`Top-level unhandled error: ${errorInfo}`);
}; };
window.addEventListener('unhandledrejection', (rejectionEvent) => { window.addEventListener('unhandledrejection', rejectionEvent => {
window.log.error(`Top-level unhandled promise rejection: ${rejectionEvent.reason}`); window.log.error(
`Top-level unhandled promise rejection: ${rejectionEvent.reason}`
);
}); });

View file

@ -124,27 +124,34 @@
}, },
safeGetVerified() { safeGetVerified() {
const promise = textsecure.storage.protocol.getVerified(this.id); const promise = textsecure.storage.protocol.getVerified(this.id);
return promise.catch(() => textsecure.storage.protocol.VerifiedStatus.DEFAULT); return promise.catch(
() => textsecure.storage.protocol.VerifiedStatus.DEFAULT
);
}, },
updateVerified() { updateVerified() {
if (this.isPrivate()) { if (this.isPrivate()) {
return Promise.all([ return Promise.all([this.safeGetVerified(), this.initialPromise]).then(
this.safeGetVerified(), results => {
this.initialPromise,
]).then((results) => {
const trust = results[0]; const trust = results[0];
// we don't return here because we don't need to wait for this to finish // we don't return here because we don't need to wait for this to finish
this.save({ verified: trust }); this.save({ verified: trust });
}); }
);
} }
const promise = this.fetchContacts(); const promise = this.fetchContacts();
return promise.then(() => Promise.all(this.contactCollection.map((contact) => { return promise
.then(() =>
Promise.all(
this.contactCollection.map(contact => {
if (!contact.isMe()) { if (!contact.isMe()) {
return contact.updateVerified(); return contact.updateVerified();
} }
return Promise.resolve(); return Promise.resolve();
}))).then(this.onMemberVerifiedChange.bind(this)); })
)
)
.then(this.onMemberVerifiedChange.bind(this));
}, },
setVerifiedDefault(options) { setVerifiedDefault(options) {
const { DEFAULT } = this.verifiedEnum; const { DEFAULT } = this.verifiedEnum;
@ -160,16 +167,19 @@
}, },
_setVerified(verified, providedOptions) { _setVerified(verified, providedOptions) {
const options = providedOptions || {}; const options = providedOptions || {};
_.defaults(options, { viaSyncMessage: false, viaContactSync: false, key: null }); _.defaults(options, {
viaSyncMessage: false,
viaContactSync: false,
key: null,
});
const { const { VERIFIED, UNVERIFIED } = this.verifiedEnum;
VERIFIED,
UNVERIFIED,
} = this.verifiedEnum;
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error('You cannot verify a group conversation. ' + throw new Error(
'You must verify individual contacts.'); 'You cannot verify a group conversation. ' +
'You must verify individual contacts.'
);
} }
const beginningVerified = this.get('verified'); const beginningVerified = this.get('verified');
@ -187,10 +197,14 @@
} }
let keychange; let keychange;
return promise.then((updatedKey) => { return promise
.then(updatedKey => {
keychange = updatedKey; keychange = updatedKey;
return new Promise((resolve => this.save({ verified }).always(resolve))); return new Promise(resolve =>
}).then(() => { this.save({ verified }).always(resolve)
);
})
.then(() => {
// Three situations result in a verification notice in the conversation: // Three situations result in a verification notice in the conversation:
// 1) The message came from an explicit verification in another client (not // 1) The message came from an explicit verification in another client (not
// a contact sync) // a contact sync)
@ -199,14 +213,14 @@
// 3) Our local verification status is VERIFIED and it hasn't changed, // 3) Our local verification status is VERIFIED and it hasn't changed,
// but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't // but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED) // want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
if (!options.viaContactSync || if (
!options.viaContactSync ||
(beginningVerified !== verified && verified !== UNVERIFIED) || (beginningVerified !== verified && verified !== UNVERIFIED) ||
(keychange && verified === VERIFIED)) { (keychange && verified === VERIFIED)
return this.addVerifiedChange( ) {
this.id, return this.addVerifiedChange(this.id, verified === VERIFIED, {
verified === VERIFIED, local: !options.viaSyncMessage,
{ local: !options.viaSyncMessage } });
);
} }
if (!options.viaSyncMessage) { if (!options.viaSyncMessage) {
return this.sendVerifySyncMessage(this.id, verified); return this.sendVerifySyncMessage(this.id, verified);
@ -216,20 +230,21 @@
}, },
sendVerifySyncMessage(number, state) { sendVerifySyncMessage(number, state) {
const promise = textsecure.storage.protocol.loadIdentityKey(number); const promise = textsecure.storage.protocol.loadIdentityKey(number);
return promise.then(key => textsecure.messaging.syncVerification( return promise.then(key =>
number, textsecure.messaging.syncVerification(number, state, key)
state, );
key
));
}, },
getIdentityKeys() { getIdentityKeys() {
const lookup = {}; const lookup = {};
if (this.isPrivate()) { if (this.isPrivate()) {
return textsecure.storage.protocol.loadIdentityKey(this.id).then((key) => { return textsecure.storage.protocol
.loadIdentityKey(this.id)
.then(key => {
lookup[this.id] = key; lookup[this.id] = key;
return lookup; return lookup;
}).catch((error) => { })
.catch(error => {
console.log( console.log(
'getIdentityKeys error for conversation', 'getIdentityKeys error for conversation',
this.idForLogging(), this.idForLogging(),
@ -240,27 +255,25 @@
} }
const promises = this.contactCollection.map(contact => const promises = this.contactCollection.map(contact =>
textsecure.storage.protocol.loadIdentityKey(contact.id).then( textsecure.storage.protocol.loadIdentityKey(contact.id).then(
(key) => { key => {
lookup[contact.id] = key; lookup[contact.id] = key;
}, },
(error) => { error => {
console.log( console.log(
'getIdentityKeys error for group member', 'getIdentityKeys error for group member',
contact.idForLogging(), contact.idForLogging(),
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
)); )
);
return Promise.all(promises).then(() => lookup); return Promise.all(promises).then(() => lookup);
}, },
replay(error, message) { replay(error, message) {
const replayable = new textsecure.ReplayableError(error); const replayable = new textsecure.ReplayableError(error);
return replayable.replay(message.attributes).catch((e) => { return replayable.replay(message.attributes).catch(e => {
console.log( console.log('replay error:', e && e.stack ? e.stack : e);
'replay error:',
e && e.stack ? e.stack : e
);
}); });
}, },
decryptOldIncomingKeyErrors() { decryptOldIncomingKeyErrors() {
@ -270,19 +283,25 @@
} }
console.log('decryptOldIncomingKeyErrors start for', this.idForLogging()); console.log('decryptOldIncomingKeyErrors start for', this.idForLogging());
const messages = this.messageCollection.filter((message) => { const messages = this.messageCollection.filter(message => {
const errors = message.get('errors'); const errors = message.get('errors');
if (!errors || !errors[0]) { if (!errors || !errors[0]) {
return false; return false;
} }
const error = _.find(errors, e => e.name === 'IncomingIdentityKeyError'); const error = _.find(
errors,
e => e.name === 'IncomingIdentityKeyError'
);
return Boolean(error); return Boolean(error);
}); });
const markComplete = () => { const markComplete = () => {
console.log('decryptOldIncomingKeyErrors complete for', this.idForLogging()); console.log(
return new Promise((resolve) => { 'decryptOldIncomingKeyErrors complete for',
this.idForLogging()
);
return new Promise(resolve => {
this.save({ decryptedOldIncomingKeyErrors: true }).always(resolve); this.save({ decryptedOldIncomingKeyErrors: true }).always(resolve);
}); });
}; };
@ -296,12 +315,16 @@
messages.length, messages.length,
'messages to process' 'messages to process'
); );
const safeDelete = message => new Promise((resolve) => { const safeDelete = message =>
new Promise(resolve => {
message.destroy().always(resolve); message.destroy().always(resolve);
}); });
const promise = this.getIdentityKeys(); const promise = this.getIdentityKeys();
return promise.then(lookup => Promise.all(_.map(messages, (message) => { return promise
.then(lookup =>
Promise.all(
_.map(messages, message => {
const source = message.get('source'); const source = message.get('source');
const error = _.find( const error = _.find(
message.get('errors'), message.get('errors'),
@ -314,16 +337,22 @@
} }
if (constantTimeEqualArrayBuffers(key, error.identityKey)) { if (constantTimeEqualArrayBuffers(key, error.identityKey)) {
return this.replay(error, message).then(() => safeDelete(message)); return this.replay(error, message).then(() =>
safeDelete(message)
);
} }
return Promise.resolve(); return Promise.resolve();
}))).catch((error) => { })
)
)
.catch(error => {
console.log( console.log(
'decryptOldIncomingKeyErrors error:', 'decryptOldIncomingKeyErrors error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
}).then(markComplete); })
.then(markComplete);
}, },
isVerified() { isVerified() {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -333,7 +362,7 @@
return false; return false;
} }
return this.contactCollection.every((contact) => { return this.contactCollection.every(contact => {
if (contact.isMe()) { if (contact.isMe()) {
return true; return true;
} }
@ -343,14 +372,16 @@
isUnverified() { isUnverified() {
if (this.isPrivate()) { if (this.isPrivate()) {
const verified = this.get('verified'); const verified = this.get('verified');
return verified !== this.verifiedEnum.VERIFIED && return (
verified !== this.verifiedEnum.DEFAULT; verified !== this.verifiedEnum.VERIFIED &&
verified !== this.verifiedEnum.DEFAULT
);
} }
if (!this.contactCollection.length) { if (!this.contactCollection.length) {
return true; return true;
} }
return this.contactCollection.any((contact) => { return this.contactCollection.any(contact => {
if (contact.isMe()) { if (contact.isMe()) {
return false; return false;
} }
@ -363,23 +394,29 @@
? new Backbone.Collection([this]) ? new Backbone.Collection([this])
: new Backbone.Collection(); : new Backbone.Collection();
} }
return new Backbone.Collection(this.contactCollection.filter((contact) => { return new Backbone.Collection(
this.contactCollection.filter(contact => {
if (contact.isMe()) { if (contact.isMe()) {
return false; return false;
} }
return contact.isUnverified(); return contact.isUnverified();
})); })
);
}, },
setApproved() { setApproved() {
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error('You cannot set a group conversation as trusted. ' + throw new Error(
'You must set individual contacts as trusted.'); 'You cannot set a group conversation as trusted. ' +
'You must set individual contacts as trusted.'
);
} }
return textsecure.storage.protocol.setApproval(this.id, true); return textsecure.storage.protocol.setApproval(this.id, true);
}, },
safeIsUntrusted() { safeIsUntrusted() {
return textsecure.storage.protocol.isUntrusted(this.id).catch(() => false); return textsecure.storage.protocol
.isUntrusted(this.id)
.catch(() => false);
}, },
isUntrusted() { isUntrusted() {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -389,18 +426,20 @@
return Promise.resolve(false); return Promise.resolve(false);
} }
return Promise.all(this.contactCollection.map((contact) => { return Promise.all(
this.contactCollection.map(contact => {
if (contact.isMe()) { if (contact.isMe()) {
return false; return false;
} }
return contact.safeIsUntrusted(); return contact.safeIsUntrusted();
})).then(results => _.any(results, result => result)); })
).then(results => _.any(results, result => result));
}, },
getUntrusted() { getUntrusted() {
// This is a bit ugly because isUntrusted() is async. Could do the work to cache // This is a bit ugly because isUntrusted() is async. Could do the work to cache
// it locally, but we really only need it for this call. // it locally, but we really only need it for this call.
if (this.isPrivate()) { if (this.isPrivate()) {
return this.isUntrusted().then((untrusted) => { return this.isUntrusted().then(untrusted => {
if (untrusted) { if (untrusted) {
return new Backbone.Collection([this]); return new Backbone.Collection([this]);
} }
@ -408,20 +447,24 @@
return new Backbone.Collection(); return new Backbone.Collection();
}); });
} }
return Promise.all(this.contactCollection.map((contact) => { return Promise.all(
this.contactCollection.map(contact => {
if (contact.isMe()) { if (contact.isMe()) {
return [false, contact]; return [false, contact];
} }
return Promise.all([contact.isUntrusted(), contact]); return Promise.all([contact.isUntrusted(), contact]);
})).then((results) => { })
const filtered = _.filter(results, (result) => { ).then(results => {
const filtered = _.filter(results, result => {
const untrusted = result[0]; const untrusted = result[0];
return untrusted; return untrusted;
}); });
return new Backbone.Collection(_.map(filtered, (result) => { return new Backbone.Collection(
_.map(filtered, result => {
const contact = result[1]; const contact = result[1];
return contact; return contact;
})); })
);
}); });
}, },
onMemberVerifiedChange() { onMemberVerifiedChange() {
@ -461,7 +504,9 @@
_.defaults(options, { local: true }); _.defaults(options, { local: true });
if (this.isMe()) { if (this.isMe()) {
console.log('refusing to add verified change advisory for our own number'); console.log(
'refusing to add verified change advisory for our own number'
);
return; return;
} }
@ -488,8 +533,8 @@
message.save().then(this.trigger.bind(this, 'newmessage', message)); message.save().then(this.trigger.bind(this, 'newmessage', message));
if (this.isPrivate()) { if (this.isPrivate()) {
ConversationController.getAllGroupsInvolvingId(id).then((groups) => { ConversationController.getAllGroupsInvolvingId(id).then(groups => {
_.forEach(groups, (group) => { _.forEach(groups, group => {
group.addVerifiedChange(id, verified, options); group.addVerifiedChange(id, verified, options);
}); });
}); });
@ -512,31 +557,36 @@
// Lastly, we don't send read syncs for any message marked read due to a read // Lastly, we don't send read syncs for any message marked read due to a read
// sync. That's a notification explosion we don't need. // sync. That's a notification explosion we don't need.
return this.queueJob(() => this.markRead( return this.queueJob(() =>
message.get('received_at'), this.markRead(message.get('received_at'), { sendReadReceipts: false })
{ sendReadReceipts: false } );
));
}, },
getUnread() { getUnread() {
const conversationId = this.id; const conversationId = this.id;
const unreadMessages = new Whisper.MessageCollection(); const unreadMessages = new Whisper.MessageCollection();
return new Promise((resolve => unreadMessages.fetch({ return new Promise(resolve =>
unreadMessages
.fetch({
index: { index: {
// 'unread' index // 'unread' index
name: 'unread', name: 'unread',
lower: [conversationId], lower: [conversationId],
upper: [conversationId, Number.MAX_VALUE], upper: [conversationId, Number.MAX_VALUE],
}, },
}).always(() => { })
.always(() => {
resolve(unreadMessages); resolve(unreadMessages);
}))); })
);
}, },
validate(attributes) { validate(attributes) {
const required = ['id', 'type']; const required = ['id', 'type'];
const missing = _.filter(required, attr => !attributes[attr]); const missing = _.filter(required, attr => !attributes[attr]);
if (missing.length) { return `Conversation must have ${missing}`; } if (missing.length) {
return `Conversation must have ${missing}`;
}
if (attributes.type !== 'private' && attributes.type !== 'group') { if (attributes.type !== 'private' && attributes.type !== 'group') {
return `Invalid conversation type: ${attributes.type}`; return `Invalid conversation type: ${attributes.type}`;
@ -572,7 +622,12 @@
const name = this.get('name'); const name = this.get('name');
if (typeof name === 'string') { if (typeof name === 'string') {
tokens.push(name.toLowerCase()); tokens.push(name.toLowerCase());
tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_()+]+/)); tokens = tokens.concat(
name
.trim()
.toLowerCase()
.split(/[\s\-_()+]+/)
);
} }
if (this.isPrivate()) { if (this.isPrivate()) {
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
@ -633,7 +688,9 @@
type: contentType, type: contentType,
}); });
const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported(contentType) const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported(
contentType
)
? await Whisper.FileInputView.makeImageThumbnail(128, objectUrl) ? await Whisper.FileInputView.makeImageThumbnail(128, objectUrl)
: await Whisper.FileInputView.makeVideoThumbnail(128, objectUrl); : await Whisper.FileInputView.makeVideoThumbnail(128, objectUrl);
@ -661,7 +718,8 @@
author: contact.id, author: contact.id,
id: quotedMessage.get('sent_at'), id: quotedMessage.get('sent_at'),
text: quotedMessage.get('body'), text: quotedMessage.get('body'),
attachments: await Promise.all((attachments || []).map(async (attachment) => { attachments: await Promise.all(
(attachments || []).map(async attachment => {
const { contentType } = attachment; const { contentType } = attachment;
const willMakeThumbnail = const willMakeThumbnail =
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) || Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
@ -674,7 +732,8 @@
? await this.makeThumbnailAttachment(attachment) ? await this.makeThumbnailAttachment(attachment)
: null, : null,
}; };
})), })
),
}; };
}, },
@ -721,7 +780,9 @@
case Message.GROUP: case Message.GROUP:
return textsecure.messaging.sendMessageToGroup; return textsecure.messaging.sendMessageToGroup;
default: default:
throw new TypeError(`Invalid conversation type: '${conversationType}'`); throw new TypeError(
`Invalid conversation type: '${conversationType}'`
);
} }
})(); })();
@ -730,9 +791,11 @@
profileKey = storage.get('profileKey'); profileKey = storage.get('profileKey');
} }
const attachmentsWithData = const attachmentsWithData = await Promise.all(
await Promise.all(messageWithSchema.attachments.map(loadAttachmentData)); messageWithSchema.attachments.map(loadAttachmentData)
message.send(sendFunction( );
message.send(
sendFunction(
this.get('id'), this.get('id'),
body, body,
attachmentsWithData, attachmentsWithData,
@ -740,7 +803,8 @@
now, now,
this.get('expireTimer'), this.get('expireTimer'),
profileKey profileKey
)); )
);
}); });
}, },
@ -749,13 +813,16 @@
await collection.fetchConversation(this.id, 1); await collection.fetchConversation(this.id, 1);
const lastMessage = collection.at(0); const lastMessage = collection.at(0);
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({ const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate(
{
currentLastMessageText: this.get('lastMessage') || null, currentLastMessageText: this.get('lastMessage') || null,
currentTimestamp: this.get('timestamp') || null, currentTimestamp: this.get('timestamp') || null,
lastMessage: lastMessage ? lastMessage.toJSON() : null, lastMessage: lastMessage ? lastMessage.toJSON() : null,
lastMessageNotificationText: lastMessage lastMessageNotificationText: lastMessage
? lastMessage.getNotificationText() : null, ? lastMessage.getNotificationText()
}); : null,
}
);
this.set(lastMessageUpdate); this.set(lastMessageUpdate);
@ -779,8 +846,10 @@
if (!expireTimer) { if (!expireTimer) {
expireTimer = null; expireTimer = null;
} }
if (this.get('expireTimer') === expireTimer || if (
(!expireTimer && !this.get('expireTimer'))) { this.get('expireTimer') === expireTimer ||
(!expireTimer && !this.get('expireTimer'))
) {
return Promise.resolve(); return Promise.resolve();
} }
@ -881,12 +950,14 @@
received_at: now, received_at: now,
group_update: groupUpdate, group_update: groupUpdate,
}); });
message.send(textsecure.messaging.updateGroup( message.send(
textsecure.messaging.updateGroup(
this.id, this.id,
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
this.get('members') this.get('members')
)); )
);
}, },
leaveGroup() { leaveGroup() {
@ -909,25 +980,30 @@
_.defaults(options, { sendReadReceipts: true }); _.defaults(options, { sendReadReceipts: true });
const conversationId = this.id; const conversationId = this.id;
Whisper.Notifications.remove(Whisper.Notifications.where({ Whisper.Notifications.remove(
Whisper.Notifications.where({
conversationId, conversationId,
})); })
);
return this.getUnread().then((providedUnreadMessages) => { return this.getUnread().then(providedUnreadMessages => {
let unreadMessages = providedUnreadMessages; let unreadMessages = providedUnreadMessages;
const promises = []; const promises = [];
const oldUnread = unreadMessages.filter(message => const oldUnread = unreadMessages.filter(
message.get('received_at') <= newestUnreadDate); message => message.get('received_at') <= newestUnreadDate
);
let read = _.map(oldUnread, (providedM) => { let read = _.map(oldUnread, providedM => {
let m = providedM; let m = providedM;
if (this.messageCollection.get(m.id)) { if (this.messageCollection.get(m.id)) {
m = this.messageCollection.get(m.id); m = this.messageCollection.get(m.id);
} else { } else {
console.log('Marked a message as read in the database, but ' + console.log(
'it was not in messageCollection.'); 'Marked a message as read in the database, but ' +
'it was not in messageCollection.'
);
} }
promises.push(m.markRead()); promises.push(m.markRead());
const errors = m.get('errors'); const errors = m.get('errors');
@ -962,7 +1038,9 @@
if (storage.get('read-receipt-setting')) { if (storage.get('read-receipt-setting')) {
_.each(_.groupBy(read, 'sender'), (receipts, sender) => { _.each(_.groupBy(read, 'sender'), (receipts, sender) => {
const timestamps = _.map(receipts, 'timestamp'); const timestamps = _.map(receipts, 'timestamp');
promises.push(textsecure.messaging.sendReadReceipts(sender, timestamps)); promises.push(
textsecure.messaging.sendReadReceipts(sender, timestamps)
);
}); });
} }
} }
@ -990,21 +1068,22 @@
getProfile(id) { getProfile(id) {
if (!textsecure.messaging) { if (!textsecure.messaging) {
const message = 'Conversation.getProfile: textsecure.messaging not available'; const message =
'Conversation.getProfile: textsecure.messaging not available';
return Promise.reject(new Error(message)); return Promise.reject(new Error(message));
} }
return textsecure.messaging.getProfile(id).then((profile) => { return textsecure.messaging
.getProfile(id)
.then(profile => {
const identityKey = dcodeIO.ByteBuffer.wrap( const identityKey = dcodeIO.ByteBuffer.wrap(
profile.identityKey, profile.identityKey,
'base64' 'base64'
).toArrayBuffer(); ).toArrayBuffer();
return textsecure.storage.protocol.saveIdentity( return textsecure.storage.protocol
`${id}.1`, .saveIdentity(`${id}.1`, identityKey, false)
identityKey, .then(changed => {
false
).then((changed) => {
if (changed) { if (changed) {
// save identity will close all sessions except for .1, so we // save identity will close all sessions except for .1, so we
// must close that one manually. // must close that one manually.
@ -1017,18 +1096,20 @@
return sessionCipher.closeOpenSessionForDevice(); return sessionCipher.closeOpenSessionForDevice();
} }
return Promise.resolve(); return Promise.resolve();
}).then(() => { })
.then(() => {
const c = ConversationController.get(id); const c = ConversationController.get(id);
return Promise.all([ return Promise.all([
c.setProfileName(profile.name), c.setProfileName(profile.name),
c.setProfileAvatar(profile.avatar), c.setProfileAvatar(profile.avatar),
]).then( ]).then(
// success // success
() => new Promise((resolve, reject) => { () =>
new Promise((resolve, reject) => {
c.save().then(resolve, reject); c.save().then(resolve, reject);
}), }),
// fail // fail
(e) => { e => {
if (e.name === 'ProfileDecryptError') { if (e.name === 'ProfileDecryptError') {
// probably the profile key has changed. // probably the profile key has changed.
console.log( console.log(
@ -1041,7 +1122,8 @@
} }
); );
}); });
}).catch((error) => { })
.catch(error => {
console.log( console.log(
'getProfile error:', 'getProfile error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -1056,10 +1138,15 @@
try { try {
// decode // decode
const data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer(); const data = dcodeIO.ByteBuffer.wrap(
encryptedName,
'base64'
).toArrayBuffer();
// decrypt // decrypt
return textsecure.crypto.decryptProfileName(data, key).then((decrypted) => { return textsecure.crypto
.decryptProfileName(data, key)
.then(decrypted => {
// encode // encode
const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'); const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
@ -1075,13 +1162,13 @@
return Promise.resolve(); return Promise.resolve();
} }
return textsecure.messaging.getAvatar(avatarPath).then((avatar) => { return textsecure.messaging.getAvatar(avatarPath).then(avatar => {
const key = this.get('profileKey'); const key = this.get('profileKey');
if (!key) { if (!key) {
return Promise.resolve(); return Promise.resolve();
} }
// decrypt // decrypt
return textsecure.crypto.decryptProfile(avatar, key).then((decrypted) => { return textsecure.crypto.decryptProfile(avatar, key).then(decrypted => {
// set // set
this.set({ this.set({
profileAvatar: { profileAvatar: {
@ -1125,9 +1212,11 @@
const first = attachments[0]; const first = attachments[0];
const { thumbnail, contentType } = first; const { thumbnail, contentType } = first;
return thumbnail || return (
thumbnail ||
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) || Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
Signal.Util.GoogleChrome.isVideoTypeSupported(contentType); Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)
);
}, },
forceRender(message) { forceRender(message) {
message.trigger('change', message); message.trigger('change', message);
@ -1163,14 +1252,18 @@
return false; return false;
} }
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && if (
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) { !Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
) {
return false; return false;
} }
const collection = new Whisper.MessageCollection(); const collection = new Whisper.MessageCollection();
await collection.fetchSentAt(id); await collection.fetchSentAt(id);
const queryMessage = collection.find(m => this.doesMessageMatch(id, author, m)); const queryMessage = collection.find(m =>
this.doesMessageMatch(id, author, m)
);
if (!queryMessage) { if (!queryMessage) {
return false; return false;
@ -1206,8 +1299,10 @@
return; return;
} }
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && if (
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) { !Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
) {
return; return;
} }
@ -1267,7 +1362,7 @@
async processQuotes(messages) { async processQuotes(messages) {
const lookup = this.makeMessagesLookup(messages); const lookup = this.makeMessagesLookup(messages);
const promises = messages.map(async (message) => { const promises = messages.map(async message => {
const { quote } = message.attributes; const { quote } = message.attributes;
if (!quote) { if (!quote) {
return; return;
@ -1350,11 +1445,16 @@
} }
const members = this.get('members') || []; const members = this.get('members') || [];
const promises = members.map(number => const promises = members.map(number =>
ConversationController.getOrCreateAndWait(number, 'private')); ConversationController.getOrCreateAndWait(number, 'private')
);
return Promise.all(promises).then((contacts) => { return Promise.all(promises).then(contacts => {
_.forEach(contacts, (contact) => { _.forEach(contacts, contact => {
this.listenTo(contact, 'change:verified', this.onMemberVerifiedChange); this.listenTo(
contact,
'change:verified',
this.onMemberVerifiedChange
);
}); });
this.contactCollection.reset(contacts); this.contactCollection.reset(contacts);
@ -1362,17 +1462,19 @@
}, },
destroyMessages() { destroyMessages() {
this.messageCollection.fetch({ this.messageCollection
.fetch({
index: { index: {
// 'conversation' index on [conversationId, received_at] // 'conversation' index on [conversationId, received_at]
name: 'conversation', name: 'conversation',
lower: [this.id], lower: [this.id],
upper: [this.id, Number.MAX_VALUE], upper: [this.id, Number.MAX_VALUE],
}, },
}).then(() => { })
.then(() => {
const { models } = this.messageCollection; const { models } = this.messageCollection;
this.messageCollection.reset([]); this.messageCollection.reset([]);
_.each(models, (message) => { _.each(models, message => {
message.destroy(); message.destroy();
}); });
this.save({ this.save({
@ -1460,10 +1562,9 @@
this.revokeAvatarUrl(); this.revokeAvatarUrl();
const avatar = this.get('avatar') || this.get('profileAvatar'); const avatar = this.get('avatar') || this.get('profileAvatar');
if (avatar) { if (avatar) {
this.avatarUrl = URL.createObjectURL(new Blob( this.avatarUrl = URL.createObjectURL(
[avatar.data], new Blob([avatar.data], { type: avatar.contentType })
{ type: avatar.contentType } );
));
} else { } else {
this.avatarUrl = null; this.avatarUrl = null;
} }
@ -1507,7 +1608,7 @@
}, },
getNotificationIcon() { getNotificationIcon() {
return new Promise((resolve) => { return new Promise(resolve => {
const avatar = this.getAvatar(); const avatar = this.getAvatar();
if (avatar.url) { if (avatar.url) {
resolve(avatar.url); resolve(avatar.url);
@ -1523,8 +1624,11 @@
} }
const conversationId = this.id; const conversationId = this.id;
return ConversationController.getOrCreateAndWait(message.get('source'), 'private') return ConversationController.getOrCreateAndWait(
.then(sender => sender.getNotificationIcon().then((iconUrl) => { message.get('source'),
'private'
).then(sender =>
sender.getNotificationIcon().then(iconUrl => {
console.log('adding notification'); console.log('adding notification');
Whisper.Notifications.add({ Whisper.Notifications.add({
title: sender.getTitle(), title: sender.getTitle(),
@ -1534,7 +1638,8 @@
conversationId, conversationId,
messageId: message.id, messageId: message.id,
}); });
})); })
);
}, },
hashCode() { hashCode() {
if (this.hash === undefined) { if (this.hash === undefined) {
@ -1545,7 +1650,7 @@
let hash = 0; let hash = 0;
for (let i = 0; i < string.length; i += 1) { for (let i = 0; i < string.length; i += 1) {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
hash = ((hash << 5) - hash) + string.charCodeAt(i); hash = (hash << 5) - hash + string.charCodeAt(i);
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
hash &= hash; // Convert to 32bit integer hash &= hash; // Convert to 32bit integer
} }
@ -1566,9 +1671,17 @@
}, },
destroyAll() { destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => { return Promise.all(
m.destroy().then(resolve).fail(reject); this.models.map(
}))); m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
}, },
search(providedQuery) { search(providedQuery) {
@ -1578,7 +1691,7 @@
const lastCharCode = query.charCodeAt(query.length - 1); const lastCharCode = query.charCodeAt(query.length - 1);
const nextChar = String.fromCharCode(lastCharCode + 1); const nextChar = String.fromCharCode(lastCharCode + 1);
const upper = query.slice(0, -1) + nextChar; const upper = query.slice(0, -1) + nextChar;
return new Promise((resolve) => { return new Promise(resolve => {
this.fetch({ this.fetch({
index: { index: {
name: 'search', // 'search' index on tokens array name: 'search', // 'search' index on tokens array
@ -1593,7 +1706,7 @@
}, },
fetchAlphabetical() { fetchAlphabetical() {
return new Promise((resolve) => { return new Promise(resolve => {
this.fetch({ this.fetch({
index: { index: {
name: 'search', // 'search' index on tokens array name: 'search', // 'search' index on tokens array
@ -1604,7 +1717,7 @@
}, },
fetchGroups(number) { fetchGroups(number) {
return new Promise((resolve) => { return new Promise(resolve => {
this.fetch({ this.fetch({
index: { index: {
name: 'group', name: 'group',
@ -1623,7 +1736,7 @@
storeName: 'conversations', storeName: 'conversations',
model: Whisper.Conversation, model: Whisper.Conversation,
fetchGroups(number) { fetchGroups(number) {
return new Promise((resolve) => { return new Promise(resolve => {
this.fetch({ this.fetch({
index: { index: {
name: 'group', name: 'group',
@ -1633,4 +1746,4 @@
}); });
}, },
}); });
}()); })();

View file

@ -32,10 +32,13 @@
this.on('unload', this.unload); this.on('unload', this.unload);
this.setToExpire(); this.setToExpire();
this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; this.VOICE_FLAG =
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
}, },
idForLogging() { idForLogging() {
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`; return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
'sent_at'
)}`;
}, },
defaults() { defaults() {
return { return {
@ -56,12 +59,13 @@
return !!(this.get('flags') & flag); return !!(this.get('flags') & flag);
}, },
isExpirationTimerUpdate() { isExpirationTimerUpdate() {
const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag); return !!(this.get('flags') & flag);
}, },
isGroupUpdate() { isGroupUpdate() {
return !!(this.get('group_update')); return !!this.get('group_update');
}, },
isIncoming() { isIncoming() {
return this.get('type') === 'incoming'; return this.get('type') === 'incoming';
@ -116,7 +120,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name)); messages.push(i18n('titleIsNow', groupUpdate.name));
} }
if (groupUpdate.joined && groupUpdate.joined.length) { if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this)); const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
);
if (names.length > 1) { if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', '))); messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else { } else {
@ -186,7 +193,7 @@
} }
const quote = this.get('quote'); const quote = this.get('quote');
const attachments = (quote && quote.attachments) || []; const attachments = (quote && quote.attachments) || [];
attachments.forEach((attachment) => { attachments.forEach(attachment => {
if (attachment.thumbnail && attachment.thumbnail.objectUrl) { if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
URL.revokeObjectURL(attachment.thumbnail.objectUrl); URL.revokeObjectURL(attachment.thumbnail.objectUrl);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -269,7 +276,8 @@
return { return {
attachments: (quote.attachments || []).map(attachment => attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)), this.processAttachment(attachment, objectUrl)
),
authorColor, authorColor,
authorProfileName, authorProfileName,
authorTitle, authorTitle,
@ -342,7 +350,8 @@
send(promise) { send(promise) {
this.trigger('pending'); this.trigger('pending');
return promise.then((result) => { return promise
.then(result => {
const now = Date.now(); const now = Date.now();
this.trigger('done'); this.trigger('done');
if (result.dataMessage) { if (result.dataMessage) {
@ -355,7 +364,8 @@
expirationStartTimestamp: now, expirationStartTimestamp: now,
}); });
this.sendSyncMessage(); this.sendSyncMessage();
}).catch((result) => { })
.catch(result => {
const now = Date.now(); const now = Date.now();
this.trigger('done'); this.trigger('done');
if (result.dataMessage) { if (result.dataMessage) {
@ -383,12 +393,14 @@
}); });
promises.push(this.sendSyncMessage()); promises.push(this.sendSyncMessage());
} }
promises = promises.concat(_.map(result.errors, (error) => { promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') { if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number); const c = ConversationController.get(error.number);
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
})); })
);
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
@ -423,12 +435,14 @@
if (this.get('synced') || !dataMessage) { if (this.get('synced') || !dataMessage) {
return Promise.resolve(); return Promise.resolve();
} }
return textsecure.messaging.sendSyncMessage( return textsecure.messaging
.sendSyncMessage(
dataMessage, dataMessage,
this.get('sent_at'), this.get('sent_at'),
this.get('destination'), this.get('destination'),
this.get('expirationStartTimestamp') this.get('expirationStartTimestamp')
).then(() => { )
.then(() => {
this.save({ synced: true, dataMessage: null }); this.save({ synced: true, dataMessage: null });
}); });
}); });
@ -440,17 +454,19 @@
if (!(errors instanceof Array)) { if (!(errors instanceof Array)) {
errors = [errors]; errors = [errors];
} }
errors.forEach((e) => { errors.forEach(e => {
console.log( console.log(
'Message.saveErrors:', 'Message.saveErrors:',
e && e.reason ? e.reason : null, e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e e && e.stack ? e.stack : e
); );
}); });
errors = errors.map((e) => { errors = errors.map(e => {
if (e.constructor === Error || if (
e.constructor === Error ||
e.constructor === TypeError || e.constructor === TypeError ||
e.constructor === ReferenceError) { e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
} }
return e; return e;
@ -463,17 +479,19 @@
hasNetworkError() { hasNetworkError() {
const error = _.find( const error = _.find(
this.get('errors'), this.get('errors'),
e => (e.name === 'MessageError' || e =>
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError') e.name === 'SignedPreKeyRotationError'
); );
return !!error; return !!error;
}, },
removeOutgoingErrors(number) { removeOutgoingErrors(number) {
const errors = _.partition( const errors = _.partition(
this.get('errors'), this.get('errors'),
e => e.number === number && e =>
e.number === number &&
(e.name === 'MessageError' || (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
@ -484,11 +502,13 @@
return errors[0][0]; return errors[0][0];
}, },
isReplayableError(e) { isReplayableError(e) {
return (e.name === 'MessageError' || return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'); e.name === 'OutgoingIdentityKeyError'
);
}, },
resend(number) { resend(number) {
const error = this.removeOutgoingErrors(number); const error = this.removeOutgoingErrors(number);
@ -513,7 +533,9 @@
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
return conversation.queueJob(() => new Promise((resolve) => { return conversation.queueJob(
() =>
new Promise(resolve => {
const now = new Date().getTime(); const now = new Date().getTime();
let attributes = { type: 'private' }; let attributes = { type: 'private' };
if (dataMessage.group) { if (dataMessage.group) {
@ -528,13 +550,15 @@
groupId: dataMessage.group.id, groupId: dataMessage.group.id,
name: dataMessage.group.name, name: dataMessage.group.name,
avatar: dataMessage.group.avatar, avatar: dataMessage.group.avatar,
members: _.union(dataMessage.group.members, conversation.get('members')), members: _.union(
dataMessage.group.members,
conversation.get('members')
),
}; };
groupUpdate = conversation.changedAttributes(_.pick( groupUpdate =
dataMessage.group, conversation.changedAttributes(
'name', _.pick(dataMessage.group, 'name', 'avatar')
'avatar' ) || {};
)) || {};
const difference = _.difference( const difference = _.difference(
attributes.members, attributes.members,
conversation.get('members') conversation.get('members')
@ -553,7 +577,10 @@
} else { } else {
groupUpdate = { left: source }; groupUpdate = { left: source };
} }
attributes.members = _.without(conversation.get('members'), source); attributes.members = _.without(
conversation.get('members'),
source
);
} }
if (groupUpdate !== null) { if (groupUpdate !== null) {
@ -574,10 +601,15 @@
schemaVersion: dataMessage.schemaVersion, schemaVersion: dataMessage.schemaVersion,
}); });
if (type === 'outgoing') { if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message); const receipts = Whisper.DeliveryReceipts.forMessage(
receipts.forEach(() => message.set({ conversation,
message
);
receipts.forEach(() =>
message.set({
delivered: (message.get('delivered') || 0) + 1, delivered: (message.get('delivered') || 0) + 1,
})); })
);
} }
attributes.active_at = now; attributes.active_at = now;
conversation.set(attributes); conversation.set(attributes);
@ -611,15 +643,19 @@
if (!message.isEndSession() && !message.isGroupUpdate()) { if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) { if (dataMessage.expireTimer) {
if (dataMessage.expireTimer !== conversation.get('expireTimer')) { if (
dataMessage.expireTimer !== conversation.get('expireTimer')
) {
conversation.updateExpirationTimer( conversation.updateExpirationTimer(
dataMessage.expireTimer, source, dataMessage.expireTimer,
source,
message.get('received_at') message.get('received_at')
); );
} }
} else if (conversation.get('expireTimer')) { } else if (conversation.get('expireTimer')) {
conversation.updateExpirationTimer( conversation.updateExpirationTimer(
null, source, null,
source,
message.get('received_at') message.get('received_at')
); );
} }
@ -627,8 +663,14 @@
if (type === 'incoming') { if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message); const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) { if (readSync) {
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) { if (
message.set('expirationStartTimestamp', readSync.get('read_at')); message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
} }
} }
if (readSync || message.isExpirationTimerUpdate()) { if (readSync || message.isExpirationTimerUpdate()) {
@ -638,12 +680,18 @@
// know about. // know about.
Whisper.ReadSyncs.notifyConversation(message); Whisper.ReadSyncs.notifyConversation(message);
} else { } else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1); conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
} }
} }
if (type === 'outgoing') { if (type === 'outgoing') {
const reads = Whisper.ReadReceipts.forMessage(conversation, message); const reads = Whisper.ReadReceipts.forMessage(
conversation,
message
);
if (reads.length) { if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader')); const readBy = reads.map(receipt => receipt.get('reader'));
message.set({ message.set({
@ -655,7 +703,10 @@
} }
const conversationTimestamp = conversation.get('timestamp'); const conversationTimestamp = conversation.get('timestamp');
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) { if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({ conversation.set({
lastMessage: message.getNotificationText(), lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
@ -672,15 +723,20 @@
ConversationController.getOrCreateAndWait( ConversationController.getOrCreateAndWait(
source, source,
'private' 'private'
).then((sender) => { ).then(sender => {
sender.setProfileKey(profileKey); sender.setProfileKey(profileKey);
}); });
} }
} }
const handleError = (error) => { const handleError = error => {
const errorForLog = error && error.stack ? error.stack : error; const errorForLog = error && error.stack ? error.stack : error;
console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog); console.log(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
return resolve(); return resolve();
}; };
@ -695,11 +751,14 @@
// line's trigger() call, we might have marked all messages unread in the // line's trigger() call, we might have marked all messages unread in the
// database. This message might already be read! // database. This message might already be read!
const previousUnread = message.get('unread'); const previousUnread = message.get('unread');
return message.fetch().then(() => { return message.fetch().then(
() => {
try { try {
if (previousUnread !== message.get('unread')) { if (previousUnread !== message.get('unread')) {
console.log('Caught race condition on new message read state! ' + console.log(
'Manually starting timers.'); 'Caught race condition on new message read state! ' +
'Manually starting timers.'
);
// We call markRead() even though the message is already marked read // We call markRead() even though the message is already marked read
// because we need to start expiration timers, etc. // because we need to start expiration timers, etc.
message.markRead(); message.markRead();
@ -717,7 +776,8 @@
} catch (e) { } catch (e) {
return handleError(e); return handleError(e);
} }
}, () => { },
() => {
try { try {
console.log( console.log(
'handleDataMessage: Message', 'handleDataMessage: Message',
@ -730,19 +790,23 @@
} catch (e) { } catch (e) {
return handleError(e); return handleError(e);
} }
}); }
);
}, handleError); }, handleError);
}, handleError); }, handleError);
})); })
);
}, },
markRead(readAt) { markRead(readAt) {
this.unset('unread'); this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
this.set('expirationStartTimestamp', readAt || Date.now()); this.set('expirationStartTimestamp', readAt || Date.now());
} }
Whisper.Notifications.remove(Whisper.Notifications.where({ Whisper.Notifications.remove(
Whisper.Notifications.where({
messageId: this.id, messageId: this.id,
})); })
);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.save().then(resolve, reject); this.save().then(resolve, reject);
}); });
@ -760,7 +824,7 @@
const now = Date.now(); const now = Date.now();
const start = this.get('expirationStartTimestamp'); const start = this.get('expirationStartTimestamp');
const delta = this.get('expireTimer') * 1000; const delta = this.get('expireTimer') * 1000;
let msFromNow = (start + delta) - now; let msFromNow = start + delta - now;
if (msFromNow < 0) { if (msFromNow < 0) {
msFromNow = 0; msFromNow = 0;
} }
@ -784,7 +848,6 @@
console.log('message', this.get('sent_at'), 'expires at', expiresAt); console.log('message', this.get('sent_at'), 'expires at', expiresAt);
} }
}, },
}); });
Whisper.MessageCollection = Backbone.Collection.extend({ Whisper.MessageCollection = Backbone.Collection.extend({
@ -804,19 +867,29 @@
} }
}, },
destroyAll() { destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => { return Promise.all(
m.destroy().then(resolve).fail(reject); this.models.map(
}))); m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
}, },
fetchSentAt(timestamp) { fetchSentAt(timestamp) {
return new Promise((resolve => this.fetch({ return new Promise(resolve =>
this.fetch({
index: { index: {
// 'receipt' index on sent_at // 'receipt' index on sent_at
name: 'receipt', name: 'receipt',
only: timestamp, only: timestamp,
}, },
}).always(resolve))); }).always(resolve)
);
}, },
getLoadedUnreadCount() { getLoadedUnreadCount() {
@ -841,7 +914,7 @@
if (unreadCount > 0) { if (unreadCount > 0) {
startingLoadedUnread = this.getLoadedUnreadCount(); startingLoadedUnread = this.getLoadedUnreadCount();
} }
return new Promise((resolve) => { return new Promise(resolve => {
let upper; let upper;
if (this.length === 0) { if (this.length === 0) {
// fetch the most recent messages first // fetch the most recent messages first
@ -893,4 +966,4 @@
}); });
}, },
}); });
}()); })();

View file

@ -20,7 +20,9 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
); );
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadImage(fileOrBlobOrURL, (canvasOrError) => { loadImage(
fileOrBlobOrURL,
canvasOrError => {
if (canvasOrError.type === 'error') { if (canvasOrError.type === 'error') {
const error = new Error('autoOrientImage: Failed to process image'); const error = new Error('autoOrientImage: Failed to process image');
error.cause = canvasOrError; error.cause = canvasOrError;
@ -35,6 +37,8 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
); );
resolve(dataURL); resolve(dataURL);
}, optionsWithDefaults); },
optionsWithDefaults
);
}); });
}; };

View file

@ -23,12 +23,7 @@ const electronRemote = require('electron').remote;
const Attachment = require('./types/attachment'); const Attachment = require('./types/attachment');
const crypto = require('./crypto'); const crypto = require('./crypto');
const { dialog, BrowserWindow } = electronRemote;
const {
dialog,
BrowserWindow,
} = electronRemote;
module.exports = { module.exports = {
getDirectoryForExport, getDirectoryForExport,
@ -44,7 +39,6 @@ module.exports = {
_getConversationLoggingName, _getConversationLoggingName,
}; };
function stringify(object) { function stringify(object) {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const key in object) { for (const key in object) {
@ -69,10 +63,12 @@ function unstringify(object) {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const key in object) { for (const key in object) {
const val = object[key]; const val = object[key];
if (val && if (
val &&
val.type === 'ArrayBuffer' && val.type === 'ArrayBuffer' &&
val.encoding === 'base64' && val.encoding === 'base64' &&
typeof val.data === 'string') { typeof val.data === 'string'
) {
object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer(); object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
} else if (val instanceof Object) { } else if (val instanceof Object) {
object[key] = unstringify(object[key]); object[key] = unstringify(object[key]);
@ -86,7 +82,9 @@ function createOutputStream(writer) {
return { return {
write(string) { write(string) {
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
wait = wait.then(() => new Promise((resolve) => { wait = wait.then(
() =>
new Promise(resolve => {
if (writer.write(string)) { if (writer.write(string)) {
resolve(); resolve();
return; return;
@ -98,7 +96,8 @@ function createOutputStream(writer) {
// We don't register for the 'error' event here, only in close(). Otherwise, // We don't register for the 'error' event here, only in close(). Otherwise,
// we'll get "Possible EventEmitter memory leak detected" warnings. // we'll get "Possible EventEmitter memory leak detected" warnings.
})); })
);
return wait; return wait;
}, },
async close() { async close() {
@ -141,7 +140,7 @@ function exportContactsAndGroups(db, fileWriter) {
stream.write('{'); stream.write('{');
_.each(storeNames, (storeName) => { _.each(storeNames, storeName => {
// Both the readwrite permission and the multi-store transaction are required to // Both the readwrite permission and the multi-store transaction are required to
// keep this function working. They serve to serialize all of these transactions, // keep this function working. They serve to serialize all of these transactions,
// one per store to be exported. // one per store to be exported.
@ -167,7 +166,7 @@ function exportContactsAndGroups(db, fileWriter) {
reject reject
); );
}; };
request.onsuccess = async (event) => { request.onsuccess = async event => {
if (count === 0) { if (count === 0) {
console.log('cursor opened'); console.log('cursor opened');
stream.write(`"${storeName}": [`); stream.write(`"${storeName}": [`);
@ -180,10 +179,7 @@ function exportContactsAndGroups(db, fileWriter) {
} }
// Preventing base64'd images from reaching the disk, making db.json too big // Preventing base64'd images from reaching the disk, making db.json too big
const item = _.omit( const item = _.omit(cursor.value, ['avatar', 'profileAvatar']);
cursor.value,
['avatar', 'profileAvatar']
);
const jsonString = JSON.stringify(stringify(item)); const jsonString = JSON.stringify(stringify(item));
stream.write(jsonString); stream.write(jsonString);
@ -235,10 +231,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
groupLookup: {}, groupLookup: {},
}); });
const { const { conversationLookup, groupLookup } = options;
conversationLookup,
groupLookup,
} = options;
const result = { const result = {
fullImport: true, fullImport: true,
}; };
@ -269,7 +262,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
console.log('Importing to these stores:', storeNames.join(', ')); console.log('Importing to these stores:', storeNames.join(', '));
let finished = false; let finished = false;
const finish = (via) => { const finish = via => {
console.log('non-messages import done via', via); console.log('non-messages import done via', via);
if (finished) { if (finished) {
resolve(result); resolve(result);
@ -287,7 +280,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
}; };
transaction.oncomplete = finish.bind(null, 'transaction complete'); transaction.oncomplete = finish.bind(null, 'transaction complete');
_.each(storeNames, (storeName) => { _.each(storeNames, storeName => {
console.log('Importing items for store', storeName); console.log('Importing items for store', storeName);
if (!importObject[storeName].length) { if (!importObject[storeName].length) {
@ -316,7 +309,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
} }
}; };
_.each(importObject[storeName], (toAdd) => { _.each(importObject[storeName], toAdd => {
toAdd = unstringify(toAdd); toAdd = unstringify(toAdd);
const haveConversationAlready = const haveConversationAlready =
@ -365,7 +358,7 @@ function createDirectory(parent, name) {
return; return;
} }
fs.mkdir(targetDir, (error) => { fs.mkdir(targetDir, error => {
if (error) { if (error) {
reject(error); reject(error);
return; return;
@ -377,7 +370,7 @@ function createDirectory(parent, name) {
} }
function createFileAndWriter(parent, name) { function createFileAndWriter(parent, name) {
return new Promise((resolve) => { return new Promise(resolve => {
const sanitized = _sanitizeFileName(name); const sanitized = _sanitizeFileName(name);
const targetPath = path.join(parent, sanitized); const targetPath = path.join(parent, sanitized);
const options = { const options = {
@ -430,7 +423,6 @@ function _trimFileName(filename) {
return `${name.join('.').slice(0, 24)}.${extension}`; return `${name.join('.').slice(0, 24)}.${extension}`;
} }
function _getExportAttachmentFileName(message, index, attachment) { function _getExportAttachmentFileName(message, index, attachment) {
if (attachment.fileName) { if (attachment.fileName) {
return _trimFileName(attachment.fileName); return _trimFileName(attachment.fileName);
@ -440,7 +432,9 @@ function _getExportAttachmentFileName(message, index, attachment) {
if (attachment.contentType) { if (attachment.contentType) {
const components = attachment.contentType.split('/'); const components = attachment.contentType.split('/');
name += `.${components.length > 1 ? components[1] : attachment.contentType}`; name += `.${
components.length > 1 ? components[1] : attachment.contentType
}`;
} }
return name; return name;
@ -477,14 +471,11 @@ async function readAttachment(dir, attachment, name, options) {
} }
async function writeThumbnail(attachment, options) { async function writeThumbnail(attachment, options) {
const { const { dir, message, index, key, newKey } = options;
dir, const filename = `${_getAnonymousAttachmentFileName(
message, message,
index, index
key, )}-thumbnail`;
newKey,
} = options;
const filename = `${_getAnonymousAttachmentFileName(message, index)}-thumbnail`;
const target = path.join(dir, filename); const target = path.join(dir, filename);
const { thumbnail } = attachment; const { thumbnail } = attachment;
@ -504,26 +495,28 @@ async function writeThumbnails(rawQuotedAttachments, options) {
const { name } = options; const { name } = options;
const { loadAttachmentData } = Signal.Migrations; const { loadAttachmentData } = Signal.Migrations;
const promises = rawQuotedAttachments.map(async (attachment) => { const promises = rawQuotedAttachments.map(async attachment => {
if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) { if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
return attachment; return attachment;
} }
return Object.assign( return Object.assign({}, attachment, {
{}, thumbnail: await loadAttachmentData(attachment.thumbnail),
attachment, });
{ thumbnail: await loadAttachmentData(attachment.thumbnail) }
);
}); });
const attachments = await Promise.all(promises); const attachments = await Promise.all(promises);
try { try {
await Promise.all(_.map( await Promise.all(
attachments, _.map(attachments, (attachment, index) =>
(attachment, index) => writeThumbnail(attachment, Object.assign({}, options, { writeThumbnail(
attachment,
Object.assign({}, options, {
index, index,
})) })
)); )
)
);
} catch (error) { } catch (error) {
console.log( console.log(
'writeThumbnails: error exporting conversation', 'writeThumbnails: error exporting conversation',
@ -536,13 +529,7 @@ async function writeThumbnails(rawQuotedAttachments, options) {
} }
async function writeAttachment(attachment, options) { async function writeAttachment(attachment, options) {
const { const { dir, message, index, key, newKey } = options;
dir,
message,
index,
key,
newKey,
} = options;
const filename = _getAnonymousAttachmentFileName(message, index); const filename = _getAnonymousAttachmentFileName(message, index);
const target = path.join(dir, filename); const target = path.join(dir, filename);
if (!Attachment.hasData(attachment)) { if (!Attachment.hasData(attachment)) {
@ -562,11 +549,13 @@ async function writeAttachments(rawAttachments, options) {
const { loadAttachmentData } = Signal.Migrations; const { loadAttachmentData } = Signal.Migrations;
const attachments = await Promise.all(rawAttachments.map(loadAttachmentData)); const attachments = await Promise.all(rawAttachments.map(loadAttachmentData));
const promises = _.map( const promises = _.map(attachments, (attachment, index) =>
attachments, writeAttachment(
(attachment, index) => writeAttachment(attachment, Object.assign({}, options, { attachment,
Object.assign({}, options, {
index, index,
})) })
)
); );
try { try {
await Promise.all(promises); await Promise.all(promises);
@ -582,12 +571,7 @@ async function writeAttachments(rawAttachments, options) {
} }
async function writeEncryptedAttachment(target, data, options = {}) { async function writeEncryptedAttachment(target, data, options = {}) {
const { const { key, newKey, filename, dir } = options;
key,
newKey,
filename,
dir,
} = options;
if (fs.existsSync(target)) { if (fs.existsSync(target)) {
if (newKey) { if (newKey) {
@ -613,13 +597,7 @@ function _sanitizeFileName(filename) {
async function exportConversation(db, conversation, options) { async function exportConversation(db, conversation, options) {
options = options || {}; options = options || {};
const { const { name, dir, attachmentsDir, key, newKey } = options;
name,
dir,
attachmentsDir,
key,
newKey,
} = options;
if (!name) { if (!name) {
throw new Error('Need a name!'); throw new Error('Need a name!');
} }
@ -670,7 +648,7 @@ async function exportConversation(db, conversation, options) {
reject reject
); );
}; };
request.onsuccess = async (event) => { request.onsuccess = async event => {
const cursor = event.target.result; const cursor = event.target.result;
if (cursor) { if (cursor) {
const message = cursor.value; const message = cursor.value;
@ -688,13 +666,12 @@ async function exportConversation(db, conversation, options) {
// eliminate attachment data from the JSON, since it will go to disk // eliminate attachment data from the JSON, since it will go to disk
// Note: this is for legacy messages only, which stored attachment data in the db // Note: this is for legacy messages only, which stored attachment data in the db
message.attachments = _.map( message.attachments = _.map(attachments, attachment =>
attachments, _.omit(attachment, ['data'])
attachment => _.omit(attachment, ['data'])
); );
// completely drop any attachments in messages cached in error objects // completely drop any attachments in messages cached in error objects
// TODO: move to lodash. Sadly, a number of the method signatures have changed! // TODO: move to lodash. Sadly, a number of the method signatures have changed!
message.errors = _.map(message.errors, (error) => { message.errors = _.map(message.errors, error => {
if (error && error.args) { if (error && error.args) {
error.args = []; error.args = [];
} }
@ -709,7 +686,8 @@ async function exportConversation(db, conversation, options) {
console.log({ backupMessage: message }); console.log({ backupMessage: message });
if (attachments && attachments.length > 0) { if (attachments && attachments.length > 0) {
const exportAttachments = () => writeAttachments(attachments, { const exportAttachments = () =>
writeAttachments(attachments, {
dir: attachmentsDir, dir: attachmentsDir,
name, name,
message, message,
@ -723,7 +701,8 @@ async function exportConversation(db, conversation, options) {
const quoteThumbnails = message.quote && message.quote.attachments; const quoteThumbnails = message.quote && message.quote.attachments;
if (quoteThumbnails && quoteThumbnails.length > 0) { if (quoteThumbnails && quoteThumbnails.length > 0) {
const exportQuoteThumbnails = () => writeThumbnails(quoteThumbnails, { const exportQuoteThumbnails = () =>
writeThumbnails(quoteThumbnails, {
dir: attachmentsDir, dir: attachmentsDir,
name, name,
message, message,
@ -739,11 +718,7 @@ async function exportConversation(db, conversation, options) {
cursor.continue(); cursor.continue();
} else { } else {
try { try {
await Promise.all([ await Promise.all([stream.write(']}'), promiseChain, stream.close()]);
stream.write(']}'),
promiseChain,
stream.close(),
]);
} catch (error) { } catch (error) {
console.log( console.log(
'exportConversation: error exporting conversation', 'exportConversation: error exporting conversation',
@ -791,12 +766,7 @@ function _getConversationLoggingName(conversation) {
function exportConversations(db, options) { function exportConversations(db, options) {
options = options || {}; options = options || {};
const { const { messagesDir, attachmentsDir, key, newKey } = options;
messagesDir,
attachmentsDir,
key,
newKey,
} = options;
if (!messagesDir) { if (!messagesDir) {
return Promise.reject(new Error('Need a messages directory!')); return Promise.reject(new Error('Need a messages directory!'));
@ -828,7 +798,7 @@ function exportConversations(db, options) {
reject reject
); );
}; };
request.onsuccess = async (event) => { request.onsuccess = async event => {
const cursor = event.target.result; const cursor = event.target.result;
if (cursor && cursor.value) { if (cursor && cursor.value) {
const conversation = cursor.value; const conversation = cursor.value;
@ -873,7 +843,7 @@ function getDirectory(options) {
buttonLabel: options.buttonLabel, buttonLabel: options.buttonLabel,
}; };
dialog.showOpenDialog(browserWindow, dialogOptions, (directory) => { dialog.showOpenDialog(browserWindow, dialogOptions, directory => {
if (!directory || !directory[0]) { if (!directory || !directory[0]) {
const error = new Error('Error choosing directory'); const error = new Error('Error choosing directory');
error.name = 'ChooseError'; error.name = 'ChooseError';
@ -940,7 +910,7 @@ async function saveAllMessages(db, rawMessages) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let finished = false; let finished = false;
const finish = (via) => { const finish = via => {
console.log('messages done saving via', via); console.log('messages done saving via', via);
if (finished) { if (finished) {
resolve(); resolve();
@ -962,7 +932,7 @@ async function saveAllMessages(db, rawMessages) {
const { conversationId } = messages[0]; const { conversationId } = messages[0];
let count = 0; let count = 0;
_.forEach(messages, (message) => { _.forEach(messages, message => {
const request = store.put(message, message.id); const request = store.put(message, message.id);
request.onsuccess = () => { request.onsuccess = () => {
count += 1; count += 1;
@ -997,11 +967,7 @@ async function importConversation(db, dir, options) {
options = options || {}; options = options || {};
_.defaults(options, { messageLookup: {} }); _.defaults(options, { messageLookup: {} });
const { const { messageLookup, attachmentsDir, key } = options;
messageLookup,
attachmentsDir,
key,
} = options;
let conversationId = 'unknown'; let conversationId = 'unknown';
let total = 0; let total = 0;
@ -1018,11 +984,13 @@ async function importConversation(db, dir, options) {
const json = JSON.parse(contents); const json = JSON.parse(contents);
if (json.messages && json.messages.length) { if (json.messages && json.messages.length) {
conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(-3)}`; conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(
-3
)}`;
} }
total = json.messages.length; total = json.messages.length;
const messages = _.filter(json.messages, (message) => { const messages = _.filter(json.messages, message => {
message = unstringify(message); message = unstringify(message);
if (messageLookup[getMessageKey(message)]) { if (messageLookup[getMessageKey(message)]) {
@ -1031,7 +999,9 @@ async function importConversation(db, dir, options) {
} }
const hasAttachments = message.attachments && message.attachments.length; const hasAttachments = message.attachments && message.attachments.length;
const hasQuotedAttachments = message.quote && message.quote.attachments && const hasQuotedAttachments =
message.quote &&
message.quote.attachments &&
message.quote.attachments.length > 0; message.quote.attachments.length > 0;
if (hasAttachments || hasQuotedAttachments) { if (hasAttachments || hasQuotedAttachments) {
@ -1039,8 +1009,8 @@ async function importConversation(db, dir, options) {
const getName = attachmentsDir const getName = attachmentsDir
? _getAnonymousAttachmentFileName ? _getAnonymousAttachmentFileName
: _getExportAttachmentFileName; : _getExportAttachmentFileName;
const parentDir = attachmentsDir || const parentDir =
path.join(dir, message.received_at.toString()); attachmentsDir || path.join(dir, message.received_at.toString());
await loadAttachments(parentDir, getName, { await loadAttachments(parentDir, getName, {
message, message,
@ -1075,12 +1045,13 @@ async function importConversations(db, dir, options) {
const contents = await getDirContents(dir); const contents = await getDirContents(dir);
let promiseChain = Promise.resolve(); let promiseChain = Promise.resolve();
_.forEach(contents, (conversationDir) => { _.forEach(contents, conversationDir => {
if (!fs.statSync(conversationDir).isDirectory()) { if (!fs.statSync(conversationDir).isDirectory()) {
return; return;
} }
const loadConversation = () => importConversation(db, conversationDir, options); const loadConversation = () =>
importConversation(db, conversationDir, options);
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(loadConversation); promiseChain = promiseChain.then(loadConversation);
@ -1142,7 +1113,7 @@ function assembleLookup(db, storeName, keyFunction) {
reject reject
); );
}; };
request.onsuccess = (event) => { request.onsuccess = event => {
const cursor = event.target.result; const cursor = event.target.result;
if (cursor && cursor.value) { if (cursor && cursor.value) {
lookup[keyFunction(cursor.value)] = true; lookup[keyFunction(cursor.value)] = true;
@ -1175,7 +1146,7 @@ function createZip(zipDir, targetDir) {
resolve(target); resolve(target);
}); });
archive.on('warning', (error) => { archive.on('warning', error => {
console.log(`Archive generation warning: ${error.stack}`); console.log(`Archive generation warning: ${error.stack}`);
}); });
archive.on('error', reject); archive.on('error', reject);
@ -1247,10 +1218,13 @@ async function exportToDirectory(directory, options) {
const attachmentsDir = await createDirectory(directory, 'attachments'); const attachmentsDir = await createDirectory(directory, 'attachments');
await exportContactAndGroupsToFile(db, stagingDir); await exportContactAndGroupsToFile(db, stagingDir);
await exportConversations(db, Object.assign({}, options, { await exportConversations(
db,
Object.assign({}, options, {
messagesDir: stagingDir, messagesDir: stagingDir,
attachmentsDir, attachmentsDir,
})); })
);
const zip = await createZip(encryptionDir, stagingDir); const zip = await createZip(encryptionDir, stagingDir);
await encryptFile(zip, path.join(directory, 'messages.zip'), options); await encryptFile(zip, path.join(directory, 'messages.zip'), options);
@ -1302,7 +1276,9 @@ async function importFromDirectory(directory, options) {
if (fs.existsSync(zipPath)) { if (fs.existsSync(zipPath)) {
// we're in the world of an encrypted, zipped backup // we're in the world of an encrypted, zipped backup
if (!options.key) { if (!options.key) {
throw new Error('Importing an encrypted backup; decryption key is required!'); throw new Error(
'Importing an encrypted backup; decryption key is required!'
);
} }
let stagingDir; let stagingDir;

View file

@ -19,8 +19,15 @@ async function encryptSymmetric(key, plaintext) {
const cipherKey = await _hmac_SHA256(key, nonce); const cipherKey = await _hmac_SHA256(key, nonce);
const macKey = await _hmac_SHA256(key, cipherKey); const macKey = await _hmac_SHA256(key, cipherKey);
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext); const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); cipherKey,
iv,
plaintext
);
const mac = _getFirstBytes(
await _hmac_SHA256(macKey, cipherText),
MAC_LENGTH
);
return _concatData([nonce, cipherText, mac]); return _concatData([nonce, cipherText, mac]);
} }
@ -39,9 +46,14 @@ async function decryptSymmetric(key, data) {
const cipherKey = await _hmac_SHA256(key, nonce); const cipherKey = await _hmac_SHA256(key, nonce);
const macKey = await _hmac_SHA256(key, cipherKey); const macKey = await _hmac_SHA256(key, cipherKey);
const ourMac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); const ourMac = _getFirstBytes(
await _hmac_SHA256(macKey, cipherText),
MAC_LENGTH
);
if (!constantTimeEqual(theirMac, ourMac)) { if (!constantTimeEqual(theirMac, ourMac)) {
throw new Error('decryptSymmetric: Failed to decrypt; MAC verification failed'); throw new Error(
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
} }
return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText); return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText);
@ -61,7 +73,6 @@ function constantTimeEqual(left, right) {
return result === 0; return result === 0;
} }
async function _hmac_SHA256(key, data) { async function _hmac_SHA256(key, data) {
const extractable = false; const extractable = false;
const cryptoKey = await window.crypto.subtle.importKey( const cryptoKey = await window.crypto.subtle.importKey(
@ -72,7 +83,11 @@ async function _hmac_SHA256(key, data) {
['sign'] ['sign']
); );
return window.crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, cryptoKey, data); return window.crypto.subtle.sign(
{ name: 'HMAC', hash: 'SHA-256' },
cryptoKey,
data
);
} }
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) { async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) {
@ -101,7 +116,6 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data); return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
} }
function _getRandomBytes(n) { function _getRandomBytes(n) {
const bytes = new Uint8Array(n); const bytes = new Uint8Array(n);
window.crypto.getRandomValues(bytes); window.crypto.getRandomValues(bytes);

View file

@ -6,14 +6,12 @@
const { isObject, isNumber } = require('lodash'); const { isObject, isNumber } = require('lodash');
exports.open = (name, version, { onUpgradeNeeded } = {}) => { exports.open = (name, version, { onUpgradeNeeded } = {}) => {
const request = indexedDB.open(name, version); const request = indexedDB.open(name, version);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onblocked = () => request.onblocked = () => reject(new Error('Database blocked'));
reject(new Error('Database blocked'));
request.onupgradeneeded = (event) => { request.onupgradeneeded = event => {
const hasRequestedSpecificVersion = isNumber(version); const hasRequestedSpecificVersion = isNumber(version);
if (!hasRequestedSpecificVersion) { if (!hasRequestedSpecificVersion) {
return; return;
@ -26,14 +24,17 @@ exports.open = (name, version, { onUpgradeNeeded } = {}) => {
return; return;
} }
reject(new Error('Database upgrade required:' + reject(
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`)); new Error(
'Database upgrade required:' +
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`
)
);
}; };
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error);
request.onsuccess = (event) => { request.onsuccess = event => {
const connection = event.target.result; const connection = event.target.result;
resolve(connection); resolve(connection);
}; };
@ -47,7 +48,7 @@ exports.completeTransaction = transaction =>
transaction.addEventListener('complete', () => resolve()); transaction.addEventListener('complete', () => resolve());
}); });
exports.getVersion = async (name) => { exports.getVersion = async name => {
const connection = await exports.open(name); const connection = await exports.open(name);
const { version } = connection; const { version } = connection;
connection.close(); connection.close();
@ -61,9 +62,7 @@ exports.getCount = async ({ store } = {}) => {
const request = store.count(); const request = store.count();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error); request.onsuccess = event => resolve(event.target.result);
request.onsuccess = event =>
resolve(event.target.result);
}); });
}; };

View file

@ -18,7 +18,6 @@ const Message = require('./types/message');
const { deferredToPromise } = require('./deferred_to_promise'); const { deferredToPromise } = require('./deferred_to_promise');
const { sleep } = require('./sleep'); const { sleep } = require('./sleep');
// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan // See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan
const SENDER_ID = '+12126647665'; const SENDER_ID = '+12126647665';
@ -27,8 +26,10 @@ exports.createConversation = async ({
numMessages, numMessages,
WhisperMessage, WhisperMessage,
} = {}) => { } = {}) => {
if (!isObject(ConversationController) || if (
!isFunction(ConversationController.getOrCreateAndWait)) { !isObject(ConversationController) ||
!isFunction(ConversationController.getOrCreateAndWait)
) {
throw new TypeError("'ConversationController' is required"); throw new TypeError("'ConversationController' is required");
} }
@ -40,8 +41,10 @@ exports.createConversation = async ({
throw new TypeError("'WhisperMessage' is required"); throw new TypeError("'WhisperMessage' is required");
} }
const conversation = const conversation = await ConversationController.getOrCreateAndWait(
await ConversationController.getOrCreateAndWait(SENDER_ID, 'private'); SENDER_ID,
'private'
);
conversation.set({ conversation.set({
active_at: Date.now(), active_at: Date.now(),
unread: numMessages, unread: numMessages,
@ -50,13 +53,15 @@ exports.createConversation = async ({
const conversationId = conversation.get('id'); const conversationId = conversation.get('id');
await Promise.all(range(0, numMessages).map(async (index) => { await Promise.all(
range(0, numMessages).map(async index => {
await sleep(index * 100); await sleep(index * 100);
console.log(`Create message ${index + 1}`); console.log(`Create message ${index + 1}`);
const messageAttributes = await createRandomMessage({ conversationId }); const messageAttributes = await createRandomMessage({ conversationId });
const message = new WhisperMessage(messageAttributes); const message = new WhisperMessage(messageAttributes);
return deferredToPromise(message.save()); return deferredToPromise(message.save());
})); })
);
}; };
const SAMPLE_MESSAGES = [ const SAMPLE_MESSAGES = [
@ -88,7 +93,8 @@ const createRandomMessage = async ({ conversationId } = {}) => {
const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE; const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE;
const attachments = hasAttachment const attachments = hasAttachment
? [await createRandomInMemoryAttachment()] : []; ? [await createRandomInMemoryAttachment()]
: [];
const type = sample(['incoming', 'outgoing']); const type = sample(['incoming', 'outgoing']);
const commonProperties = { const commonProperties = {
attachments, attachments,
@ -145,7 +151,7 @@ const createFileEntry = fileName => ({
fileName, fileName,
contentType: fileNameToContentType(fileName), contentType: fileNameToContentType(fileName),
}); });
const fileNameToContentType = (fileName) => { const fileNameToContentType = fileName => {
const fileExtension = path.extname(fileName).toLowerCase(); const fileExtension = path.extname(fileName).toLowerCase();
switch (fileExtension) { switch (fileExtension) {
case '.gif': case '.gif':

View file

@ -3,7 +3,6 @@
const FormData = require('form-data'); const FormData = require('form-data');
const got = require('got'); const got = require('got');
const BASE_URL = 'https://debuglogs.org'; const BASE_URL = 'https://debuglogs.org';
// Workaround: Submitting `FormData` using native `FormData::submit` procedure // Workaround: Submitting `FormData` using native `FormData::submit` procedure
@ -12,7 +11,7 @@ const BASE_URL = 'https://debuglogs.org';
// https://github.com/sindresorhus/got/pull/466 // https://github.com/sindresorhus/got/pull/466
const submitFormData = (form, url) => const submitFormData = (form, url) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
form.submit(url, (error) => { form.submit(url, error => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
@ -22,7 +21,7 @@ const submitFormData = (form, url) =>
}); });
// upload :: String -> Promise URL // upload :: String -> Promise URL
exports.upload = async (content) => { exports.upload = async content => {
const signedForm = await got.get(BASE_URL, { json: true }); const signedForm = await got.get(BASE_URL, { json: true });
const { fields, url } = signedForm.body; const { fields, url } = signedForm.body;

View file

@ -2,11 +2,10 @@ const addUnhandledErrorHandler = require('electron-unhandled');
const Errors = require('./types/errors'); const Errors = require('./types/errors');
// addHandler :: Unit -> Unit // addHandler :: Unit -> Unit
exports.addHandler = () => { exports.addHandler = () => {
addUnhandledErrorHandler({ addUnhandledErrorHandler({
logger: (error) => { logger: error => {
console.error( console.error(
'Uncaught error or unhandled promise rejection:', 'Uncaught error or unhandled promise rejection:',
Errors.toLogFormat(error) Errors.toLogFormat(error)

View file

@ -11,7 +11,9 @@ exports.setup = (locale, messages) => {
function getMessage(key, substitutions) { function getMessage(key, substitutions) {
const entry = messages[key]; const entry = messages[key];
if (!entry) { if (!entry) {
console.error(`i18n: Attempted to get translation for nonexistent key '${key}'`); console.error(
`i18n: Attempted to get translation for nonexistent key '${key}'`
);
return ''; return '';
} }

View file

@ -2,7 +2,6 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const POLL_INTERVAL_MS = 5 * 1000; const POLL_INTERVAL_MS = 5 * 1000;
const IDLE_THRESHOLD_MS = 20; const IDLE_THRESHOLD_MS = 20;
@ -35,14 +34,17 @@ class IdleDetector extends EventEmitter {
_scheduleNextCallback() { _scheduleNextCallback() {
this._clearScheduledCallbacks(); this._clearScheduledCallbacks();
this.handle = window.requestIdleCallback((deadline) => { this.handle = window.requestIdleCallback(deadline => {
const { didTimeout } = deadline; const { didTimeout } = deadline;
const timeRemaining = deadline.timeRemaining(); const timeRemaining = deadline.timeRemaining();
const isIdle = timeRemaining >= IDLE_THRESHOLD_MS; const isIdle = timeRemaining >= IDLE_THRESHOLD_MS;
if (isIdle || didTimeout) { if (isIdle || didTimeout) {
this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining }); this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining });
} }
this.timeoutId = setTimeout(() => this._scheduleNextCallback(), POLL_INTERVAL_MS); this.timeoutId = setTimeout(
() => this._scheduleNextCallback(),
POLL_INTERVAL_MS
);
}); });
} }
} }

View file

@ -7,7 +7,7 @@ function createLink(url, text, attrs = {}) {
const html = []; const html = [];
html.push('<a '); html.push('<a ');
html.push(`href="${url}"`); html.push(`href="${url}"`);
Object.keys(attrs).forEach((key) => { Object.keys(attrs).forEach(key => {
html.push(` ${key}="${attrs[key]}"`); html.push(` ${key}="${attrs[key]}"`);
}); });
html.push('>'); html.push('>');
@ -23,7 +23,7 @@ module.exports = (text, attrs = {}) => {
const result = []; const result = [];
let last = 0; let last = 0;
matchData.forEach((match) => { matchData.forEach(match => {
if (last < match.index) { if (last < match.index) {
result.push(text.slice(last, match.index)); result.push(text.slice(last, match.index));
} }

View file

@ -6,20 +6,13 @@
/* global IDBKeyRange */ /* global IDBKeyRange */
const { const { isFunction, isNumber, isObject, isString, last } = require('lodash');
isFunction,
isNumber,
isObject,
isString,
last,
} = require('lodash');
const database = require('./database'); const database = require('./database');
const Message = require('./types/message'); const Message = require('./types/message');
const settings = require('./settings'); const settings = require('./settings');
const { deferredToPromise } = require('./deferred_to_promise'); const { deferredToPromise } = require('./deferred_to_promise');
const MESSAGES_STORE_NAME = 'messages'; const MESSAGES_STORE_NAME = 'messages';
exports.processNext = async ({ exports.processNext = async ({
@ -29,12 +22,16 @@ exports.processNext = async ({
upgradeMessageSchema, upgradeMessageSchema,
} = {}) => { } = {}) => {
if (!isFunction(BackboneMessage)) { if (!isFunction(BackboneMessage)) {
throw new TypeError("'BackboneMessage' (Whisper.Message) constructor is required"); throw new TypeError(
"'BackboneMessage' (Whisper.Message) constructor is required"
);
} }
if (!isFunction(BackboneMessageCollection)) { if (!isFunction(BackboneMessageCollection)) {
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" + throw new TypeError(
' constructor is required'); "'BackboneMessageCollection' (Whisper.MessageCollection)" +
' constructor is required'
);
} }
if (!isNumber(numMessagesPerBatch)) { if (!isNumber(numMessagesPerBatch)) {
@ -48,16 +45,18 @@ exports.processNext = async ({
const startTime = Date.now(); const startTime = Date.now();
const fetchStartTime = Date.now(); const fetchStartTime = Date.now();
const messagesRequiringSchemaUpgrade = const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade(
await _fetchMessagesRequiringSchemaUpgrade({ {
BackboneMessageCollection, BackboneMessageCollection,
count: numMessagesPerBatch, count: numMessagesPerBatch,
}); }
);
const fetchDuration = Date.now() - fetchStartTime; const fetchDuration = Date.now() - fetchStartTime;
const upgradeStartTime = Date.now(); const upgradeStartTime = Date.now();
const upgradedMessages = const upgradedMessages = await Promise.all(
await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)); messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)
);
const upgradeDuration = Date.now() - upgradeStartTime; const upgradeDuration = Date.now() - upgradeStartTime;
const saveStartTime = Date.now(); const saveStartTime = Date.now();
@ -109,8 +108,10 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
minDatabaseVersion, minDatabaseVersion,
}); });
if (!isValidDatabaseVersion) { if (!isValidDatabaseVersion) {
throw new Error(`Expected database version (${databaseVersion})` + throw new Error(
` to be at least ${minDatabaseVersion}`); `Expected database version (${databaseVersion})` +
` to be at least ${minDatabaseVersion}`
);
} }
// NOTE: Even if we make this async using `then`, requesting `count` on an // NOTE: Even if we make this async using `then`, requesting `count` on an
@ -132,10 +133,13 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
break; break;
} }
numCumulativeMessagesProcessed += status.numMessagesProcessed; numCumulativeMessagesProcessed += status.numMessagesProcessed;
console.log('Upgrade message schema:', Object.assign({}, status, { console.log(
'Upgrade message schema:',
Object.assign({}, status, {
numTotalMessages, numTotalMessages,
numCumulativeMessagesProcessed, numCumulativeMessagesProcessed,
})); })
);
} }
console.log('Close database connection'); console.log('Close database connection');
@ -181,8 +185,10 @@ const _getConnection = async ({ databaseName, minDatabaseVersion }) => {
const databaseVersion = connection.version; const databaseVersion = connection.version;
const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion;
if (!isValidDatabaseVersion) { if (!isValidDatabaseVersion) {
throw new Error(`Expected database version (${databaseVersion})` + throw new Error(
` to be at least ${minDatabaseVersion}`); `Expected database version (${databaseVersion})` +
` to be at least ${minDatabaseVersion}`
);
} }
return connection; return connection;
@ -205,29 +211,33 @@ const _processBatch = async ({
throw new TypeError("'numMessagesPerBatch' is required"); throw new TypeError("'numMessagesPerBatch' is required");
} }
const isAttachmentMigrationComplete = const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
await settings.isAttachmentMigrationComplete(connection); connection
);
if (isAttachmentMigrationComplete) { if (isAttachmentMigrationComplete) {
return { return {
done: true, done: true,
}; };
} }
const lastProcessedIndex = const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex(
await settings.getAttachmentMigrationLastProcessedIndex(connection); connection
);
const fetchUnprocessedMessagesStartTime = Date.now(); const fetchUnprocessedMessagesStartTime = Date.now();
const unprocessedMessages = const unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex(
await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ {
connection, connection,
count: numMessagesPerBatch, count: numMessagesPerBatch,
lastIndex: lastProcessedIndex, lastIndex: lastProcessedIndex,
}); }
);
const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
const upgradeStartTime = Date.now(); const upgradeStartTime = Date.now();
const upgradedMessages = const upgradedMessages = await Promise.all(
await Promise.all(unprocessedMessages.map(upgradeMessageSchema)); unprocessedMessages.map(upgradeMessageSchema)
);
const upgradeDuration = Date.now() - upgradeStartTime; const upgradeDuration = Date.now() - upgradeStartTime;
const saveMessagesStartTime = Date.now(); const saveMessagesStartTime = Date.now();
@ -266,12 +276,12 @@ const _processBatch = async ({
}; };
}; };
const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => { const _saveMessageBackbone = ({ BackboneMessage } = {}) => message => {
const backboneMessage = new BackboneMessage(message); const backboneMessage = new BackboneMessage(message);
return deferredToPromise(backboneMessage.save()); return deferredToPromise(backboneMessage.save());
}; };
const _saveMessage = ({ transaction } = {}) => (message) => { const _saveMessage = ({ transaction } = {}) => message => {
if (!isObject(transaction)) { if (!isObject(transaction)) {
throw new TypeError("'transaction' is required"); throw new TypeError("'transaction' is required");
} }
@ -279,18 +289,20 @@ const _saveMessage = ({ transaction } = {}) => (message) => {
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
const request = messagesStore.put(message, message.id); const request = messagesStore.put(message, message.id);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => request.onsuccess = () => resolve();
resolve(); request.onerror = event => reject(event.target.error);
request.onerror = event =>
reject(event.target.error);
}); });
}; };
const _fetchMessagesRequiringSchemaUpgrade = const _fetchMessagesRequiringSchemaUpgrade = async ({
async ({ BackboneMessageCollection, count } = {}) => { BackboneMessageCollection,
count,
} = {}) => {
if (!isFunction(BackboneMessageCollection)) { if (!isFunction(BackboneMessageCollection)) {
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" + throw new TypeError(
' constructor is required'); "'BackboneMessageCollection' (Whisper.MessageCollection)" +
' constructor is required'
);
} }
if (!isNumber(count)) { if (!isNumber(count)) {
@ -298,7 +310,9 @@ const _fetchMessagesRequiringSchemaUpgrade =
} }
const collection = new BackboneMessageCollection(); const collection = new BackboneMessageCollection();
return new Promise(resolve => collection.fetch({ return new Promise(resolve =>
collection
.fetch({
limit: count, limit: count,
index: { index: {
name: 'schemaVersion', name: 'schemaVersion',
@ -306,17 +320,22 @@ const _fetchMessagesRequiringSchemaUpgrade =
excludeUpper: true, excludeUpper: true,
order: 'desc', order: 'desc',
}, },
}).always(() => { })
.always(() => {
const models = collection.models || []; const models = collection.models || [];
const messages = models.map(model => model.toJSON()); const messages = models.map(model => model.toJSON());
resolve(messages); resolve(messages);
})); })
);
}; };
// NOTE: Named dangerous because it is not as efficient as using our // NOTE: Named dangerous because it is not as efficient as using our
// `messages` `schemaVersion` index: // `messages` `schemaVersion` index:
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({
({ connection, count, lastIndex } = {}) => { connection,
count,
lastIndex,
} = {}) => {
if (!isObject(connection)) { if (!isObject(connection)) {
throw new TypeError("'connection' is required"); throw new TypeError("'connection' is required");
} }
@ -341,7 +360,7 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const items = []; const items = [];
const request = messagesStore.openCursor(range); const request = messagesStore.openCursor(range);
request.onsuccess = (event) => { request.onsuccess = event => {
const cursor = event.target.result; const cursor = event.target.result;
const hasMoreData = Boolean(cursor); const hasMoreData = Boolean(cursor);
if (!hasMoreData || items.length === count) { if (!hasMoreData || items.length === count) {
@ -352,8 +371,7 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
items.push(item); items.push(item);
cursor.continue(); cursor.continue();
}; };
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error);
}); });
}; };

View file

@ -1,4 +1,4 @@
exports.run = (transaction) => { exports.run = transaction => {
const messagesStore = transaction.objectStore('messages'); const messagesStore = transaction.objectStore('messages');
console.log("Create message attachment metadata index: 'hasAttachments'"); console.log("Create message attachment metadata index: 'hasAttachments'");
@ -8,12 +8,10 @@ exports.run = (transaction) => {
{ unique: false } { unique: false }
); );
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach((name) => { ['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => {
console.log(`Create message attachment metadata index: '${name}'`); console.log(`Create message attachment metadata index: '${name}'`);
messagesStore.createIndex( messagesStore.createIndex(name, ['conversationId', 'received_at', name], {
name, unique: false,
['conversationId', 'received_at', name], });
{ unique: false }
);
}); });
}; };

View file

@ -1,23 +1,22 @@
const Migrations0DatabaseWithAttachmentData = const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data');
require('./migrations_0_database_with_attachment_data'); const Migrations1DatabaseWithoutAttachmentData = require('./migrations_1_database_without_attachment_data');
const Migrations1DatabaseWithoutAttachmentData =
require('./migrations_1_database_without_attachment_data');
exports.getPlaceholderMigrations = () => { exports.getPlaceholderMigrations = () => {
const last0MigrationVersion = const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion();
Migrations0DatabaseWithAttachmentData.getLatestVersion(); const last1MigrationVersion = Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
const last1MigrationVersion =
Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion; const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion;
return [{ return [
{
version: lastMigrationVersion, version: lastMigrationVersion,
migrate() { migrate() {
throw new Error('Unexpected invocation of placeholder migration!' + throw new Error(
'Unexpected invocation of placeholder migration!' +
'\n\nMigrations must explicitly be run upon application startup instead' + '\n\nMigrations must explicitly be run upon application startup instead' +
' of implicitly via Backbone IndexedDB adapter at any time.'); ' of implicitly via Backbone IndexedDB adapter at any time.'
);
}, },
}]; },
];
}; };

View file

@ -3,7 +3,6 @@ const { isString, last } = require('lodash');
const { runMigrations } = require('./run_migrations'); const { runMigrations } = require('./run_migrations');
const Migration18 = require('./18'); const Migration18 = require('./18');
// IMPORTANT: The migrations below are run on a database that may be very large // IMPORTANT: The migrations below are run on a database that may be very large
// due to attachments being directly stored inside the database. Please avoid // due to attachments being directly stored inside the database. Please avoid
// any expensive operations, e.g. modifying all messages / attachments, etc., as // any expensive operations, e.g. modifying all messages / attachments, etc., as
@ -20,7 +19,9 @@ const migrations = [
unique: false, unique: false,
}); });
messages.createIndex('receipt', 'sent_at', { unique: false }); messages.createIndex('receipt', 'sent_at', { unique: false });
messages.createIndex('unread', ['conversationId', 'unread'], { unique: false }); messages.createIndex('unread', ['conversationId', 'unread'], {
unique: false,
});
messages.createIndex('expires_at', 'expires_at', { unique: false }); messages.createIndex('expires_at', 'expires_at', { unique: false });
const conversations = transaction.db.createObjectStore('conversations'); const conversations = transaction.db.createObjectStore('conversations');
@ -59,7 +60,7 @@ const migrations = [
const identityKeys = transaction.objectStore('identityKeys'); const identityKeys = transaction.objectStore('identityKeys');
const request = identityKeys.openCursor(); const request = identityKeys.openCursor();
const promises = []; const promises = [];
request.onsuccess = (event) => { request.onsuccess = event => {
const cursor = event.target.result; const cursor = event.target.result;
if (cursor) { if (cursor) {
const attributes = cursor.value; const attributes = cursor.value;
@ -67,14 +68,16 @@ const migrations = [
attributes.firstUse = false; attributes.firstUse = false;
attributes.nonblockingApproval = false; attributes.nonblockingApproval = false;
attributes.verified = 0; attributes.verified = 0;
promises.push(new Promise(((resolve, reject) => { promises.push(
new Promise((resolve, reject) => {
const putRequest = identityKeys.put(attributes, attributes.id); const putRequest = identityKeys.put(attributes, attributes.id);
putRequest.onsuccess = resolve; putRequest.onsuccess = resolve;
putRequest.onerror = (e) => { putRequest.onerror = e => {
console.log(e); console.log(e);
reject(e); reject(e);
}; };
}))); })
);
cursor.continue(); cursor.continue();
} else { } else {
// no more results // no more results
@ -84,7 +87,7 @@ const migrations = [
}); });
} }
}; };
request.onerror = (event) => { request.onerror = event => {
console.log(event); console.log(event);
}; };
}, },
@ -129,7 +132,9 @@ const migrations = [
const messagesStore = transaction.objectStore('messages'); const messagesStore = transaction.objectStore('messages');
console.log('Create index from attachment schema version to attachment'); console.log('Create index from attachment schema version to attachment');
messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false }); messagesStore.createIndex('schemaVersion', 'schemaVersion', {
unique: false,
});
const duration = Date.now() - start; const duration = Date.now() - start;

View file

@ -4,7 +4,6 @@ const db = require('../database');
const settings = require('../settings'); const settings = require('../settings');
const { runMigrations } = require('./run_migrations'); const { runMigrations } = require('./run_migrations');
// IMPORTANT: Add new migrations that need to traverse entire database, e.g. // IMPORTANT: Add new migrations that need to traverse entire database, e.g.
// messages store, below. Whenever we need this, we need to force attachment // messages store, below. Whenever we need this, we need to force attachment
// migration on startup: // migration on startup:
@ -20,7 +19,9 @@ const migrations = [
exports.run = async ({ Backbone, database } = {}) => { exports.run = async ({ Backbone, database } = {}) => {
const { canRun } = await exports.getStatus({ database }); const { canRun } = await exports.getStatus({ database });
if (!canRun) { if (!canRun) {
throw new Error('Cannot run migrations on database without attachment data'); throw new Error(
'Cannot run migrations on database without attachment data'
);
} }
await runMigrations({ Backbone, database }); await runMigrations({ Backbone, database });
@ -28,8 +29,9 @@ exports.run = async ({ Backbone, database } = {}) => {
exports.getStatus = async ({ database } = {}) => { exports.getStatus = async ({ database } = {}) => {
const connection = await db.open(database.id, database.version); const connection = await db.open(database.id, database.version);
const isAttachmentMigrationComplete = const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
await settings.isAttachmentMigrationComplete(connection); connection
);
const hasMigrations = migrations.length > 0; const hasMigrations = migrations.length > 0;
const canRun = isAttachmentMigrationComplete && hasMigrations; const canRun = isAttachmentMigrationComplete && hasMigrations;

View file

@ -1,29 +1,27 @@
/* eslint-env browser */ /* eslint-env browser */
const { const { head, isFunction, isObject, isString, last } = require('lodash');
head,
isFunction,
isObject,
isString,
last,
} = require('lodash');
const db = require('../database'); const db = require('../database');
const { deferredToPromise } = require('../deferred_to_promise'); const { deferredToPromise } = require('../deferred_to_promise');
const closeDatabaseConnection = ({ Backbone } = {}) => const closeDatabaseConnection = ({ Backbone } = {}) =>
deferredToPromise(Backbone.sync('closeall')); deferredToPromise(Backbone.sync('closeall'));
exports.runMigrations = async ({ Backbone, database } = {}) => { exports.runMigrations = async ({ Backbone, database } = {}) => {
if (!isObject(Backbone) || !isObject(Backbone.Collection) || if (
!isFunction(Backbone.Collection.extend)) { !isObject(Backbone) ||
!isObject(Backbone.Collection) ||
!isFunction(Backbone.Collection.extend)
) {
throw new TypeError("'Backbone' is required"); throw new TypeError("'Backbone' is required");
} }
if (!isObject(database) || !isString(database.id) || if (
!Array.isArray(database.migrations)) { !isObject(database) ||
!isString(database.id) ||
!Array.isArray(database.migrations)
) {
throw new TypeError("'database' is required"); throw new TypeError("'database' is required");
} }
@ -56,7 +54,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => {
await closeDatabaseConnection({ Backbone }); await closeDatabaseConnection({ Backbone });
}; };
const getMigrationVersions = (database) => { const getMigrationVersions = database => {
if (!isObject(database) || !Array.isArray(database.migrations)) { if (!isObject(database) || !Array.isArray(database.migrations)) {
throw new TypeError("'database' is required"); throw new TypeError("'database' is required");
} }
@ -64,8 +62,12 @@ const getMigrationVersions = (database) => {
const firstMigration = head(database.migrations); const firstMigration = head(database.migrations);
const lastMigration = last(database.migrations); const lastMigration = last(database.migrations);
const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null; const firstVersion = firstMigration
const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null; ? parseInt(firstMigration.version, 10)
: null;
const lastVersion = lastMigration
? parseInt(lastMigration.version, 10)
: null;
return { firstVersion, lastVersion }; return { firstVersion, lastVersion };
}; };

View file

@ -1,10 +1,7 @@
/* eslint-env node */ /* eslint-env node */
exports.isMacOS = () => exports.isMacOS = () => process.platform === 'darwin';
process.platform === 'darwin';
exports.isLinux = () => exports.isLinux = () => process.platform === 'linux';
process.platform === 'linux';
exports.isWindows = () => exports.isWindows = () => process.platform === 'win32';
process.platform === 'win32';

View file

@ -6,22 +6,20 @@ const path = require('path');
const { compose } = require('lodash/fp'); const { compose } = require('lodash/fp');
const { escapeRegExp } = require('lodash'); const { escapeRegExp } = require('lodash');
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..'); const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g; const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g; const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
const REDACTION_PLACEHOLDER = '[REDACTED]'; const REDACTION_PLACEHOLDER = '[REDACTED]';
// _redactPath :: Path -> String -> String // _redactPath :: Path -> String -> String
exports._redactPath = (filePath) => { exports._redactPath = filePath => {
if (!is.string(filePath)) { if (!is.string(filePath)) {
throw new TypeError("'filePath' must be a string"); throw new TypeError("'filePath' must be a string");
} }
const filePathPattern = exports._pathToRegExp(filePath); const filePathPattern = exports._pathToRegExp(filePath);
return (text) => { return text => {
if (!is.string(text)) { if (!is.string(text)) {
throw new TypeError("'text' must be a string"); throw new TypeError("'text' must be a string");
} }
@ -35,7 +33,7 @@ exports._redactPath = (filePath) => {
}; };
// _pathToRegExp :: Path -> Maybe RegExp // _pathToRegExp :: Path -> Maybe RegExp
exports._pathToRegExp = (filePath) => { exports._pathToRegExp = filePath => {
try { try {
const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\'); const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\');
const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\'); const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\');
@ -47,7 +45,9 @@ exports._pathToRegExp = (filePath) => {
pathWithNormalizedSlashes, pathWithNormalizedSlashes,
pathWithEscapedSlashes, pathWithEscapedSlashes,
urlEncodedPath, urlEncodedPath,
].map(escapeRegExp).join('|'); ]
.map(escapeRegExp)
.join('|');
return new RegExp(patternString, 'g'); return new RegExp(patternString, 'g');
} catch (error) { } catch (error) {
return null; return null;
@ -56,7 +56,7 @@ exports._pathToRegExp = (filePath) => {
// Public API // Public API
// redactPhoneNumbers :: String -> String // redactPhoneNumbers :: String -> String
exports.redactPhoneNumbers = (text) => { exports.redactPhoneNumbers = text => {
if (!is.string(text)) { if (!is.string(text)) {
throw new TypeError("'text' must be a string"); throw new TypeError("'text' must be a string");
} }
@ -65,7 +65,7 @@ exports.redactPhoneNumbers = (text) => {
}; };
// redactGroupIds :: String -> String // redactGroupIds :: String -> String
exports.redactGroupIds = (text) => { exports.redactGroupIds = text => {
if (!is.string(text)) { if (!is.string(text)) {
throw new TypeError("'text' must be a string"); throw new TypeError("'text' must be a string");
} }

View file

@ -1,6 +1,5 @@
const { isObject, isString } = require('lodash'); const { isObject, isString } = require('lodash');
const ITEMS_STORE_NAME = 'items'; const ITEMS_STORE_NAME = 'items';
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
@ -37,8 +36,7 @@ exports._getItem = (connection, key) => {
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
const request = itemsStore.get(key); const request = itemsStore.get(key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error);
request.onsuccess = event => request.onsuccess = event =>
resolve(event.target.result ? event.target.result.value : null); resolve(event.target.result ? event.target.result.value : null);
@ -58,11 +56,9 @@ exports._setItem = (connection, key, value) => {
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
const request = itemsStore.put({ id: key, value }, key); const request = itemsStore.put({ id: key, value }, key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error);
request.onsuccess = () => request.onsuccess = () => resolve();
resolve();
}); });
}; };
@ -79,10 +75,8 @@ exports._deleteItem = (connection, key) => {
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
const request = itemsStore.delete(key); const request = itemsStore.delete(key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error);
request.onsuccess = () => request.onsuccess = () => resolve();
resolve();
}); });
}; };

View file

@ -1,4 +1,3 @@
/* global setTimeout */ /* global setTimeout */
exports.sleep = ms => exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
new Promise(resolve => setTimeout(resolve, ms));

View file

@ -3,7 +3,6 @@ const is = require('@sindresorhus/is');
const Errors = require('./types/errors'); const Errors = require('./types/errors');
const Settings = require('./settings'); const Settings = require('./settings');
exports.syncReadReceiptConfiguration = async ({ exports.syncReadReceiptConfiguration = async ({
deviceId, deviceId,
sendRequestConfigurationSyncMessage, sendRequestConfigurationSyncMessage,

View file

@ -1,4 +1,4 @@
exports.stringToArrayBuffer = (string) => { exports.stringToArrayBuffer = string => {
if (typeof string !== 'string') { if (typeof string !== 'string') {
throw new TypeError("'string' must be a string"); throw new TypeError("'string' must be a string");
} }

View file

@ -2,9 +2,15 @@ const is = require('@sindresorhus/is');
const AttachmentTS = require('../../../ts/types/Attachment'); const AttachmentTS = require('../../../ts/types/Attachment');
const MIME = require('../../../ts/types/MIME'); const MIME = require('../../../ts/types/MIME');
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util'); const {
arrayBufferToBlob,
blobToArrayBuffer,
dataURLToBlob,
} = require('blob-util');
const { autoOrientImage } = require('../auto_orient_image'); const { autoOrientImage } = require('../auto_orient_image');
const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system'); const {
migrateDataToFileSystem,
} = require('./attachment/migrate_data_to_file_system');
// // Incoming message attachment fields // // Incoming message attachment fields
// { // {
@ -30,7 +36,7 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s
// Returns true if `rawAttachment` is a valid attachment based on our current schema. // Returns true if `rawAttachment` is a valid attachment based on our current schema.
// Over time, we can expand this definition to become more narrow, e.g. require certain // Over time, we can expand this definition to become more narrow, e.g. require certain
// fields, etc. // fields, etc.
exports.isValid = (rawAttachment) => { exports.isValid = rawAttachment => {
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is // NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
// deserialized by protobuf: // deserialized by protobuf:
if (!rawAttachment) { if (!rawAttachment) {
@ -41,12 +47,15 @@ exports.isValid = (rawAttachment) => {
}; };
// Upgrade steps // Upgrade steps
exports.autoOrientJPEG = async (attachment) => { exports.autoOrientJPEG = async attachment => {
if (!MIME.isJPEG(attachment.contentType)) { if (!MIME.isJPEG(attachment.contentType)) {
return attachment; return attachment;
} }
const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType); const dataBlob = await arrayBufferToBlob(
attachment.data,
attachment.contentType
);
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob)); const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob); const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
@ -76,7 +85,7 @@ const INVALID_CHARACTERS_PATTERN = new RegExp(
// NOTE: Expose synchronous version to do property-based testing using `testcheck`, // NOTE: Expose synchronous version to do property-based testing using `testcheck`,
// which currently doesnt support async testing: // which currently doesnt support async testing:
// https://github.com/leebyron/testcheck-js/issues/45 // https://github.com/leebyron/testcheck-js/issues/45
exports._replaceUnicodeOrderOverridesSync = (attachment) => { exports._replaceUnicodeOrderOverridesSync = attachment => {
if (!is.string(attachment.fileName)) { if (!is.string(attachment.fileName)) {
return attachment; return attachment;
} }
@ -95,9 +104,12 @@ exports._replaceUnicodeOrderOverridesSync = (attachment) => {
exports.replaceUnicodeOrderOverrides = async attachment => exports.replaceUnicodeOrderOverrides = async attachment =>
exports._replaceUnicodeOrderOverridesSync(attachment); exports._replaceUnicodeOrderOverridesSync(attachment);
exports.removeSchemaVersion = (attachment) => { exports.removeSchemaVersion = attachment => {
if (!exports.isValid(attachment)) { if (!exports.isValid(attachment)) {
console.log('Attachment.removeSchemaVersion: Invalid input attachment:', attachment); console.log(
'Attachment.removeSchemaVersion: Invalid input attachment:',
attachment
);
return attachment; return attachment;
} }
@ -115,12 +127,12 @@ exports.hasData = attachment =>
// loadData :: (RelativePath -> IO (Promise ArrayBuffer)) // loadData :: (RelativePath -> IO (Promise ArrayBuffer))
// Attachment -> // Attachment ->
// IO (Promise Attachment) // IO (Promise Attachment)
exports.loadData = (readAttachmentData) => { exports.loadData = readAttachmentData => {
if (!is.function(readAttachmentData)) { if (!is.function(readAttachmentData)) {
throw new TypeError("'readAttachmentData' must be a function"); throw new TypeError("'readAttachmentData' must be a function");
} }
return async (attachment) => { return async attachment => {
if (!exports.isValid(attachment)) { if (!exports.isValid(attachment)) {
throw new TypeError("'attachment' is not valid"); throw new TypeError("'attachment' is not valid");
} }
@ -142,12 +154,12 @@ exports.loadData = (readAttachmentData) => {
// deleteData :: (RelativePath -> IO Unit) // deleteData :: (RelativePath -> IO Unit)
// Attachment -> // Attachment ->
// IO Unit // IO Unit
exports.deleteData = (deleteAttachmentData) => { exports.deleteData = deleteAttachmentData => {
if (!is.function(deleteAttachmentData)) { if (!is.function(deleteAttachmentData)) {
throw new TypeError("'deleteAttachmentData' must be a function"); throw new TypeError("'deleteAttachmentData' must be a function");
} }
return async (attachment) => { return async attachment => {
if (!exports.isValid(attachment)) { if (!exports.isValid(attachment)) {
throw new TypeError("'attachment' is not valid"); throw new TypeError("'attachment' is not valid");
} }

View file

@ -1,10 +1,4 @@
const { const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
isArrayBuffer,
isFunction,
isUndefined,
omit,
} = require('lodash');
// type Context :: { // type Context :: {
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path) // writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
@ -13,7 +7,10 @@ const {
// migrateDataToFileSystem :: Attachment -> // migrateDataToFileSystem :: Attachment ->
// Context -> // Context ->
// Promise Attachment // Promise Attachment
exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } = {}) => { exports.migrateDataToFileSystem = async (
attachment,
{ writeNewAttachmentData } = {}
) => {
if (!isFunction(writeNewAttachmentData)) { if (!isFunction(writeNewAttachmentData)) {
throw new TypeError("'writeNewAttachmentData' must be a function"); throw new TypeError("'writeNewAttachmentData' must be a function");
} }
@ -28,15 +25,16 @@ exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData }
const isValidData = isArrayBuffer(data); const isValidData = isArrayBuffer(data);
if (!isValidData) { if (!isValidData) {
throw new TypeError('Expected `attachment.data` to be an array buffer;' + throw new TypeError(
` got: ${typeof attachment.data}`); 'Expected `attachment.data` to be an array buffer;' +
` got: ${typeof attachment.data}`
);
} }
const path = await writeNewAttachmentData(data); const path = await writeNewAttachmentData(data);
const attachmentWithoutData = omit( const attachmentWithoutData = omit(Object.assign({}, attachment, { path }), [
Object.assign({}, attachment, { path }), 'data',
['data'] ]);
);
return attachmentWithoutData; return attachmentWithoutData;
}; };

View file

@ -1,5 +1,5 @@
// toLogFormat :: Error -> String // toLogFormat :: Error -> String
exports.toLogFormat = (error) => { exports.toLogFormat = error => {
if (!error) { if (!error) {
return error; return error;
} }

View file

@ -3,9 +3,9 @@ const { isFunction, isString, omit } = require('lodash');
const Attachment = require('./attachment'); const Attachment = require('./attachment');
const Errors = require('./errors'); const Errors = require('./errors');
const SchemaVersion = require('./schema_version'); const SchemaVersion = require('./schema_version');
const { initializeAttachmentMetadata } = const {
require('../../../ts/types/message/initializeAttachmentMetadata'); initializeAttachmentMetadata,
} = require('../../../ts/types/message/initializeAttachmentMetadata');
const GROUP = 'group'; const GROUP = 'group';
const PRIVATE = 'private'; const PRIVATE = 'private';
@ -37,19 +37,17 @@ const INITIAL_SCHEMA_VERSION = 0;
// how we do database migrations: // how we do database migrations:
exports.CURRENT_SCHEMA_VERSION = 5; exports.CURRENT_SCHEMA_VERSION = 5;
// Public API // Public API
exports.GROUP = GROUP; exports.GROUP = GROUP;
exports.PRIVATE = PRIVATE; exports.PRIVATE = PRIVATE;
// Placeholder until we have stronger preconditions: // Placeholder until we have stronger preconditions:
exports.isValid = () => exports.isValid = () => true;
true;
// Schema // Schema
exports.initializeSchemaVersion = (message) => { exports.initializeSchemaVersion = message => {
const isInitialized = SchemaVersion.isValid(message.schemaVersion) && const isInitialized =
message.schemaVersion >= 1; SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
if (isInitialized) { if (isInitialized) {
return message; return message;
} }
@ -59,27 +57,23 @@ exports.initializeSchemaVersion = (message) => {
: 0; : 0;
const hasAttachments = numAttachments > 0; const hasAttachments = numAttachments > 0;
if (!hasAttachments) { if (!hasAttachments) {
return Object.assign( return Object.assign({}, message, {
{}, schemaVersion: INITIAL_SCHEMA_VERSION,
message, });
{ schemaVersion: INITIAL_SCHEMA_VERSION }
);
} }
// All attachments should have the same schema version, so we just pick // All attachments should have the same schema version, so we just pick
// the first one: // the first one:
const firstAttachment = message.attachments[0]; const firstAttachment = message.attachments[0];
const inheritedSchemaVersion = SchemaVersion.isValid(firstAttachment.schemaVersion) const inheritedSchemaVersion = SchemaVersion.isValid(
firstAttachment.schemaVersion
)
? firstAttachment.schemaVersion ? firstAttachment.schemaVersion
: INITIAL_SCHEMA_VERSION; : INITIAL_SCHEMA_VERSION;
const messageWithInitialSchema = Object.assign( const messageWithInitialSchema = Object.assign({}, message, {
{},
message,
{
schemaVersion: inheritedSchemaVersion, schemaVersion: inheritedSchemaVersion,
attachments: message.attachments.map(Attachment.removeSchemaVersion), attachments: message.attachments.map(Attachment.removeSchemaVersion),
} });
);
return messageWithInitialSchema; return messageWithInitialSchema;
}; };
@ -98,7 +92,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
return async (message, context) => { return async (message, context) => {
if (!exports.isValid(message)) { if (!exports.isValid(message)) {
console.log('Message._withSchemaVersion: Invalid input message:', message); console.log(
'Message._withSchemaVersion: Invalid input message:',
message
);
return message; return message;
} }
@ -138,15 +135,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
return message; return message;
} }
return Object.assign( return Object.assign({}, upgradedMessage, { schemaVersion });
{},
upgradedMessage,
{ schemaVersion }
);
}; };
}; };
// Public API // Public API
// _mapAttachments :: (Attachment -> Promise Attachment) -> // _mapAttachments :: (Attachment -> Promise Attachment) ->
// (Message, Context) -> // (Message, Context) ->
@ -154,19 +146,24 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
exports._mapAttachments = upgradeAttachment => async (message, context) => { exports._mapAttachments = upgradeAttachment => async (message, context) => {
const upgradeWithContext = attachment => const upgradeWithContext = attachment =>
upgradeAttachment(attachment, context); upgradeAttachment(attachment, context);
const attachments = await Promise.all(message.attachments.map(upgradeWithContext)); const attachments = await Promise.all(
message.attachments.map(upgradeWithContext)
);
return Object.assign({}, message, { attachments }); return Object.assign({}, message, { attachments });
}; };
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) -> // _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
// (Message, Context) -> // (Message, Context) ->
// Promise Message // Promise Message
exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => { exports._mapQuotedAttachments = upgradeAttachment => async (
message,
context
) => {
if (!message.quote) { if (!message.quote) {
return message; return message;
} }
const upgradeWithContext = async (attachment) => { const upgradeWithContext = async attachment => {
const { thumbnail } = attachment; const { thumbnail } = attachment;
if (!thumbnail) { if (!thumbnail) {
return attachment; return attachment;
@ -185,7 +182,9 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
const quotedAttachments = (message.quote && message.quote.attachments) || []; const quotedAttachments = (message.quote && message.quote.attachments) || [];
const attachments = await Promise.all(quotedAttachments.map(upgradeWithContext)); const attachments = await Promise.all(
quotedAttachments.map(upgradeWithContext)
);
return Object.assign({}, message, { return Object.assign({}, message, {
quote: Object.assign({}, message.quote, { quote: Object.assign({}, message.quote, {
attachments, attachments,
@ -193,8 +192,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
}); });
}; };
const toVersion0 = async message => const toVersion0 = async message => exports.initializeSchemaVersion(message);
exports.initializeSchemaVersion(message);
const toVersion1 = exports._withSchemaVersion( const toVersion1 = exports._withSchemaVersion(
1, 1,
@ -241,25 +239,28 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
return message; return message;
}; };
exports.createAttachmentLoader = (loadAttachmentData) => { exports.createAttachmentLoader = loadAttachmentData => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('`loadAttachmentData` is required'); throw new TypeError('`loadAttachmentData` is required');
} }
return async message => (Object.assign({}, message, { return async message =>
attachments: await Promise.all(message.attachments.map(loadAttachmentData)), Object.assign({}, message, {
})); attachments: await Promise.all(
message.attachments.map(loadAttachmentData)
),
});
}; };
// createAttachmentDataWriter :: (RelativePath -> IO Unit) // createAttachmentDataWriter :: (RelativePath -> IO Unit)
// Message -> // Message ->
// IO (Promise Message) // IO (Promise Message)
exports.createAttachmentDataWriter = (writeExistingAttachmentData) => { exports.createAttachmentDataWriter = writeExistingAttachmentData => {
if (!isFunction(writeExistingAttachmentData)) { if (!isFunction(writeExistingAttachmentData)) {
throw new TypeError("'writeExistingAttachmentData' must be a function"); throw new TypeError("'writeExistingAttachmentData' must be a function");
} }
return async (rawMessage) => { return async rawMessage => {
if (!exports.isValid(rawMessage)) { if (!exports.isValid(rawMessage)) {
throw new TypeError("'rawMessage' is not valid"); throw new TypeError("'rawMessage' is not valid");
} }
@ -282,17 +283,21 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
return message; return message;
} }
(attachments || []).forEach((attachment) => { (attachments || []).forEach(attachment => {
if (!Attachment.hasData(attachment)) { if (!Attachment.hasData(attachment)) {
throw new TypeError("'attachment.data' is required during message import"); throw new TypeError(
"'attachment.data' is required during message import"
);
} }
if (!isString(attachment.path)) { if (!isString(attachment.path)) {
throw new TypeError("'attachment.path' is required during message import"); throw new TypeError(
"'attachment.path' is required during message import"
);
} }
}); });
const writeThumbnails = exports._mapQuotedAttachments(async (thumbnail) => { const writeThumbnails = exports._mapQuotedAttachments(async thumbnail => {
const { data, path } = thumbnail; const { data, path } = thumbnail;
// we want to be bulletproof to thumbnails without data // we want to be bulletproof to thumbnails without data
@ -315,10 +320,12 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
{}, {},
await writeThumbnails(message), await writeThumbnails(message),
{ {
attachments: await Promise.all((attachments || []).map(async (attachment) => { attachments: await Promise.all(
(attachments || []).map(async attachment => {
await writeExistingAttachmentData(attachment); await writeExistingAttachmentData(attachment);
return omit(attachment, ['data']); return omit(attachment, ['data']);
})), })
),
} }
); );

View file

@ -1,5 +1,3 @@
const { isNumber } = require('lodash'); const { isNumber } = require('lodash');
exports.isValid = value => isNumber(value) && value >= 0;
exports.isValid = value =>
isNumber(value) && value >= 0;

View file

@ -1,4 +1,3 @@
const OS = require('../os'); const OS = require('../os');
exports.isAudioNotificationSupported = () => exports.isAudioNotificationSupported = () => !OS.isLinux();
!OS.isLinux();

View file

@ -2,7 +2,6 @@
/* global i18n: false */ /* global i18n: false */
const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds
const setMessage = () => { const setMessage = () => {

View file

@ -2,7 +2,7 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Settings } = window.Signal.Types; const { Settings } = window.Signal.Types;
@ -11,7 +11,7 @@
OFF: 'off', OFF: 'off',
COUNT: 'count', COUNT: 'count',
NAME: 'name', NAME: 'name',
MESSAGE : 'message' MESSAGE: 'message',
}; };
Whisper.Notifications = new (Backbone.Collection.extend({ Whisper.Notifications = new (Backbone.Collection.extend({
@ -27,15 +27,18 @@
update: function() { update: function() {
const { isEnabled } = this; const { isEnabled } = this;
const isFocused = window.isFocused(); const isFocused = window.isFocused();
const isAudioNotificationEnabled = storage.get('audio-notification') || false; const isAudioNotificationEnabled =
storage.get('audio-notification') || false;
const isAudioNotificationSupported = Settings.isAudioNotificationSupported(); const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
const shouldPlayNotificationSound = isAudioNotificationSupported && const shouldPlayNotificationSound =
isAudioNotificationEnabled; isAudioNotificationSupported && isAudioNotificationEnabled;
const numNotifications = this.length; const numNotifications = this.length;
console.log( console.log('Update notifications:', {
'Update notifications:', isFocused,
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound} isEnabled,
); numNotifications,
shouldPlayNotificationSound,
});
if (!isEnabled) { if (!isEnabled) {
return; return;
@ -69,7 +72,7 @@
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
var newMessageCount = [ var newMessageCount = [
numNotifications, numNotifications,
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages') numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'),
].join(' '); ].join(' ');
var last = this.last(); var last = this.last();
@ -111,7 +114,10 @@
silent: !shouldPlayNotificationSound, silent: !shouldPlayNotificationSound,
}); });
notification.onclick = this.onClick.bind(this, last.get('conversationId')); notification.onclick = this.onClick.bind(
this,
last.get('conversationId')
);
} }
// We don't want to notify the user about these same messages again // We don't want to notify the user about these same messages again

View file

@ -1,7 +1,7 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(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({
@ -16,8 +16,10 @@
ids = conversation.get('members'); ids = conversation.get('members');
} }
var receipts = this.filter(function(receipt) { var receipts = this.filter(function(receipt) {
return receipt.get('timestamp') === message.get('sent_at') return (
&& _.contains(ids, receipt.get('reader')); receipt.get('timestamp') === message.get('sent_at') &&
_.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');
@ -27,28 +29,43 @@
}, },
onReceipt: function(receipt) { onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection(); var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { return messages
if (messages.length === 0) { return; } .fetchSentAt(receipt.get('timestamp'))
.then(function() {
if (messages.length === 0) {
return;
}
var message = messages.find(function(message) { var message = messages.find(function(message) {
return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId')); return (
message.isOutgoing() &&
receipt.get('reader') === message.get('conversationId')
);
}); });
if (message) { return message; } if (message) {
return message;
}
var groups = new Whisper.GroupCollection(); var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('reader')).then(function() { return groups.fetchGroups(receipt.get('reader')).then(function() {
var ids = groups.pluck('id'); var ids = groups.pluck('id');
ids.push(receipt.get('reader')); ids.push(receipt.get('reader'));
return messages.find(function(message) { return messages.find(function(message) {
return (message.isOutgoing() && return (
_.contains(ids, message.get('conversationId'))); message.isOutgoing() &&
_.contains(ids, message.get('conversationId'))
);
}); });
}); });
}).then(function(message) { })
.then(
function(message) {
if (message) { if (message) {
var read_by = message.get('read_by') || []; var read_by = message.get('read_by') || [];
read_by.push(receipt.get('reader')); read_by.push(receipt.get('reader'));
return new Promise(function(resolve, reject) { return new Promise(
message.save({ read_by: read_by }).then(function() { function(resolve, reject) {
message.save({ read_by: read_by }).then(
function() {
// notify frontend listeners // notify frontend listeners
var conversation = ConversationController.get( var conversation = ConversationController.get(
message.get('conversationId') message.get('conversationId')
@ -59,8 +76,11 @@
this.remove(receipt); this.remove(receipt);
resolve(); resolve();
}.bind(this), reject); }.bind(this),
}.bind(this)); reject
);
}.bind(this)
);
} else { } else {
console.log( console.log(
'No message for read receipt', 'No message for read receipt',
@ -68,7 +88,9 @@
receipt.get('timestamp') receipt.get('timestamp')
); );
} }
}.bind(this)).catch(function(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,14 +1,14 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(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: function(message) {
var receipt = this.findWhere({ var receipt = this.findWhere({
sender: message.get('source'), sender: message.get('source'),
timestamp: message.get('sent_at') timestamp: message.get('sent_at'),
}); });
if (receipt) { if (receipt) {
console.log('Found early read sync for message'); console.log('Found early read sync for message');
@ -18,27 +18,35 @@
}, },
onReceipt: function(receipt) { onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection(); var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { return messages.fetchSentAt(receipt.get('timestamp')).then(
function() {
var message = messages.find(function(message) { var message = messages.find(function(message) {
return (message.isIncoming() && message.isUnread() && return (
message.get('source') === receipt.get('sender')); message.isIncoming() &&
message.isUnread() &&
message.get('source') === receipt.get('sender')
);
}); });
if (message) { if (message) {
return message.markRead(receipt.get('read_at')).then(function() { return message.markRead(receipt.get('read_at')).then(
function() {
this.notifyConversation(message); this.notifyConversation(message);
this.remove(receipt); this.remove(receipt);
}.bind(this)); }.bind(this)
);
} else { } else {
console.log( console.log(
'No message for read sync', 'No message for read sync',
receipt.get('sender'), receipt.get('timestamp') receipt.get('sender'),
receipt.get('timestamp')
); );
} }
}.bind(this)); }.bind(this)
);
}, },
notifyConversation: function(message) { notifyConversation: function(message) {
var conversation = ConversationController.get({ var conversation = ConversationController.get({
id: message.get('conversationId') id: message.get('conversationId'),
}); });
if (conversation) { if (conversation) {

View file

@ -15,11 +15,13 @@
return storage.get('chromiumRegistrationDone') === ''; return storage.get('chromiumRegistrationDone') === '';
}, },
everDone: function() { everDone: function() {
return storage.get('chromiumRegistrationDoneEver') === '' || return (
storage.get('chromiumRegistrationDone') === ''; storage.get('chromiumRegistrationDoneEver') === '' ||
storage.get('chromiumRegistrationDone') === ''
);
}, },
remove: function() { remove: function() {
storage.remove('chromiumRegistrationDone'); storage.remove('chromiumRegistrationDone');
} },
}; };
}()); })();

View file

@ -49,17 +49,26 @@
// 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) { var triggerEvents = function(events, name, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; var ev,
i = -1,
l = events.length,
a1 = args[0],
a2 = args[1],
a3 = args[2];
var logError = function(error) { var logError = function(error) {
console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error); console.log(
'Model caught error triggering',
name,
'event:',
error && error.stack ? error.stack : error
);
}; };
switch (args.length) { switch (args.length) {
case 0: case 0:
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx); (ev = events[i]).callback.call(ev.ctx);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@ -68,8 +77,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1); (ev = events[i]).callback.call(ev.ctx, a1);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@ -78,8 +86,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1, a2); (ev = events[i]).callback.call(ev.ctx, a1, a2);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@ -88,8 +95,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3); (ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@ -98,8 +104,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.apply(ev.ctx, args); (ev = events[i]).callback.apply(ev.ctx, args);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@ -122,10 +127,5 @@
return this; return this;
} }
Backbone.Model.prototype.trigger Backbone.Model.prototype.trigger = Backbone.View.prototype.trigger = Backbone.Collection.prototype.trigger = Backbone.Events.trigger = trigger;
= Backbone.View.prototype.trigger
= Backbone.Collection.prototype.trigger
= Backbone.Events.trigger
= trigger;
})(); })();

View file

@ -2,7 +2,7 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
@ -17,8 +17,12 @@
function run() { function run() {
console.log('Rotating signed prekey...'); console.log('Rotating signed prekey...');
getAccountManager().rotateSignedPreKey().catch(function() { getAccountManager()
console.log('rotateSignedPrekey() failed. Trying again in five seconds'); .rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000); setTimeout(runWhenOnline, 5000);
}); });
scheduleNextRotation(); scheduleNextRotation();
@ -29,7 +33,9 @@
if (navigator.onLine) { if (navigator.onLine) {
run(); run();
} else { } else {
console.log('We are offline; keys will be rotated when we are next online'); console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() { var listener = function() {
window.removeEventListener('online', listener); window.removeEventListener('online', listener);
run(); run();
@ -79,6 +85,6 @@
setTimeoutForNextRun(); setTimeoutForNextRun();
} }
}); });
} },
}; };
}()); })();

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@
'shouldn', 'shouldn',
'wasn', 'wasn',
'weren', 'weren',
'wouldn' 'wouldn',
]; ];
function setupLinux(locale) { function setupLinux(locale) {
@ -39,7 +39,12 @@
// apt-get install hunspell-<locale> can be run for easy access to other dictionaries // apt-get install hunspell-<locale> can be run for easy access to other dictionaries
var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell'; var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
console.log('Detected Linux. Setting up spell check with locale', locale, 'and dictionary location', location); console.log(
'Detected Linux. Setting up spell check with locale',
locale,
'and dictionary location',
location
);
spellchecker.setDictionary(locale, location); spellchecker.setDictionary(locale, location);
} else { } else {
console.log('Detected Linux. Using default en_US spell check dictionary'); console.log('Detected Linux. Using default en_US spell check dictionary');
@ -50,10 +55,17 @@
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') { if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
var location = process.env.HUNSPELL_DICTIONARIES; var location = process.env.HUNSPELL_DICTIONARIES;
console.log('Detected Windows 7 or below. Setting up spell-check with locale', locale, 'and dictionary location', location); console.log(
'Detected Windows 7 or below. Setting up spell-check with locale',
locale,
'and dictionary location',
location
);
spellchecker.setDictionary(locale, location); spellchecker.setDictionary(locale, location);
} else { } else {
console.log('Detected Windows 7 or below. Using default en_US spell check dictionary'); console.log(
'Detected Windows 7 or below. Using default en_US spell check dictionary'
);
} }
} }
@ -69,14 +81,17 @@
if (process.platform === 'linux') { if (process.platform === 'linux') {
setupLinux(locale); setupLinux(locale);
} else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) { } else if (
process.platform === 'windows' &&
semver.lt(os.release(), '8.0.0')
) {
setupWin7AndEarlier(locale); setupWin7AndEarlier(locale);
} else { } else {
// OSX and Windows 8+ have OS-level spellcheck APIs // OSX and Windows 8+ have OS-level spellcheck APIs
console.log('Using OS-level spell check API with locale', process.env.LANG); console.log('Using OS-level spell check API with locale', process.env.LANG);
} }
var simpleChecker = window.spellChecker = { var simpleChecker = (window.spellChecker = {
spellCheck: function(text) { spellCheck: function(text) {
return !this.isMisspelled(text); return !this.isMisspelled(text);
}, },
@ -101,8 +116,8 @@
}, },
add: function(text) { add: function(text) {
spellchecker.add(text); spellchecker.add(text);
} },
}; });
webFrame.setSpellCheckProvider( webFrame.setSpellCheckProvider(
'en-US', 'en-US',
@ -120,7 +135,8 @@
var selectedText = window.getSelection().toString(); var selectedText = window.getSelection().toString();
var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText); var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
var spellingSuggestions = isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5); var spellingSuggestions =
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
var menu = buildEditorContextMenu({ var menu = buildEditorContextMenu({
isMisspelled: isMisspelled, isMisspelled: isMisspelled,
spellingSuggestions: spellingSuggestions, spellingSuggestions: spellingSuggestions,

View file

@ -1,12 +1,12 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function() { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var Item = Backbone.Model.extend({ var Item = Backbone.Model.extend({
database: Whisper.Database, database: Whisper.Database,
storeName: 'items' storeName: 'items',
}); });
var ItemCollection = Backbone.Collection.extend({ var ItemCollection = Backbone.Collection.extend({
model: Item, model: Item,
@ -16,14 +16,16 @@
var ready = false; var ready = false;
var items = new ItemCollection(); var items = new ItemCollection();
items.on('reset', function() { ready = true; }); items.on('reset', function() {
ready = true;
});
window.storage = { window.storage = {
/***************************** /*****************************
*** Base Storage Routines *** *** Base Storage Routines ***
*****************************/ *****************************/
put: function(key, value) { put: function(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);
@ -35,7 +37,7 @@
}, },
get: function(key, defaultValue) { get: function(key, defaultValue) {
var item = items.get("" + key); var item = items.get('' + key);
if (!item) { if (!item) {
return defaultValue; return defaultValue;
} }
@ -43,7 +45,7 @@
}, },
remove: function(key) { remove: function(key) {
var item = items.get("" + key); var item = items.get('' + key);
if (item) { if (item) {
items.remove(item); items.remove(item);
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -63,16 +65,23 @@
fetch: function() { fetch: function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
items.fetch({reset: true}) items
.fail(() => reject(new Error('Failed to fetch from storage.' + .fetch({ reset: true })
' This may be due to an unexpected database version.'))) .fail(() =>
reject(
new Error(
'Failed to fetch from storage.' +
' This may be due to an unexpected database version.'
)
)
)
.always(resolve); .always(resolve);
}); });
}, },
reset: function() { reset: function() {
items.reset(); items.reset();
} },
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};

View file

@ -13,13 +13,14 @@
}, },
events: { events: {
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
'openInbox': 'openInbox', openInbox: 'openInbox',
'change-theme': 'applyTheme', 'change-theme': 'applyTheme',
'change-hide-menu': 'applyHideMenu', 'change-hide-menu': 'applyHideMenu',
}, },
applyTheme: function() { applyTheme: function() {
var theme = storage.get('theme-setting') || 'android'; var theme = storage.get('theme-setting') || 'android';
this.$el.removeClass('ios') this.$el
.removeClass('ios')
.removeClass('android-dark') .removeClass('android-dark')
.removeClass('android') .removeClass('android')
.addClass(theme); .addClass(theme);
@ -30,7 +31,7 @@
window.setMenuBarVisibility(!hideMenuBar); window.setMenuBarVisibility(!hideMenuBar);
}, },
openView: function(view) { openView: function(view) {
this.el.innerHTML = ""; this.el.innerHTML = '';
this.el.append(view.el); this.el.append(view.el);
this.delegateEvents(); this.delegateEvents();
}, },
@ -48,13 +49,17 @@
openImporter: function() { openImporter: function() {
window.addSetupMenuItems(); window.addSetupMenuItems();
this.resetViews(); this.resetViews();
var importView = this.importView = new Whisper.ImportView(); var importView = (this.importView = new Whisper.ImportView());
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this)); this.listenTo(
importView,
'light-import',
this.finishLightImport.bind(this)
);
this.openView(this.importView); this.openView(this.importView);
}, },
finishLightImport: function() { finishLightImport: function() {
var options = { var options = {
hasExistingData: true hasExistingData: true,
}; };
this.openInstaller(options); this.openInstaller(options);
}, },
@ -76,7 +81,7 @@
} }
this.resetViews(); this.resetViews();
var installView = this.installView = new Whisper.InstallView(options); var installView = (this.installView = new Whisper.InstallView(options));
this.openView(this.installView); this.openView(this.installView);
}, },
closeInstaller: function() { closeInstaller: function() {
@ -130,11 +135,13 @@
this.inboxView = new Whisper.InboxView({ this.inboxView = new Whisper.InboxView({
model: self, model: self,
window: window, window: window,
initialLoadComplete: options.initialLoadComplete initialLoadComplete: options.initialLoadComplete,
}); });
return ConversationController.loadPromise().then(function() { return ConversationController.loadPromise().then(
function() {
this.openView(this.inboxView); this.openView(this.inboxView);
}.bind(this)); }.bind(this)
);
} else { } else {
if (!$.contains(this.el, this.inboxView.el)) { if (!$.contains(this.el, this.inboxView.el)) {
this.openView(this.inboxView); this.openView(this.inboxView);
@ -159,9 +166,11 @@
}, },
openConversation: function(conversation) { openConversation: function(conversation) {
if (conversation) { if (conversation) {
this.openInbox().then(function() { this.openInbox().then(
function() {
this.inboxView.openConversation(null, conversation); this.inboxView.openConversation(null, conversation);
}.bind(this)); }.bind(this)
);
} }
}, },
}); });

View file

@ -10,6 +10,6 @@
templateName: 'attachment-preview', templateName: 'attachment-preview',
render_attributes: function() { render_attributes: function() {
return { source: this.src }; return { source: this.src };
} },
}); });
})(); })();

View file

@ -62,10 +62,7 @@
const VideoView = MediaView.extend({ tagName: 'video' }); const VideoView = MediaView.extend({ tagName: 'video' });
// Blacklist common file types known to be unsupported in Chrome // Blacklist common file types known to be unsupported in Chrome
const unsupportedFileTypes = [ const unsupportedFileTypes = ['audio/aiff', 'video/quicktime'];
'audio/aiff',
'video/quicktime',
];
Whisper.AttachmentView = Backbone.View.extend({ Whisper.AttachmentView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
@ -123,7 +120,10 @@
}, },
isVoiceMessage() { isVoiceMessage() {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) { if (
this.model.flags &
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
) {
return true; return true;
} }
@ -241,4 +241,4 @@
this.trigger('update'); this.trigger('update');
}, },
}); });
}()); })();

View file

@ -16,13 +16,13 @@
this.message = options.message; this.message = options.message;
this.callbacks = { this.callbacks = {
onDismiss: options.onDismiss, onDismiss: options.onDismiss,
onClick: options.onClick onClick: options.onClick,
}; };
this.render(); this.render();
}, },
render_attributes: function() { render_attributes: function() {
return { return {
message: this.message message: this.message,
}; };
}, },
onDismiss: function(e) { onDismiss: function(e) {
@ -31,6 +31,6 @@
}, },
onClick: function() { onClick: function() {
this.callbacks.onClick(); this.callbacks.onClick();
} },
}); });
})(); })();

View file

@ -21,7 +21,7 @@
this.render(); this.render();
}, },
events: { events: {
'keyup': 'onKeyup', keyup: 'onKeyup',
'click .ok': 'ok', 'click .ok': 'ok',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
@ -30,7 +30,7 @@
message: this.message, message: this.message,
showCancel: !this.hideCancel, showCancel: !this.hideCancel,
cancel: this.cancelText, cancel: this.cancelText,
ok: this.okText ok: this.okText,
}; };
}, },
ok: function() { ok: function() {
@ -52,6 +52,6 @@
}, },
focusCancel: function() { focusCancel: function() {
this.$('.cancel').focus(); this.$('.cancel').focus();
} },
}); });
})(); })();

View file

@ -12,7 +12,7 @@
className: 'contact', className: 'contact',
templateName: 'contact', templateName: 'contact',
events: { events: {
'click': 'showIdentity' click: 'showIdentity',
}, },
initialize: function(options) { initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
@ -25,7 +25,7 @@
return { return {
title: i18n('me'), title: i18n('me'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
} }
@ -36,7 +36,7 @@
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
isVerified: this.model.isVerified(), isVerified: this.model.isVerified(),
verified: i18n('verified') verified: i18n('verified'),
}; };
}, },
showIdentity: function() { showIdentity: function() {
@ -44,10 +44,10 @@
return; return;
} }
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
} },
}) }),
}); });
})(); })();

View file

@ -13,27 +13,43 @@
}, },
templateName: 'conversation-preview', templateName: 'conversation-preview',
events: { events: {
'click': 'select' click: 'select',
}, },
initialize: function() { initialize: function() {
// auto update // auto update
this.listenTo(this.model, 'change', _.debounce(this.render.bind(this), 1000)); this.listenTo(
this.model,
'change',
_.debounce(this.render.bind(this), 1000)
);
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(this.model.updateLastMessage.bind(this.model), 1000); var updateLastMessage = _.debounce(
this.listenTo(this.model.messageCollection, 'add remove', updateLastMessage); this.model.updateLastMessage.bind(this.model),
1000
);
this.listenTo(
this.model.messageCollection,
'add remove',
updateLastMessage
);
this.listenTo(this.model, 'newmessage', updateLastMessage); this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed(function() { extension.windows.onClosed(
function() {
this.stopListening(); this.stopListening();
}.bind(this)); }.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: function() {
this.$el.addClass('selected').siblings('.selected').removeClass('selected'); this.$el
.addClass('selected')
.siblings('.selected')
.removeClass('selected');
}, },
select: function(e) { select: function(e) {
@ -43,15 +59,19 @@
render: function() { render: function() {
this.$el.html( this.$el.html(
Mustache.render(_.result(this,'template', ''), { Mustache.render(
_.result(this, 'template', ''),
{
title: this.model.getTitle(), title: this.model.getTitle(),
last_message: this.model.get('lastMessage'), last_message: this.model.get('lastMessage'),
last_message_timestamp: this.model.get('timestamp'), last_message_timestamp: this.model.get('timestamp'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
unreadCount: this.model.get('unreadCount') unreadCount: this.model.get('unreadCount'),
}, this.render_partials()) },
this.render_partials()
)
); );
this.timeStampView.setElement(this.$('.last-timestamp')); this.timeStampView.setElement(this.$('.last-timestamp'));
this.timeStampView.update(); this.timeStampView.update();
@ -67,7 +87,6 @@
} }
return this; return this;
} },
}); });
})(); })();

View file

@ -56,6 +56,6 @@
if ($el && $el.length > 0) { if ($el && $el.length > 0) {
$el.remove(); $el.remove();
} }
} },
}); });
})(); })();

View file

@ -8,8 +8,7 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const isSearchable = conversation => const isSearchable = conversation => conversation.isSearchable();
conversation.isSearchable();
Whisper.NewContactView = Whisper.View.extend({ Whisper.NewContactView = Whisper.View.extend({
templateName: 'new-contact', templateName: 'new-contact',
@ -46,7 +45,9 @@
// View to display the matched contacts from typeahead // View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.ConversationListView({ this.typeahead_view = new Whisper.ConversationListView({
collection: new Whisper.ConversationCollection([], { collection: new Whisper.ConversationCollection([], {
comparator(m) { return m.getTitle().toLowerCase(); }, comparator(m) {
return m.getTitle().toLowerCase();
},
}), }),
}); });
this.$el.append(this.typeahead_view.el); this.$el.append(this.typeahead_view.el);
@ -75,8 +76,11 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
this.pending = this.pending.then(() => this.pending = this.pending.then(() =>
this.typeahead.search(query).then(() => { this.typeahead.search(query).then(() => {
this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable)); this.typeahead_view.collection.reset(
})); this.typeahead.filter(isSearchable)
);
})
);
/* eslint-enable more/no-then */ /* eslint-enable more/no-then */
this.trigger('show'); this.trigger('show');
} else { } else {
@ -105,8 +109,10 @@
} }
const newConversationId = this.new_contact_view.model.id; const newConversationId = this.new_contact_view.model.id;
const conversation = const conversation = await ConversationController.getOrCreateAndWait(
await ConversationController.getOrCreateAndWait(newConversationId, 'private'); newConversationId,
'private'
);
this.trigger('open', conversation); this.trigger('open', conversation);
this.initNewContact(); this.initNewContact();
this.resetTypeahead(); this.resetTypeahead();
@ -129,7 +135,9 @@
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.typeahead.fetchAlphabetical().then(() => { this.typeahead.fetchAlphabetical().then(() => {
if (this.typeahead.length > 0) { if (this.typeahead.length > 0) {
this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable)); this.typeahead_view.collection.reset(
this.typeahead.filter(isSearchable)
);
} else { } else {
this.showHints(); this.showHints();
} }
@ -163,4 +171,4 @@
return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/); return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/);
}, },
}); });
}()); })();

View file

@ -120,20 +120,32 @@
this.listenTo(this.model, 'destroy', this.stopListening); this.listenTo(this.model, 'destroy', this.stopListening);
this.listenTo(this.model, 'change:verified', this.onVerifiedChange); this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(this.model, 'change:color', this.updateColor); this.listenTo(this.model, 'change:color', this.updateColor);
this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar); this.listenTo(
this.model,
'change:avatar change:profileAvatar',
this.updateAvatar
);
this.listenTo(this.model, 'newmessage', this.addMessage); this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'delivered', this.updateMessage); this.listenTo(this.model, 'delivered', this.updateMessage);
this.listenTo(this.model, 'read', this.updateMessage); this.listenTo(this.model, 'read', this.updateMessage);
this.listenTo(this.model, 'opened', this.onOpened); this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'expired', this.onExpired); this.listenTo(this.model, 'expired', this.onExpired);
this.listenTo(this.model, 'prune', this.onPrune); this.listenTo(this.model, 'prune', this.onPrune);
this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection); this.listenTo(
this.model.messageCollection,
'expired',
this.onExpiredCollection
);
this.listenTo( this.listenTo(
this.model.messageCollection, this.model.messageCollection,
'scroll-to-message', 'scroll-to-message',
this.scrollToMessage this.scrollToMessage
); );
this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage); this.listenTo(
this.model.messageCollection,
'reply',
this.setQuoteMessage
);
this.lazyUpdateVerified = _.debounce( this.lazyUpdateVerified = _.debounce(
this.model.updateVerified.bind(this.model), this.model.updateVerified.bind(this.model),
@ -247,7 +259,7 @@
return; return;
} }
const oneHourAgo = Date.now() - (60 * 60 * 1000); const oneHourAgo = Date.now() - 60 * 60 * 1000;
if (this.isHidden() && this.lastActivity < oneHourAgo) { if (this.isHidden() && this.lastActivity < oneHourAgo) {
this.unload('inactivity'); this.unload('inactivity');
} else if (this.view.atBottom()) { } else if (this.view.atBottom()) {
@ -301,7 +313,7 @@
this.remove(); this.remove();
this.model.messageCollection.forEach((model) => { this.model.messageCollection.forEach(model => {
model.trigger('unload'); model.trigger('unload');
}); });
this.model.messageCollection.reset([]); this.model.messageCollection.reset([]);
@ -333,19 +345,21 @@
); );
this.model.messageCollection.remove(models); this.model.messageCollection.remove(models);
_.forEach(models, (model) => { _.forEach(models, model => {
model.trigger('unload'); model.trigger('unload');
}); });
}, },
markAllAsVerifiedDefault(unverified) { markAllAsVerifiedDefault(unverified) {
return Promise.all(unverified.map((contact) => { return Promise.all(
unverified.map(contact => {
if (contact.isUnverified()) { if (contact.isUnverified()) {
return contact.setVerifiedDefault(); return contact.setVerifiedDefault();
} }
return null; return null;
})); })
);
}, },
markAllAsApproved(untrusted) { markAllAsApproved(untrusted) {
@ -404,7 +418,10 @@
} }
}, },
toggleMicrophone() { toggleMicrophone() {
if (this.$('.send-message').val().length > 0 || this.fileInput.hasFiles()) { if (
this.$('.send-message').val().length > 0 ||
this.fileInput.hasFiles()
) {
this.$('.capture-audio').hide(); this.$('.capture-audio').hide();
} else { } else {
this.$('.capture-audio').show(); this.$('.capture-audio').show();
@ -495,11 +512,13 @@
const statusPromise = this.throttledGetProfiles(); const statusPromise = this.throttledGetProfiles();
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.statusFetch = statusPromise.then(() => this.model.updateVerified().then(() => { this.statusFetch = statusPromise.then(() =>
this.model.updateVerified().then(() => {
this.onVerifiedChange(); this.onVerifiedChange();
this.statusFetch = null; this.statusFetch = null;
console.log('done with status fetch'); console.log('done with status fetch');
})); })
);
// We schedule our catch-up decrypt right after any in-progress fetch of // We schedule our catch-up decrypt right after any in-progress fetch of
// messages from the database, then ensure that the loading screen is only // messages from the database, then ensure that the loading screen is only
@ -587,20 +606,25 @@
const conversationId = this.model.get('id'); const conversationId = this.model.get('id');
const WhisperMessageCollection = Whisper.MessageCollection; const WhisperMessageCollection = Whisper.MessageCollection;
const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments({ const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments(
{
conversationId, conversationId,
count: DEFAULT_MEDIA_FETCH_COUNT, count: DEFAULT_MEDIA_FETCH_COUNT,
WhisperMessageCollection, WhisperMessageCollection,
}); }
const documents = await Signal.Backbone.Conversation.fetchFileAttachments({ );
const documents = await Signal.Backbone.Conversation.fetchFileAttachments(
{
conversationId, conversationId,
count: DEFAULT_DOCUMENTS_FETCH_COUNT, count: DEFAULT_DOCUMENTS_FETCH_COUNT,
WhisperMessageCollection, WhisperMessageCollection,
}); }
);
// NOTE: Could we show grid previews from disk as well? // NOTE: Could we show grid previews from disk as well?
const loadMessages = Signal.Components.Types.Message const loadMessages = Signal.Components.Types.Message.loadWithObjectURL(
.loadWithObjectURL(Signal.Migrations.loadMessage); Signal.Migrations.loadMessage
);
const media = await loadMessages(rawMedia); const media = await loadMessages(rawMedia);
const { getAbsoluteAttachmentPath } = Signal.Migrations; const { getAbsoluteAttachmentPath } = Signal.Migrations;
@ -624,13 +648,15 @@
case 'media': { case 'media': {
const mediaWithObjectURL = media.map(mediaMessage => const mediaWithObjectURL = media.map(mediaMessage =>
Object.assign( Object.assign({}, mediaMessage, {
{}, objectURL: getAbsoluteAttachmentPath(
mediaMessage, mediaMessage.attachments[0].path
{ objectURL: getAbsoluteAttachmentPath(mediaMessage.attachments[0].path) } ),
)); })
const selectedIndex = media.findIndex(mediaMessage => );
mediaMessage.id === message.id); const selectedIndex = media.findIndex(
mediaMessage => mediaMessage.id === message.id
);
this.lightboxGalleryView = new Whisper.ReactWrapperView({ this.lightboxGalleryView = new Whisper.ReactWrapperView({
Component: Signal.Components.LightboxGallery, Component: Signal.Components.LightboxGallery,
props: { props: {
@ -684,7 +710,7 @@
// We need to iterate here because unseen non-messages do not contribute to // We need to iterate here because unseen non-messages do not contribute to
// the badge number, but should be reflected in the indicator's count. // the badge number, but should be reflected in the indicator's count.
this.model.messageCollection.forEach((model) => { this.model.messageCollection.forEach(model => {
if (!model.get('unread')) { if (!model.get('unread')) {
return; return;
} }
@ -744,7 +770,7 @@
const delta = endingHeight - startingHeight; const delta = endingHeight - startingHeight;
const height = this.view.outerHeight; const height = this.view.outerHeight;
const newScrollPosition = (this.view.scrollPosition + delta) - height; const newScrollPosition = this.view.scrollPosition + delta - height;
this.view.$el.scrollTop(newScrollPosition); this.view.$el.scrollTop(newScrollPosition);
}, 1); }, 1);
}, },
@ -759,15 +785,17 @@
// Avoiding await, since we want to capture the promise and make it available via // Avoiding await, since we want to capture the promise and make it available via
// this.inProgressFetch // this.inProgressFetch
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.inProgressFetch = this.model.fetchContacts() this.inProgressFetch = this.model
.fetchContacts()
.then(() => this.model.fetchMessages()) .then(() => this.model.fetchMessages())
.then(() => { .then(() => {
this.$('.bar-container').hide(); this.$('.bar-container').hide();
this.model.messageCollection.where({ unread: 1 }).forEach((m) => { this.model.messageCollection.where({ unread: 1 }).forEach(m => {
m.fetch(); m.fetch();
}); });
this.inProgressFetch = null; this.inProgressFetch = null;
}).catch((error) => { })
.catch(error => {
console.log( console.log(
'fetchMessages error:', 'fetchMessages error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -820,8 +848,10 @@
// The conversation is visible, but window is not focused // The conversation is visible, but window is not focused
if (!this.lastSeenIndicator) { if (!this.lastSeenIndicator) {
this.resetLastSeenIndicator({ scroll: false }); this.resetLastSeenIndicator({ scroll: false });
} else if (this.view.atBottom() && } else if (
this.model.get('unreadCount') === this.lastSeenIndicator.getCount()) { this.view.atBottom() &&
this.model.get('unreadCount') === this.lastSeenIndicator.getCount()
) {
// The count check ensures that the last seen indicator is still in // The count check ensures that the last seen indicator is still in
// sync with the real number of unread, so we can scroll to it. // sync with the real number of unread, so we can scroll to it.
// We only do this if we're at the bottom, because that signals that // We only do this if we're at the bottom, because that signals that
@ -1215,9 +1245,8 @@
}), }),
}); });
const selector = storage.get('theme-setting') === 'ios' const selector =
? '.bottom-bar' storage.get('theme-setting') === 'ios' ? '.bottom-bar' : '.send';
: '.send';
this.$(selector).prepend(this.quoteView.el); this.$(selector).prepend(this.quoteView.el);
this.updateMessageFieldSize({}); this.updateMessageFieldSize({});
@ -1275,7 +1304,7 @@
}, },
replace_colons(str) { replace_colons(str) {
return str.replace(emoji.rx_colons, (m) => { return str.replace(emoji.rx_colons, m => {
const idx = m.substr(1, m.length - 2); const idx = m.substr(1, m.length - 2);
const val = emoji.map.colons[idx]; const val = emoji.map.colons[idx];
if (val) { if (val) {
@ -1310,7 +1339,12 @@
updateMessageFieldSize(event) { updateMessageFieldSize(event) {
const keyCode = event.which || event.keyCode; const keyCode = event.which || event.keyCode;
if (keyCode === 13 && !event.altKey && !event.shiftKey && !event.ctrlKey) { if (
keyCode === 13 &&
!event.altKey &&
!event.shiftKey &&
!event.ctrlKey
) {
// enter pressed - submit the form now // enter pressed - submit the form now
event.preventDefault(); event.preventDefault();
this.$('.bottom-bar form').submit(); this.$('.bottom-bar form').submit();
@ -1329,7 +1363,8 @@
? this.quoteView.$el.outerHeight(includeMargin) ? this.quoteView.$el.outerHeight(includeMargin)
: 0; : 0;
const height = this.$messageField.outerHeight() + const height =
this.$messageField.outerHeight() +
$attachmentPreviews.outerHeight() + $attachmentPreviews.outerHeight() +
this.$emojiPanelContainer.outerHeight() + this.$emojiPanelContainer.outerHeight() +
quoteHeight + quoteHeight +
@ -1350,8 +1385,10 @@
}, },
isHidden() { isHidden() {
return this.$el.css('display') === 'none' || return (
this.$('.panel').css('display') === 'none'; this.$el.css('display') === 'none' ||
this.$('.panel').css('display') === 'none'
);
}, },
}); });
}()); })();

View file

@ -27,7 +27,7 @@
this.$('textarea').val(i18n('loading')); this.$('textarea').val(i18n('loading'));
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
window.log.fetch().then((text) => { window.log.fetch().then(text => {
this.$('textarea').val(text); this.$('textarea').val(text);
}); });
}, },
@ -63,7 +63,9 @@
}); });
this.$('.loading').removeClass('loading'); this.$('.loading').removeClass('loading');
view.render(); view.render();
this.$('.link').focus().select(); this.$('.link')
.focus()
.select();
}, },
}); });
}()); })();

View file

@ -11,6 +11,6 @@
templateName: 'generic-error', templateName: 'generic-error',
render_attributes: function() { render_attributes: function() {
return this.model; return this.model;
} },
}); });
})(); })();

View file

@ -29,7 +29,7 @@
}); });
function makeImageThumbnail(size, objectUrl) { function makeImageThumbnail(size, objectUrl) {
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const img = document.createElement('img'); const img = document.createElement('img');
img.onerror = reject; img.onerror = reject;
img.onload = () => { img.onload = () => {
@ -60,18 +60,20 @@
resolve(blob); resolve(blob);
}; };
img.src = objectUrl; img.src = objectUrl;
})); });
} }
function makeVideoScreenshot(objectUrl) { function makeVideoScreenshot(objectUrl) {
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const video = document.createElement('video'); const video = document.createElement('video');
function capture() { function capture() {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); canvas
.getContext('2d')
.drawImage(video, 0, 0, canvas.width, canvas.height);
const image = window.dataURLToBlobSync(canvas.toDataURL('image/png')); const image = window.dataURLToBlobSync(canvas.toDataURL('image/png'));
@ -81,7 +83,7 @@
} }
video.addEventListener('canplay', capture); video.addEventListener('canplay', capture);
video.addEventListener('error', (error) => { video.addEventListener('error', error => {
console.log( console.log(
'makeVideoThumbnail error', 'makeVideoThumbnail error',
Signal.Types.Errors.toLogFormat(error) Signal.Types.Errors.toLogFormat(error)
@ -90,7 +92,7 @@
}); });
video.src = objectUrl; video.src = objectUrl;
})); });
} }
function blobToArrayBuffer(blob) { function blobToArrayBuffer(blob) {
@ -123,7 +125,7 @@
className: 'file-input', className: 'file-input',
initialize(options) { initialize(options) {
this.$input = this.$('input[type=file]'); this.$input = this.$('input[type=file]');
this.$input.click((e) => { this.$input.click(e => {
e.stopPropagation(); e.stopPropagation();
}); });
this.thumb = new Whisper.AttachmentPreviewView(); this.thumb = new Whisper.AttachmentPreviewView();
@ -146,15 +148,18 @@
e.preventDefault(); e.preventDefault();
// hack // hack
if (this.window && this.window.chrome && this.window.chrome.fileSystem) { if (this.window && this.window.chrome && this.window.chrome.fileSystem) {
this.window.chrome.fileSystem.chooseEntry({ type: 'openFile' }, (entry) => { this.window.chrome.fileSystem.chooseEntry(
{ type: 'openFile' },
entry => {
if (!entry) { if (!entry) {
return; return;
} }
entry.file((file) => { entry.file(file => {
this.file = file; this.file = file;
this.previewImages(); this.previewImages();
}); });
}); }
);
} else { } else {
this.$input.click(); this.$input.click();
} }
@ -178,14 +183,16 @@
}, },
autoScale(file) { autoScale(file) {
if (file.type.split('/')[0] !== 'image' || if (
file.type.split('/')[0] !== 'image' ||
file.type === 'image/gif' || file.type === 'image/gif' ||
file.type === 'image/tiff') { file.type === 'image/tiff'
) {
// nothing to do // nothing to do
return Promise.resolve(file); return Promise.resolve(file);
} }
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const img = document.createElement('img'); const img = document.createElement('img');
img.onerror = reject; img.onerror = reject;
@ -195,13 +202,19 @@
const maxSize = 6000 * 1024; const maxSize = 6000 * 1024;
const maxHeight = 4096; const maxHeight = 4096;
const maxWidth = 4096; const maxWidth = 4096;
if (img.width <= maxWidth && img.height <= maxHeight && file.size <= maxSize) { if (
img.width <= maxWidth &&
img.height <= maxHeight &&
file.size <= maxSize
) {
resolve(file); resolve(file);
return; return;
} }
const canvas = loadImage.scale(img, { const canvas = loadImage.scale(img, {
canvas: true, maxWidth, maxHeight, canvas: true,
maxWidth,
maxHeight,
}); });
let quality = 0.95; let quality = 0.95;
@ -209,8 +222,10 @@
let blob; let blob;
do { do {
i -= 1; i -= 1;
blob = window.dataURLToBlobSync(canvas.toDataURL('image/jpeg', quality)); blob = window.dataURLToBlobSync(
quality = (quality * maxSize) / blob.size; canvas.toDataURL('image/jpeg', quality)
);
quality = quality * maxSize / blob.size;
// NOTE: During testing with a large image, we observed the // NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]? // `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
@ -222,7 +237,7 @@
resolve(blob); resolve(blob);
}; };
img.src = url; img.src = url;
})); });
}, },
async previewImages() { async previewImages() {
@ -271,21 +286,25 @@
const blob = await this.autoScale(file); const blob = await this.autoScale(file);
let limitKb = 1000000; let limitKb = 1000000;
const blobType = file.type === 'image/gif' const blobType =
? 'gif' file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
: contentType.split('/')[0];
switch (blobType) { switch (blobType) {
case 'image': case 'image':
limitKb = 6000; break; limitKb = 6000;
break;
case 'gif': case 'gif':
limitKb = 25000; break; limitKb = 25000;
break;
case 'audio': case 'audio':
limitKb = 100000; break; limitKb = 100000;
break;
case 'video': case 'video':
limitKb = 100000; break; limitKb = 100000;
break;
default: default:
limitKb = 100000; break; limitKb = 100000;
break;
} }
if ((blob.size / 1024).toFixed(4) >= limitKb) { if ((blob.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB']; const units = ['kB', 'MB', 'GB'];
@ -310,7 +329,9 @@
}, },
getFiles() { getFiles() {
const files = this.file ? [this.file] : Array.from(this.$input.prop('files')); const files = this.file
? [this.file]
: Array.from(this.$input.prop('files'));
const promise = Promise.all(files.map(file => this.getFile(file))); const promise = Promise.all(files.map(file => this.getFile(file)));
this.clearForm(); this.clearForm();
return promise; return promise;
@ -325,7 +346,7 @@
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE ? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
: null; : null;
const setFlags = flags => (attachment) => { const setFlags = flags => attachment => {
const newAttachment = Object.assign({}, attachment); const newAttachment = Object.assign({}, attachment);
if (flags) { if (flags) {
newAttachment.flags = flags; newAttachment.flags = flags;
@ -345,9 +366,11 @@
// Scale and crop an image to 256px square // Scale and crop an image to 256px square
const size = 256; const size = 256;
const file = this.file || this.$input.prop('files')[0]; const file = this.file || this.$input.prop('files')[0];
if (file === undefined || if (
file === undefined ||
file.type.split('/')[0] !== 'image' || file.type.split('/')[0] !== 'image' ||
file.type === 'image/gif') { file.type === 'image/gif'
) {
// nothing to do // nothing to do
return Promise.resolve(); return Promise.resolve();
} }
@ -362,9 +385,9 @@
// File -> Promise Attachment // File -> Promise Attachment
readFile(file) { readFile(file) {
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const FR = new FileReader(); const FR = new FileReader();
FR.onload = (e) => { FR.onload = e => {
resolve({ resolve({
data: e.target.result, data: e.target.result,
contentType: file.type, contentType: file.type,
@ -375,7 +398,7 @@
FR.onerror = reject; FR.onerror = reject;
FR.onabort = reject; FR.onabort = reject;
FR.readAsArrayBuffer(file); FR.readAsArrayBuffer(file);
})); });
}, },
clearForm() { clearForm() {
@ -390,9 +413,14 @@
}, },
deleteFiles(e) { deleteFiles(e) {
if (e) { e.stopPropagation(); } if (e) {
e.stopPropagation();
}
this.clearForm(); this.clearForm();
this.$input.wrap('<form>').parent('form').trigger('reset'); this.$input
.wrap('<form>')
.parent('form')
.trigger('reset');
this.$input.unwrap(); this.$input.unwrap();
this.file = null; this.file = null;
this.$input.trigger('change'); this.$input.trigger('change');
@ -450,4 +478,4 @@
Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail; Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail;
Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail; Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail;
Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot; Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot;
}()); })();

View file

@ -18,8 +18,8 @@
collection: this.model, collection: this.model,
className: 'members', className: 'members',
toInclude: { toInclude: {
listenBack: options.listenBack listenBack: options.listenBack,
} },
}); });
this.member_list_view.render(); this.member_list_view.render();
@ -33,8 +33,8 @@
return { return {
members: i18n('groupMembers'), members: i18n('groupMembers'),
summary: summary summary: summary,
}; };
} },
}); });
})(); })();

View file

@ -7,8 +7,8 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.GroupUpdateView = Backbone.View.extend({ Whisper.GroupUpdateView = Backbone.View.extend({
tagName: "div", tagName: 'div',
className: "group-update", className: 'group-update',
render: function() { render: function() {
//TODO l10n //TODO l10n
if (this.model.left) { if (this.model.left) {
@ -27,7 +27,6 @@
this.$el.text(messages.join(' ')); this.$el.text(messages.join(' '));
return this; return this;
} },
}); });
})(); })();

View file

@ -12,6 +12,6 @@
}, },
render_attributes: function() { render_attributes: function() {
return { content: this.content }; return { content: this.content };
} },
}); });
})(); })();

View file

@ -25,7 +25,9 @@
var img = document.createElement('img'); var img = document.createElement('img');
img.onload = function() { img.onload = function() {
var canvas = loadImage.scale(img, { var canvas = loadImage.scale(img, {
canvas: true, maxWidth: 100, maxHeight: 100 canvas: true,
maxWidth: 100,
maxHeight: 100,
}); });
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
@ -35,7 +37,7 @@
img.src = svgurl; img.src = svgurl;
}); });
} },
}); });
var COLORS = { var COLORS = {
@ -53,7 +55,6 @@
orange: '#FF9800', orange: '#FF9800',
deep_orange: '#FF5722', deep_orange: '#FF5722',
amber: '#FFB300', amber: '#FFB300',
blue_grey : '#607D8B' blue_grey: '#607D8B',
}; };
})(); })();

View file

@ -18,11 +18,11 @@
events: { events: {
'click .show-safety-number': 'showSafetyNumber', 'click .show-safety-number': 'showSafetyNumber',
'click .send-anyway': 'sendAnyway', 'click .send-anyway': 'sendAnyway',
'click .cancel': 'cancel' 'click .cancel': 'cancel',
}, },
showSafetyNumber: function() { showSafetyNumber: function() {
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
}, },
@ -39,13 +39,16 @@
send = i18n('resend'); send = i18n('resend');
} }
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]); var errorExplanation = i18n('identityKeyErrorOnSend', [
this.model.getTitle(),
this.model.getTitle(),
]);
return { return {
errorExplanation: errorExplanation, errorExplanation: errorExplanation,
showSafetyNumber: i18n('showSafetyNumber'), showSafetyNumber: i18n('showSafetyNumber'),
sendAnyway: send, sendAnyway: send,
cancel : i18n('cancel') cancel: i18n('cancel'),
}; };
} },
}); });
})(); })();

View file

@ -36,7 +36,7 @@
}, },
reset: function() { reset: function() {
return Whisper.Database.clear(); return Whisper.Database.clear();
} },
}; };
Whisper.ImportView = Whisper.View.extend({ Whisper.ImportView = Whisper.View.extend({
@ -102,16 +102,19 @@
this.trigger('cancel'); this.trigger('cancel');
}, },
onImport: function() { onImport: function() {
window.Signal.Backup.getDirectoryForImport().then(function(directory) { window.Signal.Backup.getDirectoryForImport().then(
function(directory) {
this.doImport(directory); this.doImport(directory);
}.bind(this), function(error) { }.bind(this),
function(error) {
if (error.name !== 'ChooseError') { if (error.name !== 'ChooseError') {
console.log( console.log(
'Error choosing directory:', 'Error choosing directory:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
}); }
);
}, },
onRegister: function() { onRegister: function() {
// 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
@ -127,15 +130,19 @@
this.render(); this.render();
// Wait for prior database interaction to complete // Wait for prior database interaction to complete
this.pending = this.pending.then(function() { this.pending = this.pending
.then(function() {
// 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(); return Whisper.Import.reset();
}).then(function() { })
.then(function() {
return Promise.all([ return Promise.all([
Whisper.Import.start(), Whisper.Import.start(),
window.Signal.Backup.importFromDirectory(directory) window.Signal.Backup.importFromDirectory(directory),
]); ]);
}).then(function(results) { })
.then(
function(results) {
var importResult = results[1]; var importResult = results[1];
// A full import changes so much we need a restart of the app // A full import changes so much we need a restart of the app
@ -146,34 +153,46 @@
// A light import just brings in contacts, groups, and messages. And we need a // A light import just brings in contacts, groups, and messages. And we need a
// normal link to finish the process. // normal link to finish the process.
return this.finishLightImport(directory); return this.finishLightImport(directory);
}.bind(this)).catch(function(error) { }.bind(this)
console.log('Error importing:', error && error.stack ? error.stack : error); )
.catch(
function(error) {
console.log(
'Error importing:',
error && error.stack ? error.stack : error
);
this.error = error || new Error('Something went wrong!'); this.error = error || new Error('Something went wrong!');
this.state = null; this.state = null;
this.render(); this.render();
return Whisper.Import.reset(); return Whisper.Import.reset();
}.bind(this)); }.bind(this)
);
}, },
finishLightImport: function(directory) { finishLightImport: function(directory) {
ConversationController.reset(); ConversationController.reset();
return ConversationController.load().then(function() { return ConversationController.load()
.then(function() {
return Promise.all([ return Promise.all([
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete(), Whisper.Import.complete(),
]); ]);
}).then(function() { })
.then(
function() {
this.state = State.LIGHT_COMPLETE; this.state = State.LIGHT_COMPLETE;
this.render(); this.render();
}.bind(this)); }.bind(this)
);
}, },
finishFullImport: function(directory) { finishFullImport: function(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.fetch() return storage
.fetch()
.then(function() { .then(function() {
return Promise.all([ return Promise.all([
// Clearing any migration-related state inherited from the Chrome App // Clearing any migration-related state inherited from the Chrome App
@ -183,12 +202,15 @@
storage.remove('migrationStorageLocation'), storage.remove('migrationStorageLocation'),
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete() Whisper.Import.complete(),
]); ]);
}).then(function() { })
.then(
function() {
this.state = State.COMPLETE; this.state = State.COMPLETE;
this.render(); this.render();
}.bind(this)); }.bind(this)
} );
},
}); });
})(); })();

View file

@ -15,7 +15,10 @@
open(conversation) { open(conversation) {
const id = `conversation-${conversation.cid}`; const id = `conversation-${conversation.cid}`;
if (id !== this.el.firstChild.id) { if (id !== this.el.firstChild.id) {
this.$el.first().find('video, audio').each(function pauseMedia() { this.$el
.first()
.find('video, audio')
.each(function pauseMedia() {
this.pause(); this.pause();
}); });
let $el = this.$(`#${id}`); let $el = this.$(`#${id}`);
@ -65,7 +68,6 @@
}, },
}); });
Whisper.AppLoadingScreen = Whisper.View.extend({ Whisper.AppLoadingScreen = Whisper.View.extend({
templateName: 'app-loading-screen', templateName: 'app-loading-screen',
className: 'app-loading-screen', className: 'app-loading-screen',
@ -147,7 +149,8 @@
); );
this.networkStatusView = new Whisper.NetworkStatusView(); this.networkStatusView = new Whisper.NetworkStatusView();
this.$el.find('.network-status-container') this.$el
.find('.network-status-container')
.append(this.networkStatusView.render().el); .append(this.networkStatusView.render().el);
extension.windows.onClosed(() => { extension.windows.onClosed(() => {
@ -194,7 +197,8 @@
default: default:
console.log( console.log(
'Whisper.InboxView::startConnectionListener:', 'Whisper.InboxView::startConnectionListener:',
'Unknown web socket status:', status 'Unknown web socket status:',
status
); );
break; break;
} }
@ -254,7 +258,9 @@
openConversation(e, conversation) { openConversation(e, conversation) {
this.searchView.hideHints(); this.searchView.hideHints();
if (conversation) { if (conversation) {
this.conversation_stack.open(ConversationController.get(conversation.id)); this.conversation_stack.open(
ConversationController.get(conversation.id)
);
this.focusConversation(); this.focusConversation();
} }
}, },
@ -279,4 +285,4 @@
}; };
}, },
}); });
}()); })();

View file

@ -34,23 +34,24 @@
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
// Keep data around if it's a re-link, or the middle of a light import // Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData; this.shouldRetainData =
Whisper.Registration.everDone() || options.hasExistingData;
}, },
render_attributes: function() { render_attributes: function() {
var errorMessage; var errorMessage;
if (this.error) { if (this.error) {
if (this.error.name === 'HTTPError' if (
&& this.error.code == TOO_MANY_DEVICES) { this.error.name === 'HTTPError' &&
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') {
// AccountManager.registerSecondDevice uses this specific // AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message // 'websocket closed' error message
errorMessage = i18n('installConnectionFailed'); errorMessage = i18n('installConnectionFailed');
@ -95,10 +96,12 @@
var accountManager = getAccountManager(); var accountManager = getAccountManager();
accountManager.registerSecondDevice( accountManager
.registerSecondDevice(
this.setProvisioningUrl.bind(this), this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this) this.confirmNumber.bind(this)
).catch(this.handleDisconnect.bind(this)); )
.catch(this.handleDisconnect.bind(this));
}, },
handleDisconnect: function(e) { handleDisconnect: function(e) {
console.log('provisioning failed', e.stack); console.log('provisioning failed', e.stack);
@ -108,9 +111,10 @@
if (e.message === 'websocket closed') { if (e.message === 'websocket closed') {
this.trigger('disconnected'); this.trigger('disconnected');
} else if (e.name !== 'HTTPError' } else if (
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) { e.name !== 'HTTPError' ||
(e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
) {
throw e; throw e;
} }
}, },
@ -155,8 +159,10 @@
this.selectStep(Steps.ENTER_NAME); this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault(); this.setDeviceNameDefault();
return new Promise(function(resolve, reject) { return new Promise(
this.$('#link-phone').submit(function(e) { function(resolve, reject) {
this.$('#link-phone').submit(
function(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -189,8 +195,10 @@
); );
finish(); finish();
}); });
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
}); });
})(); })();

View file

@ -17,15 +17,15 @@
this.theirKey = options.newKey; this.theirKey = options.newKey;
} }
this.loadKeys().then(function() { this.loadKeys().then(
function() {
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}.bind(this)); }.bind(this)
);
}, },
loadKeys: function() { loadKeys: function() {
return Promise.all([ return Promise.all([this.loadTheirKey(), this.loadOurKey()])
this.loadTheirKey(), .then(this.generateSecurityNumber.bind(this))
this.loadOurKey(),
]).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));
}, },
@ -37,32 +37,37 @@
); );
}, },
loadTheirKey: function() { loadTheirKey: function() {
return textsecure.storage.protocol.loadIdentityKey( return textsecure.storage.protocol.loadIdentityKey(this.model.id).then(
this.model.id function(theirKey) {
).then(function(theirKey) {
this.theirKey = theirKey; this.theirKey = theirKey;
}.bind(this)); }.bind(this)
);
}, },
loadOurKey: function() { loadOurKey: function() {
return textsecure.storage.protocol.loadIdentityKey( return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then(
this.ourNumber function(ourKey) {
).then(function(ourKey) {
this.ourKey = ourKey; this.ourKey = ourKey;
}.bind(this)); }.bind(this)
);
}, },
generateSecurityNumber: function() { generateSecurityNumber: function() {
return new libsignal.FingerprintGenerator(5200).createFor( return new libsignal.FingerprintGenerator(5200)
this.ourNumber, this.ourKey, this.model.id, this.theirKey .createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
).then(function(securityNumber) { .then(
function(securityNumber) {
this.securityNumber = securityNumber; this.securityNumber = securityNumber;
}.bind(this)); }.bind(this)
);
}, },
onSafetyNumberChanged: function() { onSafetyNumberChanged: function() {
this.model.getProfiles().then(this.loadKeys.bind(this)); this.model.getProfiles().then(this.loadKeys.bind(this));
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
message: i18n('changedRightAfterVerify', [this.model.getTitle(), this.model.getTitle()]), message: i18n('changedRightAfterVerify', [
hideCancel: true this.model.getTitle(),
this.model.getTitle(),
]),
hideCancel: true,
}); });
dialog.$el.insertBefore(this.el); dialog.$el.insertBefore(this.el);
@ -70,7 +75,10 @@
}, },
toggleVerified: function() { toggleVerified: function() {
this.$('button.verify').attr('disabled', true); this.$('button.verify').attr('disabled', true);
this.model.toggleVerified().catch(function(result) { this.model
.toggleVerified()
.catch(
function(result) {
if (result instanceof Error) { if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') { if (result.name === 'OutgoingIdentityKeyError') {
this.onSafetyNumberChanged(); this.onSafetyNumberChanged();
@ -89,9 +97,13 @@
}); });
} }
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
this.$('button.verify').removeAttr('disabled'); this.$('button.verify').removeAttr('disabled');
}.bind(this)); }.bind(this)
);
}, },
render_attributes: function() { render_attributes: function() {
var s = this.securityNumber; var s = this.securityNumber;
@ -103,19 +115,24 @@
var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name); var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name);
var isVerified = this.model.isVerified(); var isVerified = this.model.isVerified();
var verifyButton = isVerified ? i18n('unverify') : i18n('verify'); var verifyButton = isVerified ? i18n('unverify') : i18n('verify');
var verifiedStatus = isVerified ? i18n('isVerified', name) : i18n('isNotVerified', name); var verifiedStatus = isVerified
? i18n('isVerified', name)
: i18n('isNotVerified', name);
return { return {
learnMore: i18n('learnMore'), learnMore: i18n('learnMore'),
theirKeyUnknown: i18n('theirIdentityUnknown'), theirKeyUnknown: i18n('theirIdentityUnknown'),
yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()), yourSafetyNumberWith: i18n(
'yourSafetyNumberWith',
this.model.getTitle()
),
verifyHelp: i18n('verifyHelp', this.model.getTitle()), verifyHelp: i18n('verifyHelp', this.model.getTitle()),
verifyButton: verifyButton, verifyButton: verifyButton,
hasTheirKey: this.theirKey !== undefined, hasTheirKey: this.theirKey !== undefined,
chunks: chunks, chunks: chunks,
isVerified: isVerified, isVerified: isVerified,
verifiedStatus : verifiedStatus verifiedStatus: verifiedStatus,
}; };
} },
}); });
})(); })();

View file

@ -25,12 +25,14 @@
}, },
render_attributes: function() { render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage') var unreadMessages =
this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]); : i18n('unreadMessages', [this.count]);
return { return {
unreadMessages: unreadMessages unreadMessages: unreadMessages,
}; };
} },
}); });
})(); })();

View file

@ -35,6 +35,6 @@
render: function() { render: function() {
this.addAll(); this.addAll();
return this; return this;
} },
}); });
})(); })();

View file

@ -25,14 +25,14 @@
}); });
}, },
events: { events: {
'click': 'onClick' click: 'onClick',
}, },
onClick: function() { onClick: function() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({ var view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model, model: this.model,
listenBack: this.listenBack, listenBack: this.listenBack,
resetPanel: this.resetPanel resetPanel: this.resetPanel,
}); });
this.listenTo(view, 'send-anyway', this.onSendAnyway); this.listenTo(view, 'send-anyway', this.onSendAnyway);
@ -44,19 +44,32 @@
} }
}, },
forceSend: function() { forceSend: function() {
this.model.updateVerified().then(function() { this.model
.updateVerified()
.then(
function() {
if (this.model.isUnverified()) { if (this.model.isUnverified()) {
return this.model.setVerifiedDefault(); return this.model.setVerifiedDefault();
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
return this.model.isUntrusted(); return this.model.isUntrusted();
}.bind(this)).then(function(untrusted) { }.bind(this)
)
.then(
function(untrusted) {
if (untrusted) { if (untrusted) {
return this.model.setApproved(); return this.model.setApproved();
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
this.message.resend(this.outgoingKeyError.number); this.message.resend(this.outgoingKeyError.number);
}.bind(this)); }.bind(this)
);
}, },
onSendAnyway: function() { onSendAnyway: function() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
@ -72,9 +85,9 @@
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
errors: this.errors, errors: this.errors,
showErrorButton: showButton, showErrorButton: showButton,
errorButtonLabel : i18n('view') errorButtonLabel: i18n('view'),
}; };
} },
}); });
Whisper.MessageDetailView = Whisper.View.extend({ Whisper.MessageDetailView = Whisper.View.extend({
@ -91,7 +104,7 @@
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
events: { events: {
'click button.delete': 'onDelete' 'click button.delete': 'onDelete',
}, },
onDelete: function() { onDelete: function() {
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
@ -100,7 +113,7 @@
resolve: function() { resolve: function() {
this.model.destroy(); this.model.destroy();
this.resetPanel(); this.resetPanel();
}.bind(this) }.bind(this),
}); });
this.$el.prepend(dialog.el); this.$el.prepend(dialog.el);
@ -119,9 +132,11 @@
ids = this.conversation.getRecipients(); ids = this.conversation.getRecipients();
} }
} }
return Promise.all(ids.map(function(number) { return Promise.all(
ids.map(function(number) {
return ConversationController.getOrCreateAndWait(number, 'private'); return ConversationController.getOrCreateAndWait(number, 'private');
})); })
);
}, },
renderContact: function(contact) { renderContact: function(contact) {
var view = new ContactView({ var view = new ContactView({
@ -129,18 +144,23 @@
errors: this.grouped[contact.id], errors: this.grouped[contact.id],
listenBack: this.listenBack, listenBack: this.listenBack,
resetPanel: this.resetPanel, resetPanel: this.resetPanel,
message: this.model message: this.model,
}).render(); }).render();
this.$('.contacts').append(view.el); this.$('.contacts').append(view.el);
}, },
render: function() { render: function() {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) { var errorsWithoutNumber = _.reject(this.model.get('errors'), function(
error
) {
return Boolean(error.number); return Boolean(error.number);
}); });
this.$el.html(Mustache.render(_.result(this, 'template', ''), { this.$el.html(
Mustache.render(_.result(this, 'template', ''), {
sent_at: moment(this.model.get('sent_at')).format('LLLL'), sent_at: moment(this.model.get('sent_at')).format('LLLL'),
received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null, received_at: this.model.isIncoming()
? moment(this.model.get('received_at')).format('LLLL')
: null,
tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'), tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'),
errors: errorsWithoutNumber, errors: errorsWithoutNumber,
title: i18n('messageDetail'), title: i18n('messageDetail'),
@ -148,21 +168,26 @@
received: i18n('received'), received: i18n('received'),
errorLabel: i18n('error'), errorLabel: i18n('error'),
deleteLabel: i18n('deleteMessage'), deleteLabel: i18n('deleteMessage'),
retryDescription: i18n('retryDescription') retryDescription: i18n('retryDescription'),
})); })
);
this.view.$el.prependTo(this.$('.message-container')); this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number'); this.grouped = _.groupBy(this.model.get('errors'), 'number');
this.getContacts().then(function(contacts) { this.getContacts().then(
_.sortBy(contacts, function(c) { function(contacts) {
_.sortBy(
contacts,
function(c) {
var prefix = this.grouped[c.id] ? '0' : '1'; var prefix = this.grouped[c.id] ? '0' : '1';
// this prefix ensures that contacts with errors are listed first; // this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical // otherwise it's alphabetical
return prefix + c.getTitle(); return prefix + c.getTitle();
}.bind(this)).forEach(this.renderContact.bind(this)); }.bind(this)
}.bind(this)); ).forEach(this.renderContact.bind(this));
} }.bind(this)
);
},
}); });
})(); })();

View file

@ -10,14 +10,17 @@
className: 'message-list', className: 'message-list',
itemView: Whisper.MessageView, itemView: Whisper.MessageView,
events: { events: {
'scroll': 'onScroll', scroll: 'onScroll',
}, },
initialize: function() { initialize: function() {
Whisper.ListView.prototype.initialize.call(this); Whisper.ListView.prototype.initialize.call(this);
this.triggerLazyScroll = _.debounce(function() { this.triggerLazyScroll = _.debounce(
function() {
this.$el.trigger('lazyScroll'); this.$el.trigger('lazyScroll');
}.bind(this), 500); }.bind(this),
500
);
}, },
onScroll: function() { onScroll: function() {
this.measureScrollPosition(); this.measureScrollPosition();
@ -36,7 +39,8 @@
return this.bottomOffset < 30; return this.bottomOffset < 30;
}, },
measureScrollPosition: function() { measureScrollPosition: function() {
if (this.el.scrollHeight === 0) { // hidden if (this.el.scrollHeight === 0) {
// hidden
return; return;
} }
this.outerHeight = this.$el.outerHeight(); this.outerHeight = this.$el.outerHeight();

View file

@ -71,7 +71,10 @@
const elapsed = (totalTime - remainingTime) / totalTime; const elapsed = (totalTime - remainingTime) / totalTime;
this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`); this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`);
this.$el.css('display', 'inline-block'); this.$el.css('display', 'inline-block');
this.timeout = setTimeout(this.update.bind(this), Math.max(totalTime / 100, 500)); this.timeout = setTimeout(
this.update.bind(this),
Math.max(totalTime / 100, 500)
);
} }
return this; return this;
}, },
@ -195,9 +198,17 @@
this.listenTo(this.model, 'change:body', this.render); this.listenTo(this.model, 'change:body', this.render);
this.listenTo(this.model, 'change:delivered', this.renderDelivered); this.listenTo(this.model, 'change:delivered', this.renderDelivered);
this.listenTo(this.model, 'change:read_by', this.renderRead); this.listenTo(this.model, 'change:read_by', this.renderRead);
this.listenTo(this.model, 'change:expirationStartTimestamp', this.renderExpiring); this.listenTo(
this.model,
'change:expirationStartTimestamp',
this.renderExpiring
);
this.listenTo(this.model, 'change', this.onChange); this.listenTo(this.model, 'change', this.onChange);
this.listenTo(this.model, 'change:flags change:group_update', this.renderControl); this.listenTo(
this.model,
'change:flags change:group_update',
this.renderControl
);
this.listenTo(this.model, 'destroy', this.onDestroy); this.listenTo(this.model, 'destroy', this.onDestroy);
this.listenTo(this.model, 'unload', this.onUnload); this.listenTo(this.model, 'unload', this.onUnload);
this.listenTo(this.model, 'expired', this.onExpired); this.listenTo(this.model, 'expired', this.onExpired);
@ -225,7 +236,7 @@
this.model.get('errors'), this.model.get('errors'),
this.model.isReplayableError.bind(this.model) this.model.isReplayableError.bind(this.model)
); );
_.map(retrys, 'number').forEach((number) => { _.map(retrys, 'number').forEach(number => {
this.model.resend(number); this.model.resend(number);
}); });
}, },
@ -251,7 +262,7 @@
}, },
onExpired() { onExpired() {
this.$el.addClass('expired'); this.$el.addClass('expired');
this.$el.find('.bubble').one('webkitAnimationEnd animationend', (e) => { this.$el.find('.bubble').one('webkitAnimationEnd animationend', e => {
if (e.target === this.$('.bubble')[0]) { if (e.target === this.$('.bubble')[0]) {
this.remove(); this.remove();
} }
@ -284,8 +295,9 @@
// as our tests rely on `onUnload` synchronously removing the view from // as our tests rely on `onUnload` synchronously removing the view from
// the DOM. // the DOM.
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.loadAttachmentViews() this.loadAttachmentViews().then(views =>
.then(views => views.forEach(view => view.unload())); views.forEach(view => view.unload())
);
// No need to handle this one, since it listens to 'unload' itself: // No need to handle this one, since it listens to 'unload' itself:
// this.timerView // this.timerView
@ -321,7 +333,9 @@
} }
}, },
renderDelivered() { renderDelivered() {
if (this.model.get('delivered')) { this.$el.addClass('delivered'); } if (this.model.get('delivered')) {
this.$el.addClass('delivered');
}
}, },
renderRead() { renderRead() {
if (!_.isEmpty(this.model.get('read_by'))) { if (!_.isEmpty(this.model.get('read_by'))) {
@ -345,7 +359,9 @@
} }
if (_.size(errors) > 0) { if (_.size(errors) > 0) {
if (this.model.isIncoming()) { if (this.model.isIncoming()) {
this.$('.content').text(this.model.getDescription()).addClass('error-message'); this.$('.content')
.text(this.model.getDescription())
.addClass('error-message');
} }
this.errorIconView = new ErrorIconView({ model: errors[0] }); this.errorIconView = new ErrorIconView({ model: errors[0] });
this.errorIconView.render().$el.appendTo(this.$('.bubble')); this.errorIconView.render().$el.appendTo(this.$('.bubble'));
@ -354,7 +370,9 @@
if (!el || el.length === 0) { if (!el || el.length === 0) {
this.$('.inner-bubble').append("<div class='content'></div>"); this.$('.inner-bubble').append("<div class='content'></div>");
} }
this.$('.content').text(i18n('noContents')).addClass('error-message'); this.$('.content')
.text(i18n('noContents'))
.addClass('error-message');
} }
this.$('.meta .hasRetry').remove(); this.$('.meta .hasRetry').remove();
@ -461,18 +479,24 @@
const hasAttachments = attachments && attachments.length > 0; const hasAttachments = attachments && attachments.length > 0;
const hasBody = this.hasTextContents(); const hasBody = this.hasTextContents();
this.$el.html(Mustache.render(_.result(this, 'template', ''), { this.$el.html(
Mustache.render(
_.result(this, 'template', ''),
{
message: this.model.get('body'), message: this.model.get('body'),
hasBody, hasBody,
timestamp: this.model.get('sent_at'), timestamp: this.model.get('sent_at'),
sender: (contact && contact.getTitle()) || '', sender: (contact && contact.getTitle()) || '',
avatar: (contact && contact.getAvatar()), avatar: contact && contact.getAvatar(),
profileName: (contact && contact.getProfileName()), profileName: contact && contact.getProfileName(),
innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail', innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail',
hoverIcon: !hasErrors, hoverIcon: !hasErrors,
hasAttachments, hasAttachments,
reply: i18n('replyToMessage'), reply: i18n('replyToMessage'),
}, this.render_partials())); },
this.render_partials()
)
);
this.timeStampView.setElement(this.$('.timestamp')); this.timeStampView.setElement(this.$('.timestamp'));
this.timeStampView.update(); this.timeStampView.update();
@ -498,7 +522,9 @@
// as our code / Backbone seems to rely on `render` synchronously returning // as our code / Backbone seems to rely on `render` synchronously returning
// `this` instead of `Promise MessageView` (this): // `this` instead of `Promise MessageView` (this):
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.loadAttachmentViews().then(views => this.renderAttachmentViews(views)); this.loadAttachmentViews().then(views =>
this.renderAttachmentViews(views)
);
return this; return this;
}, },
@ -523,8 +549,10 @@
} }
const attachments = this.model.get('attachments') || []; const attachments = this.model.get('attachments') || [];
const loadedAttachmentViews = Promise.all(attachments.map(attachment => const loadedAttachmentViews = Promise.all(
new Promise(async (resolve) => { attachments.map(
attachment =>
new Promise(async resolve => {
const attachmentWithData = await loadAttachmentData(attachment); const attachmentWithData = await loadAttachmentData(attachment);
const view = new Whisper.AttachmentView({ const view = new Whisper.AttachmentView({
model: attachmentWithData, model: attachmentWithData,
@ -538,7 +566,9 @@
}); });
view.render(); view.render();
}))); })
)
);
// Memoize attachment views to avoid double loading: // Memoize attachment views to avoid double loading:
this.loadedAttachmentViews = loadedAttachmentViews; this.loadedAttachmentViews = loadedAttachmentViews;
@ -550,8 +580,10 @@
}, },
renderAttachmentView(view) { renderAttachmentView(view) {
if (!view.updated) { if (!view.updated) {
throw new Error('Invariant violation:' + throw new Error(
' Cannot render an attachment view that isnt ready'); 'Invariant violation:' +
' Cannot render an attachment view that isnt ready'
);
} }
const parent = this.$('.attachments')[0]; const parent = this.$('.attachments')[0];
@ -570,4 +602,4 @@
this.trigger('afterChangeHeight'); this.trigger('afterChangeHeight');
}, },
}); });
}()); })();

View file

@ -10,9 +10,11 @@
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(function () { extension.windows.onClosed(
function() {
clearInterval(this.renderIntervalHandle); clearInterval(this.renderIntervalHandle);
}.bind(this)); }.bind(this)
);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
@ -34,10 +36,13 @@
setSocketReconnectInterval: function(millis) { setSocketReconnectInterval: function(millis) {
this.socketReconnectWaitDuration = moment.duration(millis); this.socketReconnectWaitDuration = moment.duration(millis);
}, },
navigatorOnLine: function() { return navigator.onLine; }, navigatorOnLine: function() {
getSocketStatus: function() { return window.getSocketStatus(); }, return navigator.onLine;
},
getSocketStatus: function() {
return window.getSocketStatus();
},
getNetworkStatus: function() { getNetworkStatus: function() {
var message = ''; var message = '';
var instructions = ''; var instructions = '';
var hasInterruption = false; var hasInterruption = false;
@ -65,11 +70,16 @@
break; break;
} }
if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) { if (
socketStatus == WebSocket.CONNECTING &&
!this.withinConnectingGracePeriod
) {
hasInterruption = true; hasInterruption = true;
} }
if (this.socketReconnectWaitDuration.asSeconds() > 0) { if (this.socketReconnectWaitDuration.asSeconds() > 0) {
instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]); instructions = i18n('attemptingReconnection', [
this.socketReconnectWaitDuration.asSeconds(),
]);
} }
if (!this.navigatorOnLine()) { if (!this.navigatorOnLine()) {
hasInterruption = true; hasInterruption = true;
@ -88,7 +98,7 @@
instructions: instructions, instructions: instructions,
hasInterruption: hasInterruption, hasInterruption: hasInterruption,
action: action, action: action,
buttonClass: buttonClass buttonClass: buttonClass,
}; };
}, },
update: function() { update: function() {
@ -102,13 +112,9 @@
this.render(); this.render();
if (this.model.attributes.hasInterruption) { if (this.model.attributes.hasInterruption) {
this.$el.slideDown(); this.$el.slideDown();
} } else {
else {
this.$el.hide(); this.$el.hide();
} }
} },
}); });
})(); })();

View file

@ -6,29 +6,31 @@
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: function(options) {
this.render(); this.render();
this.avatarInput = new Whisper.FileInputView({ this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'), el: this.$('.group-avatar'),
window: options.window window: options.window,
}); });
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', function() {
this.model.contactCollection.models.forEach(function(model) { this.model.contactCollection.models.forEach(
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)); }.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({
collection: this.model.contactCollection, collection: this.model.contactCollection,
className: 'members' className: 'members',
}); });
this.member_list_view.render(); this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el); this.$('.scrollable').append(this.member_list_view.el);
@ -51,17 +53,21 @@
render_attributes: function() { render_attributes: function() {
return { return {
name: this.model.getTitle(), name: this.model.getTitle(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
}, },
send: function() { send: function() {
return this.avatarInput.getThumbnail().then(function(avatarFile) { return this.avatarInput.getThumbnail().then(
function(avatarFile) {
var now = Date.now(); var now = Date.now();
var attrs = { var attrs = {
timestamp: now, timestamp: now,
active_at: now, active_at: now,
name: this.$('.name').val(), name: this.$('.name').val(),
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id')) members: _.union(
this.model.get('members'),
this.recipients_view.recipients.pluck('id')
),
}; };
if (avatarFile) { if (avatarFile) {
attrs.avatar = avatarFile; attrs.avatar = avatarFile;
@ -76,7 +82,8 @@
this.model.updateGroup(group_update); this.model.updateGroup(group_update);
this.goBack(); this.goBack();
}.bind(this)); }.bind(this)
} );
},
}); });
})(); })();

View file

@ -13,12 +13,14 @@
this.$('input.number').intlTelInput(); this.$('input.number').intlTelInput();
}, },
events: { events: {
'change': 'validateNumber', change: 'validateNumber',
'keyup': 'validateNumber' keyup: 'validateNumber',
}, },
validateNumber: function() { validateNumber: function() {
var input = this.$('input.number'); var input = this.$('input.number');
var regionCode = this.$('li.active').attr('data-country-code').toUpperCase(); var regionCode = this.$('li.active')
.attr('data-country-code')
.toUpperCase();
var number = input.val(); var number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); var parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
@ -31,6 +33,6 @@
input.trigger('validation'); input.trigger('validation');
return parsedNumber.e164; return parsedNumber.e164;
} },
}); });
})(); })();

View file

@ -44,4 +44,4 @@
Backbone.View.prototype.remove.call(this); Backbone.View.prototype.remove.call(this);
}, },
}); });
}()); })();

View file

@ -10,21 +10,21 @@
'name', 'name',
'e164_number', 'e164_number',
'national_number', 'national_number',
'international_number' 'international_number',
], ],
database: Whisper.Database, database: Whisper.Database,
storeName: 'conversations', storeName: 'conversations',
model: Whisper.Conversation, model: Whisper.Conversation,
fetchContacts: function() { fetchContacts: function() {
return this.fetch({ reset: true, conditions: { type: 'private' } }); return this.fetch({ reset: true, conditions: { type: 'private' } });
} },
}); });
Whisper.ContactPillView = Whisper.View.extend({ Whisper.ContactPillView = Whisper.View.extend({
tagName: 'span', tagName: 'span',
className: 'recipient', className: 'recipient',
events: { events: {
'click .remove': 'removeModel' 'click .remove': 'removeModel',
}, },
templateName: 'contact_pill', templateName: 'contact_pill',
initialize: function() { initialize: function() {
@ -39,11 +39,11 @@
}, },
render_attributes: function() { render_attributes: function() {
return { name: this.model.getTitle() }; return { name: this.model.getTitle() };
} },
}); });
Whisper.RecipientListView = Whisper.ListView.extend({ Whisper.RecipientListView = Whisper.ListView.extend({
itemView: Whisper.ContactPillView itemView: Whisper.ContactPillView,
}); });
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
@ -52,7 +52,7 @@
}); });
Whisper.SuggestionListView = Whisper.ConversationListView.extend({ Whisper.SuggestionListView = Whisper.ConversationListView.extend({
itemView: Whisper.SuggestionView itemView: Whisper.SuggestionView,
}); });
Whisper.RecipientsInputView = Whisper.View.extend({ Whisper.RecipientsInputView = Whisper.View.extend({
@ -68,13 +68,13 @@
// Collection of recipients selected for the new message // Collection of recipients selected for the new message
this.recipients = new Whisper.ConversationCollection([], { this.recipients = new Whisper.ConversationCollection([], {
comparator: false comparator: false,
}); });
// View to display the selected recipients // View to display the selected recipients
this.recipients_view = new Whisper.RecipientListView({ this.recipients_view = new Whisper.RecipientListView({
collection: this.recipients, collection: this.recipients,
el: this.$('.recipients') el: this.$('.recipients'),
}); });
// Collection of contacts to match user input against // Collection of contacts to match user input against
@ -84,17 +84,18 @@
// 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) { return m.getTitle().toLowerCase(); } comparator: function(m) {
}) return m.getTitle().toLowerCase();
},
}),
}); });
this.$('.contacts').append(this.typeahead_view.el); this.$('.contacts').append(this.typeahead_view.el);
this.initNewContact(); this.initNewContact();
this.listenTo(this.typeahead, 'reset', this.filterContacts); this.listenTo(this.typeahead, 'reset', this.filterContacts);
}, },
render_attributes: function() { render_attributes: function() {
return { placeholder: this.placeholder || "name or phone number" }; return { placeholder: this.placeholder || 'name or phone number' };
}, },
events: { events: {
@ -113,9 +114,7 @@
} else { } else {
this.new_contact_view.$el.hide(); this.new_contact_view.$el.hide();
} }
this.typeahead_view.collection.reset( this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
this.typeahead.typeahead(query)
);
} else { } else {
this.resetTypeahead(); this.resetTypeahead();
} }
@ -131,8 +130,8 @@
el: this.$new_contact, el: this.$new_contact,
model: ConversationController.create({ model: ConversationController.create({
type: 'private', type: 'private',
newContact: true newContact: true,
}) }),
}).render(); }).render();
}, },
@ -176,10 +175,8 @@
this.typeahead_view.collection.reset([]); this.typeahead_view.collection.reset([]);
}, },
maybeNumber: function(number) { maybeNumber: function(number) {
return number.match(/^\+?[0-9]*$/); return number.match(/^\+?[0-9]*$/);
} },
}); });
})(); })();

View file

@ -16,7 +16,7 @@
events: { events: {
'click .close': 'close', 'click .close': 'close',
'click .finish': 'finish', 'click .finish': 'finish',
'close': 'close' close: 'close',
}, },
updateTime: function() { updateTime: function() {
var duration = moment.duration(Date.now() - this.startTime, 'ms'); var duration = moment.duration(Date.now() - this.startTime, 'ms');
@ -62,19 +62,23 @@
this.input = this.context.createGain(); this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, { this.recorder = new WebAudioRecorder(this.input, {
encoding: 'mp3', encoding: 'mp3',
workerDir: 'js/' // must end with slash workerDir: 'js/', // must end with slash
}); });
this.recorder.onComplete = this.handleBlob.bind(this); this.recorder.onComplete = this.handleBlob.bind(this);
this.recorder.onError = this.onError; this.recorder.onError = this.onError;
navigator.webkitGetUserMedia({ audio: true }, function(stream) { navigator.webkitGetUserMedia(
{ audio: true },
function(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)); }.bind(this),
this.onError.bind(this)
);
this.recorder.startRecording(); this.recorder.startRecording();
}, },
onError: function(error) { onError: function(error) {
console.log(error.stack); console.log(error.stack);
this.close(); this.close();
} },
}); });
})(); })();

View file

@ -32,8 +32,8 @@
return { return {
cssClass: cssClass, cssClass: cssClass,
moreBelow: moreBelow moreBelow: moreBelow,
}; };
} },
}); });
})(); })();

View file

@ -20,7 +20,7 @@
this.populate(); this.populate();
}, },
events: { events: {
'change': 'change' change: 'change',
}, },
change: function(e) { change: function(e) {
var value = e.target.checked; var value = e.target.checked;
@ -43,7 +43,7 @@
this.populate(); this.populate();
}, },
events: { events: {
'change': 'change' change: 'change',
}, },
change: function(e) { change: function(e) {
var value = this.$(e.target).val(); var value = this.$(e.target).val();
@ -67,26 +67,26 @@
new RadioButtonGroupView({ new RadioButtonGroupView({
el: this.$('.notification-settings'), el: this.$('.notification-settings'),
defaultValue: 'message', defaultValue: 'message',
name: 'notification-setting' name: 'notification-setting',
}); });
new RadioButtonGroupView({ new RadioButtonGroupView({
el: this.$('.theme-settings'), el: this.$('.theme-settings'),
defaultValue: 'android', defaultValue: 'android',
name: 'theme-setting', name: 'theme-setting',
event: 'change-theme' event: 'change-theme',
}); });
if (Settings.isAudioNotificationSupported()) { if (Settings.isAudioNotificationSupported()) {
new CheckboxView({ new CheckboxView({
el: this.$('.audio-notification-setting'), el: this.$('.audio-notification-setting'),
defaultValue: false, defaultValue: false,
name: 'audio-notification' name: 'audio-notification',
}); });
} }
new CheckboxView({ new CheckboxView({
el: this.$('.menu-bar-setting'), el: this.$('.menu-bar-setting'),
defaultValue: false, defaultValue: false,
name: 'hide-menu-bar', name: 'hide-menu-bar',
event: 'change-hide-menu' event: 'change-hide-menu',
}); });
if (textsecure.storage.user.getDeviceId() != '1') { if (textsecure.storage.user.getDeviceId() != '1') {
var syncView = new SyncView().render(); var syncView = new SyncView().render();
@ -160,10 +160,7 @@
}, },
async clearAllData() { async clearAllData() {
try { try {
await Promise.all([ await Promise.all([Logs.deleteAll(), Database.drop()]);
Logs.deleteAll(),
Database.drop(),
]);
} catch (error) { } catch (error) {
console.log( console.log(
'Something went wrong deleting all data:', 'Something went wrong deleting all data:',
@ -193,7 +190,7 @@
templateName: 'syncSettings', templateName: 'syncSettings',
className: 'syncSettings', className: 'syncSettings',
events: { events: {
'click .sync': 'sync' 'click .sync': 'sync',
}, },
enable: function() { enable: function() {
this.$('.sync').text(i18n('syncNow')); this.$('.sync').text(i18n('syncNow'));
@ -223,7 +220,7 @@
syncRequest.addEventListener('success', this.onsuccess.bind(this)); syncRequest.addEventListener('success', this.onsuccess.bind(this));
syncRequest.addEventListener('timeout', this.ontimeout.bind(this)); syncRequest.addEventListener('timeout', this.ontimeout.bind(this));
} else { } else {
console.log("Tried to sync from device 1"); console.log('Tried to sync from device 1');
} }
}, },
render_attributes: function() { render_attributes: function() {
@ -231,7 +228,7 @@
sync: i18n('sync'), sync: i18n('sync'),
syncNow: i18n('syncNow'), syncNow: i18n('syncNow'),
syncExplanation: i18n('syncExplanation'), syncExplanation: i18n('syncExplanation'),
syncFailed: i18n('syncFailed') syncFailed: i18n('syncFailed'),
}; };
var date = storage.get('synced_at'); var date = storage.get('synced_at');
if (date) { if (date) {
@ -241,6 +238,6 @@
attrs.syncTime = date.toLocaleTimeString(); attrs.syncTime = date.toLocaleTimeString();
} }
return attrs; return attrs;
} },
}); });
})(); })();

View file

@ -17,7 +17,9 @@
if (number) { if (number) {
this.$('input.number').val(number); this.$('input.number').val(number);
} }
this.phoneView = new Whisper.PhoneInputView({el: this.$('#phone-number-input')}); this.phoneView = new Whisper.PhoneInputView({
el: this.$('#phone-number-input'),
});
this.$('#error').hide(); this.$('#error').hide();
}, },
events: { events: {
@ -29,24 +31,37 @@
}, },
verifyCode: function(e) { verifyCode: function(e) {
var number = this.phoneView.validateNumber(); var number = this.phoneView.validateNumber();
var verificationCode = $('#code').val().replace(/\D+/g, ''); var verificationCode = $('#code')
.val()
.replace(/\D+/g, '');
this.accountManager.registerSingleDevice(number, verificationCode).then(function() { this.accountManager
.registerSingleDevice(number, verificationCode)
.then(
function() {
this.$el.trigger('openInbox'); this.$el.trigger('openInbox');
}.bind(this)).catch(this.log.bind(this)); }.bind(this)
)
.catch(this.log.bind(this));
}, },
log: function(s) { log: function(s) {
console.log(s); console.log(s);
this.$('#status').text(s); this.$('#status').text(s);
}, },
validateCode: function() { validateCode: function() {
var verificationCode = $('#code').val().replace(/\D/g, ''); var verificationCode = $('#code')
.val()
.replace(/\D/g, '');
if (verificationCode.length == 6) { if (verificationCode.length == 6) {
return verificationCode; return verificationCode;
} }
}, },
displayError: function(error) { displayError: function(error) {
this.$('#error').hide().text(error).addClass('in').fadeIn(); this.$('#error')
.hide()
.text(error)
.addClass('in')
.fadeIn();
}, },
onValidation: function() { onValidation: function() {
if (this.$('#number-container').hasClass('valid')) { if (this.$('#number-container').hasClass('valid')) {
@ -67,8 +82,12 @@
this.$('#error').hide(); this.$('#error').hide();
var number = this.phoneView.validateNumber(); var number = this.phoneView.validateNumber();
if (number) { if (number) {
this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this)); this.accountManager
this.$('#step2').addClass('in').fadeIn(); .requestVoiceVerification(number)
.catch(this.displayError.bind(this));
this.$('#step2')
.addClass('in')
.fadeIn();
} else { } else {
this.$('#number-container').addClass('invalid'); this.$('#number-container').addClass('invalid');
} }
@ -78,11 +97,15 @@
$('#error').hide(); $('#error').hide();
var number = this.phoneView.validateNumber(); var number = this.phoneView.validateNumber();
if (number) { if (number) {
this.accountManager.requestSMSVerification(number).catch(this.displayError.bind(this)); this.accountManager
this.$('#step2').addClass('in').fadeIn(); .requestSMSVerification(number)
.catch(this.displayError.bind(this));
this.$('#step2')
.addClass('in')
.fadeIn();
} else { } else {
this.$('#number-container').addClass('invalid'); this.$('#number-container').addClass('invalid');
} }
} },
}); });
})(); })();

View file

@ -13,7 +13,7 @@
this.clearTimeout(); this.clearTimeout();
var millis_now = Date.now(); var millis_now = Date.now();
var millis = this.$el.data('timestamp'); var millis = this.$el.data('timestamp');
if (millis === "") { if (millis === '') {
return; return;
} }
if (millis >= millis_now) { if (millis >= millis_now) {
@ -27,7 +27,9 @@
var millis_since = millis_now - millis; var millis_since = millis_now - millis;
if (this.delay) { if (this.delay) {
if (this.delay < 0) { this.delay = 1000; } if (this.delay < 0) {
this.delay = 1000;
}
this.timeout = setTimeout(this.update.bind(this), this.delay); this.timeout = setTimeout(this.update.bind(this), this.delay);
} }
}, },
@ -47,22 +49,34 @@
this.delay = null; this.delay = null;
return timestamp.format(this._format.M); return timestamp.format(this._format.M);
} else if (timediff.days() > 0) { } else if (timediff.days() > 0) {
this.delay = moment(timestamp).add(timediff.days() + 1,'d').diff(now); this.delay = moment(timestamp)
.add(timediff.days() + 1, 'd')
.diff(now);
return timestamp.format(this._format.d); return timestamp.format(this._format.d);
} else if (timediff.hours() > 1) { } else if (timediff.hours() > 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now); this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h'); return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.hours() === 1) { } else if (timediff.hours() === 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now); this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h'); return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.minutes() > 1) { } else if (timediff.minutes() > 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now); this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm'); return this.relativeTime(timediff.minutes(), 'm');
} else if (timediff.minutes() === 1) { } else if (timediff.minutes() === 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now); this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm'); return this.relativeTime(timediff.minutes(), 'm');
} else { } else {
this.delay = moment(timestamp).add(1,'m').diff(now); this.delay = moment(timestamp)
.add(1, 'm')
.diff(now);
return this.relativeTime(timediff.seconds(), 's'); return this.relativeTime(timediff.seconds(), 's');
} }
}, },
@ -70,19 +84,19 @@
return moment.duration(number, string).humanize(); return moment.duration(number, string).humanize();
}, },
_format: { _format: {
y: "ll", y: 'll',
M: i18n('timestampFormat_M') || "MMM D", M: i18n('timestampFormat_M') || 'MMM D',
d: "ddd" d: 'ddd',
} },
}); });
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({ Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
relativeTime: function(number, string, isFuture) { relativeTime: function(number, string, isFuture) {
return moment.duration(-1 * number, string).humanize(string !== 's'); return moment.duration(-1 * number, string).humanize(string !== 's');
}, },
_format: { _format: {
y: "lll", y: 'lll',
M: (i18n('timestampFormat_M') || "MMM D") + ' LT', M: (i18n('timestampFormat_M') || 'MMM D') + ' LT',
d: "ddd LT" d: 'ddd LT',
} },
}); });
})(); })();

View file

@ -17,12 +17,14 @@
}, },
render: function() { render: function() {
this.$el.html(Mustache.render( this.$el.html(
Mustache.render(
_.result(this, 'template', ''), _.result(this, 'template', ''),
_.result(this, 'render_attributes', '') _.result(this, 'render_attributes', '')
)); )
);
this.$el.show(); this.$el.show();
setTimeout(this.close.bind(this), 2000); setTimeout(this.close.bind(this), 2000);
} },
}); });
})(); })();

View file

@ -23,7 +23,8 @@
'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: function() {
Backbone.View.apply(this, arguments); Backbone.View.apply(this, arguments);
Mustache.parse(_.result(this, 'template')); Mustache.parse(_.result(this, 'template'));
@ -48,24 +49,28 @@
return this; return this;
}, },
confirm: function(message, okText) { confirm: function(message, okText) {
return new Promise(function(resolve, reject) { return new Promise(
function(resolve, reject) {
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
message: message, message: message,
okText: okText, okText: okText,
resolve: resolve, resolve: resolve,
reject: reject reject: reject,
}); });
this.$el.append(dialog.el); this.$el.append(dialog.el);
}.bind(this)); }.bind(this)
);
}, },
i18n_with_links: function() { i18n_with_links: function() {
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
for (var i = 1; i < args.length; ++i) { for (var i = 1; i < args.length; ++i) {
args[i] = 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"'; args[i] =
'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
} }
return i18n(args[0], args.slice(1)); return i18n(args[0], args.slice(1));
} },
},{ },
{
// Class attributes // Class attributes
Templates: (function() { Templates: (function() {
var templates = {}; var templates = {};
@ -75,6 +80,7 @@
templates[id] = $el.html(); templates[id] = $el.html();
}); });
return templates; return templates;
}()) })(),
}); }
);
})(); })();

View file

@ -2,7 +2,7 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
;(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -11,7 +11,7 @@
var events; var events;
function checkTime() { function checkTime() {
var currentTime = Date.now(); var currentTime = Date.now();
if (currentTime > (lastTime + interval * 2)) { if (currentTime > lastTime + interval * 2) {
events.trigger('timetravel'); events.trigger('timetravel');
} }
lastTime = currentTime; lastTime = currentTime;
@ -22,6 +22,6 @@
events = _events; events = _events;
lastTime = Date.now(); lastTime = Date.now();
setInterval(checkTime, interval); setInterval(checkTime, interval);
} },
}; };
}()); })();

115
main.js
View file

@ -6,13 +6,7 @@ const _ = require('lodash');
const electron = require('electron'); const electron = require('electron');
const semver = require('semver'); const semver = require('semver');
const { const { BrowserWindow, app, Menu, shell, ipcMain: ipc } = electron;
BrowserWindow,
app,
Menu,
shell,
ipcMain: ipc,
} = electron;
const packageJson = require('./package.json'); const packageJson = require('./package.json');
@ -27,7 +21,9 @@ const { createTemplate } = require('./app/menu');
GlobalErrors.addHandler(); GlobalErrors.addHandler();
const appUserModelId = `org.whispersystems.${packageJson.name}`; const appUserModelId = `org.whispersystems.${packageJson.name}`;
console.log('Set Windows Application User Model ID (AUMID)', { appUserModelId }); console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId,
});
app.setAppUserModelId(appUserModelId); app.setAppUserModelId(appUserModelId);
// Keep a global reference of the window object, if you don't, the window will // Keep a global reference of the window object, if you don't, the window will
@ -41,13 +37,13 @@ function getMainWindow() {
// Tray icon and related objects // Tray icon and related objects
let tray = null; let tray = null;
const startInTray = process.argv.some(arg => arg === '--start-in-tray'); const startInTray = process.argv.some(arg => arg === '--start-in-tray');
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon'); const usingTrayIcon =
startInTray || process.argv.some(arg => arg === '--use-tray-icon');
const config = require('./app/config'); const config = require('./app/config');
const importMode = process.argv.some(arg => arg === '--import') || config.get('import'); const importMode =
process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development'; const development = config.environment === 'development';
@ -107,7 +103,12 @@ const WINDOWS_8 = '8.0.0';
const osRelease = os.release(); const osRelease = os.release();
const polyfillNotifications = const polyfillNotifications =
os.platform() === 'win32' && semver.lt(osRelease, WINDOWS_8); os.platform() === 'win32' && semver.lt(osRelease, WINDOWS_8);
console.log('OS Release:', osRelease, '- notifications polyfill?', polyfillNotifications); console.log(
'OS Release:',
osRelease,
'- notifications polyfill?',
polyfillNotifications
);
function prepareURL(pathSegments) { function prepareURL(pathSegments) {
return url.format({ return url.format({
@ -146,7 +147,6 @@ function captureClicks(window) {
window.webContents.on('new-window', handleUrl); window.webContents.on('new-window', handleUrl);
} }
const DEFAULT_WIDTH = 800; const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 610; const DEFAULT_HEIGHT = 610;
const MIN_WIDTH = 640; const MIN_WIDTH = 640;
@ -160,22 +160,28 @@ function isVisible(window, bounds) {
const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT; const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT;
// requiring BOUNDS_BUFFER pixels on the left or right side // requiring BOUNDS_BUFFER pixels on the left or right side
const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER); const rightSideClearOfLeftBound =
const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER); window.x + window.width >= boundsX + BOUNDS_BUFFER;
const leftSideClearOfRightBound =
window.x <= boundsX + boundsWidth - BOUNDS_BUFFER;
// top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom // top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
const topClearOfUpperBound = window.y >= boundsY; const topClearOfUpperBound = window.y >= boundsY;
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER); const topClearOfLowerBound =
window.y <= boundsY + boundsHeight - BOUNDS_BUFFER;
return rightSideClearOfLeftBound && return (
rightSideClearOfLeftBound &&
leftSideClearOfRightBound && leftSideClearOfRightBound &&
topClearOfUpperBound && topClearOfUpperBound &&
topClearOfLowerBound; topClearOfLowerBound
);
} }
function createWindow() { function createWindow() {
const { screen } = electron; const { screen } = electron;
const windowOptions = Object.assign({ const windowOptions = Object.assign(
{
show: !startInTray, // allow to start minimised in tray show: !startInTray, // allow to start minimised in tray
width: DEFAULT_WIDTH, width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT, height: DEFAULT_HEIGHT,
@ -188,7 +194,16 @@ function createWindow() {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
}, },
icon: path.join(__dirname, 'images', 'icon_256.png'), icon: path.join(__dirname, 'images', 'icon_256.png'),
}, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y'])); },
_.pick(windowConfig, [
'maximized',
'autoHideMenuBar',
'width',
'height',
'x',
'y',
])
);
if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) { if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
windowOptions.width = DEFAULT_WIDTH; windowOptions.width = DEFAULT_WIDTH;
@ -203,7 +218,7 @@ function createWindow() {
delete windowOptions.autoHideMenuBar; delete windowOptions.autoHideMenuBar;
} }
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => { const visibleOnAnyScreen = _.some(screen.getAllDisplays(), display => {
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) { if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
return false; return false;
} }
@ -220,7 +235,10 @@ function createWindow() {
delete windowOptions.fullscreen; delete windowOptions.fullscreen;
} }
logger.info('Initializing BrowserWindow config: %s', JSON.stringify(windowOptions)); logger.info(
'Initializing BrowserWindow config: %s',
JSON.stringify(windowOptions)
);
// Create the browser window. // Create the browser window.
mainWindow = new BrowserWindow(windowOptions); mainWindow = new BrowserWindow(windowOptions);
@ -249,7 +267,10 @@ function createWindow() {
windowConfig.fullscreen = true; windowConfig.fullscreen = true;
} }
logger.info('Updating BrowserWindow config: %s', JSON.stringify(windowConfig)); logger.info(
'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig)
);
userConfig.set('window', windowConfig); userConfig.set('window', windowConfig);
} }
@ -263,7 +284,7 @@ function createWindow() {
}); });
// Ingested in preload.js via a sendSync call // Ingested in preload.js via a sendSync call
ipc.on('locale-data', (event) => { ipc.on('locale-data', event => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
event.returnValue = locale.messages; event.returnValue = locale.messages;
}); });
@ -271,7 +292,9 @@ function createWindow() {
if (config.environment === 'test') { if (config.environment === 'test') {
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html'])); mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html']));
} else if (config.environment === 'test-lib') { } else if (config.environment === 'test-lib') {
mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'])); mainWindow.loadURL(
prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'])
);
} else { } else {
mainWindow.loadURL(prepareURL([__dirname, 'background.html'])); mainWindow.loadURL(prepareURL([__dirname, 'background.html']));
} }
@ -283,16 +306,19 @@ function createWindow() {
captureClicks(mainWindow); captureClicks(mainWindow);
mainWindow.webContents.on('will-navigate', (e) => { mainWindow.webContents.on('will-navigate', e => {
logger.info('will-navigate'); logger.info('will-navigate');
e.preventDefault(); e.preventDefault();
}); });
// Emitted when the window is about to be closed. // Emitted when the window is about to be closed.
mainWindow.on('close', (e) => { mainWindow.on('close', e => {
// If the application is terminating, just do the default // If the application is terminating, just do the default
if (windowState.shouldQuit() || if (
config.environment === 'test' || config.environment === 'test-lib') { windowState.shouldQuit() ||
config.environment === 'test' ||
config.environment === 'test-lib'
) {
return; return;
} }
@ -337,7 +363,9 @@ function showSettings() {
} }
function openReleaseNotes() { function openReleaseNotes() {
shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`); shell.openExternal(
`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`
);
} }
function openNewBugForm() { function openNewBugForm() {
@ -345,7 +373,9 @@ function openNewBugForm() {
} }
function openSupportPage() { function openSupportPage() {
shell.openExternal('https://support.signal.org/hc/en-us/categories/202319038-Desktop'); shell.openExternal(
'https://support.signal.org/hc/en-us/categories/202319038-Desktop'
);
} }
function openForums() { function openForums() {
@ -370,7 +400,6 @@ function setupAsStandalone() {
} }
} }
let aboutWindow; let aboutWindow;
function showAbout() { function showAbout() {
if (aboutWindow) { if (aboutWindow) {
@ -416,9 +445,12 @@ app.on('ready', () => {
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`: // NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
let loggingSetupError; let loggingSetupError;
logging.initialize().catch((error) => { logging
.initialize()
.catch(error => {
loggingSetupError = error; loggingSetupError = error;
}).then(async () => { })
.then(async () => {
/* eslint-enable more/no-then */ /* eslint-enable more/no-then */
logger = logging.getLogger(); logger = logging.getLogger();
logger.info('app ready'); logger.info('app ready');
@ -428,7 +460,8 @@ app.on('ready', () => {
} }
if (!locale) { if (!locale) {
const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale(); const appLocale =
process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
locale = loadLocale({ appLocale, logger }); locale = loadLocale({ appLocale, logger });
} }
@ -472,7 +505,6 @@ function setupMenu(options) {
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} }
app.on('before-quit', () => { app.on('before-quit', () => {
windowState.markShouldQuit(); windowState.markShouldQuit();
}); });
@ -481,9 +513,11 @@ app.on('before-quit', () => {
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar // On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q // to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin' || if (
process.platform !== 'darwin' ||
config.environment === 'test' || config.environment === 'test' ||
config.environment === 'test-lib') { config.environment === 'test-lib'
) {
app.quit(); app.quit();
} }
}); });
@ -504,7 +538,7 @@ app.on('activate', () => {
// Defense in depth. We never intend to open webviews, so this prevents it completely. // Defense in depth. We never intend to open webviews, so this prevents it completely.
app.on('web-contents-created', (createEvent, win) => { app.on('web-contents-created', (createEvent, win) => {
win.on('will-attach-webview', (attachEvent) => { win.on('will-attach-webview', attachEvent => {
attachEvent.preventDefault(); attachEvent.preventDefault();
}); });
}); });
@ -523,7 +557,6 @@ ipc.on('add-setup-menu-items', () => {
}); });
}); });
ipc.on('draw-attention', () => { ipc.on('draw-attention', () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.dock.bounce(); app.dock.bounce();

View file

@ -12,7 +12,6 @@ const { deferredToPromise } = require('./js/modules/deferred_to_promise');
const { app } = electron.remote; const { app } = electron.remote;
window.PROTO_ROOT = 'protos'; window.PROTO_ROOT = 'protos';
window.config = require('url').parse(window.location.toString(), true).query; window.config = require('url').parse(window.location.toString(), true).query;
@ -21,8 +20,7 @@ window.wrapDeferred = deferredToPromise;
const ipc = electron.ipcRenderer; const ipc = electron.ipcRenderer;
window.config.localeMessages = ipc.sendSync('locale-data'); window.config.localeMessages = ipc.sendSync('locale-data');
window.setBadgeCount = count => window.setBadgeCount = count => ipc.send('set-badge-count', count);
ipc.send('set-badge-count', count);
window.drawAttention = () => { window.drawAttention = () => {
console.log('draw attention'); console.log('draw attention');
@ -44,8 +42,7 @@ window.restart = () => {
ipc.send('restart'); ipc.send('restart');
}; };
window.closeAbout = () => window.closeAbout = () => ipc.send('close-about');
ipc.send('close-about');
window.updateTrayIcon = unreadCount => window.updateTrayIcon = unreadCount =>
ipc.send('update-tray-icon', unreadCount); ipc.send('update-tray-icon', unreadCount);
@ -70,11 +67,9 @@ ipc.on('show-settings', () => {
Whisper.events.trigger('showSettings'); Whisper.events.trigger('showSettings');
}); });
window.addSetupMenuItems = () => window.addSetupMenuItems = () => ipc.send('add-setup-menu-items');
ipc.send('add-setup-menu-items');
window.removeSetupMenuItems = () => window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items');
ipc.send('remove-setup-menu-items');
// We pull these dependencies in now, from here, because they have Node.js dependencies // We pull these dependencies in now, from here, because they have Node.js dependencies
@ -101,8 +96,7 @@ window.emojiData = require('emoji-datasource');
window.EmojiPanel = require('emoji-panel'); window.EmojiPanel = require('emoji-panel');
window.filesize = require('filesize'); window.filesize = require('filesize');
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance(); window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
window.libphonenumber.PhoneNumberFormat = window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
require('google-libphonenumber').PhoneNumberFormat;
window.loadImage = require('blueimp-load-image'); window.loadImage = require('blueimp-load-image');
window.nodeBuffer = Buffer; window.nodeBuffer = Buffer;
@ -136,11 +130,15 @@ window.moment.locale(locale);
// ES2015+ modules // ES2015+ modules
const attachmentsPath = Attachments.getPath(app.getPath('userData')); const attachmentsPath = Attachments.getPath(app.getPath('userData'));
const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(attachmentsPath); const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(
attachmentsPath
);
const deleteAttachmentData = Attachments.createDeleter(attachmentsPath); const deleteAttachmentData = Attachments.createDeleter(attachmentsPath);
const readAttachmentData = Attachments.createReader(attachmentsPath); const readAttachmentData = Attachments.createReader(attachmentsPath);
const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath); const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath);
const writeExistingAttachmentData = Attachments.createWriterForExisting(attachmentsPath); const writeExistingAttachmentData = Attachments.createWriterForExisting(
attachmentsPath
);
const loadAttachmentData = Attachment.loadData(readAttachmentData); const loadAttachmentData = Attachment.loadData(readAttachmentData);
@ -151,8 +149,9 @@ const upgradeSchemaContext = {
const upgradeMessageSchema = message => const upgradeMessageSchema = message =>
Message.upgradeSchema(message, upgradeSchemaContext); Message.upgradeSchema(message, upgradeSchemaContext);
const { getPlaceholderMigrations } = const {
require('./js/modules/migrations/get_placeholder_migrations'); getPlaceholderMigrations,
} = require('./js/modules/migrations/get_placeholder_migrations');
const { IdleDetector } = require('./js/modules/idle_detector'); const { IdleDetector } = require('./js/modules/idle_detector');
window.Signal = {}; window.Signal = {};
@ -167,12 +166,12 @@ window.Signal.Logs = require('./js/modules/logs');
// React components // React components
const { Lightbox } = require('./ts/components/Lightbox'); const { Lightbox } = require('./ts/components/Lightbox');
const { LightboxGallery } = require('./ts/components/LightboxGallery'); const { LightboxGallery } = require('./ts/components/LightboxGallery');
const { MediaGallery } = const {
require('./ts/components/conversation/media-gallery/MediaGallery'); MediaGallery,
} = require('./ts/components/conversation/media-gallery/MediaGallery');
const { Quote } = require('./ts/components/conversation/Quote'); const { Quote } = require('./ts/components/conversation/Quote');
const MediaGalleryMessage = const MediaGalleryMessage = require('./ts/components/conversation/media-gallery/types/Message');
require('./ts/components/conversation/media-gallery/types/Message');
window.Signal.Components = { window.Signal.Components = {
Lightbox, Lightbox,
@ -185,18 +184,20 @@ window.Signal.Components = {
}; };
window.Signal.Migrations = {}; window.Signal.Migrations = {};
window.Signal.Migrations.deleteAttachmentData = window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData(
Attachment.deleteData(deleteAttachmentData); deleteAttachmentData
);
window.Signal.Migrations.getPlaceholderMigrations = getPlaceholderMigrations; window.Signal.Migrations.getPlaceholderMigrations = getPlaceholderMigrations;
window.Signal.Migrations.writeMessageAttachments = window.Signal.Migrations.writeMessageAttachments = Message.createAttachmentDataWriter(
Message.createAttachmentDataWriter(writeExistingAttachmentData); writeExistingAttachmentData
);
window.Signal.Migrations.getAbsoluteAttachmentPath = getAbsoluteAttachmentPath; window.Signal.Migrations.getAbsoluteAttachmentPath = getAbsoluteAttachmentPath;
window.Signal.Migrations.loadAttachmentData = loadAttachmentData; window.Signal.Migrations.loadAttachmentData = loadAttachmentData;
window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(loadAttachmentData); window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(
window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = loadAttachmentData
require('./js/modules/migrations/migrations_0_database_with_attachment_data'); );
window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = require('./js/modules/migrations/migrations_0_database_with_attachment_data');
require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = require('./js/modules/migrations/migrations_1_database_without_attachment_data');
window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema; window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema;
window.Signal.OS = require('./js/modules/os'); window.Signal.OS = require('./js/modules/os');
@ -218,8 +219,7 @@ window.Signal.Views.Initialization = require('./js/modules/views/initialization'
window.Signal.Workflow = {}; window.Signal.Workflow = {};
window.Signal.Workflow.IdleDetector = IdleDetector; window.Signal.Workflow.IdleDetector = IdleDetector;
window.Signal.Workflow.MessageDataMigrator = window.Signal.Workflow.MessageDataMigrator = require('./js/modules/messages_data_migrator');
require('./js/modules/messages_data_migrator');
// We pull this in last, because the native module involved appears to be sensitive to // We pull this in last, because the native module involved appears to be sensitive to
// /tmp mounted as noexec on Linux. // /tmp mounted as noexec on Linux.

View file

@ -3,7 +3,6 @@ const _ = require('lodash');
const packageJson = require('./package.json'); const packageJson = require('./package.json');
const { version } = packageJson; const { version } = packageJson;
const beta = /beta/; const beta = /beta/;
@ -37,7 +36,6 @@ const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass';
const PRODUCTION_STARTUP_WM_CLASS = 'Signal'; const PRODUCTION_STARTUP_WM_CLASS = 'Signal';
const BETA_STARTUP_WM_CLASS = 'Signal Beta'; const BETA_STARTUP_WM_CLASS = 'Signal Beta';
// ------- // -------
function checkValue(object, objectPath, expected) { function checkValue(object, objectPath, expected) {

View file

@ -56,5 +56,8 @@ _.set(packageJson, WIN_ASSET_PATH, WIN_ASSET_END_VALUE);
// --- // ---
fs.writeFileSync('./config/default.json', JSON.stringify(defaultConfig, null, ' ')); fs.writeFileSync(
'./config/default.json',
JSON.stringify(defaultConfig, null, ' ')
);
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));

View file

@ -2,7 +2,6 @@ const webpack = require('webpack');
const path = require('path'); const path = require('path');
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;
module.exports = { module.exports = {
@ -37,9 +36,7 @@ module.exports = {
// Exposes necessary utilities in the global scope for all readme code snippets // Exposes necessary utilities in the global scope for all readme code snippets
util: 'ts/styleguide/StyleGuideUtil', util: 'ts/styleguide/StyleGuideUtil',
}, },
contextDependencies: [ contextDependencies: [path.join(__dirname, 'ts/styleguide')],
path.join(__dirname, 'ts/styleguide'),
],
// We don't want one long, single page // We don't want one long, single page
pagePerSection: true, pagePerSection: true,
// Expose entire repository to the styleguidist server, primarily for stylesheets // Expose entire repository to the styleguidist server, primarily for stylesheets
@ -49,11 +46,13 @@ module.exports = {
// https://react-styleguidist.js.org/docs/configuration.html#template // https://react-styleguidist.js.org/docs/configuration.html#template
template: { template: {
head: { head: {
links: [{ links: [
{
rel: 'stylesheet', rel: 'stylesheet',
type: 'text/css', type: 'text/css',
href: '/stylesheets/manifest.css', href: '/stylesheets/manifest.css',
}], },
],
}, },
body: { body: {
// Brings in all the necessary components to boostrap Backbone views // Brings in all the necessary components to boostrap Backbone views
@ -157,10 +156,7 @@ module.exports = {
resolve: { resolve: {
// Necessary to enable the absolute path used in the context option above // Necessary to enable the absolute path used in the context option above
modules: [ modules: [__dirname, path.join(__dirname, 'node_modules')],
__dirname,
path.join(__dirname, 'node_modules'),
],
extensions: ['.tsx'], extensions: ['.tsx'],
}, },
@ -168,7 +164,7 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: 'ts-loader' loader: 'ts-loader',
}, },
{ {
// To test handling of attachments, we need arraybuffers in memory // To test handling of attachments, we need arraybuffers in memory

View file

@ -7,7 +7,7 @@ module.exports = {
}, },
globals: { globals: {
assert: true assert: true,
}, },
parserOptions: { parserOptions: {
@ -16,11 +16,14 @@ module.exports = {
rules: { rules: {
// We still get the value of this rule, it just allows for dev deps // We still get the value of this rule, it just allows for dev deps
'import/no-extraneous-dependencies': ['error', { 'import/no-extraneous-dependencies': [
devDependencies: true 'error',
}], {
devDependencies: true,
},
],
// We want to keep each test structured the same, even if its contents are tiny // We want to keep each test structured the same, even if its contents are tiny
'arrow-body-style': 'off', 'arrow-body-style': 'off',
} },
}; };

View file

@ -27,7 +27,7 @@ window.PROTO_ROOT = '../protos';
result: false, result: false,
message: err.message, message: err.message,
stack: err.stack, stack: err.stack,
titles: flattenTitles(test) titles: flattenTitles(test),
}); });
}); });
@ -37,7 +37,7 @@ window.PROTO_ROOT = '../protos';
SauceReporter.prototype = OriginalReporter.prototype; SauceReporter.prototype = OriginalReporter.prototype;
mocha.reporter(SauceReporter); mocha.reporter(SauceReporter);
}()); })();
// Override the database id. // Override the database id.
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -49,7 +49,7 @@ Whisper.Database.id = 'test';
*/ */
function assertEqualArrayBuffers(ab1, ab2) { function assertEqualArrayBuffers(ab1, ab2) {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
}; }
function hexToArrayBuffer(str) { function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2); var ret = new ArrayBuffer(str.length / 2);
@ -58,12 +58,14 @@ function hexToArrayBuffer(str) {
array[i] = parseInt(str.substr(i * 2, 2), 16); array[i] = parseInt(str.substr(i * 2, 2), 16);
} }
return ret; return ret;
}; }
/* Delete the database before running any tests */ /* Delete the database before running any tests */
before(function(done) { before(function(done) {
var idbReq = indexedDB.deleteDatabase('test'); var idbReq = indexedDB.deleteDatabase('test');
idbReq.onsuccess = function() { done(); }; idbReq.onsuccess = function() {
done();
};
}); });
async function clearDatabase(done) { async function clearDatabase(done) {
@ -80,5 +82,5 @@ async function clearDatabase(done) {
await messages.destroyAll(); await messages.destroyAll();
if (done) { if (done) {
done(); done();
}; }
} }

Some files were not shown because too many files have changed in this diff Show more