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,135 +132,157 @@ 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', {
dest: 'js/Mp3LameEncoder.min.js' src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js',
}, { dest: 'js/Mp3LameEncoder.min.js',
src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js', },
dest: 'js/WebAudioRecorderMp3.js' {
}, { src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js',
src: 'components/jquery/dist/jquery.js', dest: 'js/WebAudioRecorderMp3.js',
dest: 'js/jquery.js' },
}], {
src: 'components/jquery/dist/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: {
src: [ src: [
'Gruntfile', 'Gruntfile',
'js/**/*.js', 'js/**/*.js',
'!js/components.js', '!js/components.js',
'!js/libsignal-protocol-worker.js', '!js/libsignal-protocol-worker.js',
'!js/libtextsecure.js', '!js/libtextsecure.js',
'!js/modules/**/*.js', '!js/modules/**/*.js',
'!js/models/conversations.js', '!js/models/conversations.js',
'!js/models/messages.js', '!js/models/messages.js',
'!js/views/conversation_search_view.js', '!js/views/conversation_search_view.js',
'!js/views/conversation_view.js', '!js/views/conversation_view.js',
'!js/views/debug_log_view.js', '!js/views/debug_log_view.js',
'!js/views/file_input_view.js', '!js/views/file_input_view.js',
'!js/views/message_view.js', '!js/views/message_view.js',
'!js/Mp3LameEncoder.min.js', '!js/Mp3LameEncoder.min.js',
'!js/WebAudioRecorderMp3.js', '!js/WebAudioRecorderMp3.js',
'test/**/*.js', 'test/**/*.js',
'!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);
} }
}); });
// Transifex does not understand placeholders, so this task patches all non-en // Transifex does not understand placeholders, so this task patches all non-en
// locales with missing placeholders // locales with missing placeholders
grunt.registerTask('locale-patch', function(){ grunt.registerTask('locale-patch', 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(
if (subdir === 'en' || filename !== 'messages.json'){ abspath,
rootdir,
subdir,
filename
) {
if (subdir === 'en' || filename !== 'messages.json') {
return; return;
} }
var messages = grunt.file.readJSON(abspath); var messages = grunt.file.readJSON(abspath);
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;
} }
} }
@ -269,12 +293,14 @@ module.exports = function(grunt) {
}); });
grunt.registerTask('getExpireTime', function() { grunt.registerTask('getExpireTime', function() {
grunt.task.requires('gitinfo'); grunt.task.requires('gitinfo');
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,51 +316,62 @@ 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', {
extractedTo: 'linux' zip: packageJson.name + '-' + packageJson.version + '.zip',
}]; 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(
var key = [ keyBase, sha, 'dist', item.zip].join('/'); files.map(function(item) {
var url = [urlBase, key].join('/'); var key = [keyBase, sha, 'dist', item.zip].join('/');
var dest = 'release/' + item.zip; var url = [urlBase, key].join('/');
return new Promise(function(resolve) { var dest = 'release/' + item.zip;
console.log(url); return new Promise(function(resolve) {
download(url, dest, item.extractedTo, function(err) { console.log(url);
if (err) { download(url, dest, item.extractedTo, function(err) {
console.log('failed', dest, err); if (err) {
resolve(err); console.log('failed', dest, err);
} else { resolve(err);
console.log('done', dest); } else {
resolve(); console.log('done', dest);
} resolve();
}
});
}); });
}); })
})).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,65 +384,83 @@ 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()
return app.client.execute(getMochaResults).then(function(data) { .then(function() {
return Boolean(data.value); return app.client.waitUntil(
}); function() {
}, 10000, 'Expected to find window.mochaResults set!'); return app.client.execute(getMochaResults).then(function(data) {
}).then(function() { return Boolean(data.value);
return app.client.execute(getMochaResults); });
}).then(function(data) { },
var results = data.value; 10000,
if (results.failures > 0) { 'Expected to find window.mochaResults set!'
console.error(results.reports); );
})
.then(function() {
return app.client.execute(getMochaResults);
})
.then(function(data) {
var results = data.value;
if (results.failures > 0) {
console.error(results.reports);
failure = function() {
grunt.fail.fatal(
'Found ' + results.failures + ' failing unit tests.'
);
};
return app.client.log('browser');
} else {
grunt.log.ok(results.passes + ' tests passed.');
}
})
.then(function(logs) {
if (logs) {
console.error();
console.error('Because tests failed, printing browser logs:');
console.error(logs);
}
})
.catch(function(error) {
failure = function() { failure = function() {
grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.'); grunt.fail.fatal(
'Something went wrong: ' + error.message + ' ' + error.stack
);
}; };
return app.client.log('browser'); })
} else { .then(function() {
grunt.log.ok(results.passes + ' tests passed.'); // 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,
}).then(function(logs) { // but they shut the process down immediately!
if (logs) { return app.stop();
console.error(); })
console.error('Because tests failed, printing browser logs:'); .then(function() {
console.error(logs); if (failure) {
} failure();
}).catch(function (error) { }
failure = function() { cb();
grunt.fail.fatal('Something went wrong: ' + error.message + ' ' + error.stack); })
}; .catch(function(error) {
}).then(function () { console.error('Second-level error:', error.message, error.stack);
// We need to use the failure variable and this early stop to clean up before if (failure) {
// shutting down. Grunt's fail methods are the only way to set the return value, failure();
// but they shut the process down immediately! }
return app.stop(); cb();
}).then(function() { });
if (failure) {
failure();
}
cb();
}).catch(function (error) {
console.error('Second-level error:', error.message, error.stack);
if (failure) {
failure();
}
cb();
});
} }
grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() { grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() {
@ -415,80 +470,99 @@ module.exports = function(grunt) {
runTests(environment, done); runTests(environment, done);
}); });
grunt.registerTask('lib-unit-tests', 'Run libtextsecure unit tests w/Electron', function() { grunt.registerTask(
var environment = grunt.option('env') || 'test-lib'; 'lib-unit-tests',
var done = this.async(); 'Run libtextsecure unit tests w/Electron',
function() {
var environment = grunt.option('env') || 'test-lib';
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';
var environment = grunt.option('env') || 'production'; var environment = grunt.option('env') || 'production';
var asar = require('asar'); var asar = require('asar');
var config = this.data; var config = this.data;
var archive = [dir, config.archive].join('/'); var archive = [dir, config.archive].join('/');
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);
var releaseFiles = files.concat(config.files || []); var releaseFiles = files.concat(config.files || []);
releaseFiles.forEach(function(fileName) { releaseFiles.forEach(function(fileName) {
console.log(fileName); console.log(fileName);
try { try {
asar.statFile(archive, fileName); asar.statFile(archive, fileName);
return true; return true;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
throw new Error("Missing file " + fileName); throw new Error('Missing file ' + fileName);
}
});
if (config.appUpdateYML) {
var appUpdateYML = [dir, config.appUpdateYML].join('/');
if (require('fs').existsSync(appUpdateYML)) {
console.log("auto update ok");
} else {
throw new Error("Missing auto update config " + appUpdateYML);
}
} }
});
var done = this.async(); if (config.appUpdateYML) {
// A simple test to verify a visible window is opened with a title var appUpdateYML = [dir, config.appUpdateYML].join('/');
var Application = require('spectron').Application; if (require('fs').existsSync(appUpdateYML)) {
var assert = require('assert'); console.log('auto update ok');
} else {
throw new Error('Missing auto update config ' + appUpdateYML);
}
}
var app = new Application({ var done = this.async();
path: [dir, config.exe].join('/') // A simple test to verify a visible window is opened with a title
}); var Application = require('spectron').Application;
var assert = require('assert');
app.start().then(function () { var app = new Application({
path: [dir, config.exe].join('/'),
});
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 () { })
// Successfully completed test .then(
return app.stop(); function() {
}, function (error) { // Successfully completed test
// Test failed! return app.stop();
return app.stop().then(function() { },
grunt.fail.fatal('Test failed: ' + error.message + ' ' + error.stack); function(error) {
}); // Test failed!
}).then(done); return app.stop().then(function() {
grunt.fail.fatal(
'Test failed: ' + error.message + ' ' + error.stack
);
});
}
)
.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',
]);
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
(function () { (function() {
'use strict'; 'use strict';
// Browser specific functions for Chrom* // Browser specific functions for Chrom*
window.extension = window.extension || {}; window.extension = window.extension || {};
extension.windows = { extension.windows = {
onClosed: function(callback) { onClosed: function(callback) {
window.addEventListener('beforeunload', callback); window.addEventListener('beforeunload', callback);
} },
}; };
}()); })();

View file

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

View file

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

View file

@ -2,98 +2,94 @@
* 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 || {};
// EmojiConverter overrides // EmojiConverter overrides
EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) { EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) {
var match = regex.exec(str); var match = regex.exec(str);
var count = 0; var count = 0;
if (!regex.global) { if (!regex.global) {
return match ? 1 : 0; return match ? 1 : 0;
} }
while (match) { while (match) {
count += 1; count += 1;
match = regex.exec(str); match = regex.exec(str);
} }
return count; return count;
}; };
EmojiConvertor.prototype.hasNormalCharacters = function(str) { EmojiConvertor.prototype.hasNormalCharacters = function(str) {
var self = this; var self = this;
var noEmoji = str.replace(self.rx_unified, '').trim(); var noEmoji = str.replace(self.rx_unified, '').trim();
return noEmoji.length > 0; return noEmoji.length > 0;
}; };
EmojiConvertor.prototype.getSizeClass = function(str) { EmojiConvertor.prototype.getSizeClass = function(str) {
var self = this; var self = this;
if (self.hasNormalCharacters(str)) { if (self.hasNormalCharacters(str)) {
return ''; return '';
} }
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) {
} return 'medium';
else if (emojiCount > 4) { } else if (emojiCount > 2) {
return 'medium'; return 'large';
} } else {
else if (emojiCount > 2) { return 'jumbo';
return 'large'; }
} };
else {
return 'jumbo';
}
};
var imgClass = /(<img [^>]+ class="emoji)(")/g; var imgClass = /(<img [^>]+ class="emoji)(")/g;
EmojiConvertor.prototype.addClass = function(text, sizeClass) { EmojiConvertor.prototype.addClass = function(text, sizeClass) {
if (!sizeClass) { if (!sizeClass) {
return text; return text;
} }
return text.replace(imgClass, function(match, before, after) { return text.replace(imgClass, function(match, before, after) {
return before + ' ' + sizeClass + after; return before + ' ' + sizeClass + after;
}); });
}; };
var imgTitle = /(<img [^>]+ class="emoji[^>]+ title=")([^:">]+)(")/g; var imgTitle = /(<img [^>]+ class="emoji[^>]+ title=")([^:">]+)(")/g;
EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) { EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) {
return text.replace(imgTitle, function(match, before, title, after) { return text.replace(imgTitle, function(match, before, title, after) {
return before + ':' + title + ':' + after; return before + ':' + title + ':' + after;
}); });
}; };
EmojiConvertor.prototype.signalReplace = function(str) { EmojiConvertor.prototype.signalReplace = function(str) {
var sizeClass = this.getSizeClass(str); var sizeClass = this.getSizeClass(str);
var text = this.replace_unified(str); var text = this.replace_unified(str);
text = this.addClass(text, sizeClass); text = this.addClass(text, sizeClass);
return this.ensureTitlesHaveColons(text); return this.ensureTitlesHaveColons(text);
}; };
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 =
emoji.include_title = true; 'node_modules/emoji-datasource-apple/img/apple/64/';
emoji.replace_mode = 'img'; emoji.include_title = true;
emoji.supports_css = false; // needed to avoid spans with background-image emoji.replace_mode = 'img';
emoji.supports_css = false; // needed to avoid spans with background-image
window.emoji_util.parse = function($el) { window.emoji_util.parse = function($el) {
if (!$el || !$el.length) { if (!$el || !$el.length) {
return; return;
} }
$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,115 +1,124 @@
/* /*
* 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 || {};
function destroyExpiredMessages() { function destroyExpiredMessages() {
// Load messages that have expired and destroy them // Load messages that have expired and destroy them
var expired = new Whisper.MessageCollection(); var expired = new Whisper.MessageCollection();
expired.on('add', function(message) { expired.on('add', function(message) {
console.log('message', message.get('sent_at'), 'expired'); console.log('message', message.get('sent_at'), 'expired');
var conversation = message.getConversation(); var conversation = message.getConversation();
if (conversation) { if (conversation) {
conversation.trigger('expired', message); conversation.trigger('expired', message);
}
// We delete after the trigger to allow the conversation time to process
// the expiration before the message is removed from the database.
message.destroy();
});
expired.on('reset', throttledCheckExpiringMessages);
expired.fetchExpired();
}
var timeout;
function checkExpiringMessages() {
// Look up the next expiring message and set a timer to destroy it
var expiring = new Whisper.MessageCollection();
expiring.once('add', function(next) {
var expires_at = next.get('expires_at');
console.log('next message expires', new Date(expires_at).toISOString());
var wait = expires_at - Date.now();
// In the past
if (wait < 0) { wait = 0; }
// Too far in the future, since it's limited to a 32-bit value
if (wait > 2147483647) { wait = 2147483647; }
clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait);
});
expiring.fetchNextExpiring();
}
var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000);
Whisper.ExpiringMessagesListener = {
init: function(events) {
checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages);
},
update: throttledCheckExpiringMessages
};
var TimerOption = Backbone.Model.extend({
getName: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'),
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize();
},
getAbbreviated: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'), 'abbreviated'
].join('_'));
} }
// We delete after the trigger to allow the conversation time to process
// the expiration before the message is removed from the database.
message.destroy();
}); });
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ expired.on('reset', throttledCheckExpiringMessages);
model: TimerOption,
getName: function(seconds) { expired.fetchExpired();
if (!seconds) { }
seconds = 0;
} var timeout;
var o = this.findWhere({seconds: seconds}); function checkExpiringMessages() {
if (o) { return o.getName(); } // Look up the next expiring message and set a timer to destroy it
else { var expiring = new Whisper.MessageCollection();
return [seconds, 'seconds'].join(' '); expiring.once('add', function(next) {
} var expires_at = next.get('expires_at');
}, console.log('next message expires', new Date(expires_at).toISOString());
getAbbreviated: function(seconds) {
if (!seconds) { var wait = expires_at - Date.now();
seconds = 0;
} // In the past
var o = this.findWhere({seconds: seconds}); if (wait < 0) {
if (o) { return o.getAbbreviated(); } wait = 0;
else {
return [seconds, 's'].join('');
}
} }
}))([
[ 0, 'seconds' ], // Too far in the future, since it's limited to a 32-bit value
[ 5, 'seconds' ], if (wait > 2147483647) {
[ 10, 'seconds' ], wait = 2147483647;
[ 30, 'seconds' ], }
[ 1, 'minute' ],
[ 5, 'minutes' ], clearTimeout(timeout);
[ 30, 'minutes' ], timeout = setTimeout(destroyExpiredMessages, wait);
[ 1, 'hour' ], });
[ 6, 'hours' ], expiring.fetchNextExpiring();
[ 12, 'hours' ], }
[ 1, 'day' ], var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000);
[ 1, 'week' ],
Whisper.ExpiringMessagesListener = {
init: function(events) {
checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages);
},
update: throttledCheckExpiringMessages,
};
var TimerOption = Backbone.Model.extend({
getName: function() {
return (
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
moment.duration(this.get('time'), this.get('unit')).humanize()
);
},
getAbbreviated: function() {
return i18n(
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
'_'
)
);
},
});
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption,
getName: function(seconds) {
if (!seconds) {
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) {
return o.getName();
} else {
return [seconds, 'seconds'].join(' ');
}
},
getAbbreviated: function(seconds) {
if (!seconds) {
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) {
return o.getAbbreviated();
} else {
return [seconds, 's'].join('');
}
},
}))(
[
[0, 'seconds'],
[5, 'seconds'],
[10, 'seconds'],
[30, 'seconds'],
[1, 'minute'],
[5, 'minutes'],
[30, 'minutes'],
[1, 'hour'],
[6, 'hours'],
[12, 'hours'],
[1, 'day'],
[1, 'week'],
].map(function(o) { ].map(function(o) {
var duration = moment.duration(o[0], o[1]); // 5, 'seconds' var duration = moment.duration(o[0], o[1]); // 5, 'seconds'
return { return {
time: o[0], time: o[0],
unit: o[1], unit: o[1],
seconds: duration.asSeconds() seconds: duration.asSeconds(),
}; };
})); })
);
})(); })();

View file

@ -1,4 +1,4 @@
(function () { (function() {
'use strict'; 'use strict';
var windowFocused = false; var windowFocused = false;

View file

@ -2,27 +2,31 @@
* 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.KeyChangeListener = { Whisper.KeyChangeListener = {
init: function(signalProtocolStore) { init: function(signalProtocolStore) {
if (!(signalProtocolStore instanceof SignalProtocolStore)) { if (!(signalProtocolStore instanceof SignalProtocolStore)) {
throw new Error('KeyChangeListener requires a SignalProtocolStore'); throw new Error('KeyChangeListener requires a SignalProtocolStore');
} }
signalProtocolStore.on('keychange', function(id) { signalProtocolStore.on('keychange', function(id) {
ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { ConversationController.getOrCreateAndWait(id, 'private').then(function(
conversation.addKeyChange(id); conversation
) {
conversation.addKeyChange(id);
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) { ConversationController.getAllGroupsInvolvingId(id).then(function(
_.forEach(groups, function(group) { groups
group.addKeyChange(id); ) {
}); _.forEach(groups, function(group) {
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
@ -16,22 +16,22 @@
try { try {
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';
} }
}, },
splitCountryCode: function(number) { splitCountryCode: function(number) {
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) {
@ -39,11 +39,14 @@
var parsedNumber = libphonenumber.parse(number, defaultRegionCode); var parsedNumber = libphonenumber.parse(number, defaultRegionCode);
return { return {
isValidNumber: libphonenumber.isValidNumber(parsedNumber), isValidNumber: libphonenumber.isValidNumber(parsedNumber),
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', {
stream: { level: 'debug',
write(entry) { stream: {
console._log(formatLine(JSON.parse(entry))); write(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

@ -1,29 +1,29 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
(function () { (function() {
'use strict'; 'use strict';
storage.isBlocked = function(number) { storage.isBlocked = function(number) {
var numbers = storage.get('blocked', []); var numbers = storage.get('blocked', []);
return _.include(numbers, number); return _.include(numbers, number);
}; };
storage.addBlockedNumber = function(number) { storage.addBlockedNumber = function(number) {
var numbers = storage.get('blocked', []); var numbers = storage.get('blocked', []);
if (_.include(numbers, number)) { if (_.include(numbers, number)) {
return; return;
} }
console.log('adding', number, 'to blocked list'); console.log('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number)); storage.put('blocked', numbers.concat(number));
}; };
storage.removeBlockedNumber = function(number) { storage.removeBlockedNumber = function(number) {
var numbers = storage.get('blocked', []); var numbers = storage.get('blocked', []);
if (!_.include(numbers, number)) { if (!_.include(numbers, number)) {
return; return;
} }
console.log('removing', number, 'from blocked list'); console.log('removing', number, 'from blocked list');
storage.put('blocked', _.without(numbers, number)); storage.put('blocked', _.without(numbers, number));
}; };
})(); })();

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -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';
@ -79,14 +83,14 @@
if (options.parse === void 0) options.parse = true; if (options.parse === void 0) options.parse = true;
const model = this; const model = this;
const success = options.success; const success = options.success;
options.success = function (resp) { options.success = function(resp) {
model.attributes = {}; // this is the only changed line model.attributes = {}; // this is the only changed line
if (!model.set(model.parse(resp, options), options)) return false; if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options); if (success) success(model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
const error = options.error; const error = options.error;
options.error = function (resp) { options.error = function(resp) {
if (error) error(model, resp, options); if (error) error(model, resp, options);
model.trigger('error', model, resp, options); model.trigger('error', model, resp, options);
}; };
@ -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
@ -235,8 +242,8 @@
const thumbnailWithObjectUrl = !objectUrl const thumbnailWithObjectUrl = !objectUrl
? null ? null
: Object.assign({}, attachment.thumbnail || {}, { : Object.assign({}, attachment.thumbnail || {}, {
objectUrl, objectUrl,
}); });
return Object.assign({}, attachment, { return Object.assign({}, attachment, {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
@ -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,59 +350,63 @@
send(promise) { send(promise) {
this.trigger('pending'); this.trigger('pending');
return promise.then((result) => { return promise
const now = Date.now(); .then(result => {
this.trigger('done'); const now = Date.now();
if (result.dataMessage) { this.trigger('done');
this.set({ dataMessage: result.dataMessage }); if (result.dataMessage) {
} this.set({ dataMessage: result.dataMessage });
const sentTo = this.get('sent_to') || [];
this.save({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
this.sendSyncMessage();
}).catch((result) => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
let promises = [];
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles());
} }
} else { const sentTo = this.get('sent_to') || [];
this.saveErrors(result.errors); this.save({
if (result.successfulNumbers.length > 0) { sent_to: _.union(sentTo, result.successfulNumbers),
const sentTo = this.get('sent_to') || []; sent: true,
this.set({ expirationStartTimestamp: now,
sent_to: _.union(sentTo, result.successfulNumbers), });
sent: true, this.sendSyncMessage();
expirationStartTimestamp: now, })
}); .catch(result => {
promises.push(this.sendSyncMessage()); const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
} }
promises = promises.concat(_.map(result.errors, (error) => {
if (error.name === 'OutgoingIdentityKeyError') { let promises = [];
const c = ConversationController.get(error.number);
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
})); } else {
} this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
promises.push(this.sendSyncMessage());
}
promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
promises.push(c.getProfiles());
}
})
);
}
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors')); this.trigger('send-error', this.get('errors'));
});
}); });
});
}, },
someRecipientsFailed() { someRecipientsFailed() {
@ -423,14 +435,16 @@
if (this.get('synced') || !dataMessage) { if (this.get('synced') || !dataMessage) {
return Promise.resolve(); return Promise.resolve();
} }
return textsecure.messaging.sendSyncMessage( return textsecure.messaging
dataMessage, .sendSyncMessage(
this.get('sent_at'), dataMessage,
this.get('destination'), this.get('sent_at'),
this.get('expirationStartTimestamp') this.get('destination'),
).then(() => { this.get('expirationStartTimestamp')
this.save({ synced: true, dataMessage: null }); )
}); .then(() => {
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 === TypeError || e.constructor === Error ||
e.constructor === ReferenceError) { e.constructor === TypeError ||
e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
} }
return e; return e;
@ -463,32 +479,36 @@
hasNetworkError() { hasNetworkError() {
const error = _.find( const error = _.find(
this.get('errors'), this.get('errors'),
e => (e.name === 'MessageError' || e =>
e.name === 'OutgoingMessageError' || e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError') e.name === 'SendMessageNetworkError' ||
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.name === 'MessageError' || e.number === number &&
e.name === 'OutgoingMessageError' || (e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SendMessageNetworkError' ||
e.name === 'OutgoingIdentityKeyError') e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
); );
this.set({ errors: errors[1] }); this.set({ errors: errors[1] });
return errors[0][0]; return errors[0][0];
}, },
isReplayableError(e) { isReplayableError(e) {
return (e.name === 'MessageError' || return (
e.name === 'OutgoingMessageError' || e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SendMessageNetworkError' ||
e.name === 'OutgoingIdentityKeyError'); e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
}, },
resend(number) { resend(number) {
const error = this.removeOutgoingErrors(number); const error = this.removeOutgoingErrors(number);
@ -513,236 +533,280 @@
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(
const now = new Date().getTime(); () =>
let attributes = { type: 'private' }; new Promise(resolve => {
if (dataMessage.group) { const now = new Date().getTime();
let groupUpdate = null; let attributes = { type: 'private' };
attributes = { if (dataMessage.group) {
type: 'group', let groupUpdate = null;
groupId: dataMessage.group.id, attributes = {
}; type: 'group',
if (dataMessage.group.type === GROUP_TYPES.UPDATE) { groupId: dataMessage.group.id,
attributes = { };
type: 'group', if (dataMessage.group.type === GROUP_TYPES.UPDATE) {
groupId: dataMessage.group.id, attributes = {
name: dataMessage.group.name, type: 'group',
avatar: dataMessage.group.avatar, groupId: dataMessage.group.id,
members: _.union(dataMessage.group.members, conversation.get('members')), name: dataMessage.group.name,
}; avatar: dataMessage.group.avatar,
groupUpdate = conversation.changedAttributes(_.pick( members: _.union(
dataMessage.group, dataMessage.group.members,
'name', conversation.get('members')
'avatar' ),
)) || {}; };
const difference = _.difference( groupUpdate =
attributes.members, conversation.changedAttributes(
conversation.get('members') _.pick(dataMessage.group, 'name', 'avatar')
); ) || {};
if (difference.length > 0) { const difference = _.difference(
groupUpdate.joined = difference; attributes.members,
conversation.get('members')
);
if (difference.length > 0) {
groupUpdate.joined = difference;
}
if (conversation.get('left')) {
console.log('re-added to a left group');
attributes.left = false;
}
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
if (source === textsecure.storage.user.getNumber()) {
attributes.left = true;
groupUpdate = { left: 'You' };
} else {
groupUpdate = { left: source };
}
attributes.members = _.without(
conversation.get('members'),
source
);
}
if (groupUpdate !== null) {
message.set({ group_update: groupUpdate });
}
} }
if (conversation.get('left')) { message.set({
console.log('re-added to a left group'); attachments: dataMessage.attachments,
attributes.left = false; body: dataMessage.body,
} conversationId: conversation.id,
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { decrypted_at: now,
if (source === textsecure.storage.user.getNumber()) { errors: [],
attributes.left = true; flags: dataMessage.flags,
groupUpdate = { left: 'You' }; hasAttachments: dataMessage.hasAttachments,
} else { hasFileAttachments: dataMessage.hasFileAttachments,
groupUpdate = { left: source }; hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
} quote: dataMessage.quote,
attributes.members = _.without(conversation.get('members'), source); schemaVersion: dataMessage.schemaVersion,
} });
if (type === 'outgoing') {
if (groupUpdate !== null) { const receipts = Whisper.DeliveryReceipts.forMessage(
message.set({ group_update: groupUpdate }); conversation,
} message
} );
message.set({ receipts.forEach(() =>
attachments: dataMessage.attachments, message.set({
body: dataMessage.body, delivered: (message.get('delivered') || 0) + 1,
conversationId: conversation.id, })
decrypted_at: now,
errors: [],
flags: dataMessage.flags,
hasAttachments: dataMessage.hasAttachments,
hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
});
if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message);
receipts.forEach(() => message.set({
delivered: (message.get('delivered') || 0) + 1,
}));
}
attributes.active_at = now;
conversation.set(attributes);
if (message.isExpirationTimerUpdate()) {
message.set({
expirationTimerUpdate: {
source,
expireTimer: dataMessage.expireTimer,
},
});
conversation.set({ expireTimer: dataMessage.expireTimer });
} else if (dataMessage.expireTimer) {
message.set({ expireTimer: dataMessage.expireTimer });
}
// NOTE: Remove once the above uses
// `Conversation::updateExpirationTimer`:
const { expireTimer } = dataMessage;
const shouldLogExpireTimerChange =
message.isExpirationTimerUpdate() || expireTimer;
if (shouldLogExpireTimerChange) {
console.log(
'Updating expireTimer for conversation',
conversation.idForLogging(),
'to',
expireTimer,
'via `handleDataMessage`'
);
}
if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) {
if (dataMessage.expireTimer !== conversation.get('expireTimer')) {
conversation.updateExpirationTimer(
dataMessage.expireTimer, source,
message.get('received_at')
); );
} }
} else if (conversation.get('expireTimer')) { attributes.active_at = now;
conversation.updateExpirationTimer( conversation.set(attributes);
null, source,
message.get('received_at') if (message.isExpirationTimerUpdate()) {
); message.set({
} expirationTimerUpdate: {
} source,
if (type === 'incoming') { expireTimer: dataMessage.expireTimer,
const readSync = Whisper.ReadSyncs.forMessage(message); },
if (readSync) { });
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) { conversation.set({ expireTimer: dataMessage.expireTimer });
message.set('expirationStartTimestamp', readSync.get('read_at')); } else if (dataMessage.expireTimer) {
message.set({ expireTimer: dataMessage.expireTimer });
} }
}
if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as
// read, as is done when we receive a read sync for a message we already
// know about.
Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1);
}
}
if (type === 'outgoing') { // NOTE: Remove once the above uses
const reads = Whisper.ReadReceipts.forMessage(conversation, message); // `Conversation::updateExpirationTimer`:
if (reads.length) { const { expireTimer } = dataMessage;
const readBy = reads.map(receipt => receipt.get('reader')); const shouldLogExpireTimerChange =
message.set({ message.isExpirationTimerUpdate() || expireTimer;
read_by: _.union(message.get('read_by'), readBy), if (shouldLogExpireTimerChange) {
}); console.log(
} 'Updating expireTimer for conversation',
conversation.idForLogging(),
message.set({ recipients: conversation.getRecipients() }); 'to',
} expireTimer,
'via `handleDataMessage`'
const conversationTimestamp = conversation.get('timestamp'); );
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toArrayBuffer();
if (source === textsecure.storage.user.getNumber()) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.set({ profileKey });
} else {
ConversationController.getOrCreateAndWait(
source,
'private'
).then((sender) => {
sender.setProfileKey(profileKey);
});
}
}
const handleError = (error) => {
const errorForLog = error && error.stack ? error.stack : error;
console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog);
return resolve();
};
message.save().then(() => {
conversation.save().then(() => {
try {
conversation.trigger('newmessage', message);
} catch (e) {
return handleError(e);
} }
// We fetch() here because, between the message.save() above and the previous
// line's trigger() call, we might have marked all messages unread in the
// database. This message might already be read!
const previousUnread = message.get('unread');
return message.fetch().then(() => {
try {
if (previousUnread !== message.get('unread')) {
console.log('Caught race condition on new message read state! ' +
'Manually starting timers.');
// We call markRead() even though the message is already marked read
// because we need to start expiration timers, etc.
message.markRead();
}
if (message.get('unread')) { if (!message.isEndSession() && !message.isGroupUpdate()) {
return conversation.notify(message).then(() => { if (dataMessage.expireTimer) {
confirm(); if (
return resolve(); dataMessage.expireTimer !== conversation.get('expireTimer')
}, handleError); ) {
conversation.updateExpirationTimer(
dataMessage.expireTimer,
source,
message.get('received_at')
);
} }
} else if (conversation.get('expireTimer')) {
confirm(); conversation.updateExpirationTimer(
return resolve(); null,
} catch (e) { source,
return handleError(e); message.get('received_at')
}
}, () => {
try {
console.log(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
); );
confirm();
return resolve();
} catch (e) {
return handleError(e);
} }
}); }
}, handleError); if (type === 'incoming') {
}, handleError); const readSync = Whisper.ReadSyncs.forMessage(message);
})); if (readSync) {
if (
message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
}
}
if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as
// read, as is done when we receive a read sync for a message we already
// know about.
Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
}
}
if (type === 'outgoing') {
const reads = Whisper.ReadReceipts.forMessage(
conversation,
message
);
if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader'));
message.set({
read_by: _.union(message.get('read_by'), readBy),
});
}
message.set({ recipients: conversation.getRecipients() });
}
const conversationTimestamp = conversation.get('timestamp');
if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toArrayBuffer();
if (source === textsecure.storage.user.getNumber()) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.set({ profileKey });
} else {
ConversationController.getOrCreateAndWait(
source,
'private'
).then(sender => {
sender.setProfileKey(profileKey);
});
}
}
const handleError = error => {
const errorForLog = error && error.stack ? error.stack : error;
console.log(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
return resolve();
};
message.save().then(() => {
conversation.save().then(() => {
try {
conversation.trigger('newmessage', message);
} catch (e) {
return handleError(e);
}
// We fetch() here because, between the message.save() above and the previous
// line's trigger() call, we might have marked all messages unread in the
// database. This message might already be read!
const previousUnread = message.get('unread');
return message.fetch().then(
() => {
try {
if (previousUnread !== message.get('unread')) {
console.log(
'Caught race condition on new message read state! ' +
'Manually starting timers.'
);
// We call markRead() even though the message is already marked read
// because we need to start expiration timers, etc.
message.markRead();
}
if (message.get('unread')) {
return conversation.notify(message).then(() => {
confirm();
return resolve();
}, handleError);
}
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
},
() => {
try {
console.log(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
);
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
}
);
}, 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(
messageId: this.id, Whisper.Notifications.where({
})); 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 =>
index: { this.fetch({
// 'receipt' index on sent_at index: {
name: 'receipt', // 'receipt' index on sent_at
only: timestamp, name: 'receipt',
}, 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,21 +20,25 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
); );
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadImage(fileOrBlobOrURL, (canvasOrError) => { loadImage(
if (canvasOrError.type === 'error') { fileOrBlobOrURL,
const error = new Error('autoOrientImage: Failed to process image'); canvasOrError => {
error.cause = canvasOrError; if (canvasOrError.type === 'error') {
reject(error); const error = new Error('autoOrientImage: Failed to process image');
return; error.cause = canvasOrError;
} reject(error);
return;
}
const canvas = canvasOrError; const canvas = canvasOrError;
const dataURL = canvas.toDataURL( const dataURL = canvas.toDataURL(
optionsWithDefaults.type, optionsWithDefaults.type,
optionsWithDefaults.quality optionsWithDefaults.quality
); );
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.type === 'ArrayBuffer' && val &&
val.encoding === 'base64' && val.type === 'ArrayBuffer' &&
typeof val.data === 'string') { val.encoding === 'base64' &&
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,19 +82,22 @@ 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(
if (writer.write(string)) { () =>
resolve(); new Promise(resolve => {
return; if (writer.write(string)) {
} resolve();
return;
}
// If write() returns true, we don't need to wait for the drain event // If write() returns true, we don't need to wait for the drain event
// https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
writer.once('drain', resolve); writer.once('drain', resolve);
// 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,14 +309,14 @@ function importFromJsonString(db, jsonString, targetPath, options) {
} }
}; };
_.each(importObject[storeName], (toAdd) => { _.each(importObject[storeName], toAdd => {
toAdd = unstringify(toAdd); toAdd = unstringify(toAdd);
const haveConversationAlready = const haveConversationAlready =
storeName === 'conversations' && storeName === 'conversations' &&
conversationLookup[getConversationKey(toAdd)]; conversationLookup[getConversationKey(toAdd)];
const haveGroupAlready = const haveGroupAlready =
storeName === 'groups' && groupLookup[getGroupKey(toAdd)]; storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
if (haveConversationAlready || haveGroupAlready) { if (haveConversationAlready || haveGroupAlready) {
skipCount += 1; skipCount += 1;
@ -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(
index, attachment,
})) Object.assign({}, options, {
)); 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,
index, Object.assign({}, options, {
})) 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,13 +686,14 @@ 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 = () =>
dir: attachmentsDir, writeAttachments(attachments, {
name, dir: attachmentsDir,
message, name,
key, message,
newKey, key,
}); newKey,
});
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(exportAttachments); promiseChain = promiseChain.then(exportAttachments);
@ -723,13 +701,14 @@ 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 = () =>
dir: attachmentsDir, writeThumbnails(quoteThumbnails, {
name, dir: attachmentsDir,
message, name,
key, message,
newKey, key,
}); newKey,
});
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(exportQuoteThumbnails); promiseChain = promiseChain.then(exportQuoteThumbnails);
@ -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(
messagesDir: stagingDir, db,
attachmentsDir, Object.assign({}, options, {
})); messagesDir: stagingDir,
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(
await sleep(index * 100); range(0, numMessages).map(async index => {
console.log(`Create message ${index + 1}`); await sleep(index * 100);
const messageAttributes = await createRandomMessage({ conversationId }); console.log(`Create message ${index + 1}`);
const message = new WhisperMessage(messageAttributes); const messageAttributes = await createRandomMessage({ conversationId });
return deferredToPromise(message.save()); const message = new WhisperMessage(messageAttributes);
})); 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(
numTotalMessages, 'Upgrade message schema:',
numCumulativeMessagesProcessed, Object.assign({}, status, {
})); numTotalMessages,
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,83 +289,91 @@ 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,
if (!isFunction(BackboneMessageCollection)) { count,
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" + } = {}) => {
' constructor is required'); if (!isFunction(BackboneMessageCollection)) {
} throw new TypeError(
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
' constructor is required'
);
}
if (!isNumber(count)) { if (!isNumber(count)) {
throw new TypeError("'count' is required"); throw new TypeError("'count' is required");
} }
const collection = new BackboneMessageCollection(); const collection = new BackboneMessageCollection();
return new Promise(resolve => collection.fetch({ return new Promise(resolve =>
limit: count, collection
index: { .fetch({
name: 'schemaVersion', limit: count,
upper: Message.CURRENT_SCHEMA_VERSION, index: {
excludeUpper: true, name: 'schemaVersion',
order: 'desc', upper: Message.CURRENT_SCHEMA_VERSION,
}, excludeUpper: true,
}).always(() => { order: 'desc',
const models = collection.models || []; },
const messages = models.map(model => model.toJSON()); })
resolve(messages); .always(() => {
})); const models = collection.models || [];
}; const messages = models.map(model => model.toJSON());
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,
if (!isObject(connection)) { count,
throw new TypeError("'connection' is required"); lastIndex,
} } = {}) => {
if (!isObject(connection)) {
throw new TypeError("'connection' is required");
}
if (!isNumber(count)) { if (!isNumber(count)) {
throw new TypeError("'count' is required"); throw new TypeError("'count' is required");
} }
if (lastIndex && !isString(lastIndex)) { if (lastIndex && !isString(lastIndex)) {
throw new TypeError("'lastIndex' must be a string"); throw new TypeError("'lastIndex' must be a string");
} }
const hasLastIndex = Boolean(lastIndex); const hasLastIndex = Boolean(lastIndex);
const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly');
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
const excludeLowerBound = true; const excludeLowerBound = true;
const range = hasLastIndex const range = hasLastIndex
? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
: undefined; : undefined;
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) {
resolve(items); resolve(items);
return; return;
} }
const item = cursor.value; const item = cursor.value;
items.push(item); items.push(item);
cursor.continue(); cursor.continue();
}; };
request.onerror = event => request.onerror = event => reject(event.target.error);
reject(event.target.error); });
}); };
};
const _getNumMessages = async ({ connection } = {}) => { const _getNumMessages = async ({ connection } = {}) => {
if (!isObject(connection)) { if (!isObject(connection)) {

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, {
migrate() { version: lastMigrationVersion,
throw new Error('Unexpected invocation of placeholder migration!' + migrate() {
'\n\nMigrations must explicitly be run upon application startup instead' + throw new Error(
' of implicitly via Backbone IndexedDB adapter at any time.'); 'Unexpected invocation of placeholder migration!' +
'\n\nMigrations must explicitly be run upon application startup instead' +
' 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(
const putRequest = identityKeys.put(attributes, attributes.id); new Promise((resolve, reject) => {
putRequest.onsuccess = resolve; const putRequest = identityKeys.put(attributes, attributes.id);
putRequest.onerror = (e) => { putRequest.onsuccess = resolve;
console.log(e); putRequest.onerror = e => {
reject(e); console.log(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, {
{}, schemaVersion: inheritedSchemaVersion,
message, attachments: message.attachments.map(Attachment.removeSchemaVersion),
{ });
schemaVersion: inheritedSchemaVersion,
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(
await writeExistingAttachmentData(attachment); (attachments || []).map(async attachment => {
return omit(attachment, ['data']); await writeExistingAttachmentData(attachment);
})), 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,140 +2,146 @@
* 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;
var SETTINGS = { var SETTINGS = {
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({
initialize: function() { initialize: function() {
this.isEnabled = false; this.isEnabled = false;
this.on('add', this.update); this.on('add', this.update);
this.on('remove', this.onRemove); this.on('remove', this.onRemove);
}, },
onClick: function(conversationId) { onClick: function(conversationId) {
var conversation = ConversationController.get(conversationId); var conversation = ConversationController.get(conversationId);
this.trigger('click', conversation); this.trigger('click', conversation);
}, },
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 =
const isAudioNotificationSupported = Settings.isAudioNotificationSupported(); storage.get('audio-notification') || false;
const shouldPlayNotificationSound = isAudioNotificationSupported && const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
isAudioNotificationEnabled; const shouldPlayNotificationSound =
const numNotifications = this.length; isAudioNotificationSupported && isAudioNotificationEnabled;
console.log( const numNotifications = this.length;
'Update notifications:', console.log('Update notifications:', {
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound} isFocused,
); isEnabled,
numNotifications,
shouldPlayNotificationSound,
});
if (!isEnabled) { if (!isEnabled) {
return; return;
} }
const hasNotifications = numNotifications > 0; const hasNotifications = numNotifications > 0;
if (!hasNotifications) { if (!hasNotifications) {
return; return;
} }
const isNotificationOmitted = isFocused; const isNotificationOmitted = isFocused;
if (isNotificationOmitted) { if (isNotificationOmitted) {
this.clear(); this.clear();
return; return;
} }
var setting = storage.get('notification-setting') || 'message'; var setting = storage.get('notification-setting') || 'message';
if (setting === SETTINGS.OFF) { if (setting === SETTINGS.OFF) {
return; return;
} }
window.drawAttention(); window.drawAttention();
var title; var title;
var message; var message;
var iconUrl; var iconUrl;
// NOTE: i18n has more complex rules for pluralization than just // NOTE: i18n has more complex rules for pluralization than just
// distinguishing between zero (0) and other (non-zero), // distinguishing between zero (0) and other (non-zero),
// e.g. Russian: // e.g. Russian:
// 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();
switch (this.getSetting()) { switch (this.getSetting()) {
case SETTINGS.COUNT: case SETTINGS.COUNT:
title = 'Signal'; title = 'Signal';
message = newMessageCount; message = newMessageCount;
break; break;
case SETTINGS.NAME: case SETTINGS.NAME:
title = newMessageCount; title = newMessageCount;
message = 'Most recent from ' + last.get('title'); message = 'Most recent from ' + last.get('title');
iconUrl = last.get('iconUrl'); iconUrl = last.get('iconUrl');
break; break;
case SETTINGS.MESSAGE: case SETTINGS.MESSAGE:
if (numNotifications === 1) { if (numNotifications === 1) {
title = last.get('title'); title = last.get('title');
} else { } else {
title = newMessageCount; title = newMessageCount;
} }
message = last.get('message'); message = last.get('message');
iconUrl = last.get('iconUrl'); iconUrl = last.get('iconUrl');
break; break;
} }
if (window.config.polyfillNotifications) { if (window.config.polyfillNotifications) {
window.nodeNotifier.notify({ window.nodeNotifier.notify({
title: title, title: title,
message: message, message: message,
sound: false, sound: false,
}); });
window.nodeNotifier.on('click', function(notifierObject, options) { window.nodeNotifier.on('click', function(notifierObject, options) {
last.get('conversationId'); last.get('conversationId');
}); });
} else { } else {
var notification = new Notification(title, { var notification = new Notification(title, {
body : message, body: message,
icon : iconUrl, icon: iconUrl,
tag : 'signal', tag: 'signal',
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
this.clear(); this.clear();
}, },
getSetting: function() { getSetting: function() {
return storage.get('notification-setting') || SETTINGS.MESSAGE; return storage.get('notification-setting') || SETTINGS.MESSAGE;
}, },
onRemove: function() { onRemove: function() {
console.log('remove notification'); console.log('remove notification');
}, },
clear: function() { clear: function() {
console.log('remove all notifications'); console.log('remove all notifications');
this.reset([]); this.reset([]);
}, },
enable: function() { enable: function() {
const needUpdate = !this.isEnabled; const needUpdate = !this.isEnabled;
this.isEnabled = true; this.isEnabled = true;
if (needUpdate) { if (needUpdate) {
this.update(); this.update();
} }
}, },
disable: function() { disable: function() {
this.isEnabled = false; this.isEnabled = false;
}, },
}))(); }))();
})(); })();

View file

@ -1,79 +1,101 @@
/* /*
* 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({
forMessage: function(conversation, message) { forMessage: function(conversation, message) {
if (!message.isOutgoing()) { if (!message.isOutgoing()) {
return []; return [];
} }
var ids = []; var ids = [];
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
ids = [conversation.id]; ids = [conversation.id];
} else {
ids = conversation.get('members');
}
var receipts = this.filter(function(receipt) {
return (
receipt.get('timestamp') === message.get('sent_at') &&
_.contains(ids, receipt.get('reader'))
);
});
if (receipts.length) {
console.log('Found early read receipts for message');
this.remove(receipts);
}
return receipts;
},
onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection();
return messages
.fetchSentAt(receipt.get('timestamp'))
.then(function() {
if (messages.length === 0) {
return;
}
var message = messages.find(function(message) {
return (
message.isOutgoing() &&
receipt.get('reader') === message.get('conversationId')
);
});
if (message) {
return message;
}
var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('reader')).then(function() {
var ids = groups.pluck('id');
ids.push(receipt.get('reader'));
return messages.find(function(message) {
return (
message.isOutgoing() &&
_.contains(ids, message.get('conversationId'))
);
});
});
})
.then(
function(message) {
if (message) {
var read_by = message.get('read_by') || [];
read_by.push(receipt.get('reader'));
return new Promise(
function(resolve, reject) {
message.save({ read_by: read_by }).then(
function() {
// notify frontend listeners
var conversation = ConversationController.get(
message.get('conversationId')
);
if (conversation) {
conversation.trigger('read', message);
}
this.remove(receipt);
resolve();
}.bind(this),
reject
);
}.bind(this)
);
} else { } else {
ids = conversation.get('members'); console.log(
'No message for read receipt',
receipt.get('reader'),
receipt.get('timestamp')
);
} }
var receipts = this.filter(function(receipt) { }.bind(this)
return receipt.get('timestamp') === message.get('sent_at') )
&& _.contains(ids, receipt.get('reader')); .catch(function(error) {
}); console.log(
if (receipts.length) { 'ReadReceipts.onReceipt error:',
console.log('Found early read receipts for message'); error && error.stack ? error.stack : error
this.remove(receipts); );
} });
return receipts; },
}, }))();
onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
if (messages.length === 0) { return; }
var message = messages.find(function(message) {
return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId'));
});
if (message) { return message; }
var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('reader')).then(function() {
var ids = groups.pluck('id');
ids.push(receipt.get('reader'));
return messages.find(function(message) {
return (message.isOutgoing() &&
_.contains(ids, message.get('conversationId')));
});
});
}).then(function(message) {
if (message) {
var read_by = message.get('read_by') || [];
read_by.push(receipt.get('reader'));
return new Promise(function(resolve, reject) {
message.save({ read_by: read_by }).then(function() {
// notify frontend listeners
var conversation = ConversationController.get(
message.get('conversationId')
);
if (conversation) {
conversation.trigger('read', message);
}
this.remove(receipt);
resolve();
}.bind(this), reject);
}.bind(this));
} else {
console.log(
'No message for read receipt',
receipt.get('reader'),
receipt.get('timestamp')
);
}
}.bind(this)).catch(function(error) {
console.log(
'ReadReceipts.onReceipt error:',
error && error.stack ? error.stack : error
);
});
},
}))();
})(); })();

View file

@ -1,49 +1,57 @@
/* /*
* 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');
this.remove(receipt);
return receipt;
}
},
onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(
function() {
var message = messages.find(function(message) {
return (
message.isIncoming() &&
message.isUnread() &&
message.get('source') === receipt.get('sender')
);
});
if (message) {
return message.markRead(receipt.get('read_at')).then(
function() {
this.notifyConversation(message);
this.remove(receipt); this.remove(receipt);
return receipt; }.bind(this)
} );
}, } else {
onReceipt: function(receipt) { console.log(
var messages = new Whisper.MessageCollection(); 'No message for read sync',
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { receipt.get('sender'),
var message = messages.find(function(message) { receipt.get('timestamp')
return (message.isIncoming() && message.isUnread() && );
message.get('source') === receipt.get('sender')); }
}); }.bind(this)
if (message) { );
return message.markRead(receipt.get('read_at')).then(function() { },
this.notifyConversation(message); notifyConversation: function(message) {
this.remove(receipt); var conversation = ConversationController.get({
}.bind(this)); id: message.get('conversationId'),
} else { });
console.log(
'No message for read sync',
receipt.get('sender'), receipt.get('timestamp')
);
}
}.bind(this));
},
notifyConversation: function(message) {
var conversation = ConversationController.get({
id: message.get('conversationId')
});
if (conversation) { if (conversation) {
conversation.onReadMessage(message); conversation.onReadMessage(message);
} }
}, },
}))(); }))();
})(); })();

View file

@ -1,25 +1,27 @@
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
(function () { (function() {
'use strict'; 'use strict';
Whisper.Registration = { Whisper.Registration = {
markEverDone: function() { markEverDone: function() {
storage.put('chromiumRegistrationDoneEver', ''); storage.put('chromiumRegistrationDoneEver', '');
}, },
markDone: function () { markDone: function() {
this.markEverDone(); this.markEverDone();
storage.put('chromiumRegistrationDone', ''); storage.put('chromiumRegistrationDone', '');
}, },
isDone: function () { isDone: function() {
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() { );
storage.remove('chromiumRegistrationDone'); },
} remove: function() {
}; storage.remove('chromiumRegistrationDone');
}()); },
};
})();

View file

@ -1,4 +1,4 @@
(function () { (function() {
// Note: this is all the code required to customize Backbone's trigger() method to make // Note: this is all the code required to customize Backbone's trigger() method to make
// it resilient to exceptions thrown by event handlers. Indentation and code styles // it resilient to exceptions thrown by event handlers. Indentation and code styles
// were kept inline with the Backbone implementation for easier diffs. // were kept inline with the Backbone implementation for easier diffs.
@ -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,83 +2,89 @@
* 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;
var timeout; var timeout;
var scheduledTime; var scheduledTime;
function scheduleNextRotation() { function scheduleNextRotation() {
var now = Date.now(); var now = Date.now();
var nextTime = now + ROTATION_INTERVAL; var nextTime = now + ROTATION_INTERVAL;
storage.put('nextSignedKeyRotationTime', nextTime); storage.put('nextSignedKeyRotationTime', nextTime);
}
function run() {
console.log('Rotating signed prekey...');
getAccountManager()
.rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000);
});
scheduleNextRotation();
setTimeoutForNextRun();
}
function runWhenOnline() {
if (navigator.onLine) {
run();
} else {
console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() {
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
}
}
function setTimeoutForNextRun() {
var now = Date.now();
var time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) {
console.log(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
} }
function run() { scheduledTime = time;
console.log('Rotating signed prekey...'); var waitTime = time - now;
getAccountManager().rotateSignedPreKey().catch(function() { if (waitTime < 0) {
console.log('rotateSignedPrekey() failed. Trying again in five seconds'); waitTime = 0;
setTimeout(runWhenOnline, 5000); }
});
scheduleNextRotation(); clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
var initComplete;
Whisper.RotateSignedPreKeyListener = {
init: function(events, newVersion) {
if (initComplete) {
console.log('Rotate signed prekey listener: Already initialized');
return;
}
initComplete = true;
if (newVersion) {
runWhenOnline();
} else {
setTimeoutForNextRun(); setTimeoutForNextRun();
} }
function runWhenOnline() { events.on('timetravel', function() {
if (navigator.onLine) { if (Whisper.Registration.isDone()) {
run(); setTimeoutForNextRun();
} else {
console.log('We are offline; keys will be rotated when we are next online');
var listener = function() {
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
} }
} });
},
function setTimeoutForNextRun() { };
var now = Date.now(); })();
var time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) {
console.log(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
}
scheduledTime = time;
var waitTime = time - now;
if (waitTime < 0) {
waitTime = 0;
}
clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
var initComplete;
Whisper.RotateSignedPreKeyListener = {
init: function(events, newVersion) {
if (initComplete) {
console.log('Rotate signed prekey listener: Already initialized');
return;
}
initComplete = true;
if (newVersion) {
runWhenOnline();
} else {
setTimeoutForNextRun();
}
events.on('timetravel', function() {
if (Whisper.Registration.isDone()) {
setTimeoutForNextRun();
}
});
}
};
}());

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
(function () { (function() {
var electron = require('electron'); var electron = require('electron');
var remote = electron.remote; var remote = electron.remote;
var app = remote.app; var app = remote.app;
@ -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,80 +1,89 @@
/* /*
* 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,
storeName: 'items', storeName: 'items',
database: Whisper.Database, database: Whisper.Database,
}); });
var ready = false; var ready = false;
var items = new ItemCollection(); var items = new ItemCollection();
items.on('reset', function() { ready = true; }); items.on('reset', function() {
window.storage = { ready = true;
/***************************** });
*** Base Storage Routines *** window.storage = {
*****************************/ /*****************************
put: function(key, value) { *** Base Storage Routines ***
if (value === undefined) { *****************************/
throw new Error("Tried to store undefined"); put: function(key, value) {
} if (value === undefined) {
if (!ready) { throw new Error('Tried to store undefined');
console.log('Called storage.put before storage is ready. key:', key); }
} if (!ready) {
var item = items.add({id: key, value: value}, {merge: true}); console.log('Called storage.put before storage is ready. key:', key);
return new Promise(function(resolve, reject) { }
item.save().then(resolve, reject); var item = items.add({ id: key, value: value }, { merge: true });
}); return new Promise(function(resolve, reject) {
}, item.save().then(resolve, reject);
});
},
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;
} }
return item.get('value'); return item.get('value');
}, },
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) {
item.destroy().then(resolve, reject); item.destroy().then(resolve, reject);
}); });
} }
return Promise.resolve(); return Promise.resolve();
}, },
onready: function(callback) { onready: function(callback) {
if (ready) { if (ready) {
callback(); callback();
} else { } else {
items.on('reset', callback); items.on('reset', callback);
} }
}, },
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(() =>
.always(resolve); reject(
}); new Error(
}, 'Failed to fetch from storage.' +
' This may be due to an unexpected database version.'
)
)
)
.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 || {};
window.textsecure.storage.impl = window.storage; window.textsecure.storage.impl = window.storage;
})(); })();

View file

@ -1,168 +1,177 @@
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.AppView = Backbone.View.extend({ Whisper.AppView = Backbone.View.extend({
initialize: function(options) { initialize: function(options) {
this.inboxView = null; this.inboxView = null;
this.installView = null; this.installView = null;
this.applyTheme(); this.applyTheme();
this.applyHideMenu(); this.applyHideMenu();
}, },
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('android-dark') .removeClass('ios')
.removeClass('android') .removeClass('android-dark')
.addClass(theme); .removeClass('android')
}, .addClass(theme);
applyHideMenu: function() { },
var hideMenuBar = storage.get('hide-menu-bar', false); applyHideMenu: function() {
window.setAutoHideMenuBar(hideMenuBar); var hideMenuBar = storage.get('hide-menu-bar', false);
window.setMenuBarVisibility(!hideMenuBar); window.setAutoHideMenuBar(hideMenuBar);
}, window.setMenuBarVisibility(!hideMenuBar);
openView: function(view) { },
this.el.innerHTML = ""; openView: function(view) {
this.el.append(view.el); this.el.innerHTML = '';
this.delegateEvents(); this.el.append(view.el);
}, this.delegateEvents();
openDebugLog: function() { },
this.closeDebugLog(); openDebugLog: function() {
this.debugLogView = new Whisper.DebugLogView(); this.closeDebugLog();
this.debugLogView.$el.appendTo(this.el); this.debugLogView = new Whisper.DebugLogView();
}, this.debugLogView.$el.appendTo(this.el);
closeDebugLog: function() { },
if (this.debugLogView) { closeDebugLog: function() {
this.debugLogView.remove(); if (this.debugLogView) {
this.debugLogView = null; this.debugLogView.remove();
} this.debugLogView = null;
}, }
openImporter: function() { },
window.addSetupMenuItems(); openImporter: function() {
this.resetViews(); window.addSetupMenuItems();
var importView = this.importView = new Whisper.ImportView(); this.resetViews();
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this)); var importView = (this.importView = new Whisper.ImportView());
this.openView(this.importView); this.listenTo(
}, importView,
finishLightImport: function() { 'light-import',
var options = { this.finishLightImport.bind(this)
hasExistingData: true );
}; this.openView(this.importView);
this.openInstaller(options); },
}, finishLightImport: function() {
closeImporter: function() { var options = {
if (this.importView) { hasExistingData: true,
this.importView.remove(); };
this.importView = null; this.openInstaller(options);
} },
}, closeImporter: function() {
openInstaller: function(options) { if (this.importView) {
options = options || {}; this.importView.remove();
this.importView = null;
}
},
openInstaller: function(options) {
options = options || {};
// If we're in the middle of import, we don't want to show the menu options // If we're in the middle of import, we don't want to show the menu options
// allowing the user to switch to other ways to set up the app. If they // allowing the user to switch to other ways to set up the app. If they
// switched back and forth in the middle of a light import, they'd lose all // switched back and forth in the middle of a light import, they'd lose all
// that imported data. // that imported data.
if (!options.hasExistingData) { if (!options.hasExistingData) {
window.addSetupMenuItems(); window.addSetupMenuItems();
} }
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() {
if (this.installView) { if (this.installView) {
this.installView.remove(); this.installView.remove();
this.installView = null; this.installView = null;
} }
}, },
openStandalone: function() { openStandalone: function() {
if (window.config.environment !== 'production') { if (window.config.environment !== 'production') {
window.addSetupMenuItems(); window.addSetupMenuItems();
this.resetViews(); this.resetViews();
this.standaloneView = new Whisper.StandaloneRegistrationView(); this.standaloneView = new Whisper.StandaloneRegistrationView();
this.openView(this.standaloneView); this.openView(this.standaloneView);
} }
}, },
closeStandalone: function() { closeStandalone: function() {
if (this.standaloneView) { if (this.standaloneView) {
this.standaloneView.remove(); this.standaloneView.remove();
this.standaloneView = null; this.standaloneView = null;
} }
}, },
resetViews: function() { resetViews: function() {
this.closeInstaller(); this.closeInstaller();
this.closeImporter(); this.closeImporter();
this.closeStandalone(); this.closeStandalone();
}, },
openInbox: function(options) { openInbox: function(options) {
options = options || {}; options = options || {};
// The inbox can be created before the 'empty' event fires or afterwards. If // The inbox can be created before the 'empty' event fires or afterwards. If
// before, it's straightforward: the onEmpty() handler below updates the // before, it's straightforward: the onEmpty() handler below updates the
// view directly, and we're in good shape. If we create the inbox late, we // view directly, and we're in good shape. If we create the inbox late, we
// need to be sure that the current value of initialLoadComplete is provided // need to be sure that the current value of initialLoadComplete is provided
// so its loading screen doesn't stick around forever. // so its loading screen doesn't stick around forever.
// Two primary techniques at play for this situation: // Two primary techniques at play for this situation:
// - background.js has two openInbox() calls, and passes initalLoadComplete // - background.js has two openInbox() calls, and passes initalLoadComplete
// directly via the options parameter. // directly via the options parameter.
// - in other situations openInbox() will be called with no options. So this // - in other situations openInbox() will be called with no options. So this
// view keeps track of whether onEmpty() has ever been called with // view keeps track of whether onEmpty() has ever been called with
// this.initialLoadComplete. An example of this: on a phone-pairing setup. // this.initialLoadComplete. An example of this: on a phone-pairing setup.
_.defaults(options, {initialLoadComplete: this.initialLoadComplete}); _.defaults(options, { initialLoadComplete: this.initialLoadComplete });
console.log('open inbox'); console.log('open inbox');
this.closeInstaller(); this.closeInstaller();
if (!this.inboxView) { if (!this.inboxView) {
// We create the inbox immediately so we don't miss an update to // We create the inbox immediately so we don't miss an update to
// this.initialLoadComplete between the start of this method and the // this.initialLoadComplete between the start of this method and the
// creation of inboxView. // creation of inboxView.
this.inboxView = new Whisper.InboxView({ this.inboxView = new Whisper.InboxView({
model: self, model: self,
window: window, window: window,
initialLoadComplete: options.initialLoadComplete initialLoadComplete: options.initialLoadComplete,
}); });
return ConversationController.loadPromise().then(function() { return ConversationController.loadPromise().then(
this.openView(this.inboxView); function() {
}.bind(this)); this.openView(this.inboxView);
} else { }.bind(this)
if (!$.contains(this.el, this.inboxView.el)) { );
this.openView(this.inboxView); } else {
} if (!$.contains(this.el, this.inboxView.el)) {
window.focus(); // FIXME this.openView(this.inboxView);
return Promise.resolve(); }
} window.focus(); // FIXME
}, return Promise.resolve();
onEmpty: function() { }
var view = this.inboxView; },
onEmpty: function() {
var view = this.inboxView;
this.initialLoadComplete = true; this.initialLoadComplete = true;
if (view) { if (view) {
view.onEmpty(); view.onEmpty();
} }
}, },
onProgress: function(count) { onProgress: function(count) {
var view = this.inboxView; var view = this.inboxView;
if (view) { if (view) {
view.onProgress(count); view.onProgress(count);
} }
}, },
openConversation: function(conversation) { openConversation: function(conversation) {
if (conversation) { if (conversation) {
this.openInbox().then(function() { this.openInbox().then(
this.inboxView.openConversation(null, conversation); function() {
}.bind(this)); this.inboxView.openConversation(null, conversation);
} }.bind(this)
}, );
}); }
},
});
})(); })();

View file

@ -1,15 +1,15 @@
/* /*
* 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.AttachmentPreviewView = Whisper.View.extend({ Whisper.AttachmentPreviewView = Whisper.View.extend({
className: 'attachment-preview', className: 'attachment-preview',
templateName: 'attachment-preview', templateName: 'attachment-preview',
render_attributes: function() { render_attributes: function() {
return {source: this.src}; return { source: this.src };
} },
}); });
})(); })();

View file

@ -9,7 +9,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
const FileView = Whisper.View.extend({ const FileView = Whisper.View.extend({
@ -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

@ -1,36 +1,36 @@
/* /*
* 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.BannerView = Whisper.View.extend({ Whisper.BannerView = Whisper.View.extend({
className: 'banner', className: 'banner',
templateName: 'banner', templateName: 'banner',
events: { events: {
'click .dismiss': 'onDismiss', 'click .dismiss': 'onDismiss',
'click .body': 'onClick', 'click .body': 'onClick',
}, },
initialize: function(options) { initialize: function(options) {
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) {
this.callbacks.onDismiss(); this.callbacks.onDismiss();
e.stopPropagation(); e.stopPropagation();
}, },
onClick: function() { onClick: function() {
this.callbacks.onClick(); this.callbacks.onClick();
} },
}); });
})(); })();

View file

@ -1,57 +1,57 @@
/* /*
* 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.ConfirmationDialogView = Whisper.View.extend({ Whisper.ConfirmationDialogView = Whisper.View.extend({
className: 'confirmation-dialog modal', className: 'confirmation-dialog modal',
templateName: 'confirmation-dialog', templateName: 'confirmation-dialog',
initialize: function(options) { initialize: function(options) {
this.message = options.message; this.message = options.message;
this.hideCancel = options.hideCancel; this.hideCancel = options.hideCancel;
this.resolve = options.resolve; this.resolve = options.resolve;
this.okText = options.okText || i18n('ok'); this.okText = options.okText || i18n('ok');
this.reject = options.reject; this.reject = options.reject;
this.cancelText = options.cancelText || i18n('cancel'); this.cancelText = options.cancelText || i18n('cancel');
this.render(); this.render();
}, },
events: { events: {
'keyup': 'onKeyup', keyup: 'onKeyup',
'click .ok': 'ok', 'click .ok': 'ok',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
render_attributes: function() { render_attributes: function() {
return { return {
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() {
this.remove(); this.remove();
if (this.resolve) { if (this.resolve) {
this.resolve(); this.resolve();
} }
}, },
cancel: function() { cancel: function() {
this.remove(); this.remove();
if (this.reject) { if (this.reject) {
this.reject(); this.reject();
} }
}, },
onKeyup: function(event) { onKeyup: function(event) {
if (event.key === 'Escape' || event.key === 'Esc') { if (event.key === 'Escape' || event.key === 'Esc') {
this.cancel(); this.cancel();
} }
}, },
focusCancel: function() { focusCancel: function() {
this.$('.cancel').focus(); this.$('.cancel').focus();
} },
}); });
})(); })();

View file

@ -1,53 +1,53 @@
/* /*
* 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.ContactListView = Whisper.ListView.extend({ Whisper.ContactListView = Whisper.ListView.extend({
tagName: 'div', tagName: 'div',
itemView: Whisper.View.extend({ itemView: Whisper.View.extend({
tagName: 'div', tagName: 'div',
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();
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
render_attributes: function() { render_attributes: function() {
if (this.model.id === this.ourNumber) { if (this.model.id === this.ourNumber) {
return { return {
title: i18n('me'), title: i18n('me'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
} }
return { return {
class: 'clickable', class: 'clickable',
title: this.model.getTitle(), title: this.model.getTitle(),
number: this.model.getNumber(), number: this.model.getNumber(),
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() {
if (this.model.id === this.ourNumber) { if (this.model.id === this.ourNumber) {
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

@ -1,73 +1,92 @@
/* /*
* 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 || {};
// list of conversations, showing user/group and last message sent // list of conversations, showing user/group and last message sent
Whisper.ConversationListItemView = Whisper.View.extend({ Whisper.ConversationListItemView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: function() { className: function() {
return 'conversation-list-item contact ' + this.model.cid; return 'conversation-list-item contact ' + this.model.cid;
}, },
templateName: 'conversation-preview', templateName: 'conversation-preview',
events: { events: {
'click': 'select' click: 'select',
}, },
initialize: function() { initialize: function() {
// auto update // auto update
this.listenTo(this.model, 'change', _.debounce(this.render.bind(this), 1000)); this.listenTo(
this.listenTo(this.model, 'destroy', this.remove); // auto update this.model,
this.listenTo(this.model, 'opened', this.markSelected); // auto update 'change',
_.debounce(this.render.bind(this), 1000)
);
this.listenTo(this.model, 'destroy', this.remove); // 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),
this.listenTo(this.model, 'newmessage', updateLastMessage); 1000
);
this.listenTo(
this.model.messageCollection,
'add remove',
updateLastMessage
);
this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed(function() { extension.windows.onClosed(
this.stopListening(); function() {
}.bind(this)); this.stopListening();
this.timeStampView = new Whisper.TimestampView({brief: true}); }.bind(this)
this.model.updateLastMessage(); );
}, this.timeStampView = new Whisper.TimestampView({ brief: true });
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) {
this.markSelected(); this.markSelected();
this.$el.trigger('select', this.model); this.$el.trigger('select', this.model);
}, },
render: function() { render: function() {
this.$el.html( this.$el.html(
Mustache.render(_.result(this,'template', ''), { Mustache.render(
title: this.model.getTitle(), _.result(this, 'template', ''),
last_message: this.model.get('lastMessage'), {
last_message_timestamp: this.model.get('timestamp'), title: this.model.getTitle(),
number: this.model.getNumber(), last_message: this.model.get('lastMessage'),
avatar: this.model.getAvatar(), last_message_timestamp: this.model.get('timestamp'),
profileName: this.model.getProfileName(), number: this.model.getNumber(),
unreadCount: this.model.get('unreadCount') avatar: this.model.getAvatar(),
}, this.render_partials()) profileName: this.model.getProfileName(),
); unreadCount: this.model.get('unreadCount'),
this.timeStampView.setElement(this.$('.last-timestamp')); },
this.timeStampView.update(); this.render_partials()
)
);
this.timeStampView.setElement(this.$('.last-timestamp'));
this.timeStampView.update();
emoji_util.parse(this.$('.name')); emoji_util.parse(this.$('.name'));
emoji_util.parse(this.$('.last-message')); emoji_util.parse(this.$('.last-message'));
var unread = this.model.get('unreadCount'); var unread = this.model.get('unreadCount');
if (unread > 0) { if (unread > 0) {
this.$el.addClass('unread'); this.$el.addClass('unread');
} else { } else {
this.$el.removeClass('unread'); this.$el.removeClass('unread');
} }
return this; return this;
} },
});
});
})(); })();

View file

@ -1,61 +1,61 @@
/* /*
* 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.ConversationListView = Whisper.ListView.extend({ Whisper.ConversationListView = Whisper.ListView.extend({
tagName: 'div', tagName: 'div',
itemView: Whisper.ConversationListItemView, itemView: Whisper.ConversationListItemView,
updateLocation: function(conversation) { updateLocation: function(conversation) {
var $el = this.$('.' + conversation.cid); var $el = this.$('.' + conversation.cid);
if (!$el || !$el.length) { if (!$el || !$el.length) {
console.log( console.log(
'updateLocation: did not find element for conversation', 'updateLocation: did not find element for conversation',
conversation.idForLogging() conversation.idForLogging()
); );
return; return;
} }
if ($el.length > 1) { if ($el.length > 1) {
console.log( console.log(
'updateLocation: found more than one element for conversation', 'updateLocation: found more than one element for conversation',
conversation.idForLogging() conversation.idForLogging()
); );
return; return;
} }
var $allConversations = this.$('.conversation-list-item'); var $allConversations = this.$('.conversation-list-item');
var inboxCollection = getInboxCollection(); var inboxCollection = getInboxCollection();
var index = inboxCollection.indexOf(conversation); var index = inboxCollection.indexOf(conversation);
var elIndex = $allConversations.index($el); var elIndex = $allConversations.index($el);
if (elIndex < 0) { if (elIndex < 0) {
console.log( console.log(
'updateLocation: did not find index for conversation', 'updateLocation: did not find index for conversation',
conversation.idForLogging() conversation.idForLogging()
); );
} }
if (index === elIndex) { if (index === elIndex) {
return; return;
} }
if (index === 0) { if (index === 0) {
this.$el.prepend($el); this.$el.prepend($el);
} else if (index === this.collection.length - 1) { } else if (index === this.collection.length - 1) {
this.$el.append($el); this.$el.append($el);
} else { } else {
var targetConversation = inboxCollection.at(index - 1); var targetConversation = inboxCollection.at(index - 1);
var target = this.$('.' + targetConversation.cid); var target = this.$('.' + targetConversation.cid);
$el.insertAfter(target); $el.insertAfter(target);
} }
}, },
removeItem: function(conversation) { removeItem: function(conversation) {
var $el = this.$('.' + conversation.cid); var $el = this.$('.' + conversation.cid);
if ($el && $el.length > 0) { if ($el && $el.length > 0) {
$el.remove(); $el.remove();
} }
} },
}); });
})(); })();

View file

@ -3,13 +3,12 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
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

@ -13,7 +13,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -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(
if (contact.isUnverified()) { unverified.map(contact => {
return contact.setVerifiedDefault(); if (contact.isUnverified()) {
} 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.onVerifiedChange(); this.model.updateVerified().then(() => {
this.statusFetch = null; this.onVerifiedChange();
console.log('done with status fetch'); this.statusFetch = null;
})); 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, {
count: DEFAULT_MEDIA_FETCH_COUNT, conversationId,
WhisperMessageCollection, count: DEFAULT_MEDIA_FETCH_COUNT,
}); WhisperMessageCollection,
const documents = await Signal.Backbone.Conversation.fetchFileAttachments({ }
conversationId, );
count: DEFAULT_DOCUMENTS_FETCH_COUNT, const documents = await Signal.Backbone.Conversation.fetchFileAttachments(
WhisperMessageCollection, {
}); conversationId,
count: DEFAULT_DOCUMENTS_FETCH_COUNT,
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

@ -2,7 +2,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -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

@ -1,16 +1,16 @@
/* /*
* 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 ErrorView = Whisper.View.extend({ var ErrorView = Whisper.View.extend({
className: 'error', className: 'error',
templateName: 'generic-error', templateName: 'generic-error',
render_attributes: function() { render_attributes: function() {
return this.model; return this.model;
} },
}); });
})(); })();

View file

@ -7,7 +7,7 @@
/* global Signal: false */ /* global Signal: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -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(
if (!entry) { { type: 'openFile' },
return; entry => {
if (!entry) {
return;
}
entry.file(file => {
this.file = file;
this.previewImages();
});
} }
entry.file((file) => { );
this.file = file;
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 === 'image/gif' || file.type.split('/')[0] !== 'image' ||
file.type === 'image/tiff') { file.type === 'image/gif' ||
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

@ -1,40 +1,40 @@
/* /*
* 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 || {};
// TODO: take a title string which could replace the 'members' header // TODO: take a title string which could replace the 'members' header
Whisper.GroupMemberList = Whisper.View.extend({ Whisper.GroupMemberList = Whisper.View.extend({
className: 'group-member-list panel', className: 'group-member-list panel',
templateName: 'group-member-list', templateName: 'group-member-list',
initialize: function(options) { initialize: function(options) {
this.needVerify = options.needVerify; this.needVerify = options.needVerify;
this.render(); this.render();
this.member_list_view = new Whisper.ContactListView({ this.member_list_view = new Whisper.ContactListView({
collection: this.model, collection: this.model,
className: 'members', className: 'members',
toInclude: { toInclude: {
listenBack: options.listenBack listenBack: options.listenBack,
}
});
this.member_list_view.render();
this.$('.container').append(this.member_list_view.el);
}, },
render_attributes: function() { });
var summary; this.member_list_view.render();
if (this.needVerify) {
summary = i18n('membersNeedingVerification');
}
return { this.$('.container').append(this.member_list_view.el);
members: i18n('groupMembers'), },
summary: summary render_attributes: function() {
}; var summary;
} if (this.needVerify) {
}); summary = i18n('membersNeedingVerification');
}
return {
members: i18n('groupMembers'),
summary: summary,
};
},
});
})(); })();

View file

@ -1,33 +1,32 @@
/* /*
* 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.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) {
this.$el.text(this.model.left + ' left the group'); this.$el.text(this.model.left + ' left the group');
return this; return this;
} }
var messages = ['Updated the group.']; var messages = ['Updated the group.'];
if (this.model.name) { if (this.model.name) {
messages.push("Title is now '" + this.model.name + "'."); messages.push("Title is now '" + this.model.name + "'.");
} }
if (this.model.joined) { if (this.model.joined) {
messages.push(this.model.joined.join(', ') + ' joined the group'); messages.push(this.model.joined.join(', ') + ' joined the group');
} }
this.$el.text(messages.join(' ')); this.$el.text(messages.join(' '));
return this;
}
});
return this;
},
});
})(); })();

View file

@ -1,17 +1,17 @@
/* /*
* 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.HintView = Whisper.View.extend({ Whisper.HintView = Whisper.View.extend({
templateName: 'hint', templateName: 'hint',
initialize: function(options) { initialize: function(options) {
this.content = options.content; this.content = options.content;
}, },
render_attributes: function() { render_attributes: function() {
return { content: this.content }; return { content: this.content };
} },
}); });
})(); })();

View file

@ -1,59 +1,60 @@
/* /*
* 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 || {};
/* /*
* Render an avatar identicon to an svg for use in a notification. * Render an avatar identicon to an svg for use in a notification.
*/ */
Whisper.IdenticonSVGView = Whisper.View.extend({ Whisper.IdenticonSVGView = Whisper.View.extend({
templateName: 'identicon-svg', templateName: 'identicon-svg',
initialize: function(options) { initialize: function(options) {
this.render_attributes = options; this.render_attributes = options;
this.render_attributes.color = COLORS[this.render_attributes.color]; this.render_attributes.color = COLORS[this.render_attributes.color];
}, },
getSVGUrl: function() { getSVGUrl: function() {
var html = this.render().$el.html(); var html = this.render().$el.html();
var svg = new Blob([html], {type: 'image/svg+xml;charset=utf-8'}); var svg = new Blob([html], { type: 'image/svg+xml;charset=utf-8' });
return URL.createObjectURL(svg); return URL.createObjectURL(svg);
}, },
getDataUrl: function() { getDataUrl: function() {
var svgurl = this.getSVGUrl(); var svgurl = this.getSVGUrl();
return new Promise(function(resolve) { return new Promise(function(resolve) {
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,
var ctx = canvas.getContext('2d'); maxHeight: 100,
ctx.drawImage(img, 0, 0); });
URL.revokeObjectURL(svgurl); var ctx = canvas.getContext('2d');
resolve(canvas.toDataURL('image/png')); ctx.drawImage(img, 0, 0);
}; URL.revokeObjectURL(svgurl);
resolve(canvas.toDataURL('image/png'));
};
img.src = svgurl; img.src = svgurl;
}); });
} },
}); });
var COLORS = {
red : '#EF5350',
pink : '#EC407A',
purple : '#AB47BC',
deep_purple : '#7E57C2',
indigo : '#5C6BC0',
blue : '#2196F3',
light_blue : '#03A9F4',
cyan : '#00BCD4',
teal : '#009688',
green : '#4CAF50',
light_green : '#7CB342',
orange : '#FF9800',
deep_orange : '#FF5722',
amber : '#FFB300',
blue_grey : '#607D8B'
};
var COLORS = {
red: '#EF5350',
pink: '#EC407A',
purple: '#AB47BC',
deep_purple: '#7E57C2',
indigo: '#5C6BC0',
blue: '#2196F3',
light_blue: '#03A9F4',
cyan: '#00BCD4',
teal: '#009688',
green: '#4CAF50',
light_green: '#7CB342',
orange: '#FF9800',
deep_orange: '#FF5722',
amber: '#FFB300',
blue_grey: '#607D8B',
};
})(); })();

View file

@ -1,51 +1,54 @@
/* /*
* 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.IdentityKeySendErrorPanelView = Whisper.View.extend({ Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({
className: 'identity-key-send-error panel', className: 'identity-key-send-error panel',
templateName: 'identity-key-send-error', templateName: 'identity-key-send-error',
initialize: function(options) { initialize: function(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
this.wasUnverified = this.model.isUnverified(); this.wasUnverified = this.model.isUnverified();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
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);
}, },
sendAnyway: function() { sendAnyway: function() {
this.resetPanel(); this.resetPanel();
this.trigger('send-anyway'); this.trigger('send-anyway');
}, },
cancel: function() { cancel: function() {
this.resetPanel(); this.resetPanel();
}, },
render_attributes: function() { render_attributes: function() {
var send = i18n('sendAnyway'); var send = i18n('sendAnyway');
if (this.wasUnverified && !this.model.isUnverified()) { if (this.wasUnverified && !this.model.isUnverified()) {
send = i18n('resend'); send = i18n('resend');
} }
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]); var errorExplanation = i18n('identityKeyErrorOnSend', [
return { this.model.getTitle(),
errorExplanation : errorExplanation, this.model.getTitle(),
showSafetyNumber : i18n('showSafetyNumber'), ]);
sendAnyway : send, return {
cancel : i18n('cancel') errorExplanation: errorExplanation,
}; showSafetyNumber: i18n('showSafetyNumber'),
} sendAnyway: send,
}); cancel: i18n('cancel'),
};
},
});
})(); })();

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 || {};
@ -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(
this.doImport(directory); function(directory) {
}.bind(this), function(error) { this.doImport(directory);
if (error.name !== 'ChooseError') { }.bind(this),
console.log( function(error) {
'Error choosing directory:', if (error.name !== 'ChooseError') {
error && error.stack ? error.stack : error console.log(
); 'Error choosing directory:',
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,53 +130,69 @@
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
// For resilience to interruption, clear database both before and on failure .then(function() {
return Whisper.Import.reset(); // For resilience to interruption, clear database both before and on failure
}).then(function() { return Whisper.Import.reset();
return Promise.all([ })
Whisper.Import.start(), .then(function() {
window.Signal.Backup.importFromDirectory(directory) return Promise.all([
]); Whisper.Import.start(),
}).then(function(results) { window.Signal.Backup.importFromDirectory(directory),
var importResult = results[1]; ]);
})
.then(
function(results) {
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
if (importResult.fullImport) { if (importResult.fullImport) {
return this.finishFullImport(directory); return this.finishFullImport(directory);
} }
// 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()
return Promise.all([ .then(function() {
return Promise.all([
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete(), Whisper.Import.complete(),
]); ]);
}).then(function() { })
this.state = State.LIGHT_COMPLETE; .then(
this.render(); function() {
}.bind(this)); this.state = State.LIGHT_COMPLETE;
this.render();
}.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() { })
this.state = State.COMPLETE; .then(
this.render(); function() {
}.bind(this)); this.state = State.COMPLETE;
} this.render();
}.bind(this)
);
},
}); });
})(); })();

View file

@ -5,7 +5,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -15,9 +15,12 @@
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
this.pause(); .first()
}); .find('video, audio')
.each(function pauseMedia() {
this.pause();
});
let $el = this.$(`#${id}`); let $el = this.$(`#${id}`);
if ($el === null || $el.length === 0) { if ($el === null || $el.length === 0) {
const view = new Whisper.ConversationView({ const view = new Whisper.ConversationView({
@ -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

@ -1,196 +1,204 @@
/* /*
* 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 Steps = { var Steps = {
INSTALL_SIGNAL: 2, INSTALL_SIGNAL: 2,
SCAN_QR_CODE: 3, SCAN_QR_CODE: 3,
ENTER_NAME: 4, ENTER_NAME: 4,
PROGRESS_BAR: 5, PROGRESS_BAR: 5,
TOO_MANY_DEVICES: 'TooManyDevices', TOO_MANY_DEVICES: 'TooManyDevices',
NETWORK_ERROR: 'NetworkError', NETWORK_ERROR: 'NetworkError',
}; };
var DEVICE_NAME_SELECTOR = 'input.device-name'; var DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1; var CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411; var TOO_MANY_DEVICES = 411;
Whisper.InstallView = Whisper.View.extend({ Whisper.InstallView = Whisper.View.extend({
templateName: 'link-flow-template', templateName: 'link-flow-template',
className: 'main full-screen-flow', className: 'main full-screen-flow',
events: { events: {
'click .try-again': 'connect', 'click .try-again': 'connect',
'click .finish': 'finishLinking', 'click .finish': 'finishLinking',
// the actual next step happens in confirmNumber() on submit form #link-phone // the actual next step happens in confirmNumber() on submit form #link-phone
}, },
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
this.selectStep(Steps.SCAN_QR_CODE); this.selectStep(Steps.SCAN_QR_CODE);
this.connect(); this.connect();
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
// 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() { },
var errorMessage; render_attributes: function() {
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');
} else if (
this.error.name === 'HTTPError' &&
this.error.code == CONNECTION_ERROR
) {
errorMessage = i18n('installConnectionFailed');
} else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message
errorMessage = i18n('installConnectionFailed');
}
errorMessage = i18n('installTooManyDevices'); return {
} isError: true,
else if (this.error.name === 'HTTPError' errorHeader: 'Something went wrong!',
&& this.error.code == CONNECTION_ERROR) { errorMessage,
errorButton: 'Try again',
};
}
errorMessage = i18n('installConnectionFailed'); return {
} isStep3: this.step === Steps.SCAN_QR_CODE,
else if (this.error.message === 'websocket closed') { linkYourPhone: i18n('linkYourPhone'),
// AccountManager.registerSecondDevice uses this specific signalSettings: i18n('signalSettings'),
// 'websocket closed' error message linkedDevices: i18n('linkedDevices'),
errorMessage = i18n('installConnectionFailed'); androidFinalStep: i18n('plusButton'),
} appleFinalStep: i18n('linkNewDevice'),
return { isStep4: this.step === Steps.ENTER_NAME,
isError: true, chooseName: i18n('chooseDeviceName'),
errorHeader: 'Something went wrong!', finishLinkingPhoneButton: i18n('finishLinkingPhone'),
errorMessage,
errorButton: 'Try again',
};
}
return { isStep5: this.step === Steps.PROGRESS_BAR,
isStep3: this.step === Steps.SCAN_QR_CODE, syncing: i18n('initialSync'),
linkYourPhone: i18n('linkYourPhone'), };
signalSettings: i18n('signalSettings'), },
linkedDevices: i18n('linkedDevices'), selectStep: function(step) {
androidFinalStep: i18n('plusButton'), this.step = step;
appleFinalStep: i18n('linkNewDevice'), this.render();
},
connect: function() {
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
isStep4: this.step === Steps.ENTER_NAME, var accountManager = getAccountManager();
chooseName: i18n('chooseDeviceName'),
finishLinkingPhoneButton: i18n('finishLinkingPhone'),
isStep5: this.step === Steps.PROGRESS_BAR, accountManager
syncing: i18n('initialSync'), .registerSecondDevice(
}; this.setProvisioningUrl.bind(this),
}, this.confirmNumber.bind(this)
selectStep: function(step) { )
this.step = step; .catch(this.handleDisconnect.bind(this));
this.render(); },
}, handleDisconnect: function(e) {
connect: function() { console.log('provisioning failed', e.stack);
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
var accountManager = getAccountManager(); this.error = e;
this.render();
accountManager.registerSecondDevice( if (e.message === 'websocket closed') {
this.setProvisioningUrl.bind(this), this.trigger('disconnected');
this.confirmNumber.bind(this) } else if (
).catch(this.handleDisconnect.bind(this)); e.name !== 'HTTPError' ||
}, (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
handleDisconnect: function(e) { ) {
console.log('provisioning failed', e.stack); throw e;
}
},
reconnect: function() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.timeout = setTimeout(this.connect.bind(this), 10000);
},
clearQR: function() {
this.$('#qr img').remove();
this.$('#qr canvas').remove();
this.$('#qr .container').show();
this.$('#qr').removeClass('ready');
},
setProvisioningUrl: function(url) {
if ($('#qr').length === 0) {
console.log('Did not find #qr element in the DOM!');
return;
}
this.error = e; this.$('#qr .container').hide();
this.render(); this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);
this.$('#qr').removeAttr('title');
this.$('#qr').addClass('ready');
},
setDeviceNameDefault: function() {
var deviceName = textsecure.storage.user.getDeviceName();
if (e.message === 'websocket closed') { this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname);
this.trigger('disconnected'); this.$(DEVICE_NAME_SELECTOR).focus();
} else if (e.name !== 'HTTPError' },
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) { finishLinking: function() {
// We use a form so we get submit-on-enter behavior
this.$('#link-phone').submit();
},
confirmNumber: function(number) {
var tsp = textsecure.storage.protocol;
throw e; window.removeSetupMenuItems();
} this.selectStep(Steps.ENTER_NAME);
}, this.setDeviceNameDefault();
reconnect: function() {
if (this.timeout) { return new Promise(
clearTimeout(this.timeout); function(resolve, reject) {
this.timeout = null; this.$('#link-phone').submit(
} function(e) {
this.timeout = setTimeout(this.connect.bind(this), 10000); e.stopPropagation();
}, e.preventDefault();
clearQR: function() {
this.$('#qr img').remove(); var name = this.$(DEVICE_NAME_SELECTOR).val();
this.$('#qr canvas').remove(); name = name.replace(/\0/g, ''); // strip unicode null
this.$('#qr .container').show(); if (name.trim().length === 0) {
this.$('#qr').removeClass('ready'); this.$(DEVICE_NAME_SELECTOR).focus();
},
setProvisioningUrl: function(url) {
if ($('#qr').length === 0) {
console.log('Did not find #qr element in the DOM!');
return; return;
} }
this.$('#qr .container').hide(); this.selectStep(Steps.PROGRESS_BAR);
this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);
this.$('#qr').removeAttr('title');
this.$('#qr').addClass('ready');
},
setDeviceNameDefault: function() {
var deviceName = textsecure.storage.user.getDeviceName();
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname); var finish = function() {
this.$(DEVICE_NAME_SELECTOR).focus(); resolve(name);
}, };
finishLinking: function() {
// We use a form so we get submit-on-enter behavior
this.$('#link-phone').submit();
},
confirmNumber: function(number) {
var tsp = textsecure.storage.protocol;
window.removeSetupMenuItems(); // Delete all data from database unless we're in the middle
this.selectStep(Steps.ENTER_NAME); // of a re-link, or we are finishing a light import. Without this,
this.setDeviceNameDefault(); // app restarts at certain times can cause weird things to happen,
// like data from a previous incomplete light import showing up
// after a new install.
if (this.shouldRetainData) {
return finish();
}
return new Promise(function(resolve, reject) { tsp.removeAllData().then(finish, function(error) {
this.$('#link-phone').submit(function(e) { console.log(
e.stopPropagation(); 'confirmNumber: error clearing database',
e.preventDefault(); error && error.stack ? error.stack : error
);
var name = this.$(DEVICE_NAME_SELECTOR).val(); finish();
name = name.replace(/\0/g,''); // strip unicode null });
if (name.trim().length === 0) { }.bind(this)
this.$(DEVICE_NAME_SELECTOR).focus(); );
return; }.bind(this)
} );
},
this.selectStep(Steps.PROGRESS_BAR); });
var finish = function() {
resolve(name);
};
// Delete all data from database unless we're in the middle
// of a re-link, or we are finishing a light import. Without this,
// app restarts at certain times can cause weird things to happen,
// like data from a previous incomplete light import showing up
// after a new install.
if (this.shouldRetainData) {
return finish();
}
tsp.removeAllData().then(finish, function(error) {
console.log(
'confirmNumber: error clearing database',
error && error.stack ? error.stack : error
);
finish();
});
}.bind(this));
}.bind(this));
},
});
})(); })();

View file

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

View file

@ -1,36 +1,38 @@
/* /*
* 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 FIVE_SECONDS = 5 * 1000; var FIVE_SECONDS = 5 * 1000;
Whisper.LastSeenIndicatorView = Whisper.View.extend({ Whisper.LastSeenIndicatorView = Whisper.View.extend({
className: 'last-seen-indicator-view', className: 'last-seen-indicator-view',
templateName: 'last-seen-indicator-view', templateName: 'last-seen-indicator-view',
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment: function(count) {
this.count += count; this.count += count;
this.render(); this.render();
}, },
getCount: function() { getCount: function() {
return this.count; return this.count;
}, },
render_attributes: function() { render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage') var unreadMessages =
: i18n('unreadMessages', [this.count]); this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]);
return { return {
unreadMessages: unreadMessages unreadMessages: unreadMessages,
}; };
} },
}); });
})(); })();

View file

@ -1,40 +1,40 @@
/* /*
* 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 || {};
/* /*
* Generic list view that watches a given collection, wraps its members in * Generic list view that watches a given collection, wraps its members in
* a given child view and adds the child view elements to its own element. * a given child view and adds the child view elements to its own element.
*/ */
Whisper.ListView = Backbone.View.extend({ Whisper.ListView = Backbone.View.extend({
tagName: 'ul', tagName: 'ul',
itemView: Backbone.View, itemView: Backbone.View,
initialize: function(options) { initialize: function(options) {
this.options = options || {}; this.options = options || {};
this.listenTo(this.collection, 'add', this.addOne); this.listenTo(this.collection, 'add', this.addOne);
this.listenTo(this.collection, 'reset', this.addAll); this.listenTo(this.collection, 'reset', this.addAll);
}, },
addOne: function(model) { addOne: function(model) {
if (this.itemView) { if (this.itemView) {
var options = _.extend({}, this.options.toInclude, {model: model}); var options = _.extend({}, this.options.toInclude, { model: model });
var view = new this.itemView(options); var view = new this.itemView(options);
this.$el.append(view.render().el); this.$el.append(view.render().el);
this.$el.trigger('add'); this.$el.trigger('add');
} }
}, },
addAll: function() { addAll: function() {
this.$el.html(''); this.$el.html('');
this.collection.each(this.addOne, this); this.collection.each(this.addOne, this);
}, },
render: function() { render: function() {
this.addAll(); this.addAll();
return this; return this;
} },
}); });
})(); })();

View file

@ -1,168 +1,193 @@
/* /*
* 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 ContactView = Whisper.View.extend({ var ContactView = Whisper.View.extend({
className: 'contact-detail', className: 'contact-detail',
templateName: 'contact-detail', templateName: 'contact-detail',
initialize: function(options) { initialize: function(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
this.message = options.message; this.message = options.message;
var newIdentity = i18n('newIdentity'); var newIdentity = i18n('newIdentity');
this.errors = _.map(options.errors, function(error) { this.errors = _.map(options.errors, function(error) {
if (error.name === 'OutgoingIdentityKeyError') { if (error.name === 'OutgoingIdentityKeyError') {
error.message = newIdentity; error.message = newIdentity;
}
return error;
});
this.outgoingKeyError = _.find(this.errors, function(error) {
return error.name === 'OutgoingIdentityKeyError';
});
},
events: {
'click': 'onClick'
},
onClick: function() {
if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model,
listenBack: this.listenBack,
resetPanel: this.resetPanel
});
this.listenTo(view, 'send-anyway', this.onSendAnyway);
view.render();
this.listenBack(view);
view.$('.cancel').focus();
}
},
forceSend: function() {
this.model.updateVerified().then(function() {
if (this.model.isUnverified()) {
return this.model.setVerifiedDefault();
}
}.bind(this)).then(function() {
return this.model.isUntrusted();
}.bind(this)).then(function(untrusted) {
if (untrusted) {
return this.model.setApproved();
}
}.bind(this)).then(function() {
this.message.resend(this.outgoingKeyError.number);
}.bind(this));
},
onSendAnyway: function() {
if (this.outgoingKeyError) {
this.forceSend();
}
},
render_attributes: function() {
var showButton = Boolean(this.outgoingKeyError);
return {
status : this.message.getStatus(this.model.id),
name : this.model.getTitle(),
avatar : this.model.getAvatar(),
errors : this.errors,
showErrorButton : showButton,
errorButtonLabel : i18n('view')
};
} }
}); return error;
});
this.outgoingKeyError = _.find(this.errors, function(error) {
return error.name === 'OutgoingIdentityKeyError';
});
},
events: {
click: 'onClick',
},
onClick: function() {
if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model,
listenBack: this.listenBack,
resetPanel: this.resetPanel,
});
Whisper.MessageDetailView = Whisper.View.extend({ this.listenTo(view, 'send-anyway', this.onSendAnyway);
className: 'message-detail panel',
templateName: 'message-detail',
initialize: function(options) {
this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel;
this.view = new Whisper.MessageView({model: this.model}); view.render();
this.view.render();
this.conversation = options.conversation;
this.listenTo(this.model, 'change', this.render); this.listenBack(view);
}, view.$('.cancel').focus();
events: { }
'click button.delete': 'onDelete' },
}, forceSend: function() {
onDelete: function() { this.model
var dialog = new Whisper.ConfirmationDialogView({ .updateVerified()
message: i18n('deleteWarning'), .then(
okText: i18n('delete'), function() {
resolve: function() { if (this.model.isUnverified()) {
this.model.destroy(); return this.model.setVerifiedDefault();
this.resetPanel();
}.bind(this)
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
getContacts: function() {
// Return the set of models to be rendered in this view
var ids;
if (this.model.isIncoming()) {
ids = [ this.model.get('source') ];
} else if (this.model.isOutgoing()) {
ids = this.model.get('recipients');
if (!ids) {
// older messages have no recipients field
// use the current set of recipients
ids = this.conversation.getRecipients();
}
} }
return Promise.all(ids.map(function(number) { }.bind(this)
return ConversationController.getOrCreateAndWait(number, 'private'); )
})); .then(
}, function() {
renderContact: function(contact) { return this.model.isUntrusted();
var view = new ContactView({ }.bind(this)
model: contact, )
errors: this.grouped[contact.id], .then(
listenBack: this.listenBack, function(untrusted) {
resetPanel: this.resetPanel, if (untrusted) {
message: this.model return this.model.setApproved();
}).render(); }
this.$('.contacts').append(view.el); }.bind(this)
}, )
render: function() { .then(
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) { function() {
return Boolean(error.number); this.message.resend(this.outgoingKeyError.number);
}); }.bind(this)
);
},
onSendAnyway: function() {
if (this.outgoingKeyError) {
this.forceSend();
}
},
render_attributes: function() {
var showButton = Boolean(this.outgoingKeyError);
this.$el.html(Mustache.render(_.result(this, 'template', ''), { return {
sent_at : moment(this.model.get('sent_at')).format('LLLL'), status: this.message.getStatus(this.model.id),
received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null, name: this.model.getTitle(),
tofrom : this.model.isIncoming() ? i18n('from') : i18n('to'), avatar: this.model.getAvatar(),
errors : errorsWithoutNumber, errors: this.errors,
title : i18n('messageDetail'), showErrorButton: showButton,
sent : i18n('sent'), errorButtonLabel: i18n('view'),
received : i18n('received'), };
errorLabel : i18n('error'), },
deleteLabel : i18n('deleteMessage'), });
retryDescription: i18n('retryDescription')
}));
this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number'); Whisper.MessageDetailView = Whisper.View.extend({
className: 'message-detail panel',
templateName: 'message-detail',
initialize: function(options) {
this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel;
this.getContacts().then(function(contacts) { this.view = new Whisper.MessageView({ model: this.model });
_.sortBy(contacts, function(c) { this.view.render();
var prefix = this.grouped[c.id] ? '0' : '1'; this.conversation = options.conversation;
// this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical this.listenTo(this.model, 'change', this.render);
return prefix + c.getTitle(); },
}.bind(this)).forEach(this.renderContact.bind(this)); events: {
}.bind(this)); 'click button.delete': 'onDelete',
},
onDelete: function() {
var dialog = new Whisper.ConfirmationDialogView({
message: i18n('deleteWarning'),
okText: i18n('delete'),
resolve: function() {
this.model.destroy();
this.resetPanel();
}.bind(this),
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
getContacts: function() {
// Return the set of models to be rendered in this view
var ids;
if (this.model.isIncoming()) {
ids = [this.model.get('source')];
} else if (this.model.isOutgoing()) {
ids = this.model.get('recipients');
if (!ids) {
// older messages have no recipients field
// use the current set of recipients
ids = this.conversation.getRecipients();
} }
}); }
return Promise.all(
ids.map(function(number) {
return ConversationController.getOrCreateAndWait(number, 'private');
})
);
},
renderContact: function(contact) {
var view = new ContactView({
model: contact,
errors: this.grouped[contact.id],
listenBack: this.listenBack,
resetPanel: this.resetPanel,
message: this.model,
}).render();
this.$('.contacts').append(view.el);
},
render: function() {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(
error
) {
return Boolean(error.number);
});
this.$el.html(
Mustache.render(_.result(this, 'template', ''), {
sent_at: moment(this.model.get('sent_at')).format('LLLL'),
received_at: this.model.isIncoming()
? moment(this.model.get('received_at')).format('LLLL')
: null,
tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'),
errors: errorsWithoutNumber,
title: i18n('messageDetail'),
sent: i18n('sent'),
received: i18n('received'),
errorLabel: i18n('error'),
deleteLabel: i18n('deleteMessage'),
retryDescription: i18n('retryDescription'),
})
);
this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number');
this.getContacts().then(
function(contacts) {
_.sortBy(
contacts,
function(c) {
var prefix = this.grouped[c.id] ? '0' : '1';
// this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical
return prefix + c.getTitle();
}.bind(this)
).forEach(this.renderContact.bind(this));
}.bind(this)
);
},
});
})(); })();

View file

@ -1,119 +1,123 @@
/* /*
* 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.MessageListView = Whisper.ListView.extend({ Whisper.MessageListView = Whisper.ListView.extend({
tagName: 'ul', tagName: 'ul',
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(
this.$el.trigger('lazyScroll'); function() {
}.bind(this), 500); this.$el.trigger('lazyScroll');
}, }.bind(this),
onScroll: function() { 500
this.measureScrollPosition(); );
if (this.$el.scrollTop() === 0) { },
this.$el.trigger('loadMore'); onScroll: function() {
} this.measureScrollPosition();
if (this.atBottom()) { if (this.$el.scrollTop() === 0) {
this.$el.trigger('atBottom'); this.$el.trigger('loadMore');
} else if (this.bottomOffset > this.outerHeight) { }
this.$el.trigger('farFromBottom'); if (this.atBottom()) {
} this.$el.trigger('atBottom');
} else if (this.bottomOffset > this.outerHeight) {
this.$el.trigger('farFromBottom');
}
this.triggerLazyScroll(); this.triggerLazyScroll();
}, },
atBottom: function() { atBottom: function() {
return this.bottomOffset < 30; return this.bottomOffset < 30;
}, },
measureScrollPosition: function() { measureScrollPosition: function() {
if (this.el.scrollHeight === 0) { // hidden if (this.el.scrollHeight === 0) {
return; // hidden
} return;
this.outerHeight = this.$el.outerHeight(); }
this.scrollPosition = this.$el.scrollTop() + this.outerHeight; this.outerHeight = this.$el.outerHeight();
this.scrollHeight = this.el.scrollHeight; this.scrollPosition = this.$el.scrollTop() + this.outerHeight;
this.bottomOffset = this.scrollHeight - this.scrollPosition; this.scrollHeight = this.el.scrollHeight;
}, this.bottomOffset = this.scrollHeight - this.scrollPosition;
resetScrollPosition: function() { },
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight()); resetScrollPosition: function() {
}, this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
scrollToBottomIfNeeded: function() { },
// This is counter-intuitive. Our current bottomOffset is reflective of what scrollToBottomIfNeeded: function() {
// we last measured, not necessarily the current state. And this is called // This is counter-intuitive. Our current bottomOffset is reflective of what
// after we just made a change to the DOM: inserting a message, or an image // we last measured, not necessarily the current state. And this is called
// finished loading. So if we were near the bottom before, we _need_ to be // after we just made a change to the DOM: inserting a message, or an image
// at the bottom again. So we scroll to the bottom. // finished loading. So if we were near the bottom before, we _need_ to be
if (this.atBottom()) { // at the bottom again. So we scroll to the bottom.
this.scrollToBottom(); if (this.atBottom()) {
} this.scrollToBottom();
}, }
scrollToBottom: function() { },
this.$el.scrollTop(this.el.scrollHeight); scrollToBottom: function() {
this.measureScrollPosition(); this.$el.scrollTop(this.el.scrollHeight);
}, this.measureScrollPosition();
addOne: function(model) { },
var view; addOne: function(model) {
if (model.isExpirationTimerUpdate()) { var view;
view = new Whisper.ExpirationTimerUpdateView({model: model}).render(); if (model.isExpirationTimerUpdate()) {
} else if (model.get('type') === 'keychange') { view = new Whisper.ExpirationTimerUpdateView({ model: model }).render();
view = new Whisper.KeyChangeView({model: model}).render(); } else if (model.get('type') === 'keychange') {
} else if (model.get('type') === 'verified-change') { view = new Whisper.KeyChangeView({ model: model }).render();
view = new Whisper.VerifiedChangeView({model: model}).render(); } else if (model.get('type') === 'verified-change') {
} else { view = new Whisper.VerifiedChangeView({ model: model }).render();
view = new this.itemView({model: model}).render(); } else {
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition); view = new this.itemView({ model: model }).render();
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded); this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
} this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
}
var index = this.collection.indexOf(model); var index = this.collection.indexOf(model);
this.measureScrollPosition(); this.measureScrollPosition();
if (model.get('unread') && !this.atBottom()) { if (model.get('unread') && !this.atBottom()) {
this.$el.trigger('newOffscreenMessage'); this.$el.trigger('newOffscreenMessage');
} }
if (index === this.collection.length - 1) { if (index === this.collection.length - 1) {
// add to the bottom. // add to the bottom.
this.$el.append(view.el); this.$el.append(view.el);
} else if (index === 0) { } else if (index === 0) {
// add to top // add to top
this.$el.prepend(view.el); this.$el.prepend(view.el);
} else { } else {
// insert // insert
var next = this.$('#' + this.collection.at(index + 1).id); var next = this.$('#' + this.collection.at(index + 1).id);
var prev = this.$('#' + this.collection.at(index - 1).id); var prev = this.$('#' + this.collection.at(index - 1).id);
if (next.length > 0) { if (next.length > 0) {
view.$el.insertBefore(next); view.$el.insertBefore(next);
} else if (prev.length > 0) { } else if (prev.length > 0) {
view.$el.insertAfter(prev); view.$el.insertAfter(prev);
} else { } else {
// scan for the right spot // scan for the right spot
var elements = this.$el.children(); var elements = this.$el.children();
if (elements.length > 0) { if (elements.length > 0) {
for (var i = 0; i < elements.length; ++i) { for (var i = 0; i < elements.length; ++i) {
var m = this.collection.get(elements[i].id); var m = this.collection.get(elements[i].id);
var m_index = this.collection.indexOf(m); var m_index = this.collection.indexOf(m);
if (m_index > index) { if (m_index > index) {
view.$el.insertBefore(elements[i]); view.$el.insertBefore(elements[i]);
break; break;
} }
}
} else {
this.$el.append(view.el);
}
}
} }
this.scrollToBottomIfNeeded(); } else {
}, this.$el.append(view.el);
}); }
}
}
this.scrollToBottomIfNeeded();
},
});
})(); })();

View file

@ -7,7 +7,7 @@
/* global $: false */ /* global $: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
const { Signal } = window; const { Signal } = window;
@ -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(
message: this.model.get('body'), Mustache.render(
hasBody, _.result(this, 'template', ''),
timestamp: this.model.get('sent_at'), {
sender: (contact && contact.getTitle()) || '', message: this.model.get('body'),
avatar: (contact && contact.getAvatar()), hasBody,
profileName: (contact && contact.getProfileName()), timestamp: this.model.get('sent_at'),
innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail', sender: (contact && contact.getTitle()) || '',
hoverIcon: !hasErrors, avatar: contact && contact.getAvatar(),
hasAttachments, profileName: contact && contact.getProfileName(),
reply: i18n('replyToMessage'), innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail',
}, this.render_partials())); hoverIcon: !hasErrors,
hasAttachments,
reply: i18n('replyToMessage'),
},
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,22 +549,26 @@
} }
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(
const attachmentWithData = await loadAttachmentData(attachment); attachment =>
const view = new Whisper.AttachmentView({ new Promise(async resolve => {
model: attachmentWithData, const attachmentWithData = await loadAttachmentData(attachment);
timestamp: this.model.get('sent_at'), const view = new Whisper.AttachmentView({
}); model: attachmentWithData,
timestamp: this.model.get('sent_at'),
});
this.listenTo(view, 'update', () => { this.listenTo(view, 'update', () => {
// NOTE: Can we do without `updated` flag now that we use promises? // NOTE: Can we do without `updated` flag now that we use promises?
view.updated = true; view.updated = true;
resolve(view); resolve(view);
}); });
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

@ -1,114 +1,120 @@
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.NetworkStatusView = Whisper.View.extend({ Whisper.NetworkStatusView = Whisper.View.extend({
className: 'network-status', className: 'network-status',
templateName: 'networkStatus', templateName: 'networkStatus',
initialize: function() { initialize: function() {
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(
clearInterval(this.renderIntervalHandle); function() {
}.bind(this)); clearInterval(this.renderIntervalHandle);
}.bind(this)
);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
this.withinConnectingGracePeriod = true; this.withinConnectingGracePeriod = true;
this.setSocketReconnectInterval(null); this.setSocketReconnectInterval(null);
window.addEventListener('online', this.update.bind(this)); window.addEventListener('online', this.update.bind(this));
window.addEventListener('offline', this.update.bind(this)); window.addEventListener('offline', this.update.bind(this));
this.model = new Backbone.Model(); this.model = new Backbone.Model();
this.listenTo(this.model, 'change', this.onChange); this.listenTo(this.model, 'change', this.onChange);
}, },
onReconnectTimer: function() { onReconnectTimer: function() {
this.setSocketReconnectInterval(60000); this.setSocketReconnectInterval(60000);
}, },
finishConnectingGracePeriod: function() { finishConnectingGracePeriod: function() {
this.withinConnectingGracePeriod = false; this.withinConnectingGracePeriod = false;
}, },
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;
getNetworkStatus: function() { },
getSocketStatus: function() {
var message = ''; return window.getSocketStatus();
var instructions = ''; },
var hasInterruption = false; getNetworkStatus: function() {
var action = null; var message = '';
var buttonClass = null; var instructions = '';
var hasInterruption = false;
var socketStatus = this.getSocketStatus(); var action = null;
switch(socketStatus) { var buttonClass = null;
case WebSocket.CONNECTING:
message = i18n('connecting');
this.setSocketReconnectInterval(null);
break;
case WebSocket.OPEN:
this.setSocketReconnectInterval(null);
break;
case WebSocket.CLOSING:
message = i18n('disconnected');
instructions = i18n('checkNetworkConnection');
hasInterruption = true;
break;
case WebSocket.CLOSED:
message = i18n('disconnected');
instructions = i18n('checkNetworkConnection');
hasInterruption = true;
break;
}
if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) {
hasInterruption = true;
}
if (this.socketReconnectWaitDuration.asSeconds() > 0) {
instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]);
}
if (!this.navigatorOnLine()) {
hasInterruption = true;
message = i18n('offline');
instructions = i18n('checkNetworkConnection');
} else if (!Whisper.Registration.isDone()) {
hasInterruption = true;
message = i18n('Unlinked');
instructions = i18n('unlinkedWarning');
action = i18n('relink');
buttonClass = 'openInstaller';
}
return {
message: message,
instructions: instructions,
hasInterruption: hasInterruption,
action: action,
buttonClass: buttonClass
};
},
update: function() {
var status = this.getNetworkStatus();
this.model.set(status);
},
render_attributes: function() {
return this.model.attributes;
},
onChange: function() {
this.render();
if (this.model.attributes.hasInterruption) {
this.$el.slideDown();
}
else {
this.$el.hide();
}
}
});
var socketStatus = this.getSocketStatus();
switch (socketStatus) {
case WebSocket.CONNECTING:
message = i18n('connecting');
this.setSocketReconnectInterval(null);
break;
case WebSocket.OPEN:
this.setSocketReconnectInterval(null);
break;
case WebSocket.CLOSING:
message = i18n('disconnected');
instructions = i18n('checkNetworkConnection');
hasInterruption = true;
break;
case WebSocket.CLOSED:
message = i18n('disconnected');
instructions = i18n('checkNetworkConnection');
hasInterruption = true;
break;
}
if (
socketStatus == WebSocket.CONNECTING &&
!this.withinConnectingGracePeriod
) {
hasInterruption = true;
}
if (this.socketReconnectWaitDuration.asSeconds() > 0) {
instructions = i18n('attemptingReconnection', [
this.socketReconnectWaitDuration.asSeconds(),
]);
}
if (!this.navigatorOnLine()) {
hasInterruption = true;
message = i18n('offline');
instructions = i18n('checkNetworkConnection');
} else if (!Whisper.Registration.isDone()) {
hasInterruption = true;
message = i18n('Unlinked');
instructions = i18n('unlinkedWarning');
action = i18n('relink');
buttonClass = 'openInstaller';
}
return {
message: message,
instructions: instructions,
hasInterruption: hasInterruption,
action: action,
buttonClass: buttonClass,
};
},
update: function() {
var status = this.getNetworkStatus();
this.model.set(status);
},
render_attributes: function() {
return this.model.attributes;
},
onChange: function() {
this.render();
if (this.model.attributes.hasInterruption) {
this.$el.slideDown();
} else {
this.$el.hide();
}
},
});
})(); })();

View file

@ -1,82 +1,89 @@
/* /*
* 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.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(
if (this.recipients_view.typeahead.get(model)) { function(model) {
this.recipients_view.typeahead.remove(model); if (this.recipients_view.typeahead.get(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);
}, },
events: { events: {
'click .back': 'goBack', 'click .back': 'goBack',
'click .send': 'send', 'click .send': 'send',
'focusin input.search': 'showResults', 'focusin input.search': 'showResults',
'focusout input.search': 'hideResults', 'focusout input.search': 'hideResults',
}, },
hideResults: function() { hideResults: function() {
this.$('.results').hide(); this.$('.results').hide();
}, },
showResults: function() { showResults: function() {
this.$('.results').show(); this.$('.results').show();
}, },
goBack: function() { goBack: function() {
this.trigger('back'); this.trigger('back');
}, },
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(
var now = Date.now(); function(avatarFile) {
var attrs = { var now = Date.now();
timestamp: now, var attrs = {
active_at: now, timestamp: now,
name: this.$('.name').val(), active_at: now,
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id')) name: this.$('.name').val(),
}; members: _.union(
if (avatarFile) { this.model.get('members'),
attrs.avatar = avatarFile; this.recipients_view.recipients.pluck('id')
} ),
this.model.set(attrs); };
var group_update = this.model.changed; if (avatarFile) {
this.model.save(); attrs.avatar = avatarFile;
}
this.model.set(attrs);
var group_update = this.model.changed;
this.model.save();
if (group_update.avatar) { if (group_update.avatar) {
this.model.trigger('change:avatar'); this.model.trigger('change:avatar');
} }
this.model.updateGroup(group_update); this.model.updateGroup(group_update);
this.goBack(); this.goBack();
}.bind(this)); }.bind(this)
} );
}); },
});
})(); })();

View file

@ -1,36 +1,38 @@
/* /*
* 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.PhoneInputView = Whisper.View.extend({ Whisper.PhoneInputView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: 'phone-input', className: 'phone-input',
templateName: 'phone-number', templateName: 'phone-number',
initialize: function() { initialize: function() {
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')
var number = input.val(); .attr('data-country-code')
.toUpperCase();
var number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); var parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
if (parsedNumber.isValidNumber) { if (parsedNumber.isValidNumber) {
this.$('.number-container').removeClass('invalid'); this.$('.number-container').removeClass('invalid');
this.$('.number-container').addClass('valid'); this.$('.number-container').addClass('valid');
} else { } else {
this.$('.number-container').removeClass('valid'); this.$('.number-container').removeClass('valid');
} }
input.trigger('validation'); input.trigger('validation');
return parsedNumber.e164; return parsedNumber.e164;
} },
}); });
})(); })();

View file

@ -4,7 +4,7 @@
/* global ReactDOM: false */ /* global ReactDOM: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -44,4 +44,4 @@
Backbone.View.prototype.remove.call(this); Backbone.View.prototype.remove.call(this);
}, },
}); });
}()); })();

View file

@ -1,185 +1,182 @@
/* /*
* 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 ContactsTypeahead = Backbone.TypeaheadCollection.extend({ var ContactsTypeahead = Backbone.TypeaheadCollection.extend({
typeaheadAttributes: [ typeaheadAttributes: [
'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({
tagName: 'span',
className: 'recipient',
events: {
'click .remove': 'removeModel',
},
templateName: 'contact_pill',
initialize: function() {
var error = this.model.validate(this.model.attributes);
if (error) {
this.$el.addClass('error');
}
},
removeModel: function() {
this.$el.trigger('remove', { modelId: this.model.id });
this.remove();
},
render_attributes: function() {
return { name: this.model.getTitle() };
},
});
Whisper.RecipientListView = Whisper.ListView.extend({
itemView: Whisper.ContactPillView,
});
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
className: 'contact-details contact',
templateName: 'contact_name_and_number',
});
Whisper.SuggestionListView = Whisper.ConversationListView.extend({
itemView: Whisper.SuggestionView,
});
Whisper.RecipientsInputView = Whisper.View.extend({
className: 'recipients-input',
templateName: 'recipients-input',
initialize: function(options) {
if (options) {
this.placeholder = options.placeholder;
}
this.render();
this.$input = this.$('input.search');
this.$new_contact = this.$('.new-contact');
// Collection of recipients selected for the new message
this.recipients = new Whisper.ConversationCollection([], {
comparator: false,
});
// View to display the selected recipients
this.recipients_view = new Whisper.RecipientListView({
collection: this.recipients,
el: this.$('.recipients'),
});
// Collection of contacts to match user input against
this.typeahead = new ContactsTypeahead();
this.typeahead.fetchContacts();
// View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.SuggestionListView({
collection: new Whisper.ConversationCollection([], {
comparator: function(m) {
return m.getTitle().toLowerCase();
},
}),
});
this.$('.contacts').append(this.typeahead_view.el);
this.initNewContact();
this.listenTo(this.typeahead, 'reset', this.filterContacts);
},
render_attributes: function() {
return { placeholder: this.placeholder || 'name or phone number' };
},
events: {
'input input.search': 'filterContacts',
'select .new-contact': 'addNewRecipient',
'select .contacts': 'addRecipient',
'remove .recipient': 'removeRecipient',
},
filterContacts: function(e) {
var query = this.$input.val();
if (query.length) {
if (this.maybeNumber(query)) {
this.new_contact_view.model.set('id', query);
this.new_contact_view.render().$el.show();
} else {
this.new_contact_view.$el.hide();
} }
}); this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
} else {
this.resetTypeahead();
}
},
Whisper.ContactPillView = Whisper.View.extend({ initNewContact: function() {
tagName: 'span', if (this.new_contact_view) {
className: 'recipient', this.new_contact_view.undelegateEvents();
events: { this.new_contact_view.$el.hide();
'click .remove': 'removeModel' }
}, // Creates a view to display a new contact
templateName: 'contact_pill', this.new_contact_view = new Whisper.ConversationListItemView({
initialize: function() { el: this.$new_contact,
var error = this.model.validate(this.model.attributes); model: ConversationController.create({
if (error) { type: 'private',
this.$el.addClass('error'); newContact: true,
} }),
}, }).render();
removeModel: function() { },
this.$el.trigger('remove', {modelId: this.model.id});
this.remove();
},
render_attributes: function() {
return { name: this.model.getTitle() };
}
});
Whisper.RecipientListView = Whisper.ListView.extend({ addNewRecipient: function() {
itemView: Whisper.ContactPillView this.recipients.add(this.new_contact_view.model);
}); this.initNewContact();
this.resetTypeahead();
},
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ addRecipient: function(e, conversation) {
className: 'contact-details contact', this.recipients.add(this.typeahead.remove(conversation.id));
templateName: 'contact_name_and_number', this.resetTypeahead();
}); },
Whisper.SuggestionListView = Whisper.ConversationListView.extend({ removeRecipient: function(e, data) {
itemView: Whisper.SuggestionView var model = this.recipients.remove(data.modelId);
}); if (!model.get('newContact')) {
this.typeahead.add(model);
}
this.filterContacts();
},
Whisper.RecipientsInputView = Whisper.View.extend({ reset: function() {
className: 'recipients-input', this.delegateEvents();
templateName: 'recipients-input', this.typeahead_view.delegateEvents();
initialize: function(options) { this.recipients_view.delegateEvents();
if (options) { this.new_contact_view.delegateEvents();
this.placeholder = options.placeholder; this.typeahead.add(
} this.recipients.filter(function(model) {
this.render(); return !model.get('newContact');
this.$input = this.$('input.search'); })
this.$new_contact = this.$('.new-contact'); );
this.recipients.reset([]);
this.resetTypeahead();
this.typeahead.fetchContacts();
},
// Collection of recipients selected for the new message resetTypeahead: function() {
this.recipients = new Whisper.ConversationCollection([], { this.new_contact_view.$el.hide();
comparator: false this.$input.val('').focus();
}); this.typeahead_view.collection.reset([]);
},
// View to display the selected recipients
this.recipients_view = new Whisper.RecipientListView({
collection: this.recipients,
el: this.$('.recipients')
});
// Collection of contacts to match user input against
this.typeahead = new ContactsTypeahead();
this.typeahead.fetchContacts();
// View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.SuggestionListView({
collection : new Whisper.ConversationCollection([], {
comparator: function(m) { return m.getTitle().toLowerCase(); }
})
});
this.$('.contacts').append(this.typeahead_view.el);
this.initNewContact();
this.listenTo(this.typeahead, 'reset', this.filterContacts);
},
render_attributes: function() {
return { placeholder: this.placeholder || "name or phone number" };
},
events: {
'input input.search': 'filterContacts',
'select .new-contact': 'addNewRecipient',
'select .contacts': 'addRecipient',
'remove .recipient': 'removeRecipient',
},
filterContacts: function(e) {
var query = this.$input.val();
if (query.length) {
if (this.maybeNumber(query)) {
this.new_contact_view.model.set('id', query);
this.new_contact_view.render().$el.show();
} else {
this.new_contact_view.$el.hide();
}
this.typeahead_view.collection.reset(
this.typeahead.typeahead(query)
);
} else {
this.resetTypeahead();
}
},
initNewContact: function() {
if (this.new_contact_view) {
this.new_contact_view.undelegateEvents();
this.new_contact_view.$el.hide();
}
// Creates a view to display a new contact
this.new_contact_view = new Whisper.ConversationListItemView({
el: this.$new_contact,
model: ConversationController.create({
type: 'private',
newContact: true
})
}).render();
},
addNewRecipient: function() {
this.recipients.add(this.new_contact_view.model);
this.initNewContact();
this.resetTypeahead();
},
addRecipient: function(e, conversation) {
this.recipients.add(this.typeahead.remove(conversation.id));
this.resetTypeahead();
},
removeRecipient: function(e, data) {
var model = this.recipients.remove(data.modelId);
if (!model.get('newContact')) {
this.typeahead.add(model);
}
this.filterContacts();
},
reset: function() {
this.delegateEvents();
this.typeahead_view.delegateEvents();
this.recipients_view.delegateEvents();
this.new_contact_view.delegateEvents();
this.typeahead.add(
this.recipients.filter(function(model) {
return !model.get('newContact');
})
);
this.recipients.reset([]);
this.resetTypeahead();
this.typeahead.fetchContacts();
},
resetTypeahead: function() {
this.new_contact_view.$el.hide();
this.$input.val('').focus();
this.typeahead_view.collection.reset([]);
},
maybeNumber: function(number) {
return number.match(/^\+?[0-9]*$/);
}
});
maybeNumber: function(number) {
return number.match(/^\+?[0-9]*$/);
},
});
})(); })();

View file

@ -1,80 +1,84 @@
/* /*
* 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.RecorderView = Whisper.View.extend({ Whisper.RecorderView = Whisper.View.extend({
className: 'recorder clearfix', className: 'recorder clearfix',
templateName: 'recorder', templateName: 'recorder',
initialize: function() { initialize: function() {
this.startTime = Date.now(); this.startTime = Date.now();
this.interval = setInterval(this.updateTime.bind(this), 1000); this.interval = setInterval(this.updateTime.bind(this), 1000);
this.start(); this.start();
}, },
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');
var minutes = '' + Math.trunc(duration.asMinutes()); var minutes = '' + Math.trunc(duration.asMinutes());
var seconds = '' + duration.seconds(); var seconds = '' + duration.seconds();
if (seconds.length < 2) { if (seconds.length < 2) {
seconds = '0' + seconds; seconds = '0' + seconds;
} }
this.$('.time').text(minutes + ':' + seconds); this.$('.time').text(minutes + ':' + seconds);
}, },
close: function() { close: function() {
// Note: the 'close' event can be triggered by InboxView, when the user clicks // Note: the 'close' event can be triggered by InboxView, when the user clicks
// anywhere outside the recording pane. // anywhere outside the recording pane.
if (this.recorder.isRecording()) { if (this.recorder.isRecording()) {
this.recorder.cancelRecording(); this.recorder.cancelRecording();
} }
if (this.interval) { if (this.interval) {
clearInterval(this.interval); clearInterval(this.interval);
} }
if (this.source) { if (this.source) {
this.source.disconnect(); this.source.disconnect();
} }
if (this.context) { if (this.context) {
this.context.close().then(function() { this.context.close().then(function() {
console.log('audio context closed'); console.log('audio context closed');
}); });
} }
this.remove(); this.remove();
this.trigger('closed'); this.trigger('closed');
}, },
finish: function() { finish: function() {
this.recorder.finishRecording(); this.recorder.finishRecording();
this.close(); this.close();
}, },
handleBlob: function(recorder, blob) { handleBlob: function(recorder, blob) {
if (blob) { if (blob) {
this.trigger('send', blob); this.trigger('send', blob);
} }
}, },
start: function() { start: function() {
this.context = new AudioContext(); this.context = new AudioContext();
this.input = this.context.createGain(); this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, { this.recorder = new WebAudioRecorder(this.input, {
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(
this.source = this.context.createMediaStreamSource(stream); { audio: true },
this.source.connect(this.input); function(stream) {
}.bind(this), this.onError.bind(this)); this.source = this.context.createMediaStreamSource(stream);
this.recorder.startRecording(); this.source.connect(this.input);
}, }.bind(this),
onError: function(error) { this.onError.bind(this)
console.log(error.stack); );
this.close(); this.recorder.startRecording();
} },
}); onError: function(error) {
console.log(error.stack);
this.close();
},
});
})(); })();

View file

@ -1,39 +1,39 @@
/* /*
* 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.ScrollDownButtonView = Whisper.View.extend({ Whisper.ScrollDownButtonView = Whisper.View.extend({
className: 'scroll-down-button-view', className: 'scroll-down-button-view',
templateName: 'scroll-down-button-view', templateName: 'scroll-down-button-view',
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment: function(count) {
count = count || 0; count = count || 0;
this.count += count; this.count += count;
this.render(); this.render();
}, },
render_attributes: function() { render_attributes: function() {
var cssClass = this.count > 0 ? 'new-messages' : ''; var cssClass = this.count > 0 ? 'new-messages' : '';
var moreBelow = i18n('scrollDown'); var moreBelow = i18n('scrollDown');
if (this.count > 1) { if (this.count > 1) {
moreBelow = i18n('messagesBelow'); moreBelow = i18n('messagesBelow');
} else if (this.count === 1) { } else if (this.count === 1) {
moreBelow = i18n('messageBelow'); moreBelow = i18n('messageBelow');
} }
return { return {
cssClass: cssClass, cssClass: cssClass,
moreBelow: moreBelow moreBelow: moreBelow,
}; };
} },
}); });
})(); })();

View file

@ -5,127 +5,127 @@
/* eslint-disable */ /* eslint-disable */
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Database } = window.Whisper; const { Database } = window.Whisper;
const { OS, Logs } = window.Signal; const { OS, Logs } = window.Signal;
const { Settings } = window.Signal.Types; const { Settings } = window.Signal.Types;
var CheckboxView = Whisper.View.extend({ var CheckboxView = Whisper.View.extend({
initialize: function(options) { initialize: function(options) {
this.name = options.name; this.name = options.name;
this.defaultValue = options.defaultValue; this.defaultValue = options.defaultValue;
this.event = options.event; this.event = options.event;
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;
storage.put(this.name, value); storage.put(this.name, value);
console.log(this.name, 'changed to', value); console.log(this.name, 'changed to', value);
if (this.event) { if (this.event) {
this.$el.trigger(this.event); this.$el.trigger(this.event);
} }
}, },
populate: function() { populate: function() {
var value = storage.get(this.name, this.defaultValue); var value = storage.get(this.name, this.defaultValue);
this.$('input').prop('checked', !!value); this.$('input').prop('checked', !!value);
}, },
}); });
var RadioButtonGroupView = Whisper.View.extend({ var RadioButtonGroupView = Whisper.View.extend({
initialize: function(options) { initialize: function(options) {
this.name = options.name; this.name = options.name;
this.defaultValue = options.defaultValue; this.defaultValue = options.defaultValue;
this.event = options.event; this.event = options.event;
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();
storage.put(this.name, value); storage.put(this.name, value);
console.log(this.name, 'changed to', value); console.log(this.name, 'changed to', value);
if (this.event) { if (this.event) {
this.$el.trigger(this.event); this.$el.trigger(this.event);
} }
}, },
populate: function() { populate: function() {
var value = storage.get(this.name, this.defaultValue); var value = storage.get(this.name, this.defaultValue);
this.$('#' + this.name + '-' + value).attr('checked', 'checked'); this.$('#' + this.name + '-' + value).attr('checked', 'checked');
}, },
}); });
Whisper.SettingsView = Whisper.View.extend({ Whisper.SettingsView = Whisper.View.extend({
className: 'settings modal expand', className: 'settings modal expand',
templateName: 'settings', templateName: 'settings',
initialize: function() { initialize: function() {
this.deviceName = textsecure.storage.user.getDeviceName(); this.deviceName = textsecure.storage.user.getDeviceName();
this.render(); this.render();
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();
this.$('.sync-setting').append(syncView.el); this.$('.sync-setting').append(syncView.el);
} }
}, },
events: { events: {
'click .close': 'remove', 'click .close': 'remove',
'click .clear-data': 'onClearData', 'click .clear-data': 'onClearData',
}, },
render_attributes: function() { render_attributes: function() {
return { return {
deviceNameLabel: i18n('deviceName'), deviceNameLabel: i18n('deviceName'),
deviceName: this.deviceName, deviceName: this.deviceName,
theme: i18n('theme'), theme: i18n('theme'),
notifications: i18n('notifications'), notifications: i18n('notifications'),
notificationSettingsDialog: i18n('notificationSettingsDialog'), notificationSettingsDialog: i18n('notificationSettingsDialog'),
settings: i18n('settings'), settings: i18n('settings'),
disableNotifications: i18n('disableNotifications'), disableNotifications: i18n('disableNotifications'),
nameAndMessage: i18n('nameAndMessage'), nameAndMessage: i18n('nameAndMessage'),
noNameOrMessage: i18n('noNameOrMessage'), noNameOrMessage: i18n('noNameOrMessage'),
nameOnly: i18n('nameOnly'), nameOnly: i18n('nameOnly'),
audioNotificationDescription: i18n('audioNotificationDescription'), audioNotificationDescription: i18n('audioNotificationDescription'),
isAudioNotificationSupported: Settings.isAudioNotificationSupported(), isAudioNotificationSupported: Settings.isAudioNotificationSupported(),
themeAndroidDark: i18n('themeAndroidDark'), themeAndroidDark: i18n('themeAndroidDark'),
hideMenuBar: i18n('hideMenuBar'), hideMenuBar: i18n('hideMenuBar'),
clearDataHeader: i18n('clearDataHeader'), clearDataHeader: i18n('clearDataHeader'),
clearDataButton: i18n('clearDataButton'), clearDataButton: i18n('clearDataButton'),
clearDataExplanation: i18n('clearDataExplanation'), clearDataExplanation: i18n('clearDataExplanation'),
}; };
}, },
onClearData: function() { onClearData: function() {
var clearDataView = new ClearDataView().render(); var clearDataView = new ClearDataView().render();
$('body').append(clearDataView.el); $('body').append(clearDataView.el);
}, },
}); });
/* jshint ignore:start */ /* jshint ignore:start */
/* eslint-enable */ /* eslint-enable */
const CLEAR_DATA_STEPS = { const CLEAR_DATA_STEPS = {
CHOICE: 1, CHOICE: 1,
@ -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:',
@ -186,61 +183,61 @@
}, },
}); });
/* eslint-disable */ /* eslint-disable */
/* jshint ignore:end */ /* jshint ignore:end */
var SyncView = Whisper.View.extend({ var SyncView = Whisper.View.extend({
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'));
this.$('.sync').removeAttr('disabled'); this.$('.sync').removeAttr('disabled');
}, },
disable: function() { disable: function() {
this.$('.sync').attr('disabled', 'disabled'); this.$('.sync').attr('disabled', 'disabled');
this.$('.sync').text(i18n('syncing')); this.$('.sync').text(i18n('syncing'));
}, },
onsuccess: function() { onsuccess: function() {
storage.put('synced_at', Date.now()); storage.put('synced_at', Date.now());
console.log('sync successful'); console.log('sync successful');
this.enable(); this.enable();
this.render(); this.render();
}, },
ontimeout: function() { ontimeout: function() {
console.log('sync timed out'); console.log('sync timed out');
this.$('.synced_at').hide(); this.$('.synced_at').hide();
this.$('.sync_failed').show(); this.$('.sync_failed').show();
this.enable(); this.enable();
}, },
sync: function() { sync: function() {
this.$('.sync_failed').hide(); this.$('.sync_failed').hide();
if (textsecure.storage.user.getDeviceId() != '1') { if (textsecure.storage.user.getDeviceId() != '1') {
this.disable(); this.disable();
var syncRequest = window.getSyncRequest(); var syncRequest = window.getSyncRequest();
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() {
var attrs = { var attrs = {
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) {
date = new Date(date); date = new Date(date);
attrs.lastSynced = i18n('lastSynced'); attrs.lastSynced = i18n('lastSynced');
attrs.syncDate = date.toLocaleDateString(); attrs.syncDate = date.toLocaleDateString();
attrs.syncTime = date.toLocaleTimeString(); attrs.syncTime = date.toLocaleTimeString();
} }
return attrs; return attrs;
} },
}); });
})(); })();

View file

@ -1,88 +1,111 @@
/* /*
* 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.StandaloneRegistrationView = Whisper.View.extend({ Whisper.StandaloneRegistrationView = Whisper.View.extend({
templateName: 'standalone', templateName: 'standalone',
className: 'full-screen-flow', className: 'full-screen-flow',
initialize: function() { initialize: function() {
this.accountManager = getAccountManager(); this.accountManager = getAccountManager();
this.render(); this.render();
var number = textsecure.storage.user.getNumber(); var number = textsecure.storage.user.getNumber();
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({
this.$('#error').hide(); el: this.$('#phone-number-input'),
}, });
events: { this.$('#error').hide();
'validation input.number': 'onValidation', },
'click #request-voice': 'requestVoice', events: {
'click #request-sms': 'requestSMSVerification', 'validation input.number': 'onValidation',
'change #code': 'onChangeCode', 'click #request-voice': 'requestVoice',
'click #verifyCode': 'verifyCode', 'click #request-sms': 'requestSMSVerification',
}, 'change #code': 'onChangeCode',
verifyCode: function(e) { 'click #verifyCode': 'verifyCode',
var number = this.phoneView.validateNumber(); },
var verificationCode = $('#code').val().replace(/\D+/g, ''); verifyCode: function(e) {
var number = this.phoneView.validateNumber();
var verificationCode = $('#code')
.val()
.replace(/\D+/g, '');
this.accountManager.registerSingleDevice(number, verificationCode).then(function() { this.accountManager
this.$el.trigger('openInbox'); .registerSingleDevice(number, verificationCode)
}.bind(this)).catch(this.log.bind(this)); .then(
}, function() {
log: function (s) { this.$el.trigger('openInbox');
console.log(s); }.bind(this)
this.$('#status').text(s); )
}, .catch(this.log.bind(this));
validateCode: function() { },
var verificationCode = $('#code').val().replace(/\D/g, ''); log: function(s) {
if (verificationCode.length == 6) { console.log(s);
return verificationCode; this.$('#status').text(s);
} },
}, validateCode: function() {
displayError: function(error) { var verificationCode = $('#code')
this.$('#error').hide().text(error).addClass('in').fadeIn(); .val()
}, .replace(/\D/g, '');
onValidation: function() { if (verificationCode.length == 6) {
if (this.$('#number-container').hasClass('valid')) { return verificationCode;
this.$('#request-sms, #request-voice').removeAttr('disabled'); }
} else { },
this.$('#request-sms, #request-voice').prop('disabled', 'disabled'); displayError: function(error) {
} this.$('#error')
}, .hide()
onChangeCode: function() { .text(error)
if (!this.validateCode()) { .addClass('in')
this.$('#code').addClass('invalid'); .fadeIn();
} else { },
this.$('#code').removeClass('invalid'); onValidation: function() {
} if (this.$('#number-container').hasClass('valid')) {
}, this.$('#request-sms, #request-voice').removeAttr('disabled');
requestVoice: function() { } else {
window.removeSetupMenuItems(); this.$('#request-sms, #request-voice').prop('disabled', 'disabled');
this.$('#error').hide(); }
var number = this.phoneView.validateNumber(); },
if (number) { onChangeCode: function() {
this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this)); if (!this.validateCode()) {
this.$('#step2').addClass('in').fadeIn(); this.$('#code').addClass('invalid');
} else { } else {
this.$('#number-container').addClass('invalid'); this.$('#code').removeClass('invalid');
} }
}, },
requestSMSVerification: function() { requestVoice: function() {
window.removeSetupMenuItems(); window.removeSetupMenuItems();
$('#error').hide(); this.$('#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(); .requestVoiceVerification(number)
} else { .catch(this.displayError.bind(this));
this.$('#number-container').addClass('invalid'); this.$('#step2')
} .addClass('in')
} .fadeIn();
}); } else {
this.$('#number-container').addClass('invalid');
}
},
requestSMSVerification: function() {
window.removeSetupMenuItems();
$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
this.accountManager
.requestSMSVerification(number)
.catch(this.displayError.bind(this));
this.$('#step2')
.addClass('in')
.fadeIn();
} else {
this.$('#number-container').addClass('invalid');
}
},
});
})(); })();

View file

@ -1,88 +1,102 @@
/* /*
* 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.TimestampView = Whisper.View.extend({ Whisper.TimestampView = Whisper.View.extend({
initialize: function(options) { initialize: function(options) {
extension.windows.onClosed(this.clearTimeout.bind(this)); extension.windows.onClosed(this.clearTimeout.bind(this));
}, },
update: function() { update: function() {
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) {
millis = millis_now; millis = millis_now;
} }
var result = this.getRelativeTimeSpanString(millis); var result = this.getRelativeTimeSpanString(millis);
this.$el.text(result); this.$el.text(result);
var timestamp = moment(millis); var timestamp = moment(millis);
this.$el.attr('title', timestamp.format('llll')); this.$el.attr('title', timestamp.format('llll'));
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.timeout = setTimeout(this.update.bind(this), this.delay); this.delay = 1000;
}
},
clearTimeout: function() {
clearTimeout(this.timeout);
},
getRelativeTimeSpanString: function(timestamp_) {
// Convert to moment timestamp if it isn't already
var timestamp = moment(timestamp_),
now = moment(),
timediff = moment.duration(now - timestamp);
if (timediff.years() > 0) {
this.delay = null;
return timestamp.format(this._format.y);
} else if (timediff.months() > 0 || timediff.days() > 6) {
this.delay = null;
return timestamp.format(this._format.M);
} else if (timediff.days() > 0) {
this.delay = moment(timestamp).add(timediff.days() + 1,'d').diff(now);
return timestamp.format(this._format.d);
} else if (timediff.hours() > 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.hours() === 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.minutes() > 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else if (timediff.minutes() === 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else {
this.delay = moment(timestamp).add(1,'m').diff(now);
return this.relativeTime(timediff.seconds(), 's');
}
},
relativeTime : function (number, string) {
return moment.duration(number, string).humanize();
},
_format: {
y: "ll",
M: i18n('timestampFormat_M') || "MMM D",
d: "ddd"
} }
}); this.timeout = setTimeout(this.update.bind(this), this.delay);
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({ }
relativeTime : function (number, string, isFuture) { },
return moment.duration(-1 * number, string).humanize(string !== 's'); clearTimeout: function() {
}, clearTimeout(this.timeout);
_format: { },
y: "lll", getRelativeTimeSpanString: function(timestamp_) {
M: (i18n('timestampFormat_M') || "MMM D") + ' LT', // Convert to moment timestamp if it isn't already
d: "ddd LT" var timestamp = moment(timestamp_),
} now = moment(),
}); timediff = moment.duration(now - timestamp);
if (timediff.years() > 0) {
this.delay = null;
return timestamp.format(this._format.y);
} else if (timediff.months() > 0 || timediff.days() > 6) {
this.delay = null;
return timestamp.format(this._format.M);
} else if (timediff.days() > 0) {
this.delay = moment(timestamp)
.add(timediff.days() + 1, 'd')
.diff(now);
return timestamp.format(this._format.d);
} else if (timediff.hours() > 1) {
this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.hours() === 1) {
this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.minutes() > 1) {
this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else if (timediff.minutes() === 1) {
this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else {
this.delay = moment(timestamp)
.add(1, 'm')
.diff(now);
return this.relativeTime(timediff.seconds(), 's');
}
},
relativeTime: function(number, string) {
return moment.duration(number, string).humanize();
},
_format: {
y: 'll',
M: i18n('timestampFormat_M') || 'MMM D',
d: 'ddd',
},
});
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
relativeTime: function(number, string, isFuture) {
return moment.duration(-1 * number, string).humanize(string !== 's');
},
_format: {
y: 'lll',
M: (i18n('timestampFormat_M') || 'MMM D') + ' LT',
d: 'ddd LT',
},
});
})(); })();

View file

@ -1,28 +1,30 @@
/* /*
* 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.ToastView = Whisper.View.extend({ Whisper.ToastView = Whisper.View.extend({
className: 'toast', className: 'toast',
templateName: 'toast', templateName: 'toast',
initialize: function() { initialize: function() {
this.$el.hide(); this.$el.hide();
}, },
close: function() { close: function() {
this.$el.fadeOut(this.remove.bind(this)); this.$el.fadeOut(this.remove.bind(this));
}, },
render: function() { render: function() {
this.$el.html(Mustache.render( this.$el.html(
_.result(this, 'template', ''), Mustache.render(
_.result(this, 'render_attributes', '') _.result(this, 'template', ''),
)); _.result(this, 'render_attributes', '')
this.$el.show(); )
setTimeout(this.close.bind(this), 2000); );
} this.$el.show();
}); setTimeout(this.close.bind(this), 2000);
},
});
})(); })();

View file

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

View file

@ -2,26 +2,26 @@
* 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 lastTime; var lastTime;
var interval = 1000; var interval = 1000;
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;
}
Whisper.WallClockListener = { Whisper.WallClockListener = {
init: function(_events) { init: function(_events) {
events = _events; events = _events;
lastTime = Date.now(); lastTime = Date.now();
setInterval(checkTime, interval); setInterval(checkTime, interval);
} },
}; };
}()); })();

181
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,35 +160,50 @@ 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 {
width: DEFAULT_WIDTH, show: !startInTray, // allow to start minimised in tray
height: DEFAULT_HEIGHT, width: DEFAULT_WIDTH,
minWidth: MIN_WIDTH, height: DEFAULT_HEIGHT,
minHeight: MIN_HEIGHT, minWidth: MIN_WIDTH,
autoHideMenuBar: false, minHeight: MIN_HEIGHT,
webPreferences: { autoHideMenuBar: false,
nodeIntegration: false, webPreferences: {
// sandbox: true, nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'), // sandbox: true,
preload: path.join(__dirname, 'preload.js'),
},
icon: path.join(__dirname, 'images', 'icon_256.png'),
}, },
icon: path.join(__dirname, 'images', 'icon_256.png'), _.pick(windowConfig, [
}, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y'])); '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,38 +445,42 @@ 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
loggingSetupError = error; .initialize()
}).then(async () => { .catch(error => {
/* eslint-enable more/no-then */ loggingSetupError = error;
logger = logging.getLogger(); })
logger.info('app ready'); .then(async () => {
/* eslint-enable more/no-then */
logger = logging.getLogger();
logger.info('app ready');
if (loggingSetupError) { if (loggingSetupError) {
logger.error('Problem setting up logging', loggingSetupError.stack); logger.error('Problem setting up logging', loggingSetupError.stack);
} }
if (!locale) { if (!locale) {
const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale(); const appLocale =
locale = loadLocale({ appLocale, logger }); process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
} locale = loadLocale({ appLocale, logger });
}
console.log('Ensure attachments directory exists'); console.log('Ensure attachments directory exists');
const userDataPath = app.getPath('userData'); const userDataPath = app.getPath('userData');
await Attachments.ensureDirectory(userDataPath); await Attachments.ensureDirectory(userDataPath);
ready = true; ready = true;
autoUpdate.initialize(getMainWindow, locale.messages); autoUpdate.initialize(getMainWindow, locale.messages);
createWindow(); createWindow();
if (usingTrayIcon) { if (usingTrayIcon) {
tray = createTrayIcon(getMainWindow, locale.messages); tray = createTrayIcon(getMainWindow, locale.messages);
} }
setupMenu(); setupMenu();
}); });
}); });
function setupMenu(options) { function setupMenu(options) {
@ -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 (
config.environment === 'test' || process.platform !== 'darwin' ||
config.environment === 'test-lib') { config.environment === 'test' ||
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', {
type: 'text/css', rel: 'stylesheet',
href: '/stylesheets/manifest.css', type: 'text/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

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