Prettier (All The Things) (#2303)

Adopt Prettier code formatting for our entire project to reduce overhead of
formatting code. I considered adding a pre-commit hook but to make the change
more gradual, I recommend installing an editor plugin that runs Prettier on
save, e.g. `JsPrettier` for *Sublime Text*, or manually run `yarn format`.

Also: This PR makes no other changes to linting. ESLint is still opt-in as it
requires more changes than just formatting an can be done on a as-needed basis
when touching particular files (as we have done in the past.) On the other hand,
the ESLint required changes will now be smaller as they won’t involve large
formatting changes.

## Sublime Text Plugin

-  Install **JsPrettier**:  https://github.com/jonlabelle/SublimeJsPrettier
-   Settings:
      ```
      {
        "prettier_cli_path": "./node_modules/.bin/prettier",
        "auto_format_on_save": true,
        "auto_format_on_save_requires_prettier_config": true,
      }
      ```

## Changes

- [x] Disable conflicting ESLint rules
- [x] Exclude generated files and `libtextsecure`
- [x] Autoformat all JS and TS code (excluding CSS and JSON)
- [x] Apply isolated manual one-time fixes:
      80bc06486e
- [x] Goodbye Vim modelines!
      7b6e77d566
- [x] Ensure automated tests pass
- [x] Ensure app still works (smoke test)
This commit is contained in:
Daniel Gasienica
2018-04-30 18:57:15 -04:00
committed by GitHub
212 changed files with 17919 additions and 15809 deletions

View File

@@ -13,7 +13,20 @@ test/models/*.js
test/views/*.js test/views/*.js
/*.js /*.js
# typescript-generated files # Generated files
js/components.js
js/libtextsecure.js
js/libsignal-protocol-worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
# Third-party files
js/jquery.js
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
# TypeScript generated files
ts/**/*.js ts/**/*.js
# ES2015+ files # ES2015+ files

View File

@@ -2,37 +2,24 @@
module.exports = { module.exports = {
settings: { settings: {
'import/core-modules': [ 'import/core-modules': ['electron'],
'electron'
]
}, },
extends: [ extends: ['airbnb-base', 'prettier'],
'airbnb-base',
],
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',
}], },
],
// putting params on their own line helps stay within line length limit
'function-paren-newline': ['error', 'multiline'],
// 90 characters allows three+ side-by-side screens on a standard-size monitor
'max-len': ['error', {
code: 90,
ignoreUrls: true,
}],
// 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',
@@ -52,6 +39,26 @@ 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:
'arrow-parens': 'off',
'function-paren-newline': 'off',
'max-len': [
'error',
{
// Prettier generally limits line length to 80 but sometimes goes over.
// The `max-len` plugin doesnt let us omit `code` so we set it to a
// high value as a buffer to let Prettier control the line length:
code: 999,
// We still want to limit comments as before:
comments: 90,
ignoreUrls: true,
},
],
},
}; };

4
.gitignore vendored
View File

@@ -16,11 +16,11 @@ release/
# generated files # generated files
js/components.js js/components.js
libtextsecure/components.js
js/libtextsecure.js js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js
stylesheets/*.css stylesheets/*.css
test/test.js test/test.js
libtextsecure/test/test.js
# React / TypeScript # React / TypeScript
ts/**/*.js ts/**/*.js

18
.prettierignore Normal file
View File

@@ -0,0 +1,18 @@
# TODO: Partially duplicated from `.gitignore`. Remove once Prettier
# supports `.gitignore`: https://github.com/prettier/prettier/issues/2294
# generated files
js/components.js
js/libtextsecure.js
js/libsignal-protocol-worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
# Third-party files
js/jquery.js
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
/**/*.json
/**/*.css

4
.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
singleQuote: true,
trailingComma: 'es5',
};

View File

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

View File

@@ -13,7 +13,7 @@ install:
build_script: build_script:
- yarn transpile - yarn transpile
- yarn lint - yarn lint-windows
- yarn test-node - yarn test-node
- yarn nsp check - yarn nsp check
- yarn generate - yarn generate

View File

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

View File

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

View File

@@ -1,10 +1,7 @@
/*global $, Whisper, Backbone, textsecure, extension*/ /*global $, Whisper, Backbone, textsecure, extension*/
/*
* vim: ts=4:sw=4:expandtab
*/
// 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 || {};
@@ -19,7 +16,8 @@
this.reset([]); this.reset([]);
}); });
this.on('add remove change:unreadCount', this.on(
'add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000) _.debounce(this.updateUnreadCount.bind(this), 1000)
); );
this.startPruning(); this.startPruning();
@@ -52,17 +50,20 @@
}, },
updateUnreadCount: function() { updateUnreadCount: function() {
var newUnreadCount = _.reduce( var newUnreadCount = _.reduce(
this.map(function(m) { return m.get('unreadCount'); }), this.map(function(m) {
return m.get('unreadCount');
}),
function(item, memo) { function(item, memo) {
return item + memo; return item + memo;
}, },
0 0
); );
storage.put("unreadCount", newUnreadCount); storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) { if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount); window.setBadgeCount(newUnreadCount);
window.document.title = window.config.title + " (" + newUnreadCount + ")"; window.document.title =
window.config.title + ' (' + newUnreadCount + ')';
} else { } else {
window.setBadgeCount(0); window.setBadgeCount(0);
window.document.title = window.config.title; window.document.title = window.config.title;
@@ -71,12 +72,15 @@
}, },
startPruning: function() { startPruning: function() {
var halfHour = 30 * 60 * 1000; var halfHour = 30 * 60 * 1000;
this.interval = setInterval(function() { this.interval = setInterval(
function() {
this.forEach(function(conversation) { this.forEach(function(conversation) {
conversation.trigger('prune'); conversation.trigger('prune');
}); });
}.bind(this), halfHour); }.bind(this),
} halfHour
);
},
}))(); }))();
window.getInboxCollection = function() { window.getInboxCollection = function() {
@@ -86,7 +90,9 @@
window.ConversationController = { window.ConversationController = {
get: function(id) { get: function(id) {
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch'); throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
return conversations.get(id); return conversations.get(id);
@@ -104,11 +110,15 @@
} }
if (type !== 'private' && type !== 'group') { if (type !== 'private' && type !== 'group') {
throw new TypeError(`'type' must be 'private' or 'group'; got: '${type}'`); throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
} }
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch'); throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
var conversation = conversations.get(id); var conversation = conversations.get(id);
@@ -118,7 +128,7 @@
conversation = conversations.add({ conversation = conversations.add({
id: id, id: id,
type: type type: type,
}); });
conversation.initialPromise = new Promise(function(resolve, reject) { conversation.initialPromise = new Promise(function(resolve, reject) {
if (!conversation.isValid()) { if (!conversation.isValid()) {
@@ -146,7 +156,8 @@
return conversation; return conversation;
}, },
getOrCreateAndWait: function(id, type) { getOrCreateAndWait: function(id, type) {
return this._initialPromise.then(function() { return this._initialPromise.then(
function() {
var conversation = this.getOrCreate(id, type); var conversation = this.getOrCreate(id, type);
if (conversation) { if (conversation) {
@@ -158,7 +169,8 @@
return Promise.reject( return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation') new Error('getOrCreateAndWait: did not get conversation')
); );
}.bind(this)); }.bind(this)
);
}, },
getAllGroupsInvolvingId: function(id) { getAllGroupsInvolvingId: function(id) {
var groups = new Whisper.GroupCollection(); var groups = new Whisper.GroupCollection();
@@ -178,21 +190,26 @@
load: function() { load: function() {
console.log('ConversationController: starting initial fetch'); console.log('ConversationController: starting initial fetch');
this._initialPromise = new Promise(function(resolve, reject) { this._initialPromise = new Promise(
conversations.fetch().then(function() { function(resolve, reject) {
conversations.fetch().then(
function() {
console.log('ConversationController: done with initial fetch'); console.log('ConversationController: done with initial fetch');
this._initialFetchComplete = true; this._initialFetchComplete = true;
resolve(); resolve();
}.bind(this), function(error) { }.bind(this),
function(error) {
console.log( console.log(
'ConversationController: initial fetch failed', 'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
reject(error); reject(error);
}); }
}.bind(this)); );
}.bind(this)
);
return this._initialPromise; return this._initialPromise;
} },
}; };
})(); })();

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
;(function() { (function() {
'use strict'; 'use strict';
var BUILD_EXPIRATION = 0; var BUILD_EXPIRATION = 0;
try { try {
BUILD_EXPIRATION = parseInt(window.config.buildExpiration); BUILD_EXPIRATION = parseInt(window.config.buildExpiration);
if (BUILD_EXPIRATION) { if (BUILD_EXPIRATION) {
console.log("Build expires: ", new Date(BUILD_EXPIRATION).toISOString()); console.log('Build expires: ', new Date(BUILD_EXPIRATION).toISOString());
} }
} catch (e) {} } catch (e) {}
window.extension = window.extension || {}; window.extension = window.extension || {};
extension.expired = function() { extension.expired = function() {
return (BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION); return BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION;
}; };
})(); })();

View File

@@ -1,8 +1,4 @@
(function() {
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -36,10 +32,14 @@
var wait = expires_at - Date.now(); var wait = expires_at - Date.now();
// In the past // In the past
if (wait < 0) { wait = 0; } if (wait < 0) {
wait = 0;
}
// Too far in the future, since it's limited to a 32-bit value // Too far in the future, since it's limited to a 32-bit value
if (wait > 2147483647) { wait = 2147483647; } if (wait > 2147483647) {
wait = 2147483647;
}
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait); timeout = setTimeout(destroyExpiredMessages, wait);
@@ -53,20 +53,23 @@
checkExpiringMessages(); checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages); events.on('timetravel', throttledCheckExpiringMessages);
}, },
update: throttledCheckExpiringMessages update: throttledCheckExpiringMessages,
}; };
var TimerOption = Backbone.Model.extend({ var TimerOption = Backbone.Model.extend({
getName: function() { getName: function() {
return i18n([ return (
'timerOption', this.get('time'), this.get('unit'), i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize(); moment.duration(this.get('time'), this.get('unit')).humanize()
);
}, },
getAbbreviated: function() { getAbbreviated: function() {
return i18n([ return i18n(
'timerOption', this.get('time'), this.get('unit'), 'abbreviated' ['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
].join('_')); '_'
} )
);
},
}); });
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption, model: TimerOption,
@@ -74,9 +77,10 @@
if (!seconds) { if (!seconds) {
seconds = 0; seconds = 0;
} }
var o = this.findWhere({seconds: seconds}); var o = this.findWhere({ seconds: seconds });
if (o) { return o.getName(); } if (o) {
else { return o.getName();
} else {
return [seconds, 'seconds'].join(' '); return [seconds, 'seconds'].join(' ');
} }
}, },
@@ -84,32 +88,34 @@
if (!seconds) { if (!seconds) {
seconds = 0; seconds = 0;
} }
var o = this.findWhere({seconds: seconds}); var o = this.findWhere({ seconds: seconds });
if (o) { return o.getAbbreviated(); } if (o) {
else { return o.getAbbreviated();
} else {
return [seconds, 's'].join(''); return [seconds, 's'].join('');
} }
} },
}))([ }))(
[ 0, 'seconds' ], [
[ 5, 'seconds' ], [0, 'seconds'],
[ 10, 'seconds' ], [5, 'seconds'],
[ 30, 'seconds' ], [10, 'seconds'],
[ 1, 'minute' ], [30, 'seconds'],
[ 5, 'minutes' ], [1, 'minute'],
[ 30, 'minutes' ], [5, 'minutes'],
[ 1, 'hour' ], [30, 'minutes'],
[ 6, 'hours' ], [1, 'hour'],
[ 12, 'hours' ], [6, 'hours'],
[ 1, 'day' ], [12, 'hours'],
[ 1, 'week' ], [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

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

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
storage.isBlocked = function(number) { storage.isBlocked = function(number) {
var numbers = storage.get('blocked', []); var numbers = storage.get('blocked', []);

View File

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

View File

@@ -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
@@ -269,7 +276,8 @@
return { return {
attachments: (quote.attachments || []).map(attachment => attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)), this.processAttachment(attachment, objectUrl)
),
authorColor, authorColor,
authorProfileName, authorProfileName,
authorTitle, authorTitle,
@@ -342,7 +350,8 @@
send(promise) { send(promise) {
this.trigger('pending'); this.trigger('pending');
return promise.then((result) => { return promise
.then(result => {
const now = Date.now(); const now = Date.now();
this.trigger('done'); this.trigger('done');
if (result.dataMessage) { if (result.dataMessage) {
@@ -355,7 +364,8 @@
expirationStartTimestamp: now, expirationStartTimestamp: now,
}); });
this.sendSyncMessage(); this.sendSyncMessage();
}).catch((result) => { })
.catch(result => {
const now = Date.now(); const now = Date.now();
this.trigger('done'); this.trigger('done');
if (result.dataMessage) { if (result.dataMessage) {
@@ -383,12 +393,14 @@
}); });
promises.push(this.sendSyncMessage()); promises.push(this.sendSyncMessage());
} }
promises = promises.concat(_.map(result.errors, (error) => { promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') { if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number); const c = ConversationController.get(error.number);
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
})); })
);
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
@@ -423,12 +435,14 @@
if (this.get('synced') || !dataMessage) { if (this.get('synced') || !dataMessage) {
return Promise.resolve(); return Promise.resolve();
} }
return textsecure.messaging.sendSyncMessage( return textsecure.messaging
.sendSyncMessage(
dataMessage, dataMessage,
this.get('sent_at'), this.get('sent_at'),
this.get('destination'), this.get('destination'),
this.get('expirationStartTimestamp') this.get('expirationStartTimestamp')
).then(() => { )
.then(() => {
this.save({ synced: true, dataMessage: null }); this.save({ synced: true, dataMessage: null });
}); });
}); });
@@ -440,17 +454,19 @@
if (!(errors instanceof Array)) { if (!(errors instanceof Array)) {
errors = [errors]; errors = [errors];
} }
errors.forEach((e) => { errors.forEach(e => {
console.log( console.log(
'Message.saveErrors:', 'Message.saveErrors:',
e && e.reason ? e.reason : null, e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e e && e.stack ? e.stack : e
); );
}); });
errors = errors.map((e) => { errors = errors.map(e => {
if (e.constructor === Error || if (
e.constructor === Error ||
e.constructor === TypeError || e.constructor === TypeError ||
e.constructor === ReferenceError) { e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
} }
return e; return e;
@@ -463,17 +479,19 @@
hasNetworkError() { hasNetworkError() {
const error = _.find( const error = _.find(
this.get('errors'), this.get('errors'),
e => (e.name === 'MessageError' || e =>
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError') e.name === 'SignedPreKeyRotationError'
); );
return !!error; return !!error;
}, },
removeOutgoingErrors(number) { removeOutgoingErrors(number) {
const errors = _.partition( const errors = _.partition(
this.get('errors'), this.get('errors'),
e => e.number === number && e =>
e.number === number &&
(e.name === 'MessageError' || (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
@@ -484,11 +502,13 @@
return errors[0][0]; return errors[0][0];
}, },
isReplayableError(e) { isReplayableError(e) {
return (e.name === 'MessageError' || return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'); e.name === 'OutgoingIdentityKeyError'
);
}, },
resend(number) { resend(number) {
const error = this.removeOutgoingErrors(number); const error = this.removeOutgoingErrors(number);
@@ -513,7 +533,9 @@
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
return conversation.queueJob(() => new Promise((resolve) => { return conversation.queueJob(
() =>
new Promise(resolve => {
const now = new Date().getTime(); const now = new Date().getTime();
let attributes = { type: 'private' }; let attributes = { type: 'private' };
if (dataMessage.group) { if (dataMessage.group) {
@@ -528,13 +550,15 @@
groupId: dataMessage.group.id, groupId: dataMessage.group.id,
name: dataMessage.group.name, name: dataMessage.group.name,
avatar: dataMessage.group.avatar, avatar: dataMessage.group.avatar,
members: _.union(dataMessage.group.members, conversation.get('members')), members: _.union(
dataMessage.group.members,
conversation.get('members')
),
}; };
groupUpdate = conversation.changedAttributes(_.pick( groupUpdate =
dataMessage.group, conversation.changedAttributes(
'name', _.pick(dataMessage.group, 'name', 'avatar')
'avatar' ) || {};
)) || {};
const difference = _.difference( const difference = _.difference(
attributes.members, attributes.members,
conversation.get('members') conversation.get('members')
@@ -553,7 +577,10 @@
} else { } else {
groupUpdate = { left: source }; groupUpdate = { left: source };
} }
attributes.members = _.without(conversation.get('members'), source); attributes.members = _.without(
conversation.get('members'),
source
);
} }
if (groupUpdate !== null) { if (groupUpdate !== null) {
@@ -574,10 +601,15 @@
schemaVersion: dataMessage.schemaVersion, schemaVersion: dataMessage.schemaVersion,
}); });
if (type === 'outgoing') { if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message); const receipts = Whisper.DeliveryReceipts.forMessage(
receipts.forEach(() => message.set({ conversation,
message
);
receipts.forEach(() =>
message.set({
delivered: (message.get('delivered') || 0) + 1, delivered: (message.get('delivered') || 0) + 1,
})); })
);
} }
attributes.active_at = now; attributes.active_at = now;
conversation.set(attributes); conversation.set(attributes);
@@ -611,15 +643,19 @@
if (!message.isEndSession() && !message.isGroupUpdate()) { if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) { if (dataMessage.expireTimer) {
if (dataMessage.expireTimer !== conversation.get('expireTimer')) { if (
dataMessage.expireTimer !== conversation.get('expireTimer')
) {
conversation.updateExpirationTimer( conversation.updateExpirationTimer(
dataMessage.expireTimer, source, dataMessage.expireTimer,
source,
message.get('received_at') message.get('received_at')
); );
} }
} else if (conversation.get('expireTimer')) { } else if (conversation.get('expireTimer')) {
conversation.updateExpirationTimer( conversation.updateExpirationTimer(
null, source, null,
source,
message.get('received_at') message.get('received_at')
); );
} }
@@ -627,23 +663,35 @@
if (type === 'incoming') { if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message); const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) { if (readSync) {
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) { if (
message.set('expirationStartTimestamp', readSync.get('read_at')); message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
} }
} }
if (readSync || message.isExpirationTimerUpdate()) { if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread'); message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as // This is primarily to allow the conversation to mark all older
// read, as is done when we receive a read sync for a message we already // messages as read, as is done when we receive a read sync for
// know about. // a message we already know about.
Whisper.ReadSyncs.notifyConversation(message); Whisper.ReadSyncs.notifyConversation(message);
} else { } else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1); conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
} }
} }
if (type === 'outgoing') { if (type === 'outgoing') {
const reads = Whisper.ReadReceipts.forMessage(conversation, message); const reads = Whisper.ReadReceipts.forMessage(
conversation,
message
);
if (reads.length) { if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader')); const readBy = reads.map(receipt => receipt.get('reader'));
message.set({ message.set({
@@ -655,7 +703,10 @@
} }
const conversationTimestamp = conversation.get('timestamp'); const conversationTimestamp = conversation.get('timestamp');
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) { if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({ conversation.set({
lastMessage: message.getNotificationText(), lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
@@ -672,15 +723,20 @@
ConversationController.getOrCreateAndWait( ConversationController.getOrCreateAndWait(
source, source,
'private' 'private'
).then((sender) => { ).then(sender => {
sender.setProfileKey(profileKey); sender.setProfileKey(profileKey);
}); });
} }
} }
const handleError = (error) => { const handleError = error => {
const errorForLog = error && error.stack ? error.stack : error; const errorForLog = error && error.stack ? error.stack : error;
console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog); console.log(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
return resolve(); return resolve();
}; };
@@ -691,17 +747,22 @@
} catch (e) { } catch (e) {
return handleError(e); return handleError(e);
} }
// We fetch() here because, between the message.save() above and the previous // We fetch() here because, between the message.save() above and
// line's trigger() call, we might have marked all messages unread in the // the previous line's trigger() call, we might have marked all
// database. This message might already be read! // messages unread in the database. This message might already
// be read!
const previousUnread = message.get('unread'); const previousUnread = message.get('unread');
return message.fetch().then(() => { return message.fetch().then(
() => {
try { try {
if (previousUnread !== message.get('unread')) { if (previousUnread !== message.get('unread')) {
console.log('Caught race condition on new message read state! ' + console.log(
'Manually starting timers.'); 'Caught race condition on new message read state! ' +
// We call markRead() even though the message is already marked read 'Manually starting timers.'
// because we need to start expiration timers, etc. );
// We call markRead() even though the message is already
// marked read because we need to start expiration
// timers, etc.
message.markRead(); message.markRead();
} }
@@ -717,7 +778,8 @@
} catch (e) { } catch (e) {
return handleError(e); return handleError(e);
} }
}, () => { },
() => {
try { try {
console.log( console.log(
'handleDataMessage: Message', 'handleDataMessage: Message',
@@ -730,19 +792,23 @@
} catch (e) { } catch (e) {
return handleError(e); return handleError(e);
} }
}); }
);
}, handleError); }, handleError);
}, handleError); }, handleError);
})); })
);
}, },
markRead(readAt) { markRead(readAt) {
this.unset('unread'); this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
this.set('expirationStartTimestamp', readAt || Date.now()); this.set('expirationStartTimestamp', readAt || Date.now());
} }
Whisper.Notifications.remove(Whisper.Notifications.where({ Whisper.Notifications.remove(
Whisper.Notifications.where({
messageId: this.id, messageId: this.id,
})); })
);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.save().then(resolve, reject); this.save().then(resolve, reject);
}); });
@@ -760,7 +826,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 +850,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 +869,29 @@
} }
}, },
destroyAll() { destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => { return Promise.all(
m.destroy().then(resolve).fail(reject); this.models.map(
}))); m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
}, },
fetchSentAt(timestamp) { fetchSentAt(timestamp) {
return new Promise((resolve => this.fetch({ return new Promise(resolve =>
this.fetch({
index: { index: {
// 'receipt' index on sent_at // 'receipt' index on sent_at
name: 'receipt', name: 'receipt',
only: timestamp, only: timestamp,
}, },
}).always(resolve))); }).always(resolve)
);
}, },
getLoadedUnreadCount() { getLoadedUnreadCount() {
@@ -841,7 +916,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 +968,4 @@
}); });
}, },
}); });
}()); })();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,13 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
;(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({
@@ -25,17 +21,20 @@
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 =
storage.get('audio-notification') || false;
const isAudioNotificationSupported = Settings.isAudioNotificationSupported(); const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
const shouldPlayNotificationSound = isAudioNotificationSupported && const shouldPlayNotificationSound =
isAudioNotificationEnabled; isAudioNotificationSupported && isAudioNotificationEnabled;
const numNotifications = this.length; const numNotifications = this.length;
console.log( console.log('Update notifications:', {
'Update notifications:', isFocused,
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound} isEnabled,
); numNotifications,
shouldPlayNotificationSound,
});
if (!isEnabled) { if (!isEnabled) {
return; return;
@@ -69,7 +68,7 @@
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
var newMessageCount = [ var newMessageCount = [
numNotifications, numNotifications,
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages') numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'),
].join(' '); ].join(' ');
var last = this.last(); var last = this.last();
@@ -105,13 +104,16 @@
}); });
} 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

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ReadReceipts = new (Backbone.Collection.extend({ Whisper.ReadReceipts = new (Backbone.Collection.extend({
@@ -16,8 +13,10 @@
ids = conversation.get('members'); ids = conversation.get('members');
} }
var receipts = this.filter(function(receipt) { var receipts = this.filter(function(receipt) {
return receipt.get('timestamp') === message.get('sent_at') return (
&& _.contains(ids, receipt.get('reader')); receipt.get('timestamp') === message.get('sent_at') &&
_.contains(ids, receipt.get('reader'))
);
}); });
if (receipts.length) { if (receipts.length) {
console.log('Found early read receipts for message'); console.log('Found early read receipts for message');
@@ -27,28 +26,43 @@
}, },
onReceipt: function(receipt) { onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection(); var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { return messages
if (messages.length === 0) { return; } .fetchSentAt(receipt.get('timestamp'))
.then(function() {
if (messages.length === 0) {
return;
}
var message = messages.find(function(message) { var message = messages.find(function(message) {
return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId')); return (
message.isOutgoing() &&
receipt.get('reader') === message.get('conversationId')
);
}); });
if (message) { return message; } if (message) {
return message;
}
var groups = new Whisper.GroupCollection(); var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('reader')).then(function() { return groups.fetchGroups(receipt.get('reader')).then(function() {
var ids = groups.pluck('id'); var ids = groups.pluck('id');
ids.push(receipt.get('reader')); ids.push(receipt.get('reader'));
return messages.find(function(message) { return messages.find(function(message) {
return (message.isOutgoing() && return (
_.contains(ids, message.get('conversationId'))); message.isOutgoing() &&
_.contains(ids, message.get('conversationId'))
);
}); });
}); });
}).then(function(message) { })
.then(
function(message) {
if (message) { if (message) {
var read_by = message.get('read_by') || []; var read_by = message.get('read_by') || [];
read_by.push(receipt.get('reader')); read_by.push(receipt.get('reader'));
return new Promise(function(resolve, reject) { return new Promise(
message.save({ read_by: read_by }).then(function() { function(resolve, reject) {
message.save({ read_by: read_by }).then(
function() {
// notify frontend listeners // notify frontend listeners
var conversation = ConversationController.get( var conversation = ConversationController.get(
message.get('conversationId') message.get('conversationId')
@@ -59,8 +73,11 @@
this.remove(receipt); this.remove(receipt);
resolve(); resolve();
}.bind(this), reject); }.bind(this),
}.bind(this)); reject
);
}.bind(this)
);
} else { } else {
console.log( console.log(
'No message for read receipt', 'No message for read receipt',
@@ -68,7 +85,9 @@
receipt.get('timestamp') receipt.get('timestamp')
); );
} }
}.bind(this)).catch(function(error) { }.bind(this)
)
.catch(function(error) {
console.log( console.log(
'ReadReceipts.onReceipt error:', 'ReadReceipts.onReceipt error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error

View File

@@ -1,14 +1,11 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.ReadSyncs = new (Backbone.Collection.extend({ Whisper.ReadSyncs = new (Backbone.Collection.extend({
forMessage: function(message) { forMessage: function(message) {
var receipt = this.findWhere({ var receipt = this.findWhere({
sender: message.get('source'), sender: message.get('source'),
timestamp: message.get('sent_at') timestamp: message.get('sent_at'),
}); });
if (receipt) { if (receipt) {
console.log('Found early read sync for message'); console.log('Found early read sync for message');
@@ -18,27 +15,35 @@
}, },
onReceipt: function(receipt) { onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection(); var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() { return messages.fetchSentAt(receipt.get('timestamp')).then(
function() {
var message = messages.find(function(message) { var message = messages.find(function(message) {
return (message.isIncoming() && message.isUnread() && return (
message.get('source') === receipt.get('sender')); message.isIncoming() &&
message.isUnread() &&
message.get('source') === receipt.get('sender')
);
}); });
if (message) { if (message) {
return message.markRead(receipt.get('read_at')).then(function() { return message.markRead(receipt.get('read_at')).then(
function() {
this.notifyConversation(message); this.notifyConversation(message);
this.remove(receipt); this.remove(receipt);
}.bind(this)); }.bind(this)
);
} else { } else {
console.log( console.log(
'No message for read sync', 'No message for read sync',
receipt.get('sender'), receipt.get('timestamp') receipt.get('sender'),
receipt.get('timestamp')
); );
} }
}.bind(this)); }.bind(this)
);
}, },
notifyConversation: function(message) { notifyConversation: function(message) {
var conversation = ConversationController.get({ var conversation = ConversationController.get({
id: message.get('conversationId') id: message.get('conversationId'),
}); });
if (conversation) { if (conversation) {

View File

@@ -1,25 +1,24 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(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() { remove: function() {
storage.remove('chromiumRegistrationDone'); 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

@@ -1,8 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
;(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
@@ -17,8 +13,12 @@
function run() { function run() {
console.log('Rotating signed prekey...'); console.log('Rotating signed prekey...');
getAccountManager().rotateSignedPreKey().catch(function() { getAccountManager()
console.log('rotateSignedPrekey() failed. Trying again in five seconds'); .rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000); setTimeout(runWhenOnline, 5000);
}); });
scheduleNextRotation(); scheduleNextRotation();
@@ -29,7 +29,9 @@
if (navigator.onLine) { if (navigator.onLine) {
run(); run();
} else { } else {
console.log('We are offline; keys will be rotated when we are next online'); console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() { var listener = function() {
window.removeEventListener('online', listener); window.removeEventListener('online', listener);
run(); run();
@@ -79,6 +81,6 @@
setTimeoutForNextRun(); 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,12 +1,9 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var Item = Backbone.Model.extend({ var Item = Backbone.Model.extend({
database: Whisper.Database, database: Whisper.Database,
storeName: 'items' storeName: 'items',
}); });
var ItemCollection = Backbone.Collection.extend({ var ItemCollection = Backbone.Collection.extend({
model: Item, model: Item,
@@ -16,26 +13,28 @@
var ready = false; var ready = false;
var items = new ItemCollection(); var items = new ItemCollection();
items.on('reset', function() { ready = true; }); items.on('reset', function() {
ready = true;
});
window.storage = { window.storage = {
/***************************** /*****************************
*** Base Storage Routines *** *** Base Storage Routines ***
*****************************/ *****************************/
put: function(key, value) { put: function(key, value) {
if (value === undefined) { if (value === undefined) {
throw new Error("Tried to store undefined"); throw new Error('Tried to store undefined');
} }
if (!ready) { if (!ready) {
console.log('Called storage.put before storage is ready. key:', key); console.log('Called storage.put before storage is ready. key:', key);
} }
var item = items.add({id: key, value: value}, {merge: true}); var item = items.add({ id: key, value: value }, { merge: true });
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
item.save().then(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;
} }
@@ -43,7 +42,7 @@
}, },
remove: function(key) { remove: function(key) {
var item = items.get("" + key); var item = items.get('' + key);
if (item) { if (item) {
items.remove(item); items.remove(item);
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@@ -63,16 +62,23 @@
fetch: function() { fetch: function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
items.fetch({reset: true}) items
.fail(() => reject(new Error('Failed to fetch from storage.' + .fetch({ reset: true })
' This may be due to an unexpected database version.'))) .fail(() =>
reject(
new Error(
'Failed to fetch from storage.' +
' This may be due to an unexpected database version.'
)
)
)
.always(resolve); .always(resolve);
}); });
}, },
reset: function() { reset: function() {
items.reset(); items.reset();
} },
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};

View File

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

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -9,7 +6,7 @@
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',
@@ -122,8 +119,11 @@
Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
}, },
isVoiceMessage() { isVoiceMessage() {
if (
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) { 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,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -16,13 +13,13 @@
this.message = options.message; this.message = options.message;
this.callbacks = { this.callbacks = {
onDismiss: options.onDismiss, onDismiss: options.onDismiss,
onClick: options.onClick onClick: options.onClick,
}; };
this.render(); this.render();
}, },
render_attributes: function() { render_attributes: function() {
return { return {
message: this.message message: this.message,
}; };
}, },
onDismiss: function(e) { onDismiss: function(e) {
@@ -31,6 +28,6 @@
}, },
onClick: function() { onClick: function() {
this.callbacks.onClick(); this.callbacks.onClick();
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -21,7 +18,7 @@
this.render(); this.render();
}, },
events: { events: {
'keyup': 'onKeyup', keyup: 'onKeyup',
'click .ok': 'ok', 'click .ok': 'ok',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
@@ -30,7 +27,7 @@
message: this.message, message: this.message,
showCancel: !this.hideCancel, showCancel: !this.hideCancel,
cancel: this.cancelText, cancel: this.cancelText,
ok: this.okText ok: this.okText,
}; };
}, },
ok: function() { ok: function() {
@@ -52,6 +49,6 @@
}, },
focusCancel: function() { focusCancel: function() {
this.$('.cancel').focus(); this.$('.cancel').focus();
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -12,7 +9,7 @@
className: 'contact', className: 'contact',
templateName: 'contact', templateName: 'contact',
events: { events: {
'click': 'showIdentity' click: 'showIdentity',
}, },
initialize: function(options) { initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
@@ -25,7 +22,7 @@
return { return {
title: i18n('me'), title: i18n('me'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
} }
@@ -36,7 +33,7 @@
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
isVerified: this.model.isVerified(), isVerified: this.model.isVerified(),
verified: i18n('verified') verified: i18n('verified'),
}; };
}, },
showIdentity: function() { showIdentity: function() {
@@ -44,10 +41,10 @@
return; return;
} }
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
} },
}) }),
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -13,27 +10,43 @@
}, },
templateName: 'conversation-preview', templateName: 'conversation-preview',
events: { events: {
'click': 'select' click: 'select',
}, },
initialize: function() { initialize: function() {
// auto update // auto update
this.listenTo(this.model, 'change', _.debounce(this.render.bind(this), 1000)); this.listenTo(
this.model,
'change',
_.debounce(this.render.bind(this), 1000)
);
this.listenTo(this.model, 'destroy', this.remove); // auto update this.listenTo(this.model, 'destroy', this.remove); // auto update
this.listenTo(this.model, 'opened', this.markSelected); // auto update this.listenTo(this.model, 'opened', this.markSelected); // auto update
var updateLastMessage = _.debounce(this.model.updateLastMessage.bind(this.model), 1000); var updateLastMessage = _.debounce(
this.listenTo(this.model.messageCollection, 'add remove', updateLastMessage); this.model.updateLastMessage.bind(this.model),
1000
);
this.listenTo(
this.model.messageCollection,
'add remove',
updateLastMessage
);
this.listenTo(this.model, 'newmessage', updateLastMessage); this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed(function() { extension.windows.onClosed(
function() {
this.stopListening(); this.stopListening();
}.bind(this)); }.bind(this)
this.timeStampView = new Whisper.TimestampView({brief: true}); );
this.timeStampView = new Whisper.TimestampView({ brief: true });
this.model.updateLastMessage(); this.model.updateLastMessage();
}, },
markSelected: function() { markSelected: function() {
this.$el.addClass('selected').siblings('.selected').removeClass('selected'); this.$el
.addClass('selected')
.siblings('.selected')
.removeClass('selected');
}, },
select: function(e) { select: function(e) {
@@ -43,15 +56,19 @@
render: function() { render: function() {
this.$el.html( this.$el.html(
Mustache.render(_.result(this,'template', ''), { Mustache.render(
_.result(this, 'template', ''),
{
title: this.model.getTitle(), title: this.model.getTitle(),
last_message: this.model.get('lastMessage'), last_message: this.model.get('lastMessage'),
last_message_timestamp: this.model.get('timestamp'), last_message_timestamp: this.model.get('timestamp'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
unreadCount: this.model.get('unreadCount') unreadCount: this.model.get('unreadCount'),
}, this.render_partials()) },
this.render_partials()
)
); );
this.timeStampView.setElement(this.$('.last-timestamp')); this.timeStampView.setElement(this.$('.last-timestamp'));
this.timeStampView.update(); this.timeStampView.update();
@@ -67,7 +84,6 @@
} }
return this; return this;
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -56,6 +53,6 @@
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(
unverified.map(contact => {
if (contact.isUnverified()) { if (contact.isUnverified()) {
return contact.setVerifiedDefault(); return contact.setVerifiedDefault();
} }
return null; return null;
})); })
);
}, },
markAllAsApproved(untrusted) { markAllAsApproved(untrusted) {
@@ -404,7 +418,10 @@
} }
}, },
toggleMicrophone() { toggleMicrophone() {
if (this.$('.send-message').val().length > 0 || this.fileInput.hasFiles()) { if (
this.$('.send-message').val().length > 0 ||
this.fileInput.hasFiles()
) {
this.$('.capture-audio').hide(); this.$('.capture-audio').hide();
} else { } else {
this.$('.capture-audio').show(); this.$('.capture-audio').show();
@@ -495,11 +512,14 @@
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(() =>
// eslint-disable-next-line more/no-then
this.model.updateVerified().then(() => {
this.onVerifiedChange(); this.onVerifiedChange();
this.statusFetch = null; this.statusFetch = null;
console.log('done with status fetch'); console.log('done with status fetch');
})); })
);
// We schedule our catch-up decrypt right after any in-progress fetch of // We schedule our catch-up decrypt right after any in-progress fetch of
// messages from the database, then ensure that the loading screen is only // messages from the database, then ensure that the loading screen is only
@@ -587,20 +607,25 @@
const conversationId = this.model.get('id'); const conversationId = this.model.get('id');
const WhisperMessageCollection = Whisper.MessageCollection; const WhisperMessageCollection = Whisper.MessageCollection;
const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments({ const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments(
{
conversationId, conversationId,
count: DEFAULT_MEDIA_FETCH_COUNT, count: DEFAULT_MEDIA_FETCH_COUNT,
WhisperMessageCollection, WhisperMessageCollection,
}); }
const documents = await Signal.Backbone.Conversation.fetchFileAttachments({ );
const documents = await Signal.Backbone.Conversation.fetchFileAttachments(
{
conversationId, conversationId,
count: DEFAULT_DOCUMENTS_FETCH_COUNT, count: DEFAULT_DOCUMENTS_FETCH_COUNT,
WhisperMessageCollection, WhisperMessageCollection,
}); }
);
// NOTE: Could we show grid previews from disk as well? // NOTE: Could we show grid previews from disk as well?
const loadMessages = Signal.Components.Types.Message const loadMessages = Signal.Components.Types.Message.loadWithObjectURL(
.loadWithObjectURL(Signal.Migrations.loadMessage); Signal.Migrations.loadMessage
);
const media = await loadMessages(rawMedia); const media = await loadMessages(rawMedia);
const { getAbsoluteAttachmentPath } = Signal.Migrations; const { getAbsoluteAttachmentPath } = Signal.Migrations;
@@ -624,13 +649,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 +711,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 +771,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 +786,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 +849,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 +1246,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 +1305,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 +1340,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 +1364,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 +1386,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,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -11,6 +8,6 @@
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(
{ type: 'openFile' },
entry => {
if (!entry) { if (!entry) {
return; return;
} }
entry.file((file) => { entry.file(file => {
this.file = file; this.file = file;
this.previewImages(); this.previewImages();
}); });
}); }
);
} else { } else {
this.$input.click(); this.$input.click();
} }
@@ -178,14 +183,16 @@
}, },
autoScale(file) { autoScale(file) {
if (file.type.split('/')[0] !== 'image' || if (
file.type.split('/')[0] !== 'image' ||
file.type === 'image/gif' || file.type === 'image/gif' ||
file.type === 'image/tiff') { file.type === 'image/tiff'
) {
// nothing to do // nothing to do
return Promise.resolve(file); return Promise.resolve(file);
} }
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const img = document.createElement('img'); const img = document.createElement('img');
img.onerror = reject; img.onerror = reject;
@@ -195,13 +202,19 @@
const maxSize = 6000 * 1024; const maxSize = 6000 * 1024;
const maxHeight = 4096; const maxHeight = 4096;
const maxWidth = 4096; const maxWidth = 4096;
if (img.width <= maxWidth && img.height <= maxHeight && file.size <= maxSize) { if (
img.width <= maxWidth &&
img.height <= maxHeight &&
file.size <= maxSize
) {
resolve(file); resolve(file);
return; return;
} }
const canvas = loadImage.scale(img, { const canvas = loadImage.scale(img, {
canvas: true, maxWidth, maxHeight, canvas: true,
maxWidth,
maxHeight,
}); });
let quality = 0.95; let quality = 0.95;
@@ -209,8 +222,10 @@
let blob; let blob;
do { do {
i -= 1; i -= 1;
blob = window.dataURLToBlobSync(canvas.toDataURL('image/jpeg', quality)); blob = window.dataURLToBlobSync(
quality = (quality * maxSize) / blob.size; canvas.toDataURL('image/jpeg', quality)
);
quality = quality * maxSize / blob.size;
// NOTE: During testing with a large image, we observed the // NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]? // `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
@@ -222,7 +237,7 @@
resolve(blob); resolve(blob);
}; };
img.src = url; img.src = url;
})); });
}, },
async previewImages() { async previewImages() {
@@ -271,21 +286,25 @@
const blob = await this.autoScale(file); const blob = await this.autoScale(file);
let limitKb = 1000000; let limitKb = 1000000;
const blobType = file.type === 'image/gif' const blobType =
? 'gif' file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
: contentType.split('/')[0];
switch (blobType) { switch (blobType) {
case 'image': case 'image':
limitKb = 6000; break; limitKb = 6000;
break;
case 'gif': case 'gif':
limitKb = 25000; break; limitKb = 25000;
break;
case 'audio': case 'audio':
limitKb = 100000; break; limitKb = 100000;
break;
case 'video': case 'video':
limitKb = 100000; break; limitKb = 100000;
break;
default: default:
limitKb = 100000; break; limitKb = 100000;
break;
} }
if ((blob.size / 1024).toFixed(4) >= limitKb) { if ((blob.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB']; const units = ['kB', 'MB', 'GB'];
@@ -310,7 +329,9 @@
}, },
getFiles() { getFiles() {
const files = this.file ? [this.file] : Array.from(this.$input.prop('files')); const files = this.file
? [this.file]
: Array.from(this.$input.prop('files'));
const promise = Promise.all(files.map(file => this.getFile(file))); const promise = Promise.all(files.map(file => this.getFile(file)));
this.clearForm(); this.clearForm();
return promise; return promise;
@@ -325,7 +346,7 @@
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE ? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
: null; : null;
const setFlags = flags => (attachment) => { const setFlags = flags => attachment => {
const newAttachment = Object.assign({}, attachment); const newAttachment = Object.assign({}, attachment);
if (flags) { if (flags) {
newAttachment.flags = flags; newAttachment.flags = flags;
@@ -345,9 +366,11 @@
// Scale and crop an image to 256px square // Scale and crop an image to 256px square
const size = 256; const size = 256;
const file = this.file || this.$input.prop('files')[0]; const file = this.file || this.$input.prop('files')[0];
if (file === undefined || if (
file === undefined ||
file.type.split('/')[0] !== 'image' || file.type.split('/')[0] !== 'image' ||
file.type === 'image/gif') { file.type === 'image/gif'
) {
// nothing to do // nothing to do
return Promise.resolve(); return Promise.resolve();
} }
@@ -362,9 +385,9 @@
// File -> Promise Attachment // File -> Promise Attachment
readFile(file) { readFile(file) {
return new Promise(((resolve, reject) => { return new Promise((resolve, reject) => {
const FR = new FileReader(); const FR = new FileReader();
FR.onload = (e) => { FR.onload = e => {
resolve({ resolve({
data: e.target.result, data: e.target.result,
contentType: file.type, contentType: file.type,
@@ -375,7 +398,7 @@
FR.onerror = reject; FR.onerror = reject;
FR.onabort = reject; FR.onabort = reject;
FR.readAsArrayBuffer(file); FR.readAsArrayBuffer(file);
})); });
}, },
clearForm() { clearForm() {
@@ -390,9 +413,14 @@
}, },
deleteFiles(e) { deleteFiles(e) {
if (e) { e.stopPropagation(); } if (e) {
e.stopPropagation();
}
this.clearForm(); this.clearForm();
this.$input.wrap('<form>').parent('form').trigger('reset'); this.$input
.wrap('<form>')
.parent('form')
.trigger('reset');
this.$input.unwrap(); this.$input.unwrap();
this.file = null; this.file = null;
this.$input.trigger('change'); this.$input.trigger('change');
@@ -450,4 +478,4 @@
Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail; Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail;
Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail; Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail;
Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot; Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot;
}()); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -18,8 +15,8 @@
collection: this.model, collection: this.model,
className: 'members', className: 'members',
toInclude: { toInclude: {
listenBack: options.listenBack listenBack: options.listenBack,
} },
}); });
this.member_list_view.render(); this.member_list_view.render();
@@ -33,8 +30,8 @@
return { return {
members: i18n('groupMembers'), members: i18n('groupMembers'),
summary: summary summary: summary,
}; };
} },
}); });
})(); })();

View File

@@ -1,14 +1,11 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(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) {
@@ -27,7 +24,6 @@
this.$el.text(messages.join(' ')); this.$el.text(messages.join(' '));
return this; return this;
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -12,6 +9,6 @@
}, },
render_attributes: function() { render_attributes: function() {
return { content: this.content }; return { content: this.content };
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -16,16 +13,18 @@
}, },
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,
maxHeight: 100,
}); });
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
@@ -35,25 +34,24 @@
img.src = svgurl; img.src = svgurl;
}); });
} },
}); });
var COLORS = { var COLORS = {
red : '#EF5350', red: '#EF5350',
pink : '#EC407A', pink: '#EC407A',
purple : '#AB47BC', purple: '#AB47BC',
deep_purple : '#7E57C2', deep_purple: '#7E57C2',
indigo : '#5C6BC0', indigo: '#5C6BC0',
blue : '#2196F3', blue: '#2196F3',
light_blue : '#03A9F4', light_blue: '#03A9F4',
cyan : '#00BCD4', cyan: '#00BCD4',
teal : '#009688', teal: '#009688',
green : '#4CAF50', green: '#4CAF50',
light_green : '#7CB342', light_green: '#7CB342',
orange : '#FF9800', orange: '#FF9800',
deep_orange : '#FF5722', deep_orange: '#FF5722',
amber : '#FFB300', amber: '#FFB300',
blue_grey : '#607D8B' blue_grey: '#607D8B',
}; };
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -18,11 +15,11 @@
events: { events: {
'click .show-safety-number': 'showSafetyNumber', 'click .show-safety-number': 'showSafetyNumber',
'click .send-anyway': 'sendAnyway', 'click .send-anyway': 'sendAnyway',
'click .cancel': 'cancel' 'click .cancel': 'cancel',
}, },
showSafetyNumber: function() { showSafetyNumber: function() {
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
}, },
@@ -39,13 +36,16 @@
send = i18n('resend'); send = i18n('resend');
} }
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]); var errorExplanation = i18n('identityKeyErrorOnSend', [
this.model.getTitle(),
this.model.getTitle(),
]);
return { return {
errorExplanation : errorExplanation, errorExplanation: errorExplanation,
showSafetyNumber : i18n('showSafetyNumber'), showSafetyNumber: i18n('showSafetyNumber'),
sendAnyway : send, sendAnyway: send,
cancel : i18n('cancel') cancel: i18n('cancel'),
}; };
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -36,7 +33,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 +99,19 @@
this.trigger('cancel'); this.trigger('cancel');
}, },
onImport: function() { onImport: function() {
window.Signal.Backup.getDirectoryForImport().then(function(directory) { window.Signal.Backup.getDirectoryForImport().then(
function(directory) {
this.doImport(directory); this.doImport(directory);
}.bind(this), function(error) { }.bind(this),
function(error) {
if (error.name !== 'ChooseError') { if (error.name !== 'ChooseError') {
console.log( console.log(
'Error choosing directory:', 'Error choosing directory:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
}); }
);
}, },
onRegister: function() { onRegister: function() {
// AppView listens for this, and opens up InstallView to the QR code step to // AppView listens for this, and opens up InstallView to the QR code step to
@@ -127,15 +127,19 @@
this.render(); this.render();
// Wait for prior database interaction to complete // Wait for prior database interaction to complete
this.pending = this.pending.then(function() { this.pending = this.pending
.then(function() {
// For resilience to interruption, clear database both before and on failure // For resilience to interruption, clear database both before and on failure
return Whisper.Import.reset(); return Whisper.Import.reset();
}).then(function() { })
.then(function() {
return Promise.all([ return Promise.all([
Whisper.Import.start(), Whisper.Import.start(),
window.Signal.Backup.importFromDirectory(directory) window.Signal.Backup.importFromDirectory(directory),
]); ]);
}).then(function(results) { })
.then(
function(results) {
var importResult = results[1]; var importResult = results[1];
// A full import changes so much we need a restart of the app // A full import changes so much we need a restart of the app
@@ -146,34 +150,46 @@
// A light import just brings in contacts, groups, and messages. And we need a // A light import just brings in contacts, groups, and messages. And we need a
// normal link to finish the process. // normal link to finish the process.
return this.finishLightImport(directory); return this.finishLightImport(directory);
}.bind(this)).catch(function(error) { }.bind(this)
console.log('Error importing:', error && error.stack ? error.stack : error); )
.catch(
function(error) {
console.log(
'Error importing:',
error && error.stack ? error.stack : error
);
this.error = error || new Error('Something went wrong!'); this.error = error || new Error('Something went wrong!');
this.state = null; this.state = null;
this.render(); this.render();
return Whisper.Import.reset(); return Whisper.Import.reset();
}.bind(this)); }.bind(this)
);
}, },
finishLightImport: function(directory) { finishLightImport: function(directory) {
ConversationController.reset(); ConversationController.reset();
return ConversationController.load().then(function() { return ConversationController.load()
.then(function() {
return Promise.all([ return Promise.all([
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete(), Whisper.Import.complete(),
]); ]);
}).then(function() { })
.then(
function() {
this.state = State.LIGHT_COMPLETE; this.state = State.LIGHT_COMPLETE;
this.render(); this.render();
}.bind(this)); }.bind(this)
);
}, },
finishFullImport: function(directory) { finishFullImport: function(directory) {
// Catching in-memory cache up with what's in indexeddb now... // Catching in-memory cache up with what's in indexeddb now...
// NOTE: this fires storage.onready, listened to across the app. We'll restart // NOTE: this fires storage.onready, listened to across the app. We'll restart
// to complete the install to start up cleanly with everything now in the DB. // to complete the install to start up cleanly with everything now in the DB.
return storage.fetch() return storage
.fetch()
.then(function() { .then(function() {
return Promise.all([ return Promise.all([
// Clearing any migration-related state inherited from the Chrome App // Clearing any migration-related state inherited from the Chrome App
@@ -183,12 +199,15 @@
storage.remove('migrationStorageLocation'), storage.remove('migrationStorageLocation'),
Whisper.Import.saveLocation(directory), Whisper.Import.saveLocation(directory),
Whisper.Import.complete() Whisper.Import.complete(),
]); ]);
}).then(function() { })
.then(
function() {
this.state = State.COMPLETE; this.state = State.COMPLETE;
this.render(); this.render();
}.bind(this)); }.bind(this)
} );
},
}); });
})(); })();

View File

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

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -34,23 +31,24 @@
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
// Keep data around if it's a re-link, or the middle of a light import // Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData; this.shouldRetainData =
Whisper.Registration.everDone() || options.hasExistingData;
}, },
render_attributes: function() { render_attributes: function() {
var errorMessage; var errorMessage;
if (this.error) { if (this.error) {
if (this.error.name === 'HTTPError' if (
&& this.error.code == TOO_MANY_DEVICES) { this.error.name === 'HTTPError' &&
this.error.code == TOO_MANY_DEVICES
) {
errorMessage = i18n('installTooManyDevices'); errorMessage = i18n('installTooManyDevices');
} } else if (
else if (this.error.name === 'HTTPError' this.error.name === 'HTTPError' &&
&& this.error.code == CONNECTION_ERROR) { this.error.code == CONNECTION_ERROR
) {
errorMessage = i18n('installConnectionFailed'); errorMessage = i18n('installConnectionFailed');
} } else if (this.error.message === 'websocket closed') {
else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific // AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message // 'websocket closed' error message
errorMessage = i18n('installConnectionFailed'); errorMessage = i18n('installConnectionFailed');
@@ -95,10 +93,12 @@
var accountManager = getAccountManager(); var accountManager = getAccountManager();
accountManager.registerSecondDevice( accountManager
.registerSecondDevice(
this.setProvisioningUrl.bind(this), this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this) this.confirmNumber.bind(this)
).catch(this.handleDisconnect.bind(this)); )
.catch(this.handleDisconnect.bind(this));
}, },
handleDisconnect: function(e) { handleDisconnect: function(e) {
console.log('provisioning failed', e.stack); console.log('provisioning failed', e.stack);
@@ -108,9 +108,10 @@
if (e.message === 'websocket closed') { if (e.message === 'websocket closed') {
this.trigger('disconnected'); this.trigger('disconnected');
} else if (e.name !== 'HTTPError' } else if (
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) { e.name !== 'HTTPError' ||
(e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
) {
throw e; throw e;
} }
}, },
@@ -155,13 +156,15 @@
this.selectStep(Steps.ENTER_NAME); this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault(); this.setDeviceNameDefault();
return new Promise(function(resolve, reject) { return new Promise(
this.$('#link-phone').submit(function(e) { function(resolve, reject) {
this.$('#link-phone').submit(
function(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
var name = this.$(DEVICE_NAME_SELECTOR).val(); var name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g,''); // strip unicode null name = name.replace(/\0/g, ''); // strip unicode null
if (name.trim().length === 0) { if (name.trim().length === 0) {
this.$(DEVICE_NAME_SELECTOR).focus(); this.$(DEVICE_NAME_SELECTOR).focus();
return; return;
@@ -189,8 +192,10 @@
); );
finish(); finish();
}); });
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -17,15 +14,15 @@
this.theirKey = options.newKey; this.theirKey = options.newKey;
} }
this.loadKeys().then(function() { this.loadKeys().then(
function() {
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}.bind(this)); }.bind(this)
);
}, },
loadKeys: function() { loadKeys: function() {
return Promise.all([ return Promise.all([this.loadTheirKey(), this.loadOurKey()])
this.loadTheirKey(), .then(this.generateSecurityNumber.bind(this))
this.loadOurKey(),
]).then(this.generateSecurityNumber.bind(this))
.then(this.render.bind(this)); .then(this.render.bind(this));
//.then(this.makeQRCode.bind(this)); //.then(this.makeQRCode.bind(this));
}, },
@@ -37,32 +34,37 @@
); );
}, },
loadTheirKey: function() { loadTheirKey: function() {
return textsecure.storage.protocol.loadIdentityKey( return textsecure.storage.protocol.loadIdentityKey(this.model.id).then(
this.model.id function(theirKey) {
).then(function(theirKey) {
this.theirKey = theirKey; this.theirKey = theirKey;
}.bind(this)); }.bind(this)
);
}, },
loadOurKey: function() { loadOurKey: function() {
return textsecure.storage.protocol.loadIdentityKey( return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then(
this.ourNumber function(ourKey) {
).then(function(ourKey) {
this.ourKey = ourKey; this.ourKey = ourKey;
}.bind(this)); }.bind(this)
);
}, },
generateSecurityNumber: function() { generateSecurityNumber: function() {
return new libsignal.FingerprintGenerator(5200).createFor( return new libsignal.FingerprintGenerator(5200)
this.ourNumber, this.ourKey, this.model.id, this.theirKey .createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
).then(function(securityNumber) { .then(
function(securityNumber) {
this.securityNumber = securityNumber; this.securityNumber = securityNumber;
}.bind(this)); }.bind(this)
);
}, },
onSafetyNumberChanged: function() { onSafetyNumberChanged: function() {
this.model.getProfiles().then(this.loadKeys.bind(this)); this.model.getProfiles().then(this.loadKeys.bind(this));
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
message: i18n('changedRightAfterVerify', [this.model.getTitle(), this.model.getTitle()]), message: i18n('changedRightAfterVerify', [
hideCancel: true this.model.getTitle(),
this.model.getTitle(),
]),
hideCancel: true,
}); });
dialog.$el.insertBefore(this.el); dialog.$el.insertBefore(this.el);
@@ -70,7 +72,10 @@
}, },
toggleVerified: function() { toggleVerified: function() {
this.$('button.verify').attr('disabled', true); this.$('button.verify').attr('disabled', true);
this.model.toggleVerified().catch(function(result) { this.model
.toggleVerified()
.catch(
function(result) {
if (result instanceof Error) { if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') { if (result.name === 'OutgoingIdentityKeyError') {
this.onSafetyNumberChanged(); this.onSafetyNumberChanged();
@@ -89,33 +94,42 @@
}); });
} }
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
this.$('button.verify').removeAttr('disabled'); this.$('button.verify').removeAttr('disabled');
}.bind(this)); }.bind(this)
);
}, },
render_attributes: function() { render_attributes: function() {
var s = this.securityNumber; var s = this.securityNumber;
var chunks = []; var chunks = [];
for (var i = 0; i < s.length; i += 5) { for (var i = 0; i < s.length; i += 5) {
chunks.push(s.substring(i, i+5)); chunks.push(s.substring(i, i + 5));
} }
var name = this.model.getTitle(); var name = this.model.getTitle();
var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name); var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name);
var isVerified = this.model.isVerified(); var isVerified = this.model.isVerified();
var verifyButton = isVerified ? i18n('unverify') : i18n('verify'); var verifyButton = isVerified ? i18n('unverify') : i18n('verify');
var verifiedStatus = isVerified ? i18n('isVerified', name) : i18n('isNotVerified', name); var verifiedStatus = isVerified
? i18n('isVerified', name)
: i18n('isNotVerified', name);
return { return {
learnMore : i18n('learnMore'), learnMore: i18n('learnMore'),
theirKeyUnknown : i18n('theirIdentityUnknown'), theirKeyUnknown: i18n('theirIdentityUnknown'),
yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()), yourSafetyNumberWith: i18n(
verifyHelp : i18n('verifyHelp', this.model.getTitle()), 'yourSafetyNumberWith',
verifyButton : verifyButton, this.model.getTitle()
hasTheirKey : this.theirKey !== undefined, ),
chunks : chunks, verifyHelp: i18n('verifyHelp', this.model.getTitle()),
isVerified : isVerified, verifyButton: verifyButton,
verifiedStatus : verifiedStatus hasTheirKey: this.theirKey !== undefined,
chunks: chunks,
isVerified: isVerified,
verifiedStatus: verifiedStatus,
}; };
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -25,12 +22,14 @@
}, },
render_attributes: function() { render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage') var unreadMessages =
this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]); : i18n('unreadMessages', [this.count]);
return { return {
unreadMessages: unreadMessages unreadMessages: unreadMessages,
}; };
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -20,7 +17,7 @@
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');
@@ -35,6 +32,6 @@
render: function() { render: function() {
this.addAll(); this.addAll();
return this; return this;
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -25,14 +22,14 @@
}); });
}, },
events: { events: {
'click': 'onClick' click: 'onClick',
}, },
onClick: function() { onClick: function() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({ var view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model, model: this.model,
listenBack: this.listenBack, listenBack: this.listenBack,
resetPanel: this.resetPanel resetPanel: this.resetPanel,
}); });
this.listenTo(view, 'send-anyway', this.onSendAnyway); this.listenTo(view, 'send-anyway', this.onSendAnyway);
@@ -44,19 +41,32 @@
} }
}, },
forceSend: function() { forceSend: function() {
this.model.updateVerified().then(function() { this.model
.updateVerified()
.then(
function() {
if (this.model.isUnverified()) { if (this.model.isUnverified()) {
return this.model.setVerifiedDefault(); return this.model.setVerifiedDefault();
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
return this.model.isUntrusted(); return this.model.isUntrusted();
}.bind(this)).then(function(untrusted) { }.bind(this)
)
.then(
function(untrusted) {
if (untrusted) { if (untrusted) {
return this.model.setApproved(); return this.model.setApproved();
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
this.message.resend(this.outgoingKeyError.number); this.message.resend(this.outgoingKeyError.number);
}.bind(this)); }.bind(this)
);
}, },
onSendAnyway: function() { onSendAnyway: function() {
if (this.outgoingKeyError) { if (this.outgoingKeyError) {
@@ -67,14 +77,14 @@
var showButton = Boolean(this.outgoingKeyError); var showButton = Boolean(this.outgoingKeyError);
return { return {
status : this.message.getStatus(this.model.id), status: this.message.getStatus(this.model.id),
name : this.model.getTitle(), name: this.model.getTitle(),
avatar : this.model.getAvatar(), avatar: this.model.getAvatar(),
errors : this.errors, errors: this.errors,
showErrorButton : showButton, showErrorButton: showButton,
errorButtonLabel : i18n('view') errorButtonLabel: i18n('view'),
}; };
} },
}); });
Whisper.MessageDetailView = Whisper.View.extend({ Whisper.MessageDetailView = Whisper.View.extend({
@@ -84,14 +94,14 @@
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
this.view = new Whisper.MessageView({model: this.model}); this.view = new Whisper.MessageView({ model: this.model });
this.view.render(); this.view.render();
this.conversation = options.conversation; this.conversation = options.conversation;
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
events: { events: {
'click button.delete': 'onDelete' 'click button.delete': 'onDelete',
}, },
onDelete: function() { onDelete: function() {
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
@@ -100,7 +110,7 @@
resolve: function() { resolve: function() {
this.model.destroy(); this.model.destroy();
this.resetPanel(); this.resetPanel();
}.bind(this) }.bind(this),
}); });
this.$el.prepend(dialog.el); this.$el.prepend(dialog.el);
@@ -110,7 +120,7 @@
// Return the set of models to be rendered in this view // Return the set of models to be rendered in this view
var ids; var ids;
if (this.model.isIncoming()) { if (this.model.isIncoming()) {
ids = [ this.model.get('source') ]; ids = [this.model.get('source')];
} else if (this.model.isOutgoing()) { } else if (this.model.isOutgoing()) {
ids = this.model.get('recipients'); ids = this.model.get('recipients');
if (!ids) { if (!ids) {
@@ -119,9 +129,11 @@
ids = this.conversation.getRecipients(); ids = this.conversation.getRecipients();
} }
} }
return Promise.all(ids.map(function(number) { return Promise.all(
ids.map(function(number) {
return ConversationController.getOrCreateAndWait(number, 'private'); return ConversationController.getOrCreateAndWait(number, 'private');
})); })
);
}, },
renderContact: function(contact) { renderContact: function(contact) {
var view = new ContactView({ var view = new ContactView({
@@ -129,40 +141,50 @@
errors: this.grouped[contact.id], errors: this.grouped[contact.id],
listenBack: this.listenBack, listenBack: this.listenBack,
resetPanel: this.resetPanel, resetPanel: this.resetPanel,
message: this.model message: this.model,
}).render(); }).render();
this.$('.contacts').append(view.el); this.$('.contacts').append(view.el);
}, },
render: function() { render: function() {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) { var errorsWithoutNumber = _.reject(this.model.get('errors'), function(
error
) {
return Boolean(error.number); return Boolean(error.number);
}); });
this.$el.html(Mustache.render(_.result(this, 'template', ''), { this.$el.html(
sent_at : moment(this.model.get('sent_at')).format('LLLL'), Mustache.render(_.result(this, 'template', ''), {
received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null, sent_at: moment(this.model.get('sent_at')).format('LLLL'),
tofrom : this.model.isIncoming() ? i18n('from') : i18n('to'), received_at: this.model.isIncoming()
errors : errorsWithoutNumber, ? moment(this.model.get('received_at')).format('LLLL')
title : i18n('messageDetail'), : null,
sent : i18n('sent'), tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'),
received : i18n('received'), errors: errorsWithoutNumber,
errorLabel : i18n('error'), title: i18n('messageDetail'),
deleteLabel : i18n('deleteMessage'), sent: i18n('sent'),
retryDescription: i18n('retryDescription') received: i18n('received'),
})); errorLabel: i18n('error'),
deleteLabel: i18n('deleteMessage'),
retryDescription: i18n('retryDescription'),
})
);
this.view.$el.prependTo(this.$('.message-container')); this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number'); this.grouped = _.groupBy(this.model.get('errors'), 'number');
this.getContacts().then(function(contacts) { this.getContacts().then(
_.sortBy(contacts, function(c) { function(contacts) {
_.sortBy(
contacts,
function(c) {
var prefix = this.grouped[c.id] ? '0' : '1'; var prefix = this.grouped[c.id] ? '0' : '1';
// this prefix ensures that contacts with errors are listed first; // this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical // otherwise it's alphabetical
return prefix + c.getTitle(); return prefix + c.getTitle();
}.bind(this)).forEach(this.renderContact.bind(this)); }.bind(this)
}.bind(this)); ).forEach(this.renderContact.bind(this));
} }.bind(this)
);
},
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -10,14 +7,17 @@
className: 'message-list', className: 'message-list',
itemView: Whisper.MessageView, itemView: Whisper.MessageView,
events: { events: {
'scroll': 'onScroll', scroll: 'onScroll',
}, },
initialize: function() { initialize: function() {
Whisper.ListView.prototype.initialize.call(this); Whisper.ListView.prototype.initialize.call(this);
this.triggerLazyScroll = _.debounce(function() { this.triggerLazyScroll = _.debounce(
function() {
this.$el.trigger('lazyScroll'); this.$el.trigger('lazyScroll');
}.bind(this), 500); }.bind(this),
500
);
}, },
onScroll: function() { onScroll: function() {
this.measureScrollPosition(); this.measureScrollPosition();
@@ -36,7 +36,8 @@
return this.bottomOffset < 30; return this.bottomOffset < 30;
}, },
measureScrollPosition: function() { measureScrollPosition: function() {
if (this.el.scrollHeight === 0) { // hidden if (this.el.scrollHeight === 0) {
// hidden
return; return;
} }
this.outerHeight = this.$el.outerHeight(); this.outerHeight = this.$el.outerHeight();
@@ -64,13 +65,13 @@
addOne: function(model) { addOne: function(model) {
var view; var view;
if (model.isExpirationTimerUpdate()) { if (model.isExpirationTimerUpdate()) {
view = new Whisper.ExpirationTimerUpdateView({model: model}).render(); view = new Whisper.ExpirationTimerUpdateView({ model: model }).render();
} else if (model.get('type') === 'keychange') { } else if (model.get('type') === 'keychange') {
view = new Whisper.KeyChangeView({model: model}).render(); view = new Whisper.KeyChangeView({ model: model }).render();
} else if (model.get('type') === 'verified-change') { } else if (model.get('type') === 'verified-change') {
view = new Whisper.VerifiedChangeView({model: model}).render(); view = new Whisper.VerifiedChangeView({ model: model }).render();
} else { } else {
view = new this.itemView({model: model}).render(); view = new this.itemView({ model: model }).render();
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition); this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded); this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
} }

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(
Mustache.render(
_.result(this, 'template', ''),
{
message: this.model.get('body'), message: this.model.get('body'),
hasBody, hasBody,
timestamp: this.model.get('sent_at'), timestamp: this.model.get('sent_at'),
sender: (contact && contact.getTitle()) || '', sender: (contact && contact.getTitle()) || '',
avatar: (contact && contact.getAvatar()), avatar: contact && contact.getAvatar(),
profileName: (contact && contact.getProfileName()), profileName: contact && contact.getProfileName(),
innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail', innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail',
hoverIcon: !hasErrors, hoverIcon: !hasErrors,
hasAttachments, hasAttachments,
reply: i18n('replyToMessage'), reply: i18n('replyToMessage'),
}, this.render_partials())); },
this.render_partials()
)
);
this.timeStampView.setElement(this.$('.timestamp')); this.timeStampView.setElement(this.$('.timestamp'));
this.timeStampView.update(); this.timeStampView.update();
@@ -498,7 +522,9 @@
// as our code / Backbone seems to rely on `render` synchronously returning // as our code / Backbone seems to rely on `render` synchronously returning
// `this` instead of `Promise MessageView` (this): // `this` instead of `Promise MessageView` (this):
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.loadAttachmentViews().then(views => this.renderAttachmentViews(views)); this.loadAttachmentViews().then(views =>
this.renderAttachmentViews(views)
);
return this; return this;
}, },
@@ -523,8 +549,10 @@
} }
const attachments = this.model.get('attachments') || []; const attachments = this.model.get('attachments') || [];
const loadedAttachmentViews = Promise.all(attachments.map(attachment => const loadedAttachmentViews = Promise.all(
new Promise(async (resolve) => { attachments.map(
attachment =>
new Promise(async resolve => {
const attachmentWithData = await loadAttachmentData(attachment); const attachmentWithData = await loadAttachmentData(attachment);
const view = new Whisper.AttachmentView({ const view = new Whisper.AttachmentView({
model: attachmentWithData, model: attachmentWithData,
@@ -538,7 +566,9 @@
}); });
view.render(); view.render();
}))); })
)
);
// Memoize attachment views to avoid double loading: // Memoize attachment views to avoid double loading:
this.loadedAttachmentViews = loadedAttachmentViews; this.loadedAttachmentViews = loadedAttachmentViews;
@@ -550,8 +580,10 @@
}, },
renderAttachmentView(view) { renderAttachmentView(view) {
if (!view.updated) { if (!view.updated) {
throw new Error('Invariant violation:' + throw new Error(
' Cannot render an attachment view that isnt ready'); 'Invariant violation:' +
' Cannot render an attachment view that isnt ready'
);
} }
const parent = this.$('.attachments')[0]; const parent = this.$('.attachments')[0];
@@ -570,4 +602,4 @@
this.trigger('afterChangeHeight'); this.trigger('afterChangeHeight');
}, },
}); });
}()); })();

View File

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

View File

@@ -1,34 +1,33 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(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(
function(model) {
if (this.recipients_view.typeahead.get(model)) { if (this.recipients_view.typeahead.get(model)) {
this.recipients_view.typeahead.remove(model); this.recipients_view.typeahead.remove(model);
} }
}.bind(this)); }.bind(this)
);
}); });
this.recipients_view.$el.insertBefore(this.$('.container')); this.recipients_view.$el.insertBefore(this.$('.container'));
this.member_list_view = new Whisper.ContactListView({ this.member_list_view = new Whisper.ContactListView({
collection: this.model.contactCollection, collection: this.model.contactCollection,
className: 'members' className: 'members',
}); });
this.member_list_view.render(); this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el); this.$('.scrollable').append(this.member_list_view.el);
@@ -51,17 +50,21 @@
render_attributes: function() { render_attributes: function() {
return { return {
name: this.model.getTitle(), name: this.model.getTitle(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
}, },
send: function() { send: function() {
return this.avatarInput.getThumbnail().then(function(avatarFile) { return this.avatarInput.getThumbnail().then(
function(avatarFile) {
var now = Date.now(); var now = Date.now();
var attrs = { var attrs = {
timestamp: now, timestamp: now,
active_at: now, active_at: now,
name: this.$('.name').val(), name: this.$('.name').val(),
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id')) members: _.union(
this.model.get('members'),
this.recipients_view.recipients.pluck('id')
),
}; };
if (avatarFile) { if (avatarFile) {
attrs.avatar = avatarFile; attrs.avatar = avatarFile;
@@ -76,7 +79,8 @@
this.model.updateGroup(group_update); this.model.updateGroup(group_update);
this.goBack(); this.goBack();
}.bind(this)); }.bind(this)
} );
},
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -13,12 +10,14 @@
this.$('input.number').intlTelInput(); this.$('input.number').intlTelInput();
}, },
events: { events: {
'change': 'validateNumber', change: 'validateNumber',
'keyup': 'validateNumber' keyup: 'validateNumber',
}, },
validateNumber: function() { validateNumber: function() {
var input = this.$('input.number'); var input = this.$('input.number');
var regionCode = this.$('li.active').attr('data-country-code').toUpperCase(); var regionCode = this.$('li.active')
.attr('data-country-code')
.toUpperCase();
var number = input.val(); var number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); var parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
@@ -31,6 +30,6 @@
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,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -10,21 +7,21 @@
'name', 'name',
'e164_number', 'e164_number',
'national_number', 'national_number',
'international_number' 'international_number',
], ],
database: Whisper.Database, database: Whisper.Database,
storeName: 'conversations', storeName: 'conversations',
model: Whisper.Conversation, model: Whisper.Conversation,
fetchContacts: function() { fetchContacts: function() {
return this.fetch({ reset: true, conditions: { type: 'private' } }); return this.fetch({ reset: true, conditions: { type: 'private' } });
} },
}); });
Whisper.ContactPillView = Whisper.View.extend({ Whisper.ContactPillView = Whisper.View.extend({
tagName: 'span', tagName: 'span',
className: 'recipient', className: 'recipient',
events: { events: {
'click .remove': 'removeModel' 'click .remove': 'removeModel',
}, },
templateName: 'contact_pill', templateName: 'contact_pill',
initialize: function() { initialize: function() {
@@ -34,16 +31,16 @@
} }
}, },
removeModel: function() { removeModel: function() {
this.$el.trigger('remove', {modelId: this.model.id}); this.$el.trigger('remove', { modelId: this.model.id });
this.remove(); this.remove();
}, },
render_attributes: function() { render_attributes: function() {
return { name: this.model.getTitle() }; return { name: this.model.getTitle() };
} },
}); });
Whisper.RecipientListView = Whisper.ListView.extend({ Whisper.RecipientListView = Whisper.ListView.extend({
itemView: Whisper.ContactPillView itemView: Whisper.ContactPillView,
}); });
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
@@ -52,7 +49,7 @@
}); });
Whisper.SuggestionListView = Whisper.ConversationListView.extend({ Whisper.SuggestionListView = Whisper.ConversationListView.extend({
itemView: Whisper.SuggestionView itemView: Whisper.SuggestionView,
}); });
Whisper.RecipientsInputView = Whisper.View.extend({ Whisper.RecipientsInputView = Whisper.View.extend({
@@ -68,13 +65,13 @@
// Collection of recipients selected for the new message // Collection of recipients selected for the new message
this.recipients = new Whisper.ConversationCollection([], { this.recipients = new Whisper.ConversationCollection([], {
comparator: false comparator: false,
}); });
// View to display the selected recipients // View to display the selected recipients
this.recipients_view = new Whisper.RecipientListView({ this.recipients_view = new Whisper.RecipientListView({
collection: this.recipients, collection: this.recipients,
el: this.$('.recipients') el: this.$('.recipients'),
}); });
// Collection of contacts to match user input against // Collection of contacts to match user input against
@@ -83,18 +80,19 @@
// View to display the matched contacts from typeahead // View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.SuggestionListView({ this.typeahead_view = new Whisper.SuggestionListView({
collection : new Whisper.ConversationCollection([], { collection: new Whisper.ConversationCollection([], {
comparator: function(m) { return m.getTitle().toLowerCase(); } comparator: function(m) {
}) return m.getTitle().toLowerCase();
},
}),
}); });
this.$('.contacts').append(this.typeahead_view.el); this.$('.contacts').append(this.typeahead_view.el);
this.initNewContact(); this.initNewContact();
this.listenTo(this.typeahead, 'reset', this.filterContacts); this.listenTo(this.typeahead, 'reset', this.filterContacts);
}, },
render_attributes: function() { render_attributes: function() {
return { placeholder: this.placeholder || "name or phone number" }; return { placeholder: this.placeholder || 'name or phone number' };
}, },
events: { events: {
@@ -113,9 +111,7 @@
} else { } else {
this.new_contact_view.$el.hide(); this.new_contact_view.$el.hide();
} }
this.typeahead_view.collection.reset( this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
this.typeahead.typeahead(query)
);
} else { } else {
this.resetTypeahead(); this.resetTypeahead();
} }
@@ -131,8 +127,8 @@
el: this.$new_contact, el: this.$new_contact,
model: ConversationController.create({ model: ConversationController.create({
type: 'private', type: 'private',
newContact: true newContact: true,
}) }),
}).render(); }).render();
}, },
@@ -176,10 +172,8 @@
this.typeahead_view.collection.reset([]); this.typeahead_view.collection.reset([]);
}, },
maybeNumber: function(number) { maybeNumber: function(number) {
return number.match(/^\+?[0-9]*$/); return number.match(/^\+?[0-9]*$/);
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -16,7 +13,7 @@
events: { events: {
'click .close': 'close', 'click .close': 'close',
'click .finish': 'finish', 'click .finish': 'finish',
'close': 'close' close: 'close',
}, },
updateTime: function() { updateTime: function() {
var duration = moment.duration(Date.now() - this.startTime, 'ms'); var duration = moment.duration(Date.now() - this.startTime, 'ms');
@@ -62,19 +59,23 @@
this.input = this.context.createGain(); this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, { this.recorder = new WebAudioRecorder(this.input, {
encoding: 'mp3', encoding: 'mp3',
workerDir: 'js/' // must end with slash workerDir: 'js/', // must end with slash
}); });
this.recorder.onComplete = this.handleBlob.bind(this); this.recorder.onComplete = this.handleBlob.bind(this);
this.recorder.onError = this.onError; this.recorder.onError = this.onError;
navigator.webkitGetUserMedia({ audio: true }, function(stream) { navigator.webkitGetUserMedia(
{ audio: true },
function(stream) {
this.source = this.context.createMediaStreamSource(stream); this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input); this.source.connect(this.input);
}.bind(this), this.onError.bind(this)); }.bind(this),
this.onError.bind(this)
);
this.recorder.startRecording(); this.recorder.startRecording();
}, },
onError: function(error) { onError: function(error) {
console.log(error.stack); console.log(error.stack);
this.close(); this.close();
} },
}); });
})(); })();

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -32,8 +29,8 @@
return { return {
cssClass: cssClass, cssClass: cssClass,
moreBelow: moreBelow moreBelow: moreBelow,
}; };
} },
}); });
})(); })();

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
/* (function() {
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -17,12 +14,14 @@
}, },
render: function() { render: function() {
this.$el.html(Mustache.render( this.$el.html(
Mustache.render(
_.result(this, 'template', ''), _.result(this, 'template', ''),
_.result(this, 'render_attributes', '') _.result(this, 'render_attributes', '')
)); )
);
this.$el.show(); this.$el.show();
setTimeout(this.close.bind(this), 2000); setTimeout(this.close.bind(this), 2000);
} },
}); });
})(); })();

View File

@@ -1,6 +1,4 @@
/* /*
* vim: ts=4:sw=4:expandtab
*
* Whisper.View * Whisper.View
* *
* This is the base for most of our views. The Backbone view is extended * This is the base for most of our views. The Backbone view is extended
@@ -19,11 +17,12 @@
* 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() { constructor: function() {
Backbone.View.apply(this, arguments); Backbone.View.apply(this, arguments);
Mustache.parse(_.result(this, 'template')); Mustache.parse(_.result(this, 'template'));
@@ -48,24 +47,28 @@
return this; return this;
}, },
confirm: function(message, okText) { confirm: function(message, okText) {
return new Promise(function(resolve, reject) { return new Promise(
function(resolve, reject) {
var dialog = new Whisper.ConfirmationDialogView({ var dialog = new Whisper.ConfirmationDialogView({
message: message, message: message,
okText: okText, okText: okText,
resolve: resolve, resolve: resolve,
reject: reject reject: reject,
}); });
this.$el.append(dialog.el); this.$el.append(dialog.el);
}.bind(this)); }.bind(this)
);
}, },
i18n_with_links: function() { i18n_with_links: function() {
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
for (var i=1; i < args.length; ++i) { for (var i = 1; i < args.length; ++i) {
args[i] = 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"'; args[i] =
'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
} }
return i18n(args[0], args.slice(1)); return i18n(args[0], args.slice(1));
} },
},{ },
{
// Class attributes // Class attributes
Templates: (function() { Templates: (function() {
var templates = {}; var templates = {};
@@ -75,6 +78,7 @@
templates[id] = $el.html(); templates[id] = $el.html();
}); });
return templates; return templates;
}()) })(),
}); }
);
})(); })();

View File

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

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