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:
@@ -13,7 +13,20 @@ test/models/*.js
|
||||
test/views/*.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
|
||||
|
||||
# ES2015+ files
|
||||
|
53
.eslintrc.js
53
.eslintrc.js
@@ -2,37 +2,24 @@
|
||||
|
||||
module.exports = {
|
||||
settings: {
|
||||
'import/core-modules': [
|
||||
'electron'
|
||||
]
|
||||
'import/core-modules': ['electron'],
|
||||
},
|
||||
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
],
|
||||
extends: ['airbnb-base', 'prettier'],
|
||||
|
||||
plugins: [
|
||||
'mocha',
|
||||
'more',
|
||||
],
|
||||
plugins: ['mocha', 'more'],
|
||||
|
||||
rules: {
|
||||
'comma-dangle': ['error', {
|
||||
'comma-dangle': [
|
||||
'error',
|
||||
{
|
||||
arrays: 'always-multiline',
|
||||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
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`):
|
||||
'mocha/no-exclusive-tests': 'error',
|
||||
@@ -52,6 +39,26 @@ module.exports = {
|
||||
// consistently place operators at end of line except ternaries
|
||||
'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 doesn’t 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
4
.gitignore
vendored
@@ -16,11 +16,11 @@ release/
|
||||
|
||||
# generated files
|
||||
js/components.js
|
||||
libtextsecure/components.js
|
||||
js/libtextsecure.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
stylesheets/*.css
|
||||
test/test.js
|
||||
libtextsecure/test/test.js
|
||||
|
||||
# React / TypeScript
|
||||
ts/**/*.js
|
||||
|
18
.prettierignore
Normal file
18
.prettierignore
Normal 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
4
.prettierrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
};
|
285
Gruntfile.js
285
Gruntfile.js
@@ -13,11 +13,13 @@ module.exports = function(grunt) {
|
||||
|
||||
var libtextsecurecomponents = [];
|
||||
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");
|
||||
grunt.loadNpmTasks("grunt-sass");
|
||||
var importOnce = require('node-sass-import-once');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
@@ -34,15 +36,15 @@ module.exports = function(grunt) {
|
||||
src: [
|
||||
'components/mocha/mocha.js',
|
||||
'components/chai/chai.js',
|
||||
'test/_test.js'
|
||||
'test/_test.js',
|
||||
],
|
||||
dest: 'test/test.js',
|
||||
},
|
||||
//TODO: Move errors back down?
|
||||
libtextsecure: {
|
||||
options: {
|
||||
banner: ";(function() {\n",
|
||||
footer: "})();\n",
|
||||
banner: ';(function() {\n',
|
||||
footer: '})();\n',
|
||||
},
|
||||
src: [
|
||||
'libtextsecure/errors.js',
|
||||
@@ -77,21 +79,21 @@ module.exports = function(grunt) {
|
||||
'components/mock-socket/dist/mock-socket.js',
|
||||
'components/mocha/mocha.js',
|
||||
'components/chai/chai.js',
|
||||
'libtextsecure/test/_test.js'
|
||||
'libtextsecure/test/_test.js',
|
||||
],
|
||||
dest: 'libtextsecure/test/test.js',
|
||||
}
|
||||
},
|
||||
},
|
||||
sass: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
importer: importOnce
|
||||
importer: importOnce,
|
||||
},
|
||||
dev: {
|
||||
files: {
|
||||
"stylesheets/manifest.css": "stylesheets/manifest.scss"
|
||||
}
|
||||
}
|
||||
'stylesheets/manifest.css': 'stylesheets/manifest.scss',
|
||||
},
|
||||
},
|
||||
},
|
||||
jshint: {
|
||||
files: [
|
||||
@@ -117,7 +119,7 @@ module.exports = function(grunt) {
|
||||
'!js/models/messages.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'!libtextsecure/message_receiver.js',
|
||||
'_locales/**/*'
|
||||
'_locales/**/*',
|
||||
],
|
||||
options: { jshintrc: '.jshintrc' },
|
||||
},
|
||||
@@ -130,32 +132,33 @@ module.exports = function(grunt) {
|
||||
'protos/*',
|
||||
'js/**',
|
||||
'stylesheets/*.css',
|
||||
'!js/register.js'
|
||||
'!js/register.js',
|
||||
],
|
||||
res: [
|
||||
'images/**/*',
|
||||
'fonts/*',
|
||||
]
|
||||
res: ['images/**/*', 'fonts/*'],
|
||||
},
|
||||
copy: {
|
||||
deps: {
|
||||
files: [{
|
||||
files: [
|
||||
{
|
||||
src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js',
|
||||
dest: 'js/Mp3LameEncoder.min.js'
|
||||
}, {
|
||||
dest: 'js/Mp3LameEncoder.min.js',
|
||||
},
|
||||
{
|
||||
src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js',
|
||||
dest: 'js/WebAudioRecorderMp3.js'
|
||||
}, {
|
||||
dest: 'js/WebAudioRecorderMp3.js',
|
||||
},
|
||||
{
|
||||
src: 'components/jquery/dist/jquery.js',
|
||||
dest: 'js/jquery.js'
|
||||
}],
|
||||
dest: 'js/jquery.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
res: {
|
||||
files: [{ expand: true, dest: 'dist/', src: ['<%= dist.res %>'] }],
|
||||
},
|
||||
src: {
|
||||
files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }],
|
||||
}
|
||||
},
|
||||
},
|
||||
jscs: {
|
||||
all: {
|
||||
@@ -179,86 +182,107 @@ module.exports = function(grunt) {
|
||||
'!test/blanket_mocha.js',
|
||||
'!test/modules/**/*.js',
|
||||
'!test/test.js',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sass: {
|
||||
files: ['./stylesheets/*.scss'],
|
||||
tasks: ['sass']
|
||||
tasks: ['sass'],
|
||||
},
|
||||
libtextsecure: {
|
||||
files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
|
||||
tasks: ['concat:libtextsecure']
|
||||
tasks: ['concat:libtextsecure'],
|
||||
},
|
||||
dist: {
|
||||
files: ['<%= dist.src %>', '<%= dist.res %>'],
|
||||
tasks: ['copy_dist']
|
||||
tasks: ['copy_dist'],
|
||||
},
|
||||
scripts: {
|
||||
files: ['<%= jshint.files %>'],
|
||||
tasks: ['jshint']
|
||||
tasks: ['jshint'],
|
||||
},
|
||||
style: {
|
||||
files: ['<%= jscs.all.src %>'],
|
||||
tasks: ['jscs']
|
||||
tasks: ['jscs'],
|
||||
},
|
||||
transpile: {
|
||||
files: ['./ts/**/*.ts'],
|
||||
tasks: ['exec:transpile']
|
||||
}
|
||||
tasks: ['exec:transpile'],
|
||||
},
|
||||
},
|
||||
exec: {
|
||||
'tx-pull': {
|
||||
cmd: 'tx pull'
|
||||
cmd: 'tx pull',
|
||||
},
|
||||
'transpile': {
|
||||
transpile: {
|
||||
cmd: 'npm run transpile',
|
||||
}
|
||||
},
|
||||
},
|
||||
'test-release': {
|
||||
osx: {
|
||||
archive: 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar',
|
||||
appUpdateYML: 'mac/' + packageJson.productName + '.app/Contents/Resources/app-update.yml',
|
||||
exe: 'mac/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName
|
||||
archive:
|
||||
'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar',
|
||||
appUpdateYML:
|
||||
'mac/' +
|
||||
packageJson.productName +
|
||||
'.app/Contents/Resources/app-update.yml',
|
||||
exe:
|
||||
'mac/' +
|
||||
packageJson.productName +
|
||||
'.app/Contents/MacOS/' +
|
||||
packageJson.productName,
|
||||
},
|
||||
mas: {
|
||||
archive: 'mas/Signal.app/Contents/Resources/app.asar',
|
||||
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: {
|
||||
archive: 'linux-unpacked/resources/app.asar',
|
||||
exe: 'linux-unpacked/' + packageJson.name
|
||||
exe: 'linux-unpacked/' + packageJson.name,
|
||||
},
|
||||
win: {
|
||||
archive: 'win-unpacked/resources/app.asar',
|
||||
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) {
|
||||
if (/^grunt(?!(-cli)?$)/.test(key)) { // ignore grunt and grunt-cli
|
||||
if (/^grunt(?!(-cli)?$)/.test(key)) {
|
||||
// ignore grunt and grunt-cli
|
||||
grunt.loadNpmTasks(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Transifex does not understand placeholders, so this task patches all non-en
|
||||
// locales with missing placeholders
|
||||
grunt.registerTask('locale-patch', function(){
|
||||
grunt.registerTask('locale-patch', function() {
|
||||
var en = grunt.file.readJSON('_locales/en/messages.json');
|
||||
grunt.file.recurse('_locales', function(abspath, rootdir, subdir, filename){
|
||||
if (subdir === 'en' || filename !== 'messages.json'){
|
||||
grunt.file.recurse('_locales', function(
|
||||
abspath,
|
||||
rootdir,
|
||||
subdir,
|
||||
filename
|
||||
) {
|
||||
if (subdir === 'en' || filename !== 'messages.json') {
|
||||
return;
|
||||
}
|
||||
var messages = grunt.file.readJSON(abspath);
|
||||
|
||||
for (var key in messages){
|
||||
if (en[key] !== undefined && messages[key] !== undefined){
|
||||
if (en[key].placeholders !== undefined && messages[key].placeholders === undefined){
|
||||
for (var key in messages) {
|
||||
if (en[key] !== undefined && messages[key] !== undefined) {
|
||||
if (
|
||||
en[key].placeholders !== undefined &&
|
||||
messages[key].placeholders === undefined
|
||||
) {
|
||||
messages[key].placeholders = en[key].placeholders;
|
||||
}
|
||||
}
|
||||
@@ -273,8 +297,10 @@ module.exports = function(grunt) {
|
||||
var gitinfo = grunt.config.get('gitinfo');
|
||||
var commited = gitinfo.local.branch.current.lastCommitTime;
|
||||
var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
|
||||
grunt.file.write('config/local-production.json',
|
||||
JSON.stringify({ buildExpiration: time }) + '\n');
|
||||
grunt.file.write(
|
||||
'config/local-production.json',
|
||||
JSON.stringify({ buildExpiration: time }) + '\n'
|
||||
);
|
||||
});
|
||||
|
||||
grunt.registerTask('clean-release', function() {
|
||||
@@ -290,36 +316,46 @@ module.exports = function(grunt) {
|
||||
var gitinfo = grunt.config.get('gitinfo');
|
||||
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 sha = gitinfo.local.branch.current.SHA;
|
||||
var files = [{
|
||||
var files = [
|
||||
{
|
||||
zip: packageJson.name + '-' + packageJson.version + '.zip',
|
||||
extractedTo: 'linux'
|
||||
}];
|
||||
extractedTo: 'linux',
|
||||
},
|
||||
];
|
||||
|
||||
var extract = require('extract-zip');
|
||||
var download = function(url, dest, extractedTo, cb) {
|
||||
var file = fs.createWriteStream(dest);
|
||||
var request = https.get(url, function(response) {
|
||||
var request = https
|
||||
.get(url, function(response) {
|
||||
if (response.statusCode !== 200) {
|
||||
cb(response.statusCode);
|
||||
} else {
|
||||
response.pipe(file);
|
||||
file.on('finish', 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)
|
||||
if (cb) cb(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
Promise.all(files.map(function(item) {
|
||||
var key = [ keyBase, sha, 'dist', item.zip].join('/');
|
||||
Promise.all(
|
||||
files.map(function(item) {
|
||||
var key = [keyBase, sha, 'dist', item.zip].join('/');
|
||||
var url = [urlBase, key].join('/');
|
||||
var dest = 'release/' + item.zip;
|
||||
return new Promise(function(resolve) {
|
||||
@@ -334,7 +370,8 @@ module.exports = function(grunt) {
|
||||
}
|
||||
});
|
||||
});
|
||||
})).then(function(results) {
|
||||
})
|
||||
).then(function(results) {
|
||||
results.forEach(function(error) {
|
||||
if (error) {
|
||||
grunt.fail.warn('Failed to fetch some release artifacts');
|
||||
@@ -347,59 +384,77 @@ module.exports = function(grunt) {
|
||||
function runTests(environment, cb) {
|
||||
var failure;
|
||||
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({
|
||||
path: path.join(__dirname, 'node_modules', '.bin', electronBinary),
|
||||
args: [path.join(__dirname, 'main.js')],
|
||||
env: {
|
||||
NODE_ENV: environment
|
||||
}
|
||||
NODE_ENV: environment,
|
||||
},
|
||||
});
|
||||
|
||||
function getMochaResults() {
|
||||
return window.mochaResults;
|
||||
}
|
||||
|
||||
app.start().then(function() {
|
||||
return app.client.waitUntil(function() {
|
||||
app
|
||||
.start()
|
||||
.then(function() {
|
||||
return app.client.waitUntil(
|
||||
function() {
|
||||
return app.client.execute(getMochaResults).then(function(data) {
|
||||
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);
|
||||
}).then(function(data) {
|
||||
})
|
||||
.then(function(data) {
|
||||
var results = data.value;
|
||||
if (results.failures > 0) {
|
||||
console.error(results.reports);
|
||||
failure = function() {
|
||||
grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.');
|
||||
grunt.fail.fatal(
|
||||
'Found ' + results.failures + ' failing unit tests.'
|
||||
);
|
||||
};
|
||||
return app.client.log('browser');
|
||||
} else {
|
||||
grunt.log.ok(results.passes + ' tests passed.');
|
||||
}
|
||||
}).then(function(logs) {
|
||||
})
|
||||
.then(function(logs) {
|
||||
if (logs) {
|
||||
console.error();
|
||||
console.error('Because tests failed, printing browser logs:');
|
||||
console.error(logs);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
})
|
||||
.catch(function(error) {
|
||||
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
|
||||
// shutting down. Grunt's fail methods are the only way to set the return value,
|
||||
// but they shut the process down immediately!
|
||||
return app.stop();
|
||||
}).then(function() {
|
||||
})
|
||||
.then(function() {
|
||||
if (failure) {
|
||||
failure();
|
||||
}
|
||||
cb();
|
||||
}).catch(function (error) {
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Second-level error:', error.message, error.stack);
|
||||
if (failure) {
|
||||
failure();
|
||||
@@ -415,12 +470,16 @@ module.exports = function(grunt) {
|
||||
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 done = this.async();
|
||||
|
||||
runTests(environment, done);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
grunt.registerMultiTask('test-release', 'Test packaged releases', function() {
|
||||
var dir = grunt.option('dir') || 'dist';
|
||||
@@ -431,7 +490,7 @@ module.exports = function(grunt) {
|
||||
var files = [
|
||||
'config/default.json',
|
||||
'config/' + environment + '.json',
|
||||
'config/local-' + environment + '.json'
|
||||
'config/local-' + environment + '.json',
|
||||
];
|
||||
|
||||
console.log(this.target, archive);
|
||||
@@ -443,16 +502,16 @@ module.exports = function(grunt) {
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new Error("Missing file " + fileName);
|
||||
throw new Error('Missing file ' + fileName);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.appUpdateYML) {
|
||||
var appUpdateYML = [dir, config.appUpdateYML].join('/');
|
||||
if (require('fs').existsSync(appUpdateYML)) {
|
||||
console.log("auto update ok");
|
||||
console.log('auto update ok');
|
||||
} 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 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();
|
||||
}).then(function (count) {
|
||||
})
|
||||
.then(function(count) {
|
||||
assert.equal(count, 1);
|
||||
console.log('window opened');
|
||||
}).then(function () {
|
||||
})
|
||||
.then(function() {
|
||||
// Get the window's title
|
||||
return app.client.getTitle();
|
||||
}).then(function (title) {
|
||||
})
|
||||
.then(function(title) {
|
||||
// Verify the window's title
|
||||
assert.equal(title, packageJson.productName);
|
||||
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');
|
||||
}).then(function () {
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
// Successfully completed test
|
||||
return app.stop();
|
||||
}, function (error) {
|
||||
},
|
||||
function(error) {
|
||||
// Test failed!
|
||||
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']);
|
||||
@@ -497,9 +571,16 @@ module.exports = function(grunt) {
|
||||
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
|
||||
grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']);
|
||||
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
|
||||
grunt.registerTask('prep-release', ['gitinfo', 'clean-release', 'fetch-release']);
|
||||
grunt.registerTask(
|
||||
'default',
|
||||
['concat', 'copy:deps', 'sass', 'date', 'exec:transpile']
|
||||
);
|
||||
grunt.registerTask('prep-release', [
|
||||
'gitinfo',
|
||||
'clean-release',
|
||||
'fetch-release',
|
||||
]);
|
||||
grunt.registerTask('default', [
|
||||
'concat',
|
||||
'copy:deps',
|
||||
'sass',
|
||||
'date',
|
||||
'exec:transpile',
|
||||
]);
|
||||
};
|
||||
|
@@ -13,7 +13,7 @@ install:
|
||||
|
||||
build_script:
|
||||
- yarn transpile
|
||||
- yarn lint
|
||||
- yarn lint-windows
|
||||
- yarn test-node
|
||||
- yarn nsp check
|
||||
- yarn generate
|
||||
|
212
js/background.js
212
js/background.js
@@ -11,7 +11,7 @@
|
||||
/* global Whisper: false */
|
||||
/* global wrapDeferred: false */
|
||||
|
||||
;(async function() {
|
||||
(async function() {
|
||||
'use strict';
|
||||
|
||||
const { IdleDetector, MessageDataMigrator } = Signal.Workflow;
|
||||
@@ -65,7 +65,9 @@
|
||||
var USERNAME = storage.get('number_id');
|
||||
var PASSWORD = storage.get('password');
|
||||
accountManager = new textsecure.AccountManager(
|
||||
SERVER_URL, USERNAME, PASSWORD
|
||||
SERVER_URL,
|
||||
USERNAME,
|
||||
PASSWORD
|
||||
);
|
||||
accountManager.addEventListener('registration', function() {
|
||||
Whisper.Registration.markDone();
|
||||
@@ -105,18 +107,20 @@
|
||||
|
||||
if (!isMigrationWithoutIndexComplete) {
|
||||
const database = Migrations0DatabaseWithAttachmentData.getDatabase();
|
||||
const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex({
|
||||
const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex(
|
||||
{
|
||||
databaseName: database.name,
|
||||
minDatabaseVersion: database.version,
|
||||
numMessagesPerBatch: NUM_MESSAGES_PER_BATCH,
|
||||
upgradeMessageSchema,
|
||||
});
|
||||
}
|
||||
);
|
||||
console.log('Upgrade message schema (without index):', batchWithoutIndex);
|
||||
isMigrationWithoutIndexComplete = batchWithoutIndex.done;
|
||||
}
|
||||
|
||||
const areAllMigrationsComplete = isMigrationWithIndexComplete &&
|
||||
isMigrationWithoutIndexComplete;
|
||||
const areAllMigrationsComplete =
|
||||
isMigrationWithIndexComplete && isMigrationWithoutIndexComplete;
|
||||
if (areAllMigrationsComplete) {
|
||||
idleDetector.stop();
|
||||
}
|
||||
@@ -188,7 +192,9 @@
|
||||
});
|
||||
|
||||
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.ExpiringMessagesListener.init(Whisper.events);
|
||||
@@ -200,7 +206,7 @@
|
||||
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
||||
connect();
|
||||
appView.openInbox({
|
||||
initialLoadComplete: initialLoadComplete
|
||||
initialLoadComplete: initialLoadComplete,
|
||||
});
|
||||
} else if (window.config.importMode) {
|
||||
appView.openImporter();
|
||||
@@ -214,7 +220,7 @@
|
||||
Whisper.events.on('showSettings', () => {
|
||||
if (!appView || !appView.inboxView) {
|
||||
console.log(
|
||||
'background: Event: \'showSettings\':' +
|
||||
"background: Event: 'showSettings':" +
|
||||
' Expected `appView.inboxView` to exist.'
|
||||
);
|
||||
return;
|
||||
@@ -238,7 +244,7 @@
|
||||
appView.openConversation(conversation);
|
||||
} else {
|
||||
appView.openInbox({
|
||||
initialLoadComplete: initialLoadComplete
|
||||
initialLoadComplete: initialLoadComplete,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -258,7 +264,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var disconnectTimer = null;
|
||||
function onOffline() {
|
||||
console.log('offline');
|
||||
@@ -294,7 +299,9 @@
|
||||
|
||||
function isSocketOnline() {
|
||||
var socketStatus = window.getSocketStatus();
|
||||
return socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN;
|
||||
return (
|
||||
socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
@@ -317,14 +324,20 @@
|
||||
window.addEventListener('offline', onOffline);
|
||||
}
|
||||
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);
|
||||
onEmpty(); // this ensures that the loading screen is dismissed
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Whisper.Registration.everDone()) { return; }
|
||||
if (Whisper.Import.isIncomplete()) { return; }
|
||||
if (!Whisper.Registration.everDone()) {
|
||||
return;
|
||||
}
|
||||
if (Whisper.Import.isIncomplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageReceiver) {
|
||||
messageReceiver.close();
|
||||
@@ -343,7 +356,11 @@
|
||||
|
||||
// initialize the socket and start listening for messages
|
||||
messageReceiver = new textsecure.MessageReceiver(
|
||||
SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options
|
||||
SERVER_URL,
|
||||
USERNAME,
|
||||
PASSWORD,
|
||||
mySignalingKey,
|
||||
options
|
||||
);
|
||||
messageReceiver.addEventListener('message', onMessageReceived);
|
||||
messageReceiver.addEventListener('delivery', onDeliveryReceipt);
|
||||
@@ -359,7 +376,10 @@
|
||||
messageReceiver.addEventListener('configuration', onConfiguration);
|
||||
|
||||
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
|
||||
@@ -406,7 +426,7 @@
|
||||
});
|
||||
|
||||
if (Whisper.Import.isComplete()) {
|
||||
textsecure.messaging.sendRequestConfigurationSyncMessage().catch((e) => {
|
||||
textsecure.messaging.sendRequestConfigurationSyncMessage().catch(e => {
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
@@ -416,8 +436,10 @@
|
||||
const shouldSkipAttachmentMigrationForNewUsers = firstRun === true;
|
||||
if (shouldSkipAttachmentMigrationForNewUsers) {
|
||||
const database = Migrations0DatabaseWithAttachmentData.getDatabase();
|
||||
const connection =
|
||||
await Signal.Database.open(database.name, database.version);
|
||||
const connection = await Signal.Database.open(
|
||||
database.name,
|
||||
database.version
|
||||
);
|
||||
await Signal.Settings.markAttachmentMigrationComplete(connection);
|
||||
}
|
||||
idleDetector.start();
|
||||
@@ -471,7 +493,7 @@
|
||||
}
|
||||
|
||||
var c = new Whisper.Conversation({
|
||||
id: id
|
||||
id: id,
|
||||
});
|
||||
var error = c.validateNumber();
|
||||
if (error) {
|
||||
@@ -490,7 +512,7 @@
|
||||
}
|
||||
|
||||
if (details.profileKey) {
|
||||
conversation.set({profileKey: details.profileKey});
|
||||
conversation.set({ profileKey: details.profileKey });
|
||||
}
|
||||
|
||||
if (typeof details.blocked !== 'undefined') {
|
||||
@@ -501,18 +523,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
return wrapDeferred(conversation.save({
|
||||
return wrapDeferred(
|
||||
conversation.save({
|
||||
name: details.name,
|
||||
avatar: details.avatar,
|
||||
color: details.color,
|
||||
active_at: activeAt,
|
||||
})).then(function() {
|
||||
})
|
||||
).then(function() {
|
||||
const { expireTimer } = details;
|
||||
const isValidExpireTimer = typeof expireTimer === 'number';
|
||||
if (!isValidExpireTimer) {
|
||||
console.log(
|
||||
'Ignore invalid expire timer.',
|
||||
'Expected numeric `expireTimer`, got:', expireTimer
|
||||
'Expected numeric `expireTimer`, got:',
|
||||
expireTimer
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -523,7 +548,7 @@
|
||||
expireTimer,
|
||||
source,
|
||||
receivedAt,
|
||||
{fromSync: true}
|
||||
{ fromSync: true }
|
||||
);
|
||||
});
|
||||
})
|
||||
@@ -542,10 +567,7 @@
|
||||
})
|
||||
.then(ev.confirm)
|
||||
.catch(function(error) {
|
||||
console.log(
|
||||
'onContactReceived error:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
console.log('onContactReceived error:', Errors.toLogFormat(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -553,7 +575,9 @@
|
||||
var details = ev.groupDetails;
|
||||
var id = details.id;
|
||||
|
||||
return ConversationController.getOrCreateAndWait(id, 'group').then(function(conversation) {
|
||||
return ConversationController.getOrCreateAndWait(id, 'group').then(function(
|
||||
conversation
|
||||
) {
|
||||
var updates = {
|
||||
name: details.name,
|
||||
members: details.members,
|
||||
@@ -573,13 +597,15 @@
|
||||
updates.left = true;
|
||||
}
|
||||
|
||||
return wrapDeferred(conversation.save(updates)).then(function() {
|
||||
return wrapDeferred(conversation.save(updates))
|
||||
.then(function() {
|
||||
const { expireTimer } = details;
|
||||
const isValidExpireTimer = typeof expireTimer === 'number';
|
||||
if (!isValidExpireTimer) {
|
||||
console.log(
|
||||
'Ignore invalid expire timer.',
|
||||
'Expected numeric `expireTimer`, got:', expireTimer
|
||||
'Expected numeric `expireTimer`, got:',
|
||||
expireTimer
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -590,9 +616,10 @@
|
||||
expireTimer,
|
||||
source,
|
||||
receivedAt,
|
||||
{fromSync: true}
|
||||
{ fromSync: true }
|
||||
);
|
||||
}).then(ev.confirm);
|
||||
})
|
||||
.then(ev.confirm);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -605,25 +632,23 @@
|
||||
});
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||
const getDescriptorForSent = ({ message, destination }) => (
|
||||
const getDescriptorForSent = ({ message, destination }) =>
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: destination }
|
||||
);
|
||||
: { type: Message.PRIVATE, id: destination };
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source }) => (
|
||||
const getDescriptorForReceived = ({ message, source }) =>
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: source }
|
||||
);
|
||||
: { type: Message.PRIVATE, id: source };
|
||||
|
||||
function createMessageHandler({
|
||||
createMessage,
|
||||
getMessageDescriptor,
|
||||
handleProfileUpdate,
|
||||
}) {
|
||||
return async (event) => {
|
||||
return async event => {
|
||||
const { data, confirm } = event;
|
||||
|
||||
const messageDescriptor = getMessageDescriptor(data);
|
||||
@@ -647,11 +672,9 @@
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
return message.handleDataMessage(
|
||||
upgradedMessage,
|
||||
event.confirm,
|
||||
{ initialLoadComplete }
|
||||
);
|
||||
return message.handleDataMessage(upgradedMessage, event.confirm, {
|
||||
initialLoadComplete,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -677,7 +700,10 @@
|
||||
});
|
||||
|
||||
// Sent:
|
||||
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) {
|
||||
async function handleMessageSentProfileUpdate({
|
||||
confirm,
|
||||
messageDescriptor,
|
||||
}) {
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
@@ -716,9 +742,9 @@
|
||||
value: [
|
||||
message.get('source'),
|
||||
message.get('sourceDevice'),
|
||||
message.get('sent_at')
|
||||
]
|
||||
}
|
||||
message.get('sent_at'),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
fetcher.fetch(options).always(function() {
|
||||
@@ -736,13 +762,13 @@
|
||||
|
||||
function initIncomingMessage(data) {
|
||||
var message = new Whisper.Message({
|
||||
source : data.source,
|
||||
sourceDevice : data.sourceDevice,
|
||||
sent_at : data.timestamp,
|
||||
received_at : data.receivedAt || Date.now(),
|
||||
conversationId : data.source,
|
||||
type : 'incoming',
|
||||
unread : 1
|
||||
source: data.source,
|
||||
sourceDevice: data.sourceDevice,
|
||||
sent_at: data.timestamp,
|
||||
received_at: data.receivedAt || Date.now(),
|
||||
conversationId: data.source,
|
||||
type: 'incoming',
|
||||
unread: 1,
|
||||
});
|
||||
|
||||
return message;
|
||||
@@ -752,25 +778,33 @@
|
||||
var error = ev.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');
|
||||
|
||||
console.log('Client is no longer authorized; deleting local configuration');
|
||||
console.log(
|
||||
'Client is no longer authorized; deleting local configuration'
|
||||
);
|
||||
Whisper.Registration.remove();
|
||||
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
|
||||
// the conversation list, instead of showing just the QR code screen.
|
||||
Whisper.Registration.markEverDone();
|
||||
textsecure.storage.put('number_id', previousNumberId);
|
||||
console.log('Successfully cleared local configuration');
|
||||
}, function(error) {
|
||||
},
|
||||
function(error) {
|
||||
console.log(
|
||||
'Something went wrong clearing local configuration',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -800,15 +834,19 @@
|
||||
|
||||
return message.saveErrors(error).then(function() {
|
||||
var id = message.get('conversationId');
|
||||
return ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) {
|
||||
return ConversationController.getOrCreateAndWait(id, 'private').then(
|
||||
function(conversation) {
|
||||
conversation.set({
|
||||
active_at: Date.now(),
|
||||
unreadCount: conversation.get('unreadCount') + 1
|
||||
unreadCount: conversation.get('unreadCount') + 1,
|
||||
});
|
||||
|
||||
var conversation_timestamp = conversation.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') });
|
||||
}
|
||||
|
||||
@@ -822,7 +860,8 @@
|
||||
return new Promise(function(resolve, reject) {
|
||||
conversation.save().then(resolve, reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -840,9 +879,9 @@
|
||||
}
|
||||
|
||||
var receipt = Whisper.ReadReceipts.add({
|
||||
reader : reader,
|
||||
timestamp : timestamp,
|
||||
read_at : read_at,
|
||||
reader: reader,
|
||||
timestamp: timestamp,
|
||||
read_at: read_at,
|
||||
});
|
||||
|
||||
receipt.on('remove', ev.confirm);
|
||||
@@ -858,9 +897,9 @@
|
||||
console.log('read sync', sender, timestamp);
|
||||
|
||||
var receipt = Whisper.ReadSyncs.add({
|
||||
sender : sender,
|
||||
timestamp : timestamp,
|
||||
read_at : read_at
|
||||
sender: sender,
|
||||
timestamp: timestamp,
|
||||
read_at: read_at,
|
||||
});
|
||||
|
||||
receipt.on('remove', ev.confirm);
|
||||
@@ -875,18 +914,15 @@
|
||||
var state;
|
||||
|
||||
var c = new Whisper.Conversation({
|
||||
id: number
|
||||
id: number,
|
||||
});
|
||||
var error = c.validateNumber();
|
||||
if (error) {
|
||||
console.log(
|
||||
'Invalid verified sync received:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
console.log('Invalid verified sync received:', Errors.toLogFormat(error));
|
||||
return;
|
||||
}
|
||||
|
||||
switch(ev.verified.state) {
|
||||
switch (ev.verified.state) {
|
||||
case textsecure.protobuf.Verified.State.DEFAULT:
|
||||
state = 'DEFAULT';
|
||||
break;
|
||||
@@ -898,14 +934,19 @@
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('got verified sync for', number, state,
|
||||
ev.viaContactSync ? 'via contact sync' : '');
|
||||
console.log(
|
||||
'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 = {
|
||||
viaSyncMessage: true,
|
||||
viaContactSync: ev.viaContactSync,
|
||||
key: key
|
||||
key: key,
|
||||
};
|
||||
|
||||
if (state === 'VERIFIED') {
|
||||
@@ -915,7 +956,8 @@
|
||||
} else {
|
||||
return contact.setUnverified(options).then(ev.confirm);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function onDeliveryReceipt(ev) {
|
||||
@@ -928,7 +970,7 @@
|
||||
|
||||
var receipt = Whisper.DeliveryReceipts.add({
|
||||
timestamp: deliveryReceipt.timestamp,
|
||||
source: deliveryReceipt.source
|
||||
source: deliveryReceipt.source,
|
||||
});
|
||||
|
||||
ev.confirm();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
// Browser specific functions for Chrom*
|
||||
window.extension = window.extension || {};
|
||||
@@ -9,6 +6,6 @@
|
||||
extension.windows = {
|
||||
onClosed: function(callback) {
|
||||
window.addEventListener('beforeunload', callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,10 +1,7 @@
|
||||
/*global $, Whisper, Backbone, textsecure, extension*/
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
// This script should only be included in background.html
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -19,7 +16,8 @@
|
||||
this.reset([]);
|
||||
});
|
||||
|
||||
this.on('add remove change:unreadCount',
|
||||
this.on(
|
||||
'add remove change:unreadCount',
|
||||
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
||||
);
|
||||
this.startPruning();
|
||||
@@ -52,17 +50,20 @@
|
||||
},
|
||||
updateUnreadCount: function() {
|
||||
var newUnreadCount = _.reduce(
|
||||
this.map(function(m) { return m.get('unreadCount'); }),
|
||||
this.map(function(m) {
|
||||
return m.get('unreadCount');
|
||||
}),
|
||||
function(item, memo) {
|
||||
return item + memo;
|
||||
},
|
||||
0
|
||||
);
|
||||
storage.put("unreadCount", newUnreadCount);
|
||||
storage.put('unreadCount', newUnreadCount);
|
||||
|
||||
if (newUnreadCount > 0) {
|
||||
window.setBadgeCount(newUnreadCount);
|
||||
window.document.title = window.config.title + " (" + newUnreadCount + ")";
|
||||
window.document.title =
|
||||
window.config.title + ' (' + newUnreadCount + ')';
|
||||
} else {
|
||||
window.setBadgeCount(0);
|
||||
window.document.title = window.config.title;
|
||||
@@ -71,12 +72,15 @@
|
||||
},
|
||||
startPruning: function() {
|
||||
var halfHour = 30 * 60 * 1000;
|
||||
this.interval = setInterval(function() {
|
||||
this.interval = setInterval(
|
||||
function() {
|
||||
this.forEach(function(conversation) {
|
||||
conversation.trigger('prune');
|
||||
});
|
||||
}.bind(this), halfHour);
|
||||
}
|
||||
}.bind(this),
|
||||
halfHour
|
||||
);
|
||||
},
|
||||
}))();
|
||||
|
||||
window.getInboxCollection = function() {
|
||||
@@ -86,7 +90,9 @@
|
||||
window.ConversationController = {
|
||||
get: function(id) {
|
||||
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);
|
||||
@@ -104,11 +110,15 @@
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new Error('ConversationController.get() needs complete initial fetch');
|
||||
throw new Error(
|
||||
'ConversationController.get() needs complete initial fetch'
|
||||
);
|
||||
}
|
||||
|
||||
var conversation = conversations.get(id);
|
||||
@@ -118,7 +128,7 @@
|
||||
|
||||
conversation = conversations.add({
|
||||
id: id,
|
||||
type: type
|
||||
type: type,
|
||||
});
|
||||
conversation.initialPromise = new Promise(function(resolve, reject) {
|
||||
if (!conversation.isValid()) {
|
||||
@@ -146,7 +156,8 @@
|
||||
return conversation;
|
||||
},
|
||||
getOrCreateAndWait: function(id, type) {
|
||||
return this._initialPromise.then(function() {
|
||||
return this._initialPromise.then(
|
||||
function() {
|
||||
var conversation = this.getOrCreate(id, type);
|
||||
|
||||
if (conversation) {
|
||||
@@ -158,7 +169,8 @@
|
||||
return Promise.reject(
|
||||
new Error('getOrCreateAndWait: did not get conversation')
|
||||
);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
getAllGroupsInvolvingId: function(id) {
|
||||
var groups = new Whisper.GroupCollection();
|
||||
@@ -178,21 +190,26 @@
|
||||
load: function() {
|
||||
console.log('ConversationController: starting initial fetch');
|
||||
|
||||
this._initialPromise = new Promise(function(resolve, reject) {
|
||||
conversations.fetch().then(function() {
|
||||
this._initialPromise = new Promise(
|
||||
function(resolve, reject) {
|
||||
conversations.fetch().then(
|
||||
function() {
|
||||
console.log('ConversationController: done with initial fetch');
|
||||
this._initialFetchComplete = true;
|
||||
resolve();
|
||||
}.bind(this), function(error) {
|
||||
}.bind(this),
|
||||
function(error) {
|
||||
console.log(
|
||||
'ConversationController: initial fetch failed',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
reject(error);
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return this._initialPromise;
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/* global _: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const { getPlaceholderMigrations } = window.Signal.Migrations;
|
||||
@@ -24,13 +24,13 @@
|
||||
};
|
||||
|
||||
function clearStores(db, names) {
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storeNames = names || db.objectStoreNames;
|
||||
console.log('Clearing these indexeddb stores:', storeNames);
|
||||
const transaction = db.transaction(storeNames, 'readwrite');
|
||||
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('clearing all stores done via', via);
|
||||
if (finished) {
|
||||
resolve();
|
||||
@@ -50,7 +50,7 @@
|
||||
let count = 0;
|
||||
|
||||
// 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 request = store.clear();
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
);
|
||||
};
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Whisper.Database.open = () => {
|
||||
@@ -80,7 +80,7 @@
|
||||
const { version } = migrations[migrations.length - 1];
|
||||
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,
|
||||
// when the database is opened successfully, or not
|
||||
DBOpenRequest.onerror = reject;
|
||||
@@ -91,7 +91,7 @@
|
||||
// been created before, or a new version number has been
|
||||
// submitted via the window.indexedDB.open line above
|
||||
DBOpenRequest.onupgradeneeded = reject;
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Whisper.Database.clear = async () => {
|
||||
@@ -99,7 +99,7 @@
|
||||
return clearStores(db);
|
||||
};
|
||||
|
||||
Whisper.Database.clearStores = async (storeNames) => {
|
||||
Whisper.Database.clearStores = async storeNames => {
|
||||
const db = await Whisper.Database.open();
|
||||
return clearStores(db, storeNames);
|
||||
};
|
||||
@@ -107,7 +107,7 @@
|
||||
Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall'));
|
||||
|
||||
Whisper.Database.drop = () =>
|
||||
new Promise(((resolve, reject) => {
|
||||
new Promise((resolve, reject) => {
|
||||
const request = window.indexedDB.deleteDatabase(Whisper.Database.id);
|
||||
|
||||
request.onblocked = () => {
|
||||
@@ -121,7 +121,7 @@
|
||||
};
|
||||
|
||||
request.onsuccess = resolve;
|
||||
}));
|
||||
});
|
||||
|
||||
Whisper.Database.migrations = getPlaceholderMigrations();
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -9,44 +6,65 @@
|
||||
forMessage: function(conversation, message) {
|
||||
var recipients;
|
||||
if (conversation.isPrivate()) {
|
||||
recipients = [ conversation.id ];
|
||||
recipients = [conversation.id];
|
||||
} else {
|
||||
recipients = conversation.get('members') || [];
|
||||
}
|
||||
var receipts = this.filter(function(receipt) {
|
||||
return (receipt.get('timestamp') === message.get('sent_at')) &&
|
||||
(recipients.indexOf(receipt.get('source')) > -1);
|
||||
return (
|
||||
receipt.get('timestamp') === message.get('sent_at') &&
|
||||
recipients.indexOf(receipt.get('source')) > -1
|
||||
);
|
||||
});
|
||||
this.remove(receipts);
|
||||
return receipts;
|
||||
},
|
||||
onReceipt: function(receipt) {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
|
||||
if (messages.length === 0) { return; }
|
||||
return messages
|
||||
.fetchSentAt(receipt.get('timestamp'))
|
||||
.then(function() {
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
var message = messages.find(function(message) {
|
||||
return (!message.isIncoming() && receipt.get('source') === message.get('conversationId'));
|
||||
return (
|
||||
!message.isIncoming() &&
|
||||
receipt.get('source') === message.get('conversationId')
|
||||
);
|
||||
});
|
||||
if (message) { return message; }
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
var groups = new Whisper.GroupCollection();
|
||||
return groups.fetchGroups(receipt.get('source')).then(function() {
|
||||
var ids = groups.pluck('id');
|
||||
ids.push(receipt.get('source'));
|
||||
return messages.find(function(message) {
|
||||
return (!message.isIncoming() &&
|
||||
_.contains(ids, message.get('conversationId')));
|
||||
return (
|
||||
!message.isIncoming() &&
|
||||
_.contains(ids, message.get('conversationId'))
|
||||
);
|
||||
});
|
||||
});
|
||||
}).then(function(message) {
|
||||
})
|
||||
.then(
|
||||
function(message) {
|
||||
if (message) {
|
||||
var deliveries = message.get('delivered') || 0;
|
||||
var delivered_to = message.get('delivered_to') || [];
|
||||
return new Promise(function(resolve, reject) {
|
||||
message.save({
|
||||
delivered_to: _.union(delivered_to, [receipt.get('source')]),
|
||||
delivered: deliveries + 1
|
||||
}).then(function() {
|
||||
return new Promise(
|
||||
function(resolve, reject) {
|
||||
message
|
||||
.save({
|
||||
delivered_to: _.union(delivered_to, [
|
||||
receipt.get('source'),
|
||||
]),
|
||||
delivered: deliveries + 1,
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
// notify frontend listeners
|
||||
var conversation = ConversationController.get(
|
||||
message.get('conversationId')
|
||||
@@ -57,8 +75,11 @@
|
||||
|
||||
this.remove(receipt);
|
||||
resolve();
|
||||
}.bind(this), reject);
|
||||
}.bind(this));
|
||||
}.bind(this),
|
||||
reject
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
// TODO: consider keeping a list of numbers we've
|
||||
// successfully delivered to?
|
||||
} else {
|
||||
@@ -68,12 +89,14 @@
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
}
|
||||
}.bind(this)).catch(function(error) {
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(function(error) {
|
||||
console.log(
|
||||
'DeliveryReceipts.onReceipt error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
}))();
|
||||
})();
|
||||
|
@@ -1,8 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.emoji_util = window.emoji_util || {};
|
||||
|
||||
@@ -39,17 +35,13 @@
|
||||
var emojiCount = self.getCountOfAllMatches(str, self.rx_unified);
|
||||
if (emojiCount > 8) {
|
||||
return '';
|
||||
}
|
||||
else if (emojiCount > 6) {
|
||||
} else if (emojiCount > 6) {
|
||||
return 'small';
|
||||
}
|
||||
else if (emojiCount > 4) {
|
||||
} else if (emojiCount > 4) {
|
||||
return 'medium';
|
||||
}
|
||||
else if (emojiCount > 2) {
|
||||
} else if (emojiCount > 2) {
|
||||
return 'large';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return 'jumbo';
|
||||
}
|
||||
};
|
||||
@@ -83,7 +75,8 @@
|
||||
|
||||
window.emoji = new EmojiConvertor();
|
||||
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.replace_mode = 'img';
|
||||
emoji.supports_css = false; // needed to avoid spans with background-image
|
||||
@@ -95,5 +88,4 @@
|
||||
|
||||
$el.html(emoji.signalReplace($el.html()));
|
||||
};
|
||||
|
||||
})();
|
||||
|
@@ -1,16 +1,16 @@
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
var BUILD_EXPIRATION = 0;
|
||||
try {
|
||||
BUILD_EXPIRATION = parseInt(window.config.buildExpiration);
|
||||
if (BUILD_EXPIRATION) {
|
||||
console.log("Build expires: ", new Date(BUILD_EXPIRATION).toISOString());
|
||||
console.log('Build expires: ', new Date(BUILD_EXPIRATION).toISOString());
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
window.extension = window.extension || {};
|
||||
|
||||
extension.expired = function() {
|
||||
return (BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION);
|
||||
return BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION;
|
||||
};
|
||||
})();
|
||||
|
@@ -1,8 +1,4 @@
|
||||
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -36,10 +32,14 @@
|
||||
var wait = expires_at - Date.now();
|
||||
|
||||
// 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
|
||||
if (wait > 2147483647) { wait = 2147483647; }
|
||||
if (wait > 2147483647) {
|
||||
wait = 2147483647;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(destroyExpiredMessages, wait);
|
||||
@@ -53,20 +53,23 @@
|
||||
checkExpiringMessages();
|
||||
events.on('timetravel', throttledCheckExpiringMessages);
|
||||
},
|
||||
update: throttledCheckExpiringMessages
|
||||
update: throttledCheckExpiringMessages,
|
||||
};
|
||||
|
||||
var TimerOption = Backbone.Model.extend({
|
||||
getName: function() {
|
||||
return i18n([
|
||||
'timerOption', this.get('time'), this.get('unit'),
|
||||
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize();
|
||||
return (
|
||||
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
|
||||
moment.duration(this.get('time'), this.get('unit')).humanize()
|
||||
);
|
||||
},
|
||||
getAbbreviated: function() {
|
||||
return i18n([
|
||||
'timerOption', this.get('time'), this.get('unit'), 'abbreviated'
|
||||
].join('_'));
|
||||
}
|
||||
return i18n(
|
||||
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
|
||||
'_'
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
|
||||
model: TimerOption,
|
||||
@@ -74,9 +77,10 @@
|
||||
if (!seconds) {
|
||||
seconds = 0;
|
||||
}
|
||||
var o = this.findWhere({seconds: seconds});
|
||||
if (o) { return o.getName(); }
|
||||
else {
|
||||
var o = this.findWhere({ seconds: seconds });
|
||||
if (o) {
|
||||
return o.getName();
|
||||
} else {
|
||||
return [seconds, 'seconds'].join(' ');
|
||||
}
|
||||
},
|
||||
@@ -84,32 +88,34 @@
|
||||
if (!seconds) {
|
||||
seconds = 0;
|
||||
}
|
||||
var o = this.findWhere({seconds: seconds});
|
||||
if (o) { return o.getAbbreviated(); }
|
||||
else {
|
||||
var o = this.findWhere({ seconds: seconds });
|
||||
if (o) {
|
||||
return o.getAbbreviated();
|
||||
} else {
|
||||
return [seconds, 's'].join('');
|
||||
}
|
||||
}
|
||||
}))([
|
||||
[ 0, 'seconds' ],
|
||||
[ 5, 'seconds' ],
|
||||
[ 10, 'seconds' ],
|
||||
[ 30, 'seconds' ],
|
||||
[ 1, 'minute' ],
|
||||
[ 5, 'minutes' ],
|
||||
[ 30, 'minutes' ],
|
||||
[ 1, 'hour' ],
|
||||
[ 6, 'hours' ],
|
||||
[ 12, 'hours' ],
|
||||
[ 1, 'day' ],
|
||||
[ 1, 'week' ],
|
||||
},
|
||||
}))(
|
||||
[
|
||||
[0, 'seconds'],
|
||||
[5, 'seconds'],
|
||||
[10, 'seconds'],
|
||||
[30, 'seconds'],
|
||||
[1, 'minute'],
|
||||
[5, 'minutes'],
|
||||
[30, 'minutes'],
|
||||
[1, 'hour'],
|
||||
[6, 'hours'],
|
||||
[12, 'hours'],
|
||||
[1, 'day'],
|
||||
[1, 'week'],
|
||||
].map(function(o) {
|
||||
var duration = moment.duration(o[0], o[1]); // 5, 'seconds'
|
||||
return {
|
||||
time: o[0],
|
||||
unit: o[1],
|
||||
seconds: duration.asSeconds()
|
||||
seconds: duration.asSeconds(),
|
||||
};
|
||||
}));
|
||||
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var windowFocused = false;
|
||||
|
@@ -1,8 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
;(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -13,16 +9,20 @@
|
||||
}
|
||||
|
||||
signalProtocolStore.on('keychange', function(id) {
|
||||
ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) {
|
||||
ConversationController.getOrCreateAndWait(id, 'private').then(function(
|
||||
conversation
|
||||
) {
|
||||
conversation.addKeyChange(id);
|
||||
|
||||
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) {
|
||||
ConversationController.getAllGroupsInvolvingId(id).then(function(
|
||||
groups
|
||||
) {
|
||||
_.forEach(groups, function(group) {
|
||||
group.addKeyChange(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
"use strict";
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This file extends the libphonenumber object with a set of phonenumbery
|
||||
@@ -16,8 +13,8 @@
|
||||
try {
|
||||
var parsedNumber = libphonenumber.parse(number);
|
||||
return libphonenumber.getRegionCodeForNumber(parsedNumber);
|
||||
} catch(e) {
|
||||
return "ZZ";
|
||||
} catch (e) {
|
||||
return 'ZZ';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,13 +22,13 @@
|
||||
var parsedNumber = libphonenumber.parse(number);
|
||||
return {
|
||||
country_code: parsedNumber.values_[1],
|
||||
national_number: parsedNumber.values_[2]
|
||||
national_number: parsedNumber.values_[2],
|
||||
};
|
||||
},
|
||||
|
||||
getCountryCode: function(regionCode) {
|
||||
var cc = libphonenumber.getCountryCodeForRegion(regionCode);
|
||||
return (cc !== 0) ? cc : "";
|
||||
return cc !== 0 ? cc : '';
|
||||
},
|
||||
|
||||
parseNumber: function(number, defaultRegionCode) {
|
||||
@@ -43,7 +40,10 @@
|
||||
regionCode: libphonenumber.getRegionCodeForNumber(parsedNumber),
|
||||
countryCode: '' + parsedNumber.getCountryCode(),
|
||||
nationalNumber: '' + parsedNumber.getNationalNumber(),
|
||||
e164: libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.E164)
|
||||
e164: libphonenumber.format(
|
||||
parsedNumber,
|
||||
libphonenumber.PhoneNumberFormat.E164
|
||||
),
|
||||
};
|
||||
} catch (ex) {
|
||||
return { error: ex, isValidNumber: false };
|
||||
@@ -52,244 +52,244 @@
|
||||
|
||||
getAllRegionCodes: function() {
|
||||
return {
|
||||
"AD":"Andorra",
|
||||
"AE":"United Arab Emirates",
|
||||
"AF":"Afghanistan",
|
||||
"AG":"Antigua and Barbuda",
|
||||
"AI":"Anguilla",
|
||||
"AL":"Albania",
|
||||
"AM":"Armenia",
|
||||
"AO":"Angola",
|
||||
"AR":"Argentina",
|
||||
"AS":"AmericanSamoa",
|
||||
"AT":"Austria",
|
||||
"AU":"Australia",
|
||||
"AW":"Aruba",
|
||||
"AX":"Åland Islands",
|
||||
"AZ":"Azerbaijan",
|
||||
"BA":"Bosnia and Herzegovina",
|
||||
"BB":"Barbados",
|
||||
"BD":"Bangladesh",
|
||||
"BE":"Belgium",
|
||||
"BF":"Burkina Faso",
|
||||
"BG":"Bulgaria",
|
||||
"BH":"Bahrain",
|
||||
"BI":"Burundi",
|
||||
"BJ":"Benin",
|
||||
"BL":"Saint Barthélemy",
|
||||
"BM":"Bermuda",
|
||||
"BN":"Brunei Darussalam",
|
||||
"BO":"Bolivia, Plurinational State of",
|
||||
"BR":"Brazil",
|
||||
"BS":"Bahamas",
|
||||
"BT":"Bhutan",
|
||||
"BW":"Botswana",
|
||||
"BY":"Belarus",
|
||||
"BZ":"Belize",
|
||||
"CA":"Canada",
|
||||
"CC":"Cocos (Keeling) Islands",
|
||||
"CD":"Congo, The Democratic Republic of the",
|
||||
"CF":"Central African Republic",
|
||||
"CG":"Congo",
|
||||
"CH":"Switzerland",
|
||||
"CI":"Cote d'Ivoire",
|
||||
"CK":"Cook Islands",
|
||||
"CL":"Chile",
|
||||
"CM":"Cameroon",
|
||||
"CN":"China",
|
||||
"CO":"Colombia",
|
||||
"CR":"Costa Rica",
|
||||
"CU":"Cuba",
|
||||
"CV":"Cape Verde",
|
||||
"CX":"Christmas Island",
|
||||
"CY":"Cyprus",
|
||||
"CZ":"Czech Republic",
|
||||
"DE":"Germany",
|
||||
"DJ":"Djibouti",
|
||||
"DK":"Denmark",
|
||||
"DM":"Dominica",
|
||||
"DO":"Dominican Republic",
|
||||
"DZ":"Algeria",
|
||||
"EC":"Ecuador",
|
||||
"EE":"Estonia",
|
||||
"EG":"Egypt",
|
||||
"ER":"Eritrea",
|
||||
"ES":"Spain",
|
||||
"ET":"Ethiopia",
|
||||
"FI":"Finland",
|
||||
"FJ":"Fiji",
|
||||
"FK":"Falkland Islands (Malvinas)",
|
||||
"FM":"Micronesia, Federated States of",
|
||||
"FO":"Faroe Islands",
|
||||
"FR":"France",
|
||||
"GA":"Gabon",
|
||||
"GB":"United Kingdom",
|
||||
"GD":"Grenada",
|
||||
"GE":"Georgia",
|
||||
"GF":"French Guiana",
|
||||
"GG":"Guernsey",
|
||||
"GH":"Ghana",
|
||||
"GI":"Gibraltar",
|
||||
"GL":"Greenland",
|
||||
"GM":"Gambia",
|
||||
"GN":"Guinea",
|
||||
"GP":"Guadeloupe",
|
||||
"GQ":"Equatorial Guinea",
|
||||
"GR":"Ελλάδα",
|
||||
"GT":"Guatemala",
|
||||
"GU":"Guam",
|
||||
"GW":"Guinea-Bissau",
|
||||
"GY":"Guyana",
|
||||
"HK":"Hong Kong",
|
||||
"HN":"Honduras",
|
||||
"HR":"Croatia",
|
||||
"HT":"Haiti",
|
||||
"HU":"Magyarország",
|
||||
"ID":"Indonesia",
|
||||
"IE":"Ireland",
|
||||
"IL":"Israel",
|
||||
"IM":"Isle of Man",
|
||||
"IN":"India",
|
||||
"IO":"British Indian Ocean Territory",
|
||||
"IQ":"Iraq",
|
||||
"IR":"Iran, Islamic Republic of",
|
||||
"IS":"Iceland",
|
||||
"IT":"Italy",
|
||||
"JE":"Jersey",
|
||||
"JM":"Jamaica",
|
||||
"JO":"Jordan",
|
||||
"JP":"Japan",
|
||||
"KE":"Kenya",
|
||||
"KG":"Kyrgyzstan",
|
||||
"KH":"Cambodia",
|
||||
"KI":"Kiribati",
|
||||
"KM":"Comoros",
|
||||
"KN":"Saint Kitts and Nevis",
|
||||
"KP":"Korea, Democratic People's Republic of",
|
||||
"KR":"Korea, Republic of",
|
||||
"KW":"Kuwait",
|
||||
"KY":"Cayman Islands",
|
||||
"KZ":"Kazakhstan",
|
||||
"LA":"Lao People's Democratic Republic",
|
||||
"LB":"Lebanon",
|
||||
"LC":"Saint Lucia",
|
||||
"LI":"Liechtenstein",
|
||||
"LK":"Sri Lanka",
|
||||
"LR":"Liberia",
|
||||
"LS":"Lesotho",
|
||||
"LT":"Lithuania",
|
||||
"LU":"Luxembourg",
|
||||
"LV":"Latvia",
|
||||
"LY":"Libyan Arab Jamahiriya",
|
||||
"MA":"Morocco",
|
||||
"MC":"Monaco",
|
||||
"MD":"Moldova, Republic of",
|
||||
"ME":"Црна Гора",
|
||||
"MF":"Saint Martin",
|
||||
"MG":"Madagascar",
|
||||
"MH":"Marshall Islands",
|
||||
"MK":"Macedonia, The Former Yugoslav Republic of",
|
||||
"ML":"Mali",
|
||||
"MM":"Myanmar",
|
||||
"MN":"Mongolia",
|
||||
"MO":"Macao",
|
||||
"MP":"Northern Mariana Islands",
|
||||
"MQ":"Martinique",
|
||||
"MR":"Mauritania",
|
||||
"MS":"Montserrat",
|
||||
"MT":"Malta",
|
||||
"MU":"Mauritius",
|
||||
"MV":"Maldives",
|
||||
"MW":"Malawi",
|
||||
"MX":"Mexico",
|
||||
"MY":"Malaysia",
|
||||
"MZ":"Mozambique",
|
||||
"NA":"Namibia",
|
||||
"NC":"New Caledonia",
|
||||
"NE":"Niger",
|
||||
"NF":"Norfolk Island",
|
||||
"NG":"Nigeria",
|
||||
"NI":"Nicaragua",
|
||||
"NL":"Netherlands",
|
||||
"NO":"Norway",
|
||||
"NP":"Nepal",
|
||||
"NR":"Nauru",
|
||||
"NU":"Niue",
|
||||
"NZ":"New Zealand",
|
||||
"OM":"Oman",
|
||||
"PA":"Panama",
|
||||
"PE":"Peru",
|
||||
"PF":"French Polynesia",
|
||||
"PG":"Papua New Guinea",
|
||||
"PH":"Philippines",
|
||||
"PK":"Pakistan",
|
||||
"PL":"Polska",
|
||||
"PM":"Saint Pierre and Miquelon",
|
||||
"PR":"Puerto Rico",
|
||||
"PS":"Palestinian Territory, Occupied",
|
||||
"PT":"Portugal",
|
||||
"PW":"Palau",
|
||||
"PY":"Paraguay",
|
||||
"QA":"Qatar",
|
||||
"RE":"Réunion",
|
||||
"RO":"Romania",
|
||||
"RS":"Србија",
|
||||
"RU":"Russia",
|
||||
"RW":"Rwanda",
|
||||
"SA":"Saudi Arabia",
|
||||
"SB":"Solomon Islands",
|
||||
"SC":"Seychelles",
|
||||
"SD":"Sudan",
|
||||
"SE":"Sweden",
|
||||
"SG":"Singapore",
|
||||
"SH":"Saint Helena, Ascension and Tristan Da Cunha",
|
||||
"SI":"Slovenia",
|
||||
"SJ":"Svalbard and Jan Mayen",
|
||||
"SK":"Slovakia",
|
||||
"SL":"Sierra Leone",
|
||||
"SM":"San Marino",
|
||||
"SN":"Senegal",
|
||||
"SO":"Somalia",
|
||||
"SR":"Suriname",
|
||||
"ST":"Sao Tome and Principe",
|
||||
"SV":"El Salvador",
|
||||
"SY":"Syrian Arab Republic",
|
||||
"SZ":"Swaziland",
|
||||
"TC":"Turks and Caicos Islands",
|
||||
"TD":"Chad",
|
||||
"TG":"Togo",
|
||||
"TH":"Thailand",
|
||||
"TJ":"Tajikistan",
|
||||
"TK":"Tokelau",
|
||||
"TL":"Timor-Leste",
|
||||
"TM":"Turkmenistan",
|
||||
"TN":"Tunisia",
|
||||
"TO":"Tonga",
|
||||
"TR":"Turkey",
|
||||
"TT":"Trinidad and Tobago",
|
||||
"TV":"Tuvalu",
|
||||
"TW":"Taiwan, Province of China",
|
||||
"TZ":"Tanzania, United Republic of",
|
||||
"UA":"Ukraine",
|
||||
"UG":"Uganda",
|
||||
"US":"United States",
|
||||
"UY":"Uruguay",
|
||||
"UZ":"Uzbekistan",
|
||||
"VA":"Holy See (Vatican City State)",
|
||||
"VC":"Saint Vincent and the Grenadines",
|
||||
"VE":"Venezuela",
|
||||
"VG":"Virgin Islands, British",
|
||||
"VI":"Virgin Islands, U.S.",
|
||||
"VN":"Viet Nam",
|
||||
"VU":"Vanuatu",
|
||||
"WF":"Wallis and Futuna",
|
||||
"WS":"Samoa",
|
||||
"YE":"Yemen",
|
||||
"YT":"Mayotte",
|
||||
"ZA":"South Africa",
|
||||
"ZM":"Zambia",
|
||||
"ZW":"Zimbabwe"
|
||||
AD: 'Andorra',
|
||||
AE: 'United Arab Emirates',
|
||||
AF: 'Afghanistan',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AI: 'Anguilla',
|
||||
AL: 'Albania',
|
||||
AM: 'Armenia',
|
||||
AO: 'Angola',
|
||||
AR: 'Argentina',
|
||||
AS: 'AmericanSamoa',
|
||||
AT: 'Austria',
|
||||
AU: 'Australia',
|
||||
AW: 'Aruba',
|
||||
AX: 'Åland Islands',
|
||||
AZ: 'Azerbaijan',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BB: 'Barbados',
|
||||
BD: 'Bangladesh',
|
||||
BE: 'Belgium',
|
||||
BF: 'Burkina Faso',
|
||||
BG: 'Bulgaria',
|
||||
BH: 'Bahrain',
|
||||
BI: 'Burundi',
|
||||
BJ: 'Benin',
|
||||
BL: 'Saint Barthélemy',
|
||||
BM: 'Bermuda',
|
||||
BN: 'Brunei Darussalam',
|
||||
BO: 'Bolivia, Plurinational State of',
|
||||
BR: 'Brazil',
|
||||
BS: 'Bahamas',
|
||||
BT: 'Bhutan',
|
||||
BW: 'Botswana',
|
||||
BY: 'Belarus',
|
||||
BZ: 'Belize',
|
||||
CA: 'Canada',
|
||||
CC: 'Cocos (Keeling) Islands',
|
||||
CD: 'Congo, The Democratic Republic of the',
|
||||
CF: 'Central African Republic',
|
||||
CG: 'Congo',
|
||||
CH: 'Switzerland',
|
||||
CI: "Cote d'Ivoire",
|
||||
CK: 'Cook Islands',
|
||||
CL: 'Chile',
|
||||
CM: 'Cameroon',
|
||||
CN: 'China',
|
||||
CO: 'Colombia',
|
||||
CR: 'Costa Rica',
|
||||
CU: 'Cuba',
|
||||
CV: 'Cape Verde',
|
||||
CX: 'Christmas Island',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czech Republic',
|
||||
DE: 'Germany',
|
||||
DJ: 'Djibouti',
|
||||
DK: 'Denmark',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic',
|
||||
DZ: 'Algeria',
|
||||
EC: 'Ecuador',
|
||||
EE: 'Estonia',
|
||||
EG: 'Egypt',
|
||||
ER: 'Eritrea',
|
||||
ES: 'Spain',
|
||||
ET: 'Ethiopia',
|
||||
FI: 'Finland',
|
||||
FJ: 'Fiji',
|
||||
FK: 'Falkland Islands (Malvinas)',
|
||||
FM: 'Micronesia, Federated States of',
|
||||
FO: 'Faroe Islands',
|
||||
FR: 'France',
|
||||
GA: 'Gabon',
|
||||
GB: 'United Kingdom',
|
||||
GD: 'Grenada',
|
||||
GE: 'Georgia',
|
||||
GF: 'French Guiana',
|
||||
GG: 'Guernsey',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GL: 'Greenland',
|
||||
GM: 'Gambia',
|
||||
GN: 'Guinea',
|
||||
GP: 'Guadeloupe',
|
||||
GQ: 'Equatorial Guinea',
|
||||
GR: 'Ελλάδα',
|
||||
GT: 'Guatemala',
|
||||
GU: 'Guam',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HK: 'Hong Kong',
|
||||
HN: 'Honduras',
|
||||
HR: 'Croatia',
|
||||
HT: 'Haiti',
|
||||
HU: 'Magyarország',
|
||||
ID: 'Indonesia',
|
||||
IE: 'Ireland',
|
||||
IL: 'Israel',
|
||||
IM: 'Isle of Man',
|
||||
IN: 'India',
|
||||
IO: 'British Indian Ocean Territory',
|
||||
IQ: 'Iraq',
|
||||
IR: 'Iran, Islamic Republic of',
|
||||
IS: 'Iceland',
|
||||
IT: 'Italy',
|
||||
JE: 'Jersey',
|
||||
JM: 'Jamaica',
|
||||
JO: 'Jordan',
|
||||
JP: 'Japan',
|
||||
KE: 'Kenya',
|
||||
KG: 'Kyrgyzstan',
|
||||
KH: 'Cambodia',
|
||||
KI: 'Kiribati',
|
||||
KM: 'Comoros',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
KP: "Korea, Democratic People's Republic of",
|
||||
KR: 'Korea, Republic of',
|
||||
KW: 'Kuwait',
|
||||
KY: 'Cayman Islands',
|
||||
KZ: 'Kazakhstan',
|
||||
LA: "Lao People's Democratic Republic",
|
||||
LB: 'Lebanon',
|
||||
LC: 'Saint Lucia',
|
||||
LI: 'Liechtenstein',
|
||||
LK: 'Sri Lanka',
|
||||
LR: 'Liberia',
|
||||
LS: 'Lesotho',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
LV: 'Latvia',
|
||||
LY: 'Libyan Arab Jamahiriya',
|
||||
MA: 'Morocco',
|
||||
MC: 'Monaco',
|
||||
MD: 'Moldova, Republic of',
|
||||
ME: 'Црна Гора',
|
||||
MF: 'Saint Martin',
|
||||
MG: 'Madagascar',
|
||||
MH: 'Marshall Islands',
|
||||
MK: 'Macedonia, The Former Yugoslav Republic of',
|
||||
ML: 'Mali',
|
||||
MM: 'Myanmar',
|
||||
MN: 'Mongolia',
|
||||
MO: 'Macao',
|
||||
MP: 'Northern Mariana Islands',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MS: 'Montserrat',
|
||||
MT: 'Malta',
|
||||
MU: 'Mauritius',
|
||||
MV: 'Maldives',
|
||||
MW: 'Malawi',
|
||||
MX: 'Mexico',
|
||||
MY: 'Malaysia',
|
||||
MZ: 'Mozambique',
|
||||
NA: 'Namibia',
|
||||
NC: 'New Caledonia',
|
||||
NE: 'Niger',
|
||||
NF: 'Norfolk Island',
|
||||
NG: 'Nigeria',
|
||||
NI: 'Nicaragua',
|
||||
NL: 'Netherlands',
|
||||
NO: 'Norway',
|
||||
NP: 'Nepal',
|
||||
NR: 'Nauru',
|
||||
NU: 'Niue',
|
||||
NZ: 'New Zealand',
|
||||
OM: 'Oman',
|
||||
PA: 'Panama',
|
||||
PE: 'Peru',
|
||||
PF: 'French Polynesia',
|
||||
PG: 'Papua New Guinea',
|
||||
PH: 'Philippines',
|
||||
PK: 'Pakistan',
|
||||
PL: 'Polska',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
PR: 'Puerto Rico',
|
||||
PS: 'Palestinian Territory, Occupied',
|
||||
PT: 'Portugal',
|
||||
PW: 'Palau',
|
||||
PY: 'Paraguay',
|
||||
QA: 'Qatar',
|
||||
RE: 'Réunion',
|
||||
RO: 'Romania',
|
||||
RS: 'Србија',
|
||||
RU: 'Russia',
|
||||
RW: 'Rwanda',
|
||||
SA: 'Saudi Arabia',
|
||||
SB: 'Solomon Islands',
|
||||
SC: 'Seychelles',
|
||||
SD: 'Sudan',
|
||||
SE: 'Sweden',
|
||||
SG: 'Singapore',
|
||||
SH: 'Saint Helena, Ascension and Tristan Da Cunha',
|
||||
SI: 'Slovenia',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SK: 'Slovakia',
|
||||
SL: 'Sierra Leone',
|
||||
SM: 'San Marino',
|
||||
SN: 'Senegal',
|
||||
SO: 'Somalia',
|
||||
SR: 'Suriname',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SV: 'El Salvador',
|
||||
SY: 'Syrian Arab Republic',
|
||||
SZ: 'Swaziland',
|
||||
TC: 'Turks and Caicos Islands',
|
||||
TD: 'Chad',
|
||||
TG: 'Togo',
|
||||
TH: 'Thailand',
|
||||
TJ: 'Tajikistan',
|
||||
TK: 'Tokelau',
|
||||
TL: 'Timor-Leste',
|
||||
TM: 'Turkmenistan',
|
||||
TN: 'Tunisia',
|
||||
TO: 'Tonga',
|
||||
TR: 'Turkey',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TV: 'Tuvalu',
|
||||
TW: 'Taiwan, Province of China',
|
||||
TZ: 'Tanzania, United Republic of',
|
||||
UA: 'Ukraine',
|
||||
UG: 'Uganda',
|
||||
US: 'United States',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VA: 'Holy See (Vatican City State)',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
VE: 'Venezuela',
|
||||
VG: 'Virgin Islands, British',
|
||||
VI: 'Virgin Islands, U.S.',
|
||||
VN: 'Viet Nam',
|
||||
VU: 'Vanuatu',
|
||||
WF: 'Wallis and Futuna',
|
||||
WS: 'Samoa',
|
||||
YE: 'Yemen',
|
||||
YT: 'Mayotte',
|
||||
ZA: 'South Africa',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
};
|
||||
} // getAllRegionCodes
|
||||
}, // getAllRegionCodes
|
||||
}; // libphonenumber.util
|
||||
})();
|
||||
|
@@ -34,7 +34,7 @@ function log(...args) {
|
||||
console._log(...consoleArgs);
|
||||
|
||||
// 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') {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
@@ -55,7 +55,6 @@ if (window.console) {
|
||||
console.log = log;
|
||||
}
|
||||
|
||||
|
||||
// The mechanics of preparing a log for publish
|
||||
|
||||
function getHeader() {
|
||||
@@ -85,7 +84,7 @@ function format(entries) {
|
||||
}
|
||||
|
||||
function fetch() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
ipc.send('fetch-log');
|
||||
|
||||
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.
|
||||
const logger = bunyan.createLogger({
|
||||
name: 'log',
|
||||
streams: [{
|
||||
streams: [
|
||||
{
|
||||
level: 'debug',
|
||||
stream: {
|
||||
write(entry) {
|
||||
console._log(formatLine(JSON.parse(entry)));
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 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.addEventListener('unhandledrejection', (rejectionEvent) => {
|
||||
window.log.error(`Top-level unhandled promise rejection: ${rejectionEvent.reason}`);
|
||||
window.addEventListener('unhandledrejection', rejectionEvent => {
|
||||
window.log.error(
|
||||
`Top-level unhandled promise rejection: ${rejectionEvent.reason}`
|
||||
);
|
||||
});
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
storage.isBlocked = function(number) {
|
||||
var numbers = storage.get('blocked', []);
|
||||
|
@@ -14,7 +14,7 @@
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -124,27 +124,34 @@
|
||||
},
|
||||
safeGetVerified() {
|
||||
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() {
|
||||
if (this.isPrivate()) {
|
||||
return Promise.all([
|
||||
this.safeGetVerified(),
|
||||
this.initialPromise,
|
||||
]).then((results) => {
|
||||
return Promise.all([this.safeGetVerified(), this.initialPromise]).then(
|
||||
results => {
|
||||
const trust = results[0];
|
||||
// we don't return here because we don't need to wait for this to finish
|
||||
this.save({ verified: trust });
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
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()) {
|
||||
return contact.updateVerified();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}))).then(this.onMemberVerifiedChange.bind(this));
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(this.onMemberVerifiedChange.bind(this));
|
||||
},
|
||||
setVerifiedDefault(options) {
|
||||
const { DEFAULT } = this.verifiedEnum;
|
||||
@@ -160,16 +167,19 @@
|
||||
},
|
||||
_setVerified(verified, providedOptions) {
|
||||
const options = providedOptions || {};
|
||||
_.defaults(options, { viaSyncMessage: false, viaContactSync: false, key: null });
|
||||
_.defaults(options, {
|
||||
viaSyncMessage: false,
|
||||
viaContactSync: false,
|
||||
key: null,
|
||||
});
|
||||
|
||||
const {
|
||||
VERIFIED,
|
||||
UNVERIFIED,
|
||||
} = this.verifiedEnum;
|
||||
const { VERIFIED, UNVERIFIED } = this.verifiedEnum;
|
||||
|
||||
if (!this.isPrivate()) {
|
||||
throw new Error('You cannot verify a group conversation. ' +
|
||||
'You must verify individual contacts.');
|
||||
throw new Error(
|
||||
'You cannot verify a group conversation. ' +
|
||||
'You must verify individual contacts.'
|
||||
);
|
||||
}
|
||||
|
||||
const beginningVerified = this.get('verified');
|
||||
@@ -187,10 +197,14 @@
|
||||
}
|
||||
|
||||
let keychange;
|
||||
return promise.then((updatedKey) => {
|
||||
return promise
|
||||
.then(updatedKey => {
|
||||
keychange = updatedKey;
|
||||
return new Promise((resolve => this.save({ verified }).always(resolve)));
|
||||
}).then(() => {
|
||||
return new Promise(resolve =>
|
||||
this.save({ verified }).always(resolve)
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// Three situations result in a verification notice in the conversation:
|
||||
// 1) The message came from an explicit verification in another client (not
|
||||
// a contact sync)
|
||||
@@ -199,14 +213,14 @@
|
||||
// 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
|
||||
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
|
||||
if (!options.viaContactSync ||
|
||||
if (
|
||||
!options.viaContactSync ||
|
||||
(beginningVerified !== verified && verified !== UNVERIFIED) ||
|
||||
(keychange && verified === VERIFIED)) {
|
||||
return this.addVerifiedChange(
|
||||
this.id,
|
||||
verified === VERIFIED,
|
||||
{ local: !options.viaSyncMessage }
|
||||
);
|
||||
(keychange && verified === VERIFIED)
|
||||
) {
|
||||
return this.addVerifiedChange(this.id, verified === VERIFIED, {
|
||||
local: !options.viaSyncMessage,
|
||||
});
|
||||
}
|
||||
if (!options.viaSyncMessage) {
|
||||
return this.sendVerifySyncMessage(this.id, verified);
|
||||
@@ -216,20 +230,21 @@
|
||||
},
|
||||
sendVerifySyncMessage(number, state) {
|
||||
const promise = textsecure.storage.protocol.loadIdentityKey(number);
|
||||
return promise.then(key => textsecure.messaging.syncVerification(
|
||||
number,
|
||||
state,
|
||||
key
|
||||
));
|
||||
return promise.then(key =>
|
||||
textsecure.messaging.syncVerification(number, state, key)
|
||||
);
|
||||
},
|
||||
getIdentityKeys() {
|
||||
const lookup = {};
|
||||
|
||||
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;
|
||||
return lookup;
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(
|
||||
'getIdentityKeys error for conversation',
|
||||
this.idForLogging(),
|
||||
@@ -240,27 +255,25 @@
|
||||
}
|
||||
const promises = this.contactCollection.map(contact =>
|
||||
textsecure.storage.protocol.loadIdentityKey(contact.id).then(
|
||||
(key) => {
|
||||
key => {
|
||||
lookup[contact.id] = key;
|
||||
},
|
||||
(error) => {
|
||||
error => {
|
||||
console.log(
|
||||
'getIdentityKeys error for group member',
|
||||
contact.idForLogging(),
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(() => lookup);
|
||||
},
|
||||
replay(error, message) {
|
||||
const replayable = new textsecure.ReplayableError(error);
|
||||
return replayable.replay(message.attributes).catch((e) => {
|
||||
console.log(
|
||||
'replay error:',
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
return replayable.replay(message.attributes).catch(e => {
|
||||
console.log('replay error:', e && e.stack ? e.stack : e);
|
||||
});
|
||||
},
|
||||
decryptOldIncomingKeyErrors() {
|
||||
@@ -270,19 +283,25 @@
|
||||
}
|
||||
console.log('decryptOldIncomingKeyErrors start for', this.idForLogging());
|
||||
|
||||
const messages = this.messageCollection.filter((message) => {
|
||||
const messages = this.messageCollection.filter(message => {
|
||||
const errors = message.get('errors');
|
||||
if (!errors || !errors[0]) {
|
||||
return false;
|
||||
}
|
||||
const error = _.find(errors, e => e.name === 'IncomingIdentityKeyError');
|
||||
const error = _.find(
|
||||
errors,
|
||||
e => e.name === 'IncomingIdentityKeyError'
|
||||
);
|
||||
|
||||
return Boolean(error);
|
||||
});
|
||||
|
||||
const markComplete = () => {
|
||||
console.log('decryptOldIncomingKeyErrors complete for', this.idForLogging());
|
||||
return new Promise((resolve) => {
|
||||
console.log(
|
||||
'decryptOldIncomingKeyErrors complete for',
|
||||
this.idForLogging()
|
||||
);
|
||||
return new Promise(resolve => {
|
||||
this.save({ decryptedOldIncomingKeyErrors: true }).always(resolve);
|
||||
});
|
||||
};
|
||||
@@ -296,12 +315,16 @@
|
||||
messages.length,
|
||||
'messages to process'
|
||||
);
|
||||
const safeDelete = message => new Promise((resolve) => {
|
||||
const safeDelete = message =>
|
||||
new Promise(resolve => {
|
||||
message.destroy().always(resolve);
|
||||
});
|
||||
|
||||
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 error = _.find(
|
||||
message.get('errors'),
|
||||
@@ -314,16 +337,22 @@
|
||||
}
|
||||
|
||||
if (constantTimeEqualArrayBuffers(key, error.identityKey)) {
|
||||
return this.replay(error, message).then(() => safeDelete(message));
|
||||
return this.replay(error, message).then(() =>
|
||||
safeDelete(message)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}))).catch((error) => {
|
||||
})
|
||||
)
|
||||
)
|
||||
.catch(error => {
|
||||
console.log(
|
||||
'decryptOldIncomingKeyErrors error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}).then(markComplete);
|
||||
})
|
||||
.then(markComplete);
|
||||
},
|
||||
isVerified() {
|
||||
if (this.isPrivate()) {
|
||||
@@ -333,7 +362,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.contactCollection.every((contact) => {
|
||||
return this.contactCollection.every(contact => {
|
||||
if (contact.isMe()) {
|
||||
return true;
|
||||
}
|
||||
@@ -343,14 +372,16 @@
|
||||
isUnverified() {
|
||||
if (this.isPrivate()) {
|
||||
const verified = this.get('verified');
|
||||
return verified !== this.verifiedEnum.VERIFIED &&
|
||||
verified !== this.verifiedEnum.DEFAULT;
|
||||
return (
|
||||
verified !== this.verifiedEnum.VERIFIED &&
|
||||
verified !== this.verifiedEnum.DEFAULT
|
||||
);
|
||||
}
|
||||
if (!this.contactCollection.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.contactCollection.any((contact) => {
|
||||
return this.contactCollection.any(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
@@ -363,23 +394,29 @@
|
||||
? new Backbone.Collection([this])
|
||||
: new Backbone.Collection();
|
||||
}
|
||||
return new Backbone.Collection(this.contactCollection.filter((contact) => {
|
||||
return new Backbone.Collection(
|
||||
this.contactCollection.filter(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
return contact.isUnverified();
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
setApproved() {
|
||||
if (!this.isPrivate()) {
|
||||
throw new Error('You cannot set a group conversation as trusted. ' +
|
||||
'You must set individual contacts as trusted.');
|
||||
throw new Error(
|
||||
'You cannot set a group conversation as trusted. ' +
|
||||
'You must set individual contacts as trusted.'
|
||||
);
|
||||
}
|
||||
|
||||
return textsecure.storage.protocol.setApproval(this.id, true);
|
||||
},
|
||||
safeIsUntrusted() {
|
||||
return textsecure.storage.protocol.isUntrusted(this.id).catch(() => false);
|
||||
return textsecure.storage.protocol
|
||||
.isUntrusted(this.id)
|
||||
.catch(() => false);
|
||||
},
|
||||
isUntrusted() {
|
||||
if (this.isPrivate()) {
|
||||
@@ -389,18 +426,20 @@
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return Promise.all(this.contactCollection.map((contact) => {
|
||||
return Promise.all(
|
||||
this.contactCollection.map(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
return contact.safeIsUntrusted();
|
||||
})).then(results => _.any(results, result => result));
|
||||
})
|
||||
).then(results => _.any(results, result => result));
|
||||
},
|
||||
getUntrusted() {
|
||||
// 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.
|
||||
if (this.isPrivate()) {
|
||||
return this.isUntrusted().then((untrusted) => {
|
||||
return this.isUntrusted().then(untrusted => {
|
||||
if (untrusted) {
|
||||
return new Backbone.Collection([this]);
|
||||
}
|
||||
@@ -408,20 +447,24 @@
|
||||
return new Backbone.Collection();
|
||||
});
|
||||
}
|
||||
return Promise.all(this.contactCollection.map((contact) => {
|
||||
return Promise.all(
|
||||
this.contactCollection.map(contact => {
|
||||
if (contact.isMe()) {
|
||||
return [false, 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];
|
||||
return untrusted;
|
||||
});
|
||||
return new Backbone.Collection(_.map(filtered, (result) => {
|
||||
return new Backbone.Collection(
|
||||
_.map(filtered, result => {
|
||||
const contact = result[1];
|
||||
return contact;
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
onMemberVerifiedChange() {
|
||||
@@ -461,7 +504,9 @@
|
||||
_.defaults(options, { local: true });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -488,8 +533,8 @@
|
||||
message.save().then(this.trigger.bind(this, 'newmessage', message));
|
||||
|
||||
if (this.isPrivate()) {
|
||||
ConversationController.getAllGroupsInvolvingId(id).then((groups) => {
|
||||
_.forEach(groups, (group) => {
|
||||
ConversationController.getAllGroupsInvolvingId(id).then(groups => {
|
||||
_.forEach(groups, group => {
|
||||
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
|
||||
// sync. That's a notification explosion we don't need.
|
||||
return this.queueJob(() => this.markRead(
|
||||
message.get('received_at'),
|
||||
{ sendReadReceipts: false }
|
||||
));
|
||||
return this.queueJob(() =>
|
||||
this.markRead(message.get('received_at'), { sendReadReceipts: false })
|
||||
);
|
||||
},
|
||||
|
||||
getUnread() {
|
||||
const conversationId = this.id;
|
||||
const unreadMessages = new Whisper.MessageCollection();
|
||||
return new Promise((resolve => unreadMessages.fetch({
|
||||
return new Promise(resolve =>
|
||||
unreadMessages
|
||||
.fetch({
|
||||
index: {
|
||||
// 'unread' index
|
||||
name: 'unread',
|
||||
lower: [conversationId],
|
||||
upper: [conversationId, Number.MAX_VALUE],
|
||||
},
|
||||
}).always(() => {
|
||||
})
|
||||
.always(() => {
|
||||
resolve(unreadMessages);
|
||||
})));
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
validate(attributes) {
|
||||
const required = ['id', 'type'];
|
||||
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') {
|
||||
return `Invalid conversation type: ${attributes.type}`;
|
||||
@@ -572,7 +622,12 @@
|
||||
const name = this.get('name');
|
||||
if (typeof name === 'string') {
|
||||
tokens.push(name.toLowerCase());
|
||||
tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_()+]+/));
|
||||
tokens = tokens.concat(
|
||||
name
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[\s\-_()+]+/)
|
||||
);
|
||||
}
|
||||
if (this.isPrivate()) {
|
||||
const regionCode = storage.get('regionCode');
|
||||
@@ -633,7 +688,9 @@
|
||||
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.makeVideoThumbnail(128, objectUrl);
|
||||
|
||||
@@ -661,7 +718,8 @@
|
||||
author: contact.id,
|
||||
id: quotedMessage.get('sent_at'),
|
||||
text: quotedMessage.get('body'),
|
||||
attachments: await Promise.all((attachments || []).map(async (attachment) => {
|
||||
attachments: await Promise.all(
|
||||
(attachments || []).map(async attachment => {
|
||||
const { contentType } = attachment;
|
||||
const willMakeThumbnail =
|
||||
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
|
||||
@@ -674,7 +732,8 @@
|
||||
? await this.makeThumbnailAttachment(attachment)
|
||||
: null,
|
||||
};
|
||||
})),
|
||||
})
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -721,7 +780,9 @@
|
||||
case Message.GROUP:
|
||||
return textsecure.messaging.sendMessageToGroup;
|
||||
default:
|
||||
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
|
||||
throw new TypeError(
|
||||
`Invalid conversation type: '${conversationType}'`
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -730,9 +791,11 @@
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
|
||||
const attachmentsWithData =
|
||||
await Promise.all(messageWithSchema.attachments.map(loadAttachmentData));
|
||||
message.send(sendFunction(
|
||||
const attachmentsWithData = await Promise.all(
|
||||
messageWithSchema.attachments.map(loadAttachmentData)
|
||||
);
|
||||
message.send(
|
||||
sendFunction(
|
||||
this.get('id'),
|
||||
body,
|
||||
attachmentsWithData,
|
||||
@@ -740,7 +803,8 @@
|
||||
now,
|
||||
this.get('expireTimer'),
|
||||
profileKey
|
||||
));
|
||||
)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -749,13 +813,16 @@
|
||||
await collection.fetchConversation(this.id, 1);
|
||||
const lastMessage = collection.at(0);
|
||||
|
||||
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({
|
||||
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate(
|
||||
{
|
||||
currentLastMessageText: this.get('lastMessage') || null,
|
||||
currentTimestamp: this.get('timestamp') || null,
|
||||
lastMessage: lastMessage ? lastMessage.toJSON() : null,
|
||||
lastMessageNotificationText: lastMessage
|
||||
? lastMessage.getNotificationText() : null,
|
||||
});
|
||||
? lastMessage.getNotificationText()
|
||||
: null,
|
||||
}
|
||||
);
|
||||
|
||||
this.set(lastMessageUpdate);
|
||||
|
||||
@@ -779,8 +846,10 @@
|
||||
if (!expireTimer) {
|
||||
expireTimer = null;
|
||||
}
|
||||
if (this.get('expireTimer') === expireTimer ||
|
||||
(!expireTimer && !this.get('expireTimer'))) {
|
||||
if (
|
||||
this.get('expireTimer') === expireTimer ||
|
||||
(!expireTimer && !this.get('expireTimer'))
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -881,12 +950,14 @@
|
||||
received_at: now,
|
||||
group_update: groupUpdate,
|
||||
});
|
||||
message.send(textsecure.messaging.updateGroup(
|
||||
message.send(
|
||||
textsecure.messaging.updateGroup(
|
||||
this.id,
|
||||
this.get('name'),
|
||||
this.get('avatar'),
|
||||
this.get('members')
|
||||
));
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
@@ -909,25 +980,30 @@
|
||||
_.defaults(options, { sendReadReceipts: true });
|
||||
|
||||
const conversationId = this.id;
|
||||
Whisper.Notifications.remove(Whisper.Notifications.where({
|
||||
Whisper.Notifications.remove(
|
||||
Whisper.Notifications.where({
|
||||
conversationId,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
return this.getUnread().then((providedUnreadMessages) => {
|
||||
return this.getUnread().then(providedUnreadMessages => {
|
||||
let unreadMessages = providedUnreadMessages;
|
||||
|
||||
const promises = [];
|
||||
const oldUnread = unreadMessages.filter(message =>
|
||||
message.get('received_at') <= newestUnreadDate);
|
||||
const oldUnread = unreadMessages.filter(
|
||||
message => message.get('received_at') <= newestUnreadDate
|
||||
);
|
||||
|
||||
let read = _.map(oldUnread, (providedM) => {
|
||||
let read = _.map(oldUnread, providedM => {
|
||||
let m = providedM;
|
||||
|
||||
if (this.messageCollection.get(m.id)) {
|
||||
m = this.messageCollection.get(m.id);
|
||||
} else {
|
||||
console.log('Marked a message as read in the database, but ' +
|
||||
'it was not in messageCollection.');
|
||||
console.log(
|
||||
'Marked a message as read in the database, but ' +
|
||||
'it was not in messageCollection.'
|
||||
);
|
||||
}
|
||||
promises.push(m.markRead());
|
||||
const errors = m.get('errors');
|
||||
@@ -962,7 +1038,9 @@
|
||||
if (storage.get('read-receipt-setting')) {
|
||||
_.each(_.groupBy(read, 'sender'), (receipts, sender) => {
|
||||
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) {
|
||||
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 textsecure.messaging.getProfile(id).then((profile) => {
|
||||
return textsecure.messaging
|
||||
.getProfile(id)
|
||||
.then(profile => {
|
||||
const identityKey = dcodeIO.ByteBuffer.wrap(
|
||||
profile.identityKey,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
|
||||
return textsecure.storage.protocol.saveIdentity(
|
||||
`${id}.1`,
|
||||
identityKey,
|
||||
false
|
||||
).then((changed) => {
|
||||
return textsecure.storage.protocol
|
||||
.saveIdentity(`${id}.1`, identityKey, false)
|
||||
.then(changed => {
|
||||
if (changed) {
|
||||
// save identity will close all sessions except for .1, so we
|
||||
// must close that one manually.
|
||||
@@ -1017,18 +1096,20 @@
|
||||
return sessionCipher.closeOpenSessionForDevice();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
const c = ConversationController.get(id);
|
||||
return Promise.all([
|
||||
c.setProfileName(profile.name),
|
||||
c.setProfileAvatar(profile.avatar),
|
||||
]).then(
|
||||
// success
|
||||
() => new Promise((resolve, reject) => {
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
c.save().then(resolve, reject);
|
||||
}),
|
||||
// fail
|
||||
(e) => {
|
||||
e => {
|
||||
if (e.name === 'ProfileDecryptError') {
|
||||
// probably the profile key has changed.
|
||||
console.log(
|
||||
@@ -1041,7 +1122,8 @@
|
||||
}
|
||||
);
|
||||
});
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(
|
||||
'getProfile error:',
|
||||
error && error.stack ? error.stack : error
|
||||
@@ -1056,10 +1138,15 @@
|
||||
|
||||
try {
|
||||
// decode
|
||||
const data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer();
|
||||
const data = dcodeIO.ByteBuffer.wrap(
|
||||
encryptedName,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
|
||||
// decrypt
|
||||
return textsecure.crypto.decryptProfileName(data, key).then((decrypted) => {
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(data, key)
|
||||
.then(decrypted => {
|
||||
// encode
|
||||
const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
|
||||
|
||||
@@ -1075,13 +1162,13 @@
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return textsecure.messaging.getAvatar(avatarPath).then((avatar) => {
|
||||
return textsecure.messaging.getAvatar(avatarPath).then(avatar => {
|
||||
const key = this.get('profileKey');
|
||||
if (!key) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// decrypt
|
||||
return textsecure.crypto.decryptProfile(avatar, key).then((decrypted) => {
|
||||
return textsecure.crypto.decryptProfile(avatar, key).then(decrypted => {
|
||||
// set
|
||||
this.set({
|
||||
profileAvatar: {
|
||||
@@ -1125,9 +1212,11 @@
|
||||
const first = attachments[0];
|
||||
const { thumbnail, contentType } = first;
|
||||
|
||||
return thumbnail ||
|
||||
return (
|
||||
thumbnail ||
|
||||
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
|
||||
Signal.Util.GoogleChrome.isVideoTypeSupported(contentType);
|
||||
Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)
|
||||
);
|
||||
},
|
||||
forceRender(message) {
|
||||
message.trigger('change', message);
|
||||
@@ -1163,14 +1252,18 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) {
|
||||
if (
|
||||
!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collection = new Whisper.MessageCollection();
|
||||
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) {
|
||||
return false;
|
||||
@@ -1206,8 +1299,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) {
|
||||
if (
|
||||
!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1267,7 +1362,7 @@
|
||||
async processQuotes(messages) {
|
||||
const lookup = this.makeMessagesLookup(messages);
|
||||
|
||||
const promises = messages.map(async (message) => {
|
||||
const promises = messages.map(async message => {
|
||||
const { quote } = message.attributes;
|
||||
if (!quote) {
|
||||
return;
|
||||
@@ -1350,11 +1445,16 @@
|
||||
}
|
||||
const members = this.get('members') || [];
|
||||
const promises = members.map(number =>
|
||||
ConversationController.getOrCreateAndWait(number, 'private'));
|
||||
ConversationController.getOrCreateAndWait(number, 'private')
|
||||
);
|
||||
|
||||
return Promise.all(promises).then((contacts) => {
|
||||
_.forEach(contacts, (contact) => {
|
||||
this.listenTo(contact, 'change:verified', this.onMemberVerifiedChange);
|
||||
return Promise.all(promises).then(contacts => {
|
||||
_.forEach(contacts, contact => {
|
||||
this.listenTo(
|
||||
contact,
|
||||
'change:verified',
|
||||
this.onMemberVerifiedChange
|
||||
);
|
||||
});
|
||||
|
||||
this.contactCollection.reset(contacts);
|
||||
@@ -1362,17 +1462,19 @@
|
||||
},
|
||||
|
||||
destroyMessages() {
|
||||
this.messageCollection.fetch({
|
||||
this.messageCollection
|
||||
.fetch({
|
||||
index: {
|
||||
// 'conversation' index on [conversationId, received_at]
|
||||
name: 'conversation',
|
||||
lower: [this.id],
|
||||
upper: [this.id, Number.MAX_VALUE],
|
||||
},
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
const { models } = this.messageCollection;
|
||||
this.messageCollection.reset([]);
|
||||
_.each(models, (message) => {
|
||||
_.each(models, message => {
|
||||
message.destroy();
|
||||
});
|
||||
this.save({
|
||||
@@ -1460,10 +1562,9 @@
|
||||
this.revokeAvatarUrl();
|
||||
const avatar = this.get('avatar') || this.get('profileAvatar');
|
||||
if (avatar) {
|
||||
this.avatarUrl = URL.createObjectURL(new Blob(
|
||||
[avatar.data],
|
||||
{ type: avatar.contentType }
|
||||
));
|
||||
this.avatarUrl = URL.createObjectURL(
|
||||
new Blob([avatar.data], { type: avatar.contentType })
|
||||
);
|
||||
} else {
|
||||
this.avatarUrl = null;
|
||||
}
|
||||
@@ -1507,7 +1608,7 @@
|
||||
},
|
||||
|
||||
getNotificationIcon() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const avatar = this.getAvatar();
|
||||
if (avatar.url) {
|
||||
resolve(avatar.url);
|
||||
@@ -1523,8 +1624,11 @@
|
||||
}
|
||||
const conversationId = this.id;
|
||||
|
||||
return ConversationController.getOrCreateAndWait(message.get('source'), 'private')
|
||||
.then(sender => sender.getNotificationIcon().then((iconUrl) => {
|
||||
return ConversationController.getOrCreateAndWait(
|
||||
message.get('source'),
|
||||
'private'
|
||||
).then(sender =>
|
||||
sender.getNotificationIcon().then(iconUrl => {
|
||||
console.log('adding notification');
|
||||
Whisper.Notifications.add({
|
||||
title: sender.getTitle(),
|
||||
@@ -1534,7 +1638,8 @@
|
||||
conversationId,
|
||||
messageId: message.id,
|
||||
});
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
hashCode() {
|
||||
if (this.hash === undefined) {
|
||||
@@ -1545,7 +1650,7 @@
|
||||
let hash = 0;
|
||||
for (let i = 0; i < string.length; i += 1) {
|
||||
// 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
|
||||
hash &= hash; // Convert to 32bit integer
|
||||
}
|
||||
@@ -1566,9 +1671,17 @@
|
||||
},
|
||||
|
||||
destroyAll() {
|
||||
return Promise.all(this.models.map(m => new Promise((resolve, reject) => {
|
||||
m.destroy().then(resolve).fail(reject);
|
||||
})));
|
||||
return Promise.all(
|
||||
this.models.map(
|
||||
m =>
|
||||
new Promise((resolve, reject) => {
|
||||
m
|
||||
.destroy()
|
||||
.then(resolve)
|
||||
.fail(reject);
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
search(providedQuery) {
|
||||
@@ -1578,7 +1691,7 @@
|
||||
const lastCharCode = query.charCodeAt(query.length - 1);
|
||||
const nextChar = String.fromCharCode(lastCharCode + 1);
|
||||
const upper = query.slice(0, -1) + nextChar;
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.fetch({
|
||||
index: {
|
||||
name: 'search', // 'search' index on tokens array
|
||||
@@ -1593,7 +1706,7 @@
|
||||
},
|
||||
|
||||
fetchAlphabetical() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.fetch({
|
||||
index: {
|
||||
name: 'search', // 'search' index on tokens array
|
||||
@@ -1604,7 +1717,7 @@
|
||||
},
|
||||
|
||||
fetchGroups(number) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.fetch({
|
||||
index: {
|
||||
name: 'group',
|
||||
@@ -1623,7 +1736,7 @@
|
||||
storeName: 'conversations',
|
||||
model: Whisper.Conversation,
|
||||
fetchGroups(number) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.fetch({
|
||||
index: {
|
||||
name: 'group',
|
||||
@@ -1633,4 +1746,4 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -9,7 +9,7 @@
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -32,10 +32,13 @@
|
||||
this.on('unload', this.unload);
|
||||
this.setToExpire();
|
||||
|
||||
this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
this.VOICE_FLAG =
|
||||
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
},
|
||||
idForLogging() {
|
||||
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`;
|
||||
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
|
||||
'sent_at'
|
||||
)}`;
|
||||
},
|
||||
defaults() {
|
||||
return {
|
||||
@@ -56,12 +59,13 @@
|
||||
return !!(this.get('flags') & flag);
|
||||
},
|
||||
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
|
||||
return !!(this.get('flags') & flag);
|
||||
},
|
||||
isGroupUpdate() {
|
||||
return !!(this.get('group_update'));
|
||||
return !!this.get('group_update');
|
||||
},
|
||||
isIncoming() {
|
||||
return this.get('type') === 'incoming';
|
||||
@@ -79,14 +83,14 @@
|
||||
if (options.parse === void 0) options.parse = true;
|
||||
const model = this;
|
||||
const success = options.success;
|
||||
options.success = function (resp) {
|
||||
options.success = function(resp) {
|
||||
model.attributes = {}; // this is the only changed line
|
||||
if (!model.set(model.parse(resp, options), options)) return false;
|
||||
if (success) success(model, resp, options);
|
||||
model.trigger('sync', model, resp, options);
|
||||
};
|
||||
const error = options.error;
|
||||
options.error = function (resp) {
|
||||
options.error = function(resp) {
|
||||
if (error) error(model, resp, options);
|
||||
model.trigger('error', model, resp, options);
|
||||
};
|
||||
@@ -116,7 +120,10 @@
|
||||
messages.push(i18n('titleIsNow', groupUpdate.name));
|
||||
}
|
||||
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) {
|
||||
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
|
||||
} else {
|
||||
@@ -186,7 +193,7 @@
|
||||
}
|
||||
const quote = this.get('quote');
|
||||
const attachments = (quote && quote.attachments) || [];
|
||||
attachments.forEach((attachment) => {
|
||||
attachments.forEach(attachment => {
|
||||
if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
|
||||
URL.revokeObjectURL(attachment.thumbnail.objectUrl);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -269,7 +276,8 @@
|
||||
|
||||
return {
|
||||
attachments: (quote.attachments || []).map(attachment =>
|
||||
this.processAttachment(attachment, objectUrl)),
|
||||
this.processAttachment(attachment, objectUrl)
|
||||
),
|
||||
authorColor,
|
||||
authorProfileName,
|
||||
authorTitle,
|
||||
@@ -342,7 +350,8 @@
|
||||
|
||||
send(promise) {
|
||||
this.trigger('pending');
|
||||
return promise.then((result) => {
|
||||
return promise
|
||||
.then(result => {
|
||||
const now = Date.now();
|
||||
this.trigger('done');
|
||||
if (result.dataMessage) {
|
||||
@@ -355,7 +364,8 @@
|
||||
expirationStartTimestamp: now,
|
||||
});
|
||||
this.sendSyncMessage();
|
||||
}).catch((result) => {
|
||||
})
|
||||
.catch(result => {
|
||||
const now = Date.now();
|
||||
this.trigger('done');
|
||||
if (result.dataMessage) {
|
||||
@@ -383,12 +393,14 @@
|
||||
});
|
||||
promises.push(this.sendSyncMessage());
|
||||
}
|
||||
promises = promises.concat(_.map(result.errors, (error) => {
|
||||
promises = promises.concat(
|
||||
_.map(result.errors, error => {
|
||||
if (error.name === 'OutgoingIdentityKeyError') {
|
||||
const c = ConversationController.get(error.number);
|
||||
promises.push(c.getProfiles());
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
@@ -423,12 +435,14 @@
|
||||
if (this.get('synced') || !dataMessage) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return textsecure.messaging.sendSyncMessage(
|
||||
return textsecure.messaging
|
||||
.sendSyncMessage(
|
||||
dataMessage,
|
||||
this.get('sent_at'),
|
||||
this.get('destination'),
|
||||
this.get('expirationStartTimestamp')
|
||||
).then(() => {
|
||||
)
|
||||
.then(() => {
|
||||
this.save({ synced: true, dataMessage: null });
|
||||
});
|
||||
});
|
||||
@@ -440,17 +454,19 @@
|
||||
if (!(errors instanceof Array)) {
|
||||
errors = [errors];
|
||||
}
|
||||
errors.forEach((e) => {
|
||||
errors.forEach(e => {
|
||||
console.log(
|
||||
'Message.saveErrors:',
|
||||
e && e.reason ? e.reason : null,
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
});
|
||||
errors = errors.map((e) => {
|
||||
if (e.constructor === Error ||
|
||||
errors = errors.map(e => {
|
||||
if (
|
||||
e.constructor === Error ||
|
||||
e.constructor === TypeError ||
|
||||
e.constructor === ReferenceError) {
|
||||
e.constructor === ReferenceError
|
||||
) {
|
||||
return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
|
||||
}
|
||||
return e;
|
||||
@@ -463,17 +479,19 @@
|
||||
hasNetworkError() {
|
||||
const error = _.find(
|
||||
this.get('errors'),
|
||||
e => (e.name === 'MessageError' ||
|
||||
e =>
|
||||
e.name === 'MessageError' ||
|
||||
e.name === 'OutgoingMessageError' ||
|
||||
e.name === 'SendMessageNetworkError' ||
|
||||
e.name === 'SignedPreKeyRotationError')
|
||||
e.name === 'SignedPreKeyRotationError'
|
||||
);
|
||||
return !!error;
|
||||
},
|
||||
removeOutgoingErrors(number) {
|
||||
const errors = _.partition(
|
||||
this.get('errors'),
|
||||
e => e.number === number &&
|
||||
e =>
|
||||
e.number === number &&
|
||||
(e.name === 'MessageError' ||
|
||||
e.name === 'OutgoingMessageError' ||
|
||||
e.name === 'SendMessageNetworkError' ||
|
||||
@@ -484,11 +502,13 @@
|
||||
return errors[0][0];
|
||||
},
|
||||
isReplayableError(e) {
|
||||
return (e.name === 'MessageError' ||
|
||||
return (
|
||||
e.name === 'MessageError' ||
|
||||
e.name === 'OutgoingMessageError' ||
|
||||
e.name === 'SendMessageNetworkError' ||
|
||||
e.name === 'SignedPreKeyRotationError' ||
|
||||
e.name === 'OutgoingIdentityKeyError');
|
||||
e.name === 'OutgoingIdentityKeyError'
|
||||
);
|
||||
},
|
||||
resend(number) {
|
||||
const error = this.removeOutgoingErrors(number);
|
||||
@@ -513,7 +533,9 @@
|
||||
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
|
||||
|
||||
const conversation = ConversationController.get(conversationId);
|
||||
return conversation.queueJob(() => new Promise((resolve) => {
|
||||
return conversation.queueJob(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
const now = new Date().getTime();
|
||||
let attributes = { type: 'private' };
|
||||
if (dataMessage.group) {
|
||||
@@ -528,13 +550,15 @@
|
||||
groupId: dataMessage.group.id,
|
||||
name: dataMessage.group.name,
|
||||
avatar: dataMessage.group.avatar,
|
||||
members: _.union(dataMessage.group.members, conversation.get('members')),
|
||||
members: _.union(
|
||||
dataMessage.group.members,
|
||||
conversation.get('members')
|
||||
),
|
||||
};
|
||||
groupUpdate = conversation.changedAttributes(_.pick(
|
||||
dataMessage.group,
|
||||
'name',
|
||||
'avatar'
|
||||
)) || {};
|
||||
groupUpdate =
|
||||
conversation.changedAttributes(
|
||||
_.pick(dataMessage.group, 'name', 'avatar')
|
||||
) || {};
|
||||
const difference = _.difference(
|
||||
attributes.members,
|
||||
conversation.get('members')
|
||||
@@ -553,7 +577,10 @@
|
||||
} else {
|
||||
groupUpdate = { left: source };
|
||||
}
|
||||
attributes.members = _.without(conversation.get('members'), source);
|
||||
attributes.members = _.without(
|
||||
conversation.get('members'),
|
||||
source
|
||||
);
|
||||
}
|
||||
|
||||
if (groupUpdate !== null) {
|
||||
@@ -574,10 +601,15 @@
|
||||
schemaVersion: dataMessage.schemaVersion,
|
||||
});
|
||||
if (type === 'outgoing') {
|
||||
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message);
|
||||
receipts.forEach(() => message.set({
|
||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||
conversation,
|
||||
message
|
||||
);
|
||||
receipts.forEach(() =>
|
||||
message.set({
|
||||
delivered: (message.get('delivered') || 0) + 1,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
attributes.active_at = now;
|
||||
conversation.set(attributes);
|
||||
@@ -611,15 +643,19 @@
|
||||
|
||||
if (!message.isEndSession() && !message.isGroupUpdate()) {
|
||||
if (dataMessage.expireTimer) {
|
||||
if (dataMessage.expireTimer !== conversation.get('expireTimer')) {
|
||||
if (
|
||||
dataMessage.expireTimer !== conversation.get('expireTimer')
|
||||
) {
|
||||
conversation.updateExpirationTimer(
|
||||
dataMessage.expireTimer, source,
|
||||
dataMessage.expireTimer,
|
||||
source,
|
||||
message.get('received_at')
|
||||
);
|
||||
}
|
||||
} else if (conversation.get('expireTimer')) {
|
||||
conversation.updateExpirationTimer(
|
||||
null, source,
|
||||
null,
|
||||
source,
|
||||
message.get('received_at')
|
||||
);
|
||||
}
|
||||
@@ -627,23 +663,35 @@
|
||||
if (type === 'incoming') {
|
||||
const readSync = Whisper.ReadSyncs.forMessage(message);
|
||||
if (readSync) {
|
||||
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) {
|
||||
message.set('expirationStartTimestamp', readSync.get('read_at'));
|
||||
if (
|
||||
message.get('expireTimer') &&
|
||||
!message.get('expirationStartTimestamp')
|
||||
) {
|
||||
message.set(
|
||||
'expirationStartTimestamp',
|
||||
readSync.get('read_at')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (readSync || message.isExpirationTimerUpdate()) {
|
||||
message.unset('unread');
|
||||
// This is primarily to allow the conversation to mark all older messages as
|
||||
// read, as is done when we receive a read sync for a message we already
|
||||
// know about.
|
||||
// This is primarily to allow the conversation to mark all older
|
||||
// messages as read, as is done when we receive a read sync for
|
||||
// a message we already know about.
|
||||
Whisper.ReadSyncs.notifyConversation(message);
|
||||
} else {
|
||||
conversation.set('unreadCount', conversation.get('unreadCount') + 1);
|
||||
conversation.set(
|
||||
'unreadCount',
|
||||
conversation.get('unreadCount') + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'outgoing') {
|
||||
const reads = Whisper.ReadReceipts.forMessage(conversation, message);
|
||||
const reads = Whisper.ReadReceipts.forMessage(
|
||||
conversation,
|
||||
message
|
||||
);
|
||||
if (reads.length) {
|
||||
const readBy = reads.map(receipt => receipt.get('reader'));
|
||||
message.set({
|
||||
@@ -655,7 +703,10 @@
|
||||
}
|
||||
|
||||
const conversationTimestamp = conversation.get('timestamp');
|
||||
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) {
|
||||
if (
|
||||
!conversationTimestamp ||
|
||||
message.get('sent_at') > conversationTimestamp
|
||||
) {
|
||||
conversation.set({
|
||||
lastMessage: message.getNotificationText(),
|
||||
timestamp: message.get('sent_at'),
|
||||
@@ -672,15 +723,20 @@
|
||||
ConversationController.getOrCreateAndWait(
|
||||
source,
|
||||
'private'
|
||||
).then((sender) => {
|
||||
).then(sender => {
|
||||
sender.setProfileKey(profileKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (error) => {
|
||||
const handleError = 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();
|
||||
};
|
||||
|
||||
@@ -691,17 +747,22 @@
|
||||
} catch (e) {
|
||||
return handleError(e);
|
||||
}
|
||||
// We fetch() here because, between the message.save() above and the previous
|
||||
// line's trigger() call, we might have marked all messages unread in the
|
||||
// database. This message might already be read!
|
||||
// We fetch() here because, between the message.save() above and
|
||||
// the previous line's trigger() call, we might have marked all
|
||||
// messages unread in the database. This message might already
|
||||
// be read!
|
||||
const previousUnread = message.get('unread');
|
||||
return message.fetch().then(() => {
|
||||
return message.fetch().then(
|
||||
() => {
|
||||
try {
|
||||
if (previousUnread !== message.get('unread')) {
|
||||
console.log('Caught race condition on new message read state! ' +
|
||||
'Manually starting timers.');
|
||||
// We call markRead() even though the message is already marked read
|
||||
// because we need to start expiration timers, etc.
|
||||
console.log(
|
||||
'Caught race condition on new message read state! ' +
|
||||
'Manually starting timers.'
|
||||
);
|
||||
// We call markRead() even though the message is already
|
||||
// marked read because we need to start expiration
|
||||
// timers, etc.
|
||||
message.markRead();
|
||||
}
|
||||
|
||||
@@ -717,7 +778,8 @@
|
||||
} catch (e) {
|
||||
return handleError(e);
|
||||
}
|
||||
}, () => {
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
console.log(
|
||||
'handleDataMessage: Message',
|
||||
@@ -730,19 +792,23 @@
|
||||
} catch (e) {
|
||||
return handleError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}, handleError);
|
||||
}, handleError);
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
markRead(readAt) {
|
||||
this.unset('unread');
|
||||
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
|
||||
this.set('expirationStartTimestamp', readAt || Date.now());
|
||||
}
|
||||
Whisper.Notifications.remove(Whisper.Notifications.where({
|
||||
Whisper.Notifications.remove(
|
||||
Whisper.Notifications.where({
|
||||
messageId: this.id,
|
||||
}));
|
||||
})
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.save().then(resolve, reject);
|
||||
});
|
||||
@@ -760,7 +826,7 @@
|
||||
const now = Date.now();
|
||||
const start = this.get('expirationStartTimestamp');
|
||||
const delta = this.get('expireTimer') * 1000;
|
||||
let msFromNow = (start + delta) - now;
|
||||
let msFromNow = start + delta - now;
|
||||
if (msFromNow < 0) {
|
||||
msFromNow = 0;
|
||||
}
|
||||
@@ -784,7 +850,6 @@
|
||||
console.log('message', this.get('sent_at'), 'expires at', expiresAt);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Whisper.MessageCollection = Backbone.Collection.extend({
|
||||
@@ -804,19 +869,29 @@
|
||||
}
|
||||
},
|
||||
destroyAll() {
|
||||
return Promise.all(this.models.map(m => new Promise((resolve, reject) => {
|
||||
m.destroy().then(resolve).fail(reject);
|
||||
})));
|
||||
return Promise.all(
|
||||
this.models.map(
|
||||
m =>
|
||||
new Promise((resolve, reject) => {
|
||||
m
|
||||
.destroy()
|
||||
.then(resolve)
|
||||
.fail(reject);
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
fetchSentAt(timestamp) {
|
||||
return new Promise((resolve => this.fetch({
|
||||
return new Promise(resolve =>
|
||||
this.fetch({
|
||||
index: {
|
||||
// 'receipt' index on sent_at
|
||||
name: 'receipt',
|
||||
only: timestamp,
|
||||
},
|
||||
}).always(resolve)));
|
||||
}).always(resolve)
|
||||
);
|
||||
},
|
||||
|
||||
getLoadedUnreadCount() {
|
||||
@@ -841,7 +916,7 @@
|
||||
if (unreadCount > 0) {
|
||||
startingLoadedUnread = this.getLoadedUnreadCount();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
let upper;
|
||||
if (this.length === 0) {
|
||||
// fetch the most recent messages first
|
||||
@@ -893,4 +968,4 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -20,7 +20,9 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(fileOrBlobOrURL, (canvasOrError) => {
|
||||
loadImage(
|
||||
fileOrBlobOrURL,
|
||||
canvasOrError => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.cause = canvasOrError;
|
||||
@@ -35,6 +37,8 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
||||
);
|
||||
|
||||
resolve(dataURL);
|
||||
}, optionsWithDefaults);
|
||||
},
|
||||
optionsWithDefaults
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@@ -23,12 +23,7 @@ const electronRemote = require('electron').remote;
|
||||
const Attachment = require('./types/attachment');
|
||||
const crypto = require('./crypto');
|
||||
|
||||
|
||||
const {
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
} = electronRemote;
|
||||
|
||||
const { dialog, BrowserWindow } = electronRemote;
|
||||
|
||||
module.exports = {
|
||||
getDirectoryForExport,
|
||||
@@ -44,7 +39,6 @@ module.exports = {
|
||||
_getConversationLoggingName,
|
||||
};
|
||||
|
||||
|
||||
function stringify(object) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in object) {
|
||||
@@ -69,10 +63,12 @@ function unstringify(object) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in object) {
|
||||
const val = object[key];
|
||||
if (val &&
|
||||
if (
|
||||
val &&
|
||||
val.type === 'ArrayBuffer' &&
|
||||
val.encoding === 'base64' &&
|
||||
typeof val.data === 'string') {
|
||||
typeof val.data === 'string'
|
||||
) {
|
||||
object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
|
||||
} else if (val instanceof Object) {
|
||||
object[key] = unstringify(object[key]);
|
||||
@@ -86,7 +82,9 @@ function createOutputStream(writer) {
|
||||
return {
|
||||
write(string) {
|
||||
// eslint-disable-next-line more/no-then
|
||||
wait = wait.then(() => new Promise((resolve) => {
|
||||
wait = wait.then(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
if (writer.write(string)) {
|
||||
resolve();
|
||||
return;
|
||||
@@ -98,7 +96,8 @@ function createOutputStream(writer) {
|
||||
|
||||
// We don't register for the 'error' event here, only in close(). Otherwise,
|
||||
// we'll get "Possible EventEmitter memory leak detected" warnings.
|
||||
}));
|
||||
})
|
||||
);
|
||||
return wait;
|
||||
},
|
||||
async close() {
|
||||
@@ -141,7 +140,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
||||
|
||||
stream.write('{');
|
||||
|
||||
_.each(storeNames, (storeName) => {
|
||||
_.each(storeNames, storeName => {
|
||||
// Both the readwrite permission and the multi-store transaction are required to
|
||||
// keep this function working. They serve to serialize all of these transactions,
|
||||
// one per store to be exported.
|
||||
@@ -167,7 +166,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
if (count === 0) {
|
||||
console.log('cursor opened');
|
||||
stream.write(`"${storeName}": [`);
|
||||
@@ -180,10 +179,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
||||
}
|
||||
|
||||
// Preventing base64'd images from reaching the disk, making db.json too big
|
||||
const item = _.omit(
|
||||
cursor.value,
|
||||
['avatar', 'profileAvatar']
|
||||
);
|
||||
const item = _.omit(cursor.value, ['avatar', 'profileAvatar']);
|
||||
|
||||
const jsonString = JSON.stringify(stringify(item));
|
||||
stream.write(jsonString);
|
||||
@@ -235,10 +231,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
||||
groupLookup: {},
|
||||
});
|
||||
|
||||
const {
|
||||
conversationLookup,
|
||||
groupLookup,
|
||||
} = options;
|
||||
const { conversationLookup, groupLookup } = options;
|
||||
const result = {
|
||||
fullImport: true,
|
||||
};
|
||||
@@ -269,7 +262,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
||||
console.log('Importing to these stores:', storeNames.join(', '));
|
||||
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('non-messages import done via', via);
|
||||
if (finished) {
|
||||
resolve(result);
|
||||
@@ -287,7 +280,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
||||
};
|
||||
transaction.oncomplete = finish.bind(null, 'transaction complete');
|
||||
|
||||
_.each(storeNames, (storeName) => {
|
||||
_.each(storeNames, storeName => {
|
||||
console.log('Importing items for store', storeName);
|
||||
|
||||
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);
|
||||
|
||||
const haveConversationAlready =
|
||||
@@ -365,7 +358,7 @@ function createDirectory(parent, name) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdir(targetDir, (error) => {
|
||||
fs.mkdir(targetDir, error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
@@ -377,7 +370,7 @@ function createDirectory(parent, name) {
|
||||
}
|
||||
|
||||
function createFileAndWriter(parent, name) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const sanitized = _sanitizeFileName(name);
|
||||
const targetPath = path.join(parent, sanitized);
|
||||
const options = {
|
||||
@@ -430,7 +423,6 @@ function _trimFileName(filename) {
|
||||
return `${name.join('.').slice(0, 24)}.${extension}`;
|
||||
}
|
||||
|
||||
|
||||
function _getExportAttachmentFileName(message, index, attachment) {
|
||||
if (attachment.fileName) {
|
||||
return _trimFileName(attachment.fileName);
|
||||
@@ -440,7 +432,9 @@ function _getExportAttachmentFileName(message, index, attachment) {
|
||||
|
||||
if (attachment.contentType) {
|
||||
const components = attachment.contentType.split('/');
|
||||
name += `.${components.length > 1 ? components[1] : attachment.contentType}`;
|
||||
name += `.${
|
||||
components.length > 1 ? components[1] : attachment.contentType
|
||||
}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
@@ -477,14 +471,11 @@ async function readAttachment(dir, attachment, name, options) {
|
||||
}
|
||||
|
||||
async function writeThumbnail(attachment, options) {
|
||||
const {
|
||||
dir,
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = `${_getAnonymousAttachmentFileName(
|
||||
message,
|
||||
index,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const filename = `${_getAnonymousAttachmentFileName(message, index)}-thumbnail`;
|
||||
index
|
||||
)}-thumbnail`;
|
||||
const target = path.join(dir, filename);
|
||||
const { thumbnail } = attachment;
|
||||
|
||||
@@ -504,26 +495,28 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
||||
const { name } = options;
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const promises = rawQuotedAttachments.map(async (attachment) => {
|
||||
const promises = rawQuotedAttachments.map(async attachment => {
|
||||
if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
attachment,
|
||||
{ thumbnail: await loadAttachmentData(attachment.thumbnail) }
|
||||
);
|
||||
return Object.assign({}, attachment, {
|
||||
thumbnail: await loadAttachmentData(attachment.thumbnail),
|
||||
});
|
||||
});
|
||||
|
||||
const attachments = await Promise.all(promises);
|
||||
try {
|
||||
await Promise.all(_.map(
|
||||
attachments,
|
||||
(attachment, index) => writeThumbnail(attachment, Object.assign({}, options, {
|
||||
await Promise.all(
|
||||
_.map(attachments, (attachment, index) =>
|
||||
writeThumbnail(
|
||||
attachment,
|
||||
Object.assign({}, options, {
|
||||
index,
|
||||
}))
|
||||
));
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'writeThumbnails: error exporting conversation',
|
||||
@@ -536,13 +529,7 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
||||
}
|
||||
|
||||
async function writeAttachment(attachment, options) {
|
||||
const {
|
||||
dir,
|
||||
message,
|
||||
index,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = _getAnonymousAttachmentFileName(message, index);
|
||||
const target = path.join(dir, filename);
|
||||
if (!Attachment.hasData(attachment)) {
|
||||
@@ -562,11 +549,13 @@ async function writeAttachments(rawAttachments, options) {
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const attachments = await Promise.all(rawAttachments.map(loadAttachmentData));
|
||||
const promises = _.map(
|
||||
attachments,
|
||||
(attachment, index) => writeAttachment(attachment, Object.assign({}, options, {
|
||||
const promises = _.map(attachments, (attachment, index) =>
|
||||
writeAttachment(
|
||||
attachment,
|
||||
Object.assign({}, options, {
|
||||
index,
|
||||
}))
|
||||
})
|
||||
)
|
||||
);
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
@@ -582,12 +571,7 @@ async function writeAttachments(rawAttachments, options) {
|
||||
}
|
||||
|
||||
async function writeEncryptedAttachment(target, data, options = {}) {
|
||||
const {
|
||||
key,
|
||||
newKey,
|
||||
filename,
|
||||
dir,
|
||||
} = options;
|
||||
const { key, newKey, filename, dir } = options;
|
||||
|
||||
if (fs.existsSync(target)) {
|
||||
if (newKey) {
|
||||
@@ -613,13 +597,7 @@ function _sanitizeFileName(filename) {
|
||||
|
||||
async function exportConversation(db, conversation, options) {
|
||||
options = options || {};
|
||||
const {
|
||||
name,
|
||||
dir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { name, dir, attachmentsDir, key, newKey } = options;
|
||||
if (!name) {
|
||||
throw new Error('Need a name!');
|
||||
}
|
||||
@@ -670,7 +648,7 @@ async function exportConversation(db, conversation, options) {
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
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
|
||||
// Note: this is for legacy messages only, which stored attachment data in the db
|
||||
message.attachments = _.map(
|
||||
attachments,
|
||||
attachment => _.omit(attachment, ['data'])
|
||||
message.attachments = _.map(attachments, attachment =>
|
||||
_.omit(attachment, ['data'])
|
||||
);
|
||||
// completely drop any attachments in messages cached in error objects
|
||||
// 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) {
|
||||
error.args = [];
|
||||
}
|
||||
@@ -709,7 +686,8 @@ async function exportConversation(db, conversation, options) {
|
||||
|
||||
console.log({ backupMessage: message });
|
||||
if (attachments && attachments.length > 0) {
|
||||
const exportAttachments = () => writeAttachments(attachments, {
|
||||
const exportAttachments = () =>
|
||||
writeAttachments(attachments, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
@@ -723,7 +701,8 @@ async function exportConversation(db, conversation, options) {
|
||||
|
||||
const quoteThumbnails = message.quote && message.quote.attachments;
|
||||
if (quoteThumbnails && quoteThumbnails.length > 0) {
|
||||
const exportQuoteThumbnails = () => writeThumbnails(quoteThumbnails, {
|
||||
const exportQuoteThumbnails = () =>
|
||||
writeThumbnails(quoteThumbnails, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
@@ -739,11 +718,7 @@ async function exportConversation(db, conversation, options) {
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
await Promise.all([
|
||||
stream.write(']}'),
|
||||
promiseChain,
|
||||
stream.close(),
|
||||
]);
|
||||
await Promise.all([stream.write(']}'), promiseChain, stream.close()]);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'exportConversation: error exporting conversation',
|
||||
@@ -791,12 +766,7 @@ function _getConversationLoggingName(conversation) {
|
||||
|
||||
function exportConversations(db, options) {
|
||||
options = options || {};
|
||||
const {
|
||||
messagesDir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { messagesDir, attachmentsDir, key, newKey } = options;
|
||||
|
||||
if (!messagesDir) {
|
||||
return Promise.reject(new Error('Need a messages directory!'));
|
||||
@@ -828,7 +798,7 @@ function exportConversations(db, options) {
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
const conversation = cursor.value;
|
||||
@@ -873,7 +843,7 @@ function getDirectory(options) {
|
||||
buttonLabel: options.buttonLabel,
|
||||
};
|
||||
|
||||
dialog.showOpenDialog(browserWindow, dialogOptions, (directory) => {
|
||||
dialog.showOpenDialog(browserWindow, dialogOptions, directory => {
|
||||
if (!directory || !directory[0]) {
|
||||
const error = new Error('Error choosing directory');
|
||||
error.name = 'ChooseError';
|
||||
@@ -940,7 +910,7 @@ async function saveAllMessages(db, rawMessages) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('messages done saving via', via);
|
||||
if (finished) {
|
||||
resolve();
|
||||
@@ -962,7 +932,7 @@ async function saveAllMessages(db, rawMessages) {
|
||||
const { conversationId } = messages[0];
|
||||
let count = 0;
|
||||
|
||||
_.forEach(messages, (message) => {
|
||||
_.forEach(messages, message => {
|
||||
const request = store.put(message, message.id);
|
||||
request.onsuccess = () => {
|
||||
count += 1;
|
||||
@@ -997,11 +967,7 @@ async function importConversation(db, dir, options) {
|
||||
options = options || {};
|
||||
_.defaults(options, { messageLookup: {} });
|
||||
|
||||
const {
|
||||
messageLookup,
|
||||
attachmentsDir,
|
||||
key,
|
||||
} = options;
|
||||
const { messageLookup, attachmentsDir, key } = options;
|
||||
|
||||
let conversationId = 'unknown';
|
||||
let total = 0;
|
||||
@@ -1018,11 +984,13 @@ async function importConversation(db, dir, options) {
|
||||
|
||||
const json = JSON.parse(contents);
|
||||
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;
|
||||
|
||||
const messages = _.filter(json.messages, (message) => {
|
||||
const messages = _.filter(json.messages, message => {
|
||||
message = unstringify(message);
|
||||
|
||||
if (messageLookup[getMessageKey(message)]) {
|
||||
@@ -1031,7 +999,9 @@ async function importConversation(db, dir, options) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (hasAttachments || hasQuotedAttachments) {
|
||||
@@ -1039,8 +1009,8 @@ async function importConversation(db, dir, options) {
|
||||
const getName = attachmentsDir
|
||||
? _getAnonymousAttachmentFileName
|
||||
: _getExportAttachmentFileName;
|
||||
const parentDir = attachmentsDir ||
|
||||
path.join(dir, message.received_at.toString());
|
||||
const parentDir =
|
||||
attachmentsDir || path.join(dir, message.received_at.toString());
|
||||
|
||||
await loadAttachments(parentDir, getName, {
|
||||
message,
|
||||
@@ -1075,12 +1045,13 @@ async function importConversations(db, dir, options) {
|
||||
const contents = await getDirContents(dir);
|
||||
let promiseChain = Promise.resolve();
|
||||
|
||||
_.forEach(contents, (conversationDir) => {
|
||||
_.forEach(contents, conversationDir => {
|
||||
if (!fs.statSync(conversationDir).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadConversation = () => importConversation(db, conversationDir, options);
|
||||
const loadConversation = () =>
|
||||
importConversation(db, conversationDir, options);
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(loadConversation);
|
||||
@@ -1142,7 +1113,7 @@ function assembleLookup(db, storeName, keyFunction) {
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
lookup[keyFunction(cursor.value)] = true;
|
||||
@@ -1175,7 +1146,7 @@ function createZip(zipDir, targetDir) {
|
||||
resolve(target);
|
||||
});
|
||||
|
||||
archive.on('warning', (error) => {
|
||||
archive.on('warning', error => {
|
||||
console.log(`Archive generation warning: ${error.stack}`);
|
||||
});
|
||||
archive.on('error', reject);
|
||||
@@ -1247,10 +1218,13 @@ async function exportToDirectory(directory, options) {
|
||||
const attachmentsDir = await createDirectory(directory, 'attachments');
|
||||
|
||||
await exportContactAndGroupsToFile(db, stagingDir);
|
||||
await exportConversations(db, Object.assign({}, options, {
|
||||
await exportConversations(
|
||||
db,
|
||||
Object.assign({}, options, {
|
||||
messagesDir: stagingDir,
|
||||
attachmentsDir,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const zip = await createZip(encryptionDir, stagingDir);
|
||||
await encryptFile(zip, path.join(directory, 'messages.zip'), options);
|
||||
@@ -1302,7 +1276,9 @@ async function importFromDirectory(directory, options) {
|
||||
if (fs.existsSync(zipPath)) {
|
||||
// we're in the world of an encrypted, zipped backup
|
||||
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;
|
||||
|
@@ -19,8 +19,15 @@ async function encryptSymmetric(key, plaintext) {
|
||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext);
|
||||
const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH);
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
||||
cipherKey,
|
||||
iv,
|
||||
plaintext
|
||||
);
|
||||
const mac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
|
||||
return _concatData([nonce, cipherText, mac]);
|
||||
}
|
||||
@@ -39,9 +46,14 @@ async function decryptSymmetric(key, data) {
|
||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
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)) {
|
||||
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);
|
||||
@@ -61,7 +73,6 @@ function constantTimeEqual(left, right) {
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
|
||||
async function _hmac_SHA256(key, data) {
|
||||
const extractable = false;
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
@@ -72,7 +83,11 @@ async function _hmac_SHA256(key, data) {
|
||||
['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) {
|
||||
@@ -101,7 +116,6 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
||||
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
||||
}
|
||||
|
||||
|
||||
function _getRandomBytes(n) {
|
||||
const bytes = new Uint8Array(n);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
|
@@ -6,14 +6,12 @@
|
||||
|
||||
const { isObject, isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
||||
const request = indexedDB.open(name, version);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onblocked = () =>
|
||||
reject(new Error('Database blocked'));
|
||||
request.onblocked = () => reject(new Error('Database blocked'));
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
request.onupgradeneeded = event => {
|
||||
const hasRequestedSpecificVersion = isNumber(version);
|
||||
if (!hasRequestedSpecificVersion) {
|
||||
return;
|
||||
@@ -26,14 +24,17 @@ exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`));
|
||||
reject(
|
||||
new Error(
|
||||
'Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const connection = event.target.result;
|
||||
resolve(connection);
|
||||
};
|
||||
@@ -47,7 +48,7 @@ exports.completeTransaction = transaction =>
|
||||
transaction.addEventListener('complete', () => resolve());
|
||||
});
|
||||
|
||||
exports.getVersion = async (name) => {
|
||||
exports.getVersion = async name => {
|
||||
const connection = await exports.open(name);
|
||||
const { version } = connection;
|
||||
connection.close();
|
||||
@@ -61,9 +62,7 @@ exports.getCount = async ({ store } = {}) => {
|
||||
|
||||
const request = store.count();
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onsuccess = event =>
|
||||
resolve(event.target.result);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
request.onsuccess = event => resolve(event.target.result);
|
||||
});
|
||||
};
|
||||
|
@@ -18,7 +18,6 @@ const Message = require('./types/message');
|
||||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
const { sleep } = require('./sleep');
|
||||
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan
|
||||
const SENDER_ID = '+12126647665';
|
||||
|
||||
@@ -27,8 +26,10 @@ exports.createConversation = async ({
|
||||
numMessages,
|
||||
WhisperMessage,
|
||||
} = {}) => {
|
||||
if (!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)) {
|
||||
if (
|
||||
!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)
|
||||
) {
|
||||
throw new TypeError("'ConversationController' is required");
|
||||
}
|
||||
|
||||
@@ -40,8 +41,10 @@ exports.createConversation = async ({
|
||||
throw new TypeError("'WhisperMessage' is required");
|
||||
}
|
||||
|
||||
const conversation =
|
||||
await ConversationController.getOrCreateAndWait(SENDER_ID, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
SENDER_ID,
|
||||
'private'
|
||||
);
|
||||
conversation.set({
|
||||
active_at: Date.now(),
|
||||
unread: numMessages,
|
||||
@@ -50,13 +53,15 @@ exports.createConversation = async ({
|
||||
|
||||
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);
|
||||
console.log(`Create message ${index + 1}`);
|
||||
const messageAttributes = await createRandomMessage({ conversationId });
|
||||
const message = new WhisperMessage(messageAttributes);
|
||||
return deferredToPromise(message.save());
|
||||
}));
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const SAMPLE_MESSAGES = [
|
||||
@@ -88,7 +93,8 @@ const createRandomMessage = async ({ conversationId } = {}) => {
|
||||
|
||||
const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE;
|
||||
const attachments = hasAttachment
|
||||
? [await createRandomInMemoryAttachment()] : [];
|
||||
? [await createRandomInMemoryAttachment()]
|
||||
: [];
|
||||
const type = sample(['incoming', 'outgoing']);
|
||||
const commonProperties = {
|
||||
attachments,
|
||||
@@ -145,7 +151,7 @@ const createFileEntry = fileName => ({
|
||||
fileName,
|
||||
contentType: fileNameToContentType(fileName),
|
||||
});
|
||||
const fileNameToContentType = (fileName) => {
|
||||
const fileNameToContentType = fileName => {
|
||||
const fileExtension = path.extname(fileName).toLowerCase();
|
||||
switch (fileExtension) {
|
||||
case '.gif':
|
||||
|
@@ -3,7 +3,6 @@
|
||||
const FormData = require('form-data');
|
||||
const got = require('got');
|
||||
|
||||
|
||||
const BASE_URL = 'https://debuglogs.org';
|
||||
|
||||
// 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
|
||||
const submitFormData = (form, url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
form.submit(url, (error) => {
|
||||
form.submit(url, error => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
@@ -22,7 +21,7 @@ const submitFormData = (form, url) =>
|
||||
});
|
||||
|
||||
// upload :: String -> Promise URL
|
||||
exports.upload = async (content) => {
|
||||
exports.upload = async content => {
|
||||
const signedForm = await got.get(BASE_URL, { json: true });
|
||||
const { fields, url } = signedForm.body;
|
||||
|
||||
|
@@ -2,11 +2,10 @@ const addUnhandledErrorHandler = require('electron-unhandled');
|
||||
|
||||
const Errors = require('./types/errors');
|
||||
|
||||
|
||||
// addHandler :: Unit -> Unit
|
||||
exports.addHandler = () => {
|
||||
addUnhandledErrorHandler({
|
||||
logger: (error) => {
|
||||
logger: error => {
|
||||
console.error(
|
||||
'Uncaught error or unhandled promise rejection:',
|
||||
Errors.toLogFormat(error)
|
||||
|
@@ -11,7 +11,9 @@ exports.setup = (locale, messages) => {
|
||||
function getMessage(key, substitutions) {
|
||||
const entry = messages[key];
|
||||
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 '';
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
|
||||
const POLL_INTERVAL_MS = 5 * 1000;
|
||||
const IDLE_THRESHOLD_MS = 20;
|
||||
|
||||
@@ -35,14 +34,17 @@ class IdleDetector extends EventEmitter {
|
||||
|
||||
_scheduleNextCallback() {
|
||||
this._clearScheduledCallbacks();
|
||||
this.handle = window.requestIdleCallback((deadline) => {
|
||||
this.handle = window.requestIdleCallback(deadline => {
|
||||
const { didTimeout } = deadline;
|
||||
const timeRemaining = deadline.timeRemaining();
|
||||
const isIdle = timeRemaining >= IDLE_THRESHOLD_MS;
|
||||
if (isIdle || didTimeout) {
|
||||
this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining });
|
||||
}
|
||||
this.timeoutId = setTimeout(() => this._scheduleNextCallback(), POLL_INTERVAL_MS);
|
||||
this.timeoutId = setTimeout(
|
||||
() => this._scheduleNextCallback(),
|
||||
POLL_INTERVAL_MS
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ function createLink(url, text, attrs = {}) {
|
||||
const html = [];
|
||||
html.push('<a ');
|
||||
html.push(`href="${url}"`);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
Object.keys(attrs).forEach(key => {
|
||||
html.push(` ${key}="${attrs[key]}"`);
|
||||
});
|
||||
html.push('>');
|
||||
@@ -23,7 +23,7 @@ module.exports = (text, attrs = {}) => {
|
||||
const result = [];
|
||||
let last = 0;
|
||||
|
||||
matchData.forEach((match) => {
|
||||
matchData.forEach(match => {
|
||||
if (last < match.index) {
|
||||
result.push(text.slice(last, match.index));
|
||||
}
|
||||
|
@@ -6,20 +6,13 @@
|
||||
|
||||
/* global IDBKeyRange */
|
||||
|
||||
const {
|
||||
isFunction,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
const { isFunction, isNumber, isObject, isString, last } = require('lodash');
|
||||
|
||||
const database = require('./database');
|
||||
const Message = require('./types/message');
|
||||
const settings = require('./settings');
|
||||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
|
||||
|
||||
const MESSAGES_STORE_NAME = 'messages';
|
||||
|
||||
exports.processNext = async ({
|
||||
@@ -29,12 +22,16 @@ exports.processNext = async ({
|
||||
upgradeMessageSchema,
|
||||
} = {}) => {
|
||||
if (!isFunction(BackboneMessage)) {
|
||||
throw new TypeError("'BackboneMessage' (Whisper.Message) constructor is required");
|
||||
throw new TypeError(
|
||||
"'BackboneMessage' (Whisper.Message) constructor is required"
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required');
|
||||
throw new TypeError(
|
||||
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNumber(numMessagesPerBatch)) {
|
||||
@@ -48,16 +45,18 @@ exports.processNext = async ({
|
||||
const startTime = Date.now();
|
||||
|
||||
const fetchStartTime = Date.now();
|
||||
const messagesRequiringSchemaUpgrade =
|
||||
await _fetchMessagesRequiringSchemaUpgrade({
|
||||
const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade(
|
||||
{
|
||||
BackboneMessageCollection,
|
||||
count: numMessagesPerBatch,
|
||||
});
|
||||
}
|
||||
);
|
||||
const fetchDuration = Date.now() - fetchStartTime;
|
||||
|
||||
const upgradeStartTime = Date.now();
|
||||
const upgradedMessages =
|
||||
await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema));
|
||||
const upgradedMessages = await Promise.all(
|
||||
messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)
|
||||
);
|
||||
const upgradeDuration = Date.now() - upgradeStartTime;
|
||||
|
||||
const saveStartTime = Date.now();
|
||||
@@ -109,8 +108,10 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
||||
minDatabaseVersion,
|
||||
});
|
||||
if (!isValidDatabaseVersion) {
|
||||
throw new Error(`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`);
|
||||
throw new Error(
|
||||
`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Even if we make this async using `then`, requesting `count` on an
|
||||
@@ -132,10 +133,13 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
||||
break;
|
||||
}
|
||||
numCumulativeMessagesProcessed += status.numMessagesProcessed;
|
||||
console.log('Upgrade message schema:', Object.assign({}, status, {
|
||||
console.log(
|
||||
'Upgrade message schema:',
|
||||
Object.assign({}, status, {
|
||||
numTotalMessages,
|
||||
numCumulativeMessagesProcessed,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Close database connection');
|
||||
@@ -181,8 +185,10 @@ const _getConnection = async ({ databaseName, minDatabaseVersion }) => {
|
||||
const databaseVersion = connection.version;
|
||||
const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion;
|
||||
if (!isValidDatabaseVersion) {
|
||||
throw new Error(`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`);
|
||||
throw new Error(
|
||||
`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
return connection;
|
||||
@@ -205,29 +211,33 @@ const _processBatch = async ({
|
||||
throw new TypeError("'numMessagesPerBatch' is required");
|
||||
}
|
||||
|
||||
const isAttachmentMigrationComplete =
|
||||
await settings.isAttachmentMigrationComplete(connection);
|
||||
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
|
||||
connection
|
||||
);
|
||||
if (isAttachmentMigrationComplete) {
|
||||
return {
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
|
||||
const lastProcessedIndex =
|
||||
await settings.getAttachmentMigrationLastProcessedIndex(connection);
|
||||
const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex(
|
||||
connection
|
||||
);
|
||||
|
||||
const fetchUnprocessedMessagesStartTime = Date.now();
|
||||
const unprocessedMessages =
|
||||
await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({
|
||||
const unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex(
|
||||
{
|
||||
connection,
|
||||
count: numMessagesPerBatch,
|
||||
lastIndex: lastProcessedIndex,
|
||||
});
|
||||
}
|
||||
);
|
||||
const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
|
||||
|
||||
const upgradeStartTime = Date.now();
|
||||
const upgradedMessages =
|
||||
await Promise.all(unprocessedMessages.map(upgradeMessageSchema));
|
||||
const upgradedMessages = await Promise.all(
|
||||
unprocessedMessages.map(upgradeMessageSchema)
|
||||
);
|
||||
const upgradeDuration = Date.now() - upgradeStartTime;
|
||||
|
||||
const saveMessagesStartTime = Date.now();
|
||||
@@ -266,12 +276,12 @@ const _processBatch = async ({
|
||||
};
|
||||
};
|
||||
|
||||
const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => {
|
||||
const _saveMessageBackbone = ({ BackboneMessage } = {}) => message => {
|
||||
const backboneMessage = new BackboneMessage(message);
|
||||
return deferredToPromise(backboneMessage.save());
|
||||
};
|
||||
|
||||
const _saveMessage = ({ transaction } = {}) => (message) => {
|
||||
const _saveMessage = ({ transaction } = {}) => message => {
|
||||
if (!isObject(transaction)) {
|
||||
throw new TypeError("'transaction' is required");
|
||||
}
|
||||
@@ -279,18 +289,20 @@ const _saveMessage = ({ transaction } = {}) => (message) => {
|
||||
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
|
||||
const request = messagesStore.put(message, message.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = event => reject(event.target.error);
|
||||
});
|
||||
};
|
||||
|
||||
const _fetchMessagesRequiringSchemaUpgrade =
|
||||
async ({ BackboneMessageCollection, count } = {}) => {
|
||||
const _fetchMessagesRequiringSchemaUpgrade = async ({
|
||||
BackboneMessageCollection,
|
||||
count,
|
||||
} = {}) => {
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required');
|
||||
throw new TypeError(
|
||||
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNumber(count)) {
|
||||
@@ -298,7 +310,9 @@ const _fetchMessagesRequiringSchemaUpgrade =
|
||||
}
|
||||
|
||||
const collection = new BackboneMessageCollection();
|
||||
return new Promise(resolve => collection.fetch({
|
||||
return new Promise(resolve =>
|
||||
collection
|
||||
.fetch({
|
||||
limit: count,
|
||||
index: {
|
||||
name: 'schemaVersion',
|
||||
@@ -306,17 +320,22 @@ const _fetchMessagesRequiringSchemaUpgrade =
|
||||
excludeUpper: true,
|
||||
order: 'desc',
|
||||
},
|
||||
}).always(() => {
|
||||
})
|
||||
.always(() => {
|
||||
const models = collection.models || [];
|
||||
const messages = models.map(model => model.toJSON());
|
||||
resolve(messages);
|
||||
}));
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// NOTE: Named ‘dangerous’ because it is not as efficient as using our
|
||||
// `messages` `schemaVersion` index:
|
||||
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
|
||||
({ connection, count, lastIndex } = {}) => {
|
||||
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({
|
||||
connection,
|
||||
count,
|
||||
lastIndex,
|
||||
} = {}) => {
|
||||
if (!isObject(connection)) {
|
||||
throw new TypeError("'connection' is required");
|
||||
}
|
||||
@@ -341,7 +360,7 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
|
||||
return new Promise((resolve, reject) => {
|
||||
const items = [];
|
||||
const request = messagesStore.openCursor(range);
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
const hasMoreData = Boolean(cursor);
|
||||
if (!hasMoreData || items.length === count) {
|
||||
@@ -352,10 +371,9 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
|
||||
items.push(item);
|
||||
cursor.continue();
|
||||
};
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const _getNumMessages = async ({ connection } = {}) => {
|
||||
if (!isObject(connection)) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
exports.run = (transaction) => {
|
||||
exports.run = transaction => {
|
||||
const messagesStore = transaction.objectStore('messages');
|
||||
|
||||
console.log("Create message attachment metadata index: 'hasAttachments'");
|
||||
@@ -8,12 +8,10 @@ exports.run = (transaction) => {
|
||||
{ unique: false }
|
||||
);
|
||||
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach((name) => {
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => {
|
||||
console.log(`Create message attachment metadata index: '${name}'`);
|
||||
messagesStore.createIndex(
|
||||
name,
|
||||
['conversationId', 'received_at', name],
|
||||
{ unique: false }
|
||||
);
|
||||
messagesStore.createIndex(name, ['conversationId', 'received_at', name], {
|
||||
unique: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -1,23 +1,22 @@
|
||||
const Migrations0DatabaseWithAttachmentData =
|
||||
require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData =
|
||||
require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData = require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
exports.getPlaceholderMigrations = () => {
|
||||
const last0MigrationVersion =
|
||||
Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion =
|
||||
Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion = Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
|
||||
const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion;
|
||||
|
||||
return [{
|
||||
return [
|
||||
{
|
||||
version: lastMigrationVersion,
|
||||
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' +
|
||||
' of implicitly via Backbone IndexedDB adapter at any time.');
|
||||
' of implicitly via Backbone IndexedDB adapter at any time.'
|
||||
);
|
||||
},
|
||||
}];
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@@ -3,7 +3,6 @@ const { isString, last } = require('lodash');
|
||||
const { runMigrations } = require('./run_migrations');
|
||||
const Migration18 = require('./18');
|
||||
|
||||
|
||||
// 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
|
||||
// any expensive operations, e.g. modifying all messages / attachments, etc., as
|
||||
@@ -20,7 +19,9 @@ const migrations = [
|
||||
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 });
|
||||
|
||||
const conversations = transaction.db.createObjectStore('conversations');
|
||||
@@ -59,7 +60,7 @@ const migrations = [
|
||||
const identityKeys = transaction.objectStore('identityKeys');
|
||||
const request = identityKeys.openCursor();
|
||||
const promises = [];
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const attributes = cursor.value;
|
||||
@@ -67,14 +68,16 @@ const migrations = [
|
||||
attributes.firstUse = false;
|
||||
attributes.nonblockingApproval = false;
|
||||
attributes.verified = 0;
|
||||
promises.push(new Promise(((resolve, reject) => {
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
const putRequest = identityKeys.put(attributes, attributes.id);
|
||||
putRequest.onsuccess = resolve;
|
||||
putRequest.onerror = (e) => {
|
||||
putRequest.onerror = e => {
|
||||
console.log(e);
|
||||
reject(e);
|
||||
};
|
||||
})));
|
||||
})
|
||||
);
|
||||
cursor.continue();
|
||||
} else {
|
||||
// no more results
|
||||
@@ -84,7 +87,7 @@ const migrations = [
|
||||
});
|
||||
}
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
request.onerror = event => {
|
||||
console.log(event);
|
||||
};
|
||||
},
|
||||
@@ -129,7 +132,9 @@ const migrations = [
|
||||
|
||||
const messagesStore = transaction.objectStore('messages');
|
||||
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;
|
||||
|
||||
|
@@ -4,7 +4,6 @@ const db = require('../database');
|
||||
const settings = require('../settings');
|
||||
const { runMigrations } = require('./run_migrations');
|
||||
|
||||
|
||||
// IMPORTANT: Add new migrations that need to traverse entire database, e.g.
|
||||
// messages store, below. Whenever we need this, we need to force attachment
|
||||
// migration on startup:
|
||||
@@ -20,7 +19,9 @@ const migrations = [
|
||||
exports.run = async ({ Backbone, database } = {}) => {
|
||||
const { canRun } = await exports.getStatus({ database });
|
||||
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 });
|
||||
@@ -28,8 +29,9 @@ exports.run = async ({ Backbone, database } = {}) => {
|
||||
|
||||
exports.getStatus = async ({ database } = {}) => {
|
||||
const connection = await db.open(database.id, database.version);
|
||||
const isAttachmentMigrationComplete =
|
||||
await settings.isAttachmentMigrationComplete(connection);
|
||||
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
|
||||
connection
|
||||
);
|
||||
const hasMigrations = migrations.length > 0;
|
||||
|
||||
const canRun = isAttachmentMigrationComplete && hasMigrations;
|
||||
|
@@ -1,29 +1,27 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const {
|
||||
head,
|
||||
isFunction,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
|
||||
const { head, isFunction, isObject, isString, last } = require('lodash');
|
||||
|
||||
const db = require('../database');
|
||||
const { deferredToPromise } = require('../deferred_to_promise');
|
||||
|
||||
|
||||
const closeDatabaseConnection = ({ Backbone } = {}) =>
|
||||
deferredToPromise(Backbone.sync('closeall'));
|
||||
|
||||
exports.runMigrations = async ({ Backbone, database } = {}) => {
|
||||
if (!isObject(Backbone) || !isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)) {
|
||||
if (
|
||||
!isObject(Backbone) ||
|
||||
!isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)
|
||||
) {
|
||||
throw new TypeError("'Backbone' is required");
|
||||
}
|
||||
|
||||
if (!isObject(database) || !isString(database.id) ||
|
||||
!Array.isArray(database.migrations)) {
|
||||
if (
|
||||
!isObject(database) ||
|
||||
!isString(database.id) ||
|
||||
!Array.isArray(database.migrations)
|
||||
) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
|
||||
@@ -56,7 +54,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => {
|
||||
await closeDatabaseConnection({ Backbone });
|
||||
};
|
||||
|
||||
const getMigrationVersions = (database) => {
|
||||
const getMigrationVersions = database => {
|
||||
if (!isObject(database) || !Array.isArray(database.migrations)) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
@@ -64,8 +62,12 @@ const getMigrationVersions = (database) => {
|
||||
const firstMigration = head(database.migrations);
|
||||
const lastMigration = last(database.migrations);
|
||||
|
||||
const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null;
|
||||
const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null;
|
||||
const firstVersion = firstMigration
|
||||
? parseInt(firstMigration.version, 10)
|
||||
: null;
|
||||
const lastVersion = lastMigration
|
||||
? parseInt(lastMigration.version, 10)
|
||||
: null;
|
||||
|
||||
return { firstVersion, lastVersion };
|
||||
};
|
||||
|
@@ -1,10 +1,7 @@
|
||||
/* eslint-env node */
|
||||
|
||||
exports.isMacOS = () =>
|
||||
process.platform === 'darwin';
|
||||
exports.isMacOS = () => process.platform === 'darwin';
|
||||
|
||||
exports.isLinux = () =>
|
||||
process.platform === 'linux';
|
||||
exports.isLinux = () => process.platform === 'linux';
|
||||
|
||||
exports.isWindows = () =>
|
||||
process.platform === 'win32';
|
||||
exports.isWindows = () => process.platform === 'win32';
|
||||
|
@@ -6,22 +6,20 @@ const path = require('path');
|
||||
const { compose } = require('lodash/fp');
|
||||
const { escapeRegExp } = require('lodash');
|
||||
|
||||
|
||||
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
|
||||
// _redactPath :: Path -> String -> String
|
||||
exports._redactPath = (filePath) => {
|
||||
exports._redactPath = filePath => {
|
||||
if (!is.string(filePath)) {
|
||||
throw new TypeError("'filePath' must be a string");
|
||||
}
|
||||
|
||||
const filePathPattern = exports._pathToRegExp(filePath);
|
||||
|
||||
return (text) => {
|
||||
return text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
@@ -35,7 +33,7 @@ exports._redactPath = (filePath) => {
|
||||
};
|
||||
|
||||
// _pathToRegExp :: Path -> Maybe RegExp
|
||||
exports._pathToRegExp = (filePath) => {
|
||||
exports._pathToRegExp = filePath => {
|
||||
try {
|
||||
const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\');
|
||||
const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\');
|
||||
@@ -47,7 +45,9 @@ exports._pathToRegExp = (filePath) => {
|
||||
pathWithNormalizedSlashes,
|
||||
pathWithEscapedSlashes,
|
||||
urlEncodedPath,
|
||||
].map(escapeRegExp).join('|');
|
||||
]
|
||||
.map(escapeRegExp)
|
||||
.join('|');
|
||||
return new RegExp(patternString, 'g');
|
||||
} catch (error) {
|
||||
return null;
|
||||
@@ -56,7 +56,7 @@ exports._pathToRegExp = (filePath) => {
|
||||
|
||||
// Public API
|
||||
// redactPhoneNumbers :: String -> String
|
||||
exports.redactPhoneNumbers = (text) => {
|
||||
exports.redactPhoneNumbers = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
@@ -65,7 +65,7 @@ exports.redactPhoneNumbers = (text) => {
|
||||
};
|
||||
|
||||
// redactGroupIds :: String -> String
|
||||
exports.redactGroupIds = (text) => {
|
||||
exports.redactGroupIds = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
const { isObject, isString } = require('lodash');
|
||||
|
||||
|
||||
const ITEMS_STORE_NAME = 'items';
|
||||
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
||||
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
||||
@@ -37,8 +36,7 @@ exports._getItem = (connection, key) => {
|
||||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.get(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = event =>
|
||||
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 request = itemsStore.put({ id: key, value }, key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,10 +75,8 @@ exports._deleteItem = (connection, key) => {
|
||||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.delete(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* global setTimeout */
|
||||
|
||||
exports.sleep = ms =>
|
||||
new Promise(resolve => setTimeout(resolve, ms));
|
||||
exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
@@ -3,7 +3,6 @@ const is = require('@sindresorhus/is');
|
||||
const Errors = require('./types/errors');
|
||||
const Settings = require('./settings');
|
||||
|
||||
|
||||
exports.syncReadReceiptConfiguration = async ({
|
||||
deviceId,
|
||||
sendRequestConfigurationSyncMessage,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
exports.stringToArrayBuffer = (string) => {
|
||||
exports.stringToArrayBuffer = string => {
|
||||
if (typeof string !== 'string') {
|
||||
throw new TypeError("'string' must be a string");
|
||||
}
|
||||
|
@@ -2,9 +2,15 @@ const is = require('@sindresorhus/is');
|
||||
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
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 { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system');
|
||||
const {
|
||||
migrateDataToFileSystem,
|
||||
} = require('./attachment/migrate_data_to_file_system');
|
||||
|
||||
// // 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.
|
||||
// Over time, we can expand this definition to become more narrow, e.g. require certain
|
||||
// fields, etc.
|
||||
exports.isValid = (rawAttachment) => {
|
||||
exports.isValid = rawAttachment => {
|
||||
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
||||
// deserialized by protobuf:
|
||||
if (!rawAttachment) {
|
||||
@@ -41,12 +47,15 @@ exports.isValid = (rawAttachment) => {
|
||||
};
|
||||
|
||||
// Upgrade steps
|
||||
exports.autoOrientJPEG = async (attachment) => {
|
||||
exports.autoOrientJPEG = async attachment => {
|
||||
if (!MIME.isJPEG(attachment.contentType)) {
|
||||
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 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`,
|
||||
// which currently doesn’t support async testing:
|
||||
// https://github.com/leebyron/testcheck-js/issues/45
|
||||
exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
||||
exports._replaceUnicodeOrderOverridesSync = attachment => {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
@@ -95,9 +104,12 @@ exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
||||
exports.replaceUnicodeOrderOverrides = async attachment =>
|
||||
exports._replaceUnicodeOrderOverridesSync(attachment);
|
||||
|
||||
exports.removeSchemaVersion = (attachment) => {
|
||||
exports.removeSchemaVersion = attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
console.log('Attachment.removeSchemaVersion: Invalid input attachment:', attachment);
|
||||
console.log(
|
||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||
attachment
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
@@ -115,12 +127,12 @@ exports.hasData = attachment =>
|
||||
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
|
||||
// Attachment ->
|
||||
// IO (Promise Attachment)
|
||||
exports.loadData = (readAttachmentData) => {
|
||||
exports.loadData = readAttachmentData => {
|
||||
if (!is.function(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
@@ -142,12 +154,12 @@ exports.loadData = (readAttachmentData) => {
|
||||
// deleteData :: (RelativePath -> IO Unit)
|
||||
// Attachment ->
|
||||
// IO Unit
|
||||
exports.deleteData = (deleteAttachmentData) => {
|
||||
exports.deleteData = deleteAttachmentData => {
|
||||
if (!is.function(deleteAttachmentData)) {
|
||||
throw new TypeError("'deleteAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
@@ -1,10 +1,4 @@
|
||||
const {
|
||||
isArrayBuffer,
|
||||
isFunction,
|
||||
isUndefined,
|
||||
omit,
|
||||
} = require('lodash');
|
||||
|
||||
const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
|
||||
|
||||
// type Context :: {
|
||||
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
|
||||
@@ -13,7 +7,10 @@ const {
|
||||
// migrateDataToFileSystem :: Attachment ->
|
||||
// Context ->
|
||||
// Promise Attachment
|
||||
exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } = {}) => {
|
||||
exports.migrateDataToFileSystem = async (
|
||||
attachment,
|
||||
{ writeNewAttachmentData } = {}
|
||||
) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||
}
|
||||
@@ -28,15 +25,16 @@ exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData }
|
||||
|
||||
const isValidData = isArrayBuffer(data);
|
||||
if (!isValidData) {
|
||||
throw new TypeError('Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`);
|
||||
throw new TypeError(
|
||||
'Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`
|
||||
);
|
||||
}
|
||||
|
||||
const path = await writeNewAttachmentData(data);
|
||||
|
||||
const attachmentWithoutData = omit(
|
||||
Object.assign({}, attachment, { path }),
|
||||
['data']
|
||||
);
|
||||
const attachmentWithoutData = omit(Object.assign({}, attachment, { path }), [
|
||||
'data',
|
||||
]);
|
||||
return attachmentWithoutData;
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// toLogFormat :: Error -> String
|
||||
exports.toLogFormat = (error) => {
|
||||
exports.toLogFormat = error => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
|
@@ -3,9 +3,9 @@ const { isFunction, isString, omit } = require('lodash');
|
||||
const Attachment = require('./attachment');
|
||||
const Errors = require('./errors');
|
||||
const SchemaVersion = require('./schema_version');
|
||||
const { initializeAttachmentMetadata } =
|
||||
require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const {
|
||||
initializeAttachmentMetadata,
|
||||
} = require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const GROUP = 'group';
|
||||
const PRIVATE = 'private';
|
||||
@@ -37,19 +37,17 @@ const INITIAL_SCHEMA_VERSION = 0;
|
||||
// how we do database migrations:
|
||||
exports.CURRENT_SCHEMA_VERSION = 5;
|
||||
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
exports.PRIVATE = PRIVATE;
|
||||
|
||||
// Placeholder until we have stronger preconditions:
|
||||
exports.isValid = () =>
|
||||
true;
|
||||
exports.isValid = () => true;
|
||||
|
||||
// Schema
|
||||
exports.initializeSchemaVersion = (message) => {
|
||||
const isInitialized = SchemaVersion.isValid(message.schemaVersion) &&
|
||||
message.schemaVersion >= 1;
|
||||
exports.initializeSchemaVersion = message => {
|
||||
const isInitialized =
|
||||
SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
|
||||
if (isInitialized) {
|
||||
return message;
|
||||
}
|
||||
@@ -59,27 +57,23 @@ exports.initializeSchemaVersion = (message) => {
|
||||
: 0;
|
||||
const hasAttachments = numAttachments > 0;
|
||||
if (!hasAttachments) {
|
||||
return Object.assign(
|
||||
{},
|
||||
message,
|
||||
{ schemaVersion: INITIAL_SCHEMA_VERSION }
|
||||
);
|
||||
return Object.assign({}, message, {
|
||||
schemaVersion: INITIAL_SCHEMA_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
// All attachments should have the same schema version, so we just pick
|
||||
// the first one:
|
||||
const firstAttachment = message.attachments[0];
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(firstAttachment.schemaVersion)
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(
|
||||
firstAttachment.schemaVersion
|
||||
)
|
||||
? firstAttachment.schemaVersion
|
||||
: INITIAL_SCHEMA_VERSION;
|
||||
const messageWithInitialSchema = Object.assign(
|
||||
{},
|
||||
message,
|
||||
{
|
||||
const messageWithInitialSchema = Object.assign({}, message, {
|
||||
schemaVersion: inheritedSchemaVersion,
|
||||
attachments: message.attachments.map(Attachment.removeSchemaVersion),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return messageWithInitialSchema;
|
||||
};
|
||||
@@ -98,7 +92,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
||||
|
||||
return async (message, context) => {
|
||||
if (!exports.isValid(message)) {
|
||||
console.log('Message._withSchemaVersion: Invalid input message:', message);
|
||||
console.log(
|
||||
'Message._withSchemaVersion: Invalid input message:',
|
||||
message
|
||||
);
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -138,15 +135,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
||||
return message;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
upgradedMessage,
|
||||
{ schemaVersion }
|
||||
);
|
||||
return Object.assign({}, upgradedMessage, { schemaVersion });
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Public API
|
||||
// _mapAttachments :: (Attachment -> Promise Attachment) ->
|
||||
// (Message, Context) ->
|
||||
@@ -154,19 +146,24 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
||||
exports._mapAttachments = upgradeAttachment => async (message, context) => {
|
||||
const upgradeWithContext = attachment =>
|
||||
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 });
|
||||
};
|
||||
|
||||
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
|
||||
// (Message, Context) ->
|
||||
// Promise Message
|
||||
exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => {
|
||||
exports._mapQuotedAttachments = upgradeAttachment => async (
|
||||
message,
|
||||
context
|
||||
) => {
|
||||
if (!message.quote) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const upgradeWithContext = async (attachment) => {
|
||||
const upgradeWithContext = async attachment => {
|
||||
const { thumbnail } = attachment;
|
||||
if (!thumbnail) {
|
||||
return attachment;
|
||||
@@ -185,7 +182,9 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
|
||||
|
||||
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, {
|
||||
quote: Object.assign({}, message.quote, {
|
||||
attachments,
|
||||
@@ -193,8 +192,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
|
||||
});
|
||||
};
|
||||
|
||||
const toVersion0 = async message =>
|
||||
exports.initializeSchemaVersion(message);
|
||||
const toVersion0 = async message => exports.initializeSchemaVersion(message);
|
||||
|
||||
const toVersion1 = exports._withSchemaVersion(
|
||||
1,
|
||||
@@ -241,25 +239,28 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
||||
return message;
|
||||
};
|
||||
|
||||
exports.createAttachmentLoader = (loadAttachmentData) => {
|
||||
exports.createAttachmentLoader = loadAttachmentData => {
|
||||
if (!isFunction(loadAttachmentData)) {
|
||||
throw new TypeError('`loadAttachmentData` is required');
|
||||
}
|
||||
|
||||
return async message => (Object.assign({}, message, {
|
||||
attachments: await Promise.all(message.attachments.map(loadAttachmentData)),
|
||||
}));
|
||||
return async message =>
|
||||
Object.assign({}, message, {
|
||||
attachments: await Promise.all(
|
||||
message.attachments.map(loadAttachmentData)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// createAttachmentDataWriter :: (RelativePath -> IO Unit)
|
||||
// Message ->
|
||||
// IO (Promise Message)
|
||||
exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
||||
exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
||||
if (!isFunction(writeExistingAttachmentData)) {
|
||||
throw new TypeError("'writeExistingAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (rawMessage) => {
|
||||
return async rawMessage => {
|
||||
if (!exports.isValid(rawMessage)) {
|
||||
throw new TypeError("'rawMessage' is not valid");
|
||||
}
|
||||
@@ -282,17 +283,21 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
||||
return message;
|
||||
}
|
||||
|
||||
(attachments || []).forEach((attachment) => {
|
||||
(attachments || []).forEach(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)) {
|
||||
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;
|
||||
|
||||
// we want to be bulletproof to thumbnails without data
|
||||
@@ -315,10 +320,12 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
||||
{},
|
||||
await writeThumbnails(message),
|
||||
{
|
||||
attachments: await Promise.all((attachments || []).map(async (attachment) => {
|
||||
attachments: await Promise.all(
|
||||
(attachments || []).map(async attachment => {
|
||||
await writeExistingAttachmentData(attachment);
|
||||
return omit(attachment, ['data']);
|
||||
})),
|
||||
})
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
const { isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.isValid = value =>
|
||||
isNumber(value) && value >= 0;
|
||||
exports.isValid = value => isNumber(value) && value >= 0;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const OS = require('../os');
|
||||
|
||||
exports.isAudioNotificationSupported = () =>
|
||||
!OS.isLinux();
|
||||
exports.isAudioNotificationSupported = () => !OS.isLinux();
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
/* global i18n: false */
|
||||
|
||||
|
||||
const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds
|
||||
|
||||
const setMessage = () => {
|
||||
|
@@ -1,17 +1,13 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
const { Settings } = window.Signal.Types;
|
||||
|
||||
var SETTINGS = {
|
||||
OFF : 'off',
|
||||
COUNT : 'count',
|
||||
NAME : 'name',
|
||||
MESSAGE : 'message'
|
||||
OFF: 'off',
|
||||
COUNT: 'count',
|
||||
NAME: 'name',
|
||||
MESSAGE: 'message',
|
||||
};
|
||||
|
||||
Whisper.Notifications = new (Backbone.Collection.extend({
|
||||
@@ -25,17 +21,20 @@
|
||||
this.trigger('click', conversation);
|
||||
},
|
||||
update: function() {
|
||||
const {isEnabled} = this;
|
||||
const { isEnabled } = this;
|
||||
const isFocused = window.isFocused();
|
||||
const isAudioNotificationEnabled = storage.get('audio-notification') || false;
|
||||
const isAudioNotificationEnabled =
|
||||
storage.get('audio-notification') || false;
|
||||
const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
|
||||
const shouldPlayNotificationSound = isAudioNotificationSupported &&
|
||||
isAudioNotificationEnabled;
|
||||
const shouldPlayNotificationSound =
|
||||
isAudioNotificationSupported && isAudioNotificationEnabled;
|
||||
const numNotifications = this.length;
|
||||
console.log(
|
||||
'Update notifications:',
|
||||
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound}
|
||||
);
|
||||
console.log('Update notifications:', {
|
||||
isFocused,
|
||||
isEnabled,
|
||||
numNotifications,
|
||||
shouldPlayNotificationSound,
|
||||
});
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
@@ -69,7 +68,7 @@
|
||||
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
|
||||
var newMessageCount = [
|
||||
numNotifications,
|
||||
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages')
|
||||
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'),
|
||||
].join(' ');
|
||||
|
||||
var last = this.last();
|
||||
@@ -105,13 +104,16 @@
|
||||
});
|
||||
} else {
|
||||
var notification = new Notification(title, {
|
||||
body : message,
|
||||
icon : iconUrl,
|
||||
tag : 'signal',
|
||||
silent : !shouldPlayNotificationSound,
|
||||
body: message,
|
||||
icon: iconUrl,
|
||||
tag: 'signal',
|
||||
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
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
Whisper.ReadReceipts = new (Backbone.Collection.extend({
|
||||
@@ -16,8 +13,10 @@
|
||||
ids = conversation.get('members');
|
||||
}
|
||||
var receipts = this.filter(function(receipt) {
|
||||
return receipt.get('timestamp') === message.get('sent_at')
|
||||
&& _.contains(ids, receipt.get('reader'));
|
||||
return (
|
||||
receipt.get('timestamp') === message.get('sent_at') &&
|
||||
_.contains(ids, receipt.get('reader'))
|
||||
);
|
||||
});
|
||||
if (receipts.length) {
|
||||
console.log('Found early read receipts for message');
|
||||
@@ -27,28 +26,43 @@
|
||||
},
|
||||
onReceipt: function(receipt) {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
|
||||
if (messages.length === 0) { return; }
|
||||
return messages
|
||||
.fetchSentAt(receipt.get('timestamp'))
|
||||
.then(function() {
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
var message = messages.find(function(message) {
|
||||
return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId'));
|
||||
return (
|
||||
message.isOutgoing() &&
|
||||
receipt.get('reader') === message.get('conversationId')
|
||||
);
|
||||
});
|
||||
if (message) { return message; }
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
var groups = new Whisper.GroupCollection();
|
||||
return groups.fetchGroups(receipt.get('reader')).then(function() {
|
||||
var ids = groups.pluck('id');
|
||||
ids.push(receipt.get('reader'));
|
||||
return messages.find(function(message) {
|
||||
return (message.isOutgoing() &&
|
||||
_.contains(ids, message.get('conversationId')));
|
||||
return (
|
||||
message.isOutgoing() &&
|
||||
_.contains(ids, message.get('conversationId'))
|
||||
);
|
||||
});
|
||||
});
|
||||
}).then(function(message) {
|
||||
})
|
||||
.then(
|
||||
function(message) {
|
||||
if (message) {
|
||||
var read_by = message.get('read_by') || [];
|
||||
read_by.push(receipt.get('reader'));
|
||||
return new Promise(function(resolve, reject) {
|
||||
message.save({ read_by: read_by }).then(function() {
|
||||
return new Promise(
|
||||
function(resolve, reject) {
|
||||
message.save({ read_by: read_by }).then(
|
||||
function() {
|
||||
// notify frontend listeners
|
||||
var conversation = ConversationController.get(
|
||||
message.get('conversationId')
|
||||
@@ -59,8 +73,11 @@
|
||||
|
||||
this.remove(receipt);
|
||||
resolve();
|
||||
}.bind(this), reject);
|
||||
}.bind(this));
|
||||
}.bind(this),
|
||||
reject
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'No message for read receipt',
|
||||
@@ -68,7 +85,9 @@
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
}
|
||||
}.bind(this)).catch(function(error) {
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(function(error) {
|
||||
console.log(
|
||||
'ReadReceipts.onReceipt error:',
|
||||
error && error.stack ? error.stack : error
|
||||
|
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
Whisper.ReadSyncs = new (Backbone.Collection.extend({
|
||||
forMessage: function(message) {
|
||||
var receipt = this.findWhere({
|
||||
sender: message.get('source'),
|
||||
timestamp: message.get('sent_at')
|
||||
timestamp: message.get('sent_at'),
|
||||
});
|
||||
if (receipt) {
|
||||
console.log('Found early read sync for message');
|
||||
@@ -18,27 +15,35 @@
|
||||
},
|
||||
onReceipt: function(receipt) {
|
||||
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) {
|
||||
return (message.isIncoming() && message.isUnread() &&
|
||||
message.get('source') === receipt.get('sender'));
|
||||
return (
|
||||
message.isIncoming() &&
|
||||
message.isUnread() &&
|
||||
message.get('source') === receipt.get('sender')
|
||||
);
|
||||
});
|
||||
if (message) {
|
||||
return message.markRead(receipt.get('read_at')).then(function() {
|
||||
return message.markRead(receipt.get('read_at')).then(
|
||||
function() {
|
||||
this.notifyConversation(message);
|
||||
this.remove(receipt);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'No message for read sync',
|
||||
receipt.get('sender'), receipt.get('timestamp')
|
||||
receipt.get('sender'),
|
||||
receipt.get('timestamp')
|
||||
);
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
notifyConversation: function(message) {
|
||||
var conversation = ConversationController.get({
|
||||
id: message.get('conversationId')
|
||||
id: message.get('conversationId'),
|
||||
});
|
||||
|
||||
if (conversation) {
|
||||
|
@@ -1,25 +1,24 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
Whisper.Registration = {
|
||||
markEverDone: function() {
|
||||
storage.put('chromiumRegistrationDoneEver', '');
|
||||
},
|
||||
markDone: function () {
|
||||
markDone: function() {
|
||||
this.markEverDone();
|
||||
storage.put('chromiumRegistrationDone', '');
|
||||
},
|
||||
isDone: function () {
|
||||
isDone: function() {
|
||||
return storage.get('chromiumRegistrationDone') === '';
|
||||
},
|
||||
everDone: function() {
|
||||
return storage.get('chromiumRegistrationDoneEver') === '' ||
|
||||
storage.get('chromiumRegistrationDone') === '';
|
||||
return (
|
||||
storage.get('chromiumRegistrationDoneEver') === '' ||
|
||||
storage.get('chromiumRegistrationDone') === ''
|
||||
);
|
||||
},
|
||||
remove: function() {
|
||||
storage.remove('chromiumRegistrationDone');
|
||||
}
|
||||
},
|
||||
};
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function() {
|
||||
// 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
|
||||
// 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
|
||||
// Backbone events have 3 arguments).
|
||||
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) {
|
||||
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) {
|
||||
case 0:
|
||||
while (++i < l) {
|
||||
try {
|
||||
(ev = events[i]).callback.call(ev.ctx);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
@@ -68,8 +77,7 @@
|
||||
while (++i < l) {
|
||||
try {
|
||||
(ev = events[i]).callback.call(ev.ctx, a1);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
@@ -78,8 +86,7 @@
|
||||
while (++i < l) {
|
||||
try {
|
||||
(ev = events[i]).callback.call(ev.ctx, a1, a2);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
@@ -88,8 +95,7 @@
|
||||
while (++i < l) {
|
||||
try {
|
||||
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
@@ -98,8 +104,7 @@
|
||||
while (++i < l) {
|
||||
try {
|
||||
(ev = events[i]).callback.apply(ev.ctx, args);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
@@ -122,10 +127,5 @@
|
||||
return this;
|
||||
}
|
||||
|
||||
Backbone.Model.prototype.trigger
|
||||
= Backbone.View.prototype.trigger
|
||||
= Backbone.Collection.prototype.trigger
|
||||
= Backbone.Events.trigger
|
||||
= trigger;
|
||||
Backbone.Model.prototype.trigger = Backbone.View.prototype.trigger = Backbone.Collection.prototype.trigger = Backbone.Events.trigger = trigger;
|
||||
})();
|
||||
|
||||
|
@@ -1,8 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
;(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
|
||||
@@ -17,8 +13,12 @@
|
||||
|
||||
function run() {
|
||||
console.log('Rotating signed prekey...');
|
||||
getAccountManager().rotateSignedPreKey().catch(function() {
|
||||
console.log('rotateSignedPrekey() failed. Trying again in five seconds');
|
||||
getAccountManager()
|
||||
.rotateSignedPreKey()
|
||||
.catch(function() {
|
||||
console.log(
|
||||
'rotateSignedPrekey() failed. Trying again in five seconds'
|
||||
);
|
||||
setTimeout(runWhenOnline, 5000);
|
||||
});
|
||||
scheduleNextRotation();
|
||||
@@ -29,7 +29,9 @@
|
||||
if (navigator.onLine) {
|
||||
run();
|
||||
} 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() {
|
||||
window.removeEventListener('online', listener);
|
||||
run();
|
||||
@@ -79,6 +81,6 @@
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}());
|
||||
})();
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function() {
|
||||
var electron = require('electron');
|
||||
var remote = electron.remote;
|
||||
var app = remote.app;
|
||||
@@ -31,7 +31,7 @@
|
||||
'shouldn',
|
||||
'wasn',
|
||||
'weren',
|
||||
'wouldn'
|
||||
'wouldn',
|
||||
];
|
||||
|
||||
function setupLinux(locale) {
|
||||
@@ -39,7 +39,12 @@
|
||||
// apt-get install hunspell-<locale> can be run for easy access to other dictionaries
|
||||
var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
|
||||
|
||||
console.log('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);
|
||||
} else {
|
||||
console.log('Detected Linux. Using default en_US spell check dictionary');
|
||||
@@ -50,10 +55,17 @@
|
||||
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
|
||||
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);
|
||||
} 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') {
|
||||
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);
|
||||
} else {
|
||||
// OSX and Windows 8+ have OS-level spellcheck APIs
|
||||
console.log('Using OS-level spell check API with locale', process.env.LANG);
|
||||
}
|
||||
|
||||
var simpleChecker = window.spellChecker = {
|
||||
var simpleChecker = (window.spellChecker = {
|
||||
spellCheck: function(text) {
|
||||
return !this.isMisspelled(text);
|
||||
},
|
||||
@@ -101,8 +116,8 @@
|
||||
},
|
||||
add: function(text) {
|
||||
spellchecker.add(text);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
webFrame.setSpellCheckProvider(
|
||||
'en-US',
|
||||
@@ -120,7 +135,8 @@
|
||||
|
||||
var selectedText = window.getSelection().toString();
|
||||
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({
|
||||
isMisspelled: isMisspelled,
|
||||
spellingSuggestions: spellingSuggestions,
|
||||
|
@@ -1,12 +1,9 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
var Item = Backbone.Model.extend({
|
||||
database: Whisper.Database,
|
||||
storeName: 'items'
|
||||
storeName: 'items',
|
||||
});
|
||||
var ItemCollection = Backbone.Collection.extend({
|
||||
model: Item,
|
||||
@@ -16,26 +13,28 @@
|
||||
|
||||
var ready = false;
|
||||
var items = new ItemCollection();
|
||||
items.on('reset', function() { ready = true; });
|
||||
items.on('reset', function() {
|
||||
ready = true;
|
||||
});
|
||||
window.storage = {
|
||||
/*****************************
|
||||
*** Base Storage Routines ***
|
||||
*****************************/
|
||||
put: function(key, value) {
|
||||
if (value === undefined) {
|
||||
throw new Error("Tried to store undefined");
|
||||
throw new Error('Tried to store undefined');
|
||||
}
|
||||
if (!ready) {
|
||||
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) {
|
||||
item.save().then(resolve, reject);
|
||||
});
|
||||
},
|
||||
|
||||
get: function(key, defaultValue) {
|
||||
var item = items.get("" + key);
|
||||
var item = items.get('' + key);
|
||||
if (!item) {
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -43,7 +42,7 @@
|
||||
},
|
||||
|
||||
remove: function(key) {
|
||||
var item = items.get("" + key);
|
||||
var item = items.get('' + key);
|
||||
if (item) {
|
||||
items.remove(item);
|
||||
return new Promise(function(resolve, reject) {
|
||||
@@ -63,16 +62,23 @@
|
||||
|
||||
fetch: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
items.fetch({reset: true})
|
||||
.fail(() => reject(new Error('Failed to fetch from storage.' +
|
||||
' This may be due to an unexpected database version.')))
|
||||
items
|
||||
.fetch({ reset: true })
|
||||
.fail(() =>
|
||||
reject(
|
||||
new Error(
|
||||
'Failed to fetch from storage.' +
|
||||
' This may be due to an unexpected database version.'
|
||||
)
|
||||
)
|
||||
)
|
||||
.always(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
items.reset();
|
||||
}
|
||||
},
|
||||
};
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -13,13 +13,14 @@
|
||||
},
|
||||
events: {
|
||||
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
|
||||
'openInbox': 'openInbox',
|
||||
openInbox: 'openInbox',
|
||||
'change-theme': 'applyTheme',
|
||||
'change-hide-menu': 'applyHideMenu',
|
||||
},
|
||||
applyTheme: function() {
|
||||
var theme = storage.get('theme-setting') || 'android';
|
||||
this.$el.removeClass('ios')
|
||||
this.$el
|
||||
.removeClass('ios')
|
||||
.removeClass('android-dark')
|
||||
.removeClass('android')
|
||||
.addClass(theme);
|
||||
@@ -30,7 +31,7 @@
|
||||
window.setMenuBarVisibility(!hideMenuBar);
|
||||
},
|
||||
openView: function(view) {
|
||||
this.el.innerHTML = "";
|
||||
this.el.innerHTML = '';
|
||||
this.el.append(view.el);
|
||||
this.delegateEvents();
|
||||
},
|
||||
@@ -48,13 +49,17 @@
|
||||
openImporter: function() {
|
||||
window.addSetupMenuItems();
|
||||
this.resetViews();
|
||||
var importView = this.importView = new Whisper.ImportView();
|
||||
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this));
|
||||
var importView = (this.importView = new Whisper.ImportView());
|
||||
this.listenTo(
|
||||
importView,
|
||||
'light-import',
|
||||
this.finishLightImport.bind(this)
|
||||
);
|
||||
this.openView(this.importView);
|
||||
},
|
||||
finishLightImport: function() {
|
||||
var options = {
|
||||
hasExistingData: true
|
||||
hasExistingData: true,
|
||||
};
|
||||
this.openInstaller(options);
|
||||
},
|
||||
@@ -76,7 +81,7 @@
|
||||
}
|
||||
|
||||
this.resetViews();
|
||||
var installView = this.installView = new Whisper.InstallView(options);
|
||||
var installView = (this.installView = new Whisper.InstallView(options));
|
||||
this.openView(this.installView);
|
||||
},
|
||||
closeInstaller: function() {
|
||||
@@ -118,7 +123,7 @@
|
||||
// - in other situations openInbox() will be called with no options. So this
|
||||
// view keeps track of whether onEmpty() has ever been called with
|
||||
// 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');
|
||||
this.closeInstaller();
|
||||
@@ -130,11 +135,13 @@
|
||||
this.inboxView = new Whisper.InboxView({
|
||||
model: self,
|
||||
window: window,
|
||||
initialLoadComplete: options.initialLoadComplete
|
||||
initialLoadComplete: options.initialLoadComplete,
|
||||
});
|
||||
return ConversationController.loadPromise().then(function() {
|
||||
return ConversationController.loadPromise().then(
|
||||
function() {
|
||||
this.openView(this.inboxView);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
if (!$.contains(this.el, this.inboxView.el)) {
|
||||
this.openView(this.inboxView);
|
||||
@@ -159,9 +166,11 @@
|
||||
},
|
||||
openConversation: function(conversation) {
|
||||
if (conversation) {
|
||||
this.openInbox().then(function() {
|
||||
this.openInbox().then(
|
||||
function() {
|
||||
this.inboxView.openConversation(null, conversation);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -9,7 +6,7 @@
|
||||
className: 'attachment-preview',
|
||||
templateName: 'attachment-preview',
|
||||
render_attributes: function() {
|
||||
return {source: this.src};
|
||||
}
|
||||
return { source: this.src };
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -9,7 +9,7 @@
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const FileView = Whisper.View.extend({
|
||||
@@ -62,10 +62,7 @@
|
||||
const VideoView = MediaView.extend({ tagName: 'video' });
|
||||
|
||||
// Blacklist common file types known to be unsupported in Chrome
|
||||
const unsupportedFileTypes = [
|
||||
'audio/aiff',
|
||||
'video/quicktime',
|
||||
];
|
||||
const unsupportedFileTypes = ['audio/aiff', 'video/quicktime'];
|
||||
|
||||
Whisper.AttachmentView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
@@ -122,8 +119,11 @@
|
||||
Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
|
||||
},
|
||||
isVoiceMessage() {
|
||||
if (
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -241,4 +241,4 @@
|
||||
this.trigger('update');
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -16,13 +13,13 @@
|
||||
this.message = options.message;
|
||||
this.callbacks = {
|
||||
onDismiss: options.onDismiss,
|
||||
onClick: options.onClick
|
||||
onClick: options.onClick,
|
||||
};
|
||||
this.render();
|
||||
},
|
||||
render_attributes: function() {
|
||||
return {
|
||||
message: this.message
|
||||
message: this.message,
|
||||
};
|
||||
},
|
||||
onDismiss: function(e) {
|
||||
@@ -31,6 +28,6 @@
|
||||
},
|
||||
onClick: function() {
|
||||
this.callbacks.onClick();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -21,7 +18,7 @@
|
||||
this.render();
|
||||
},
|
||||
events: {
|
||||
'keyup': 'onKeyup',
|
||||
keyup: 'onKeyup',
|
||||
'click .ok': 'ok',
|
||||
'click .cancel': 'cancel',
|
||||
},
|
||||
@@ -30,7 +27,7 @@
|
||||
message: this.message,
|
||||
showCancel: !this.hideCancel,
|
||||
cancel: this.cancelText,
|
||||
ok: this.okText
|
||||
ok: this.okText,
|
||||
};
|
||||
},
|
||||
ok: function() {
|
||||
@@ -52,6 +49,6 @@
|
||||
},
|
||||
focusCancel: function() {
|
||||
this.$('.cancel').focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -12,7 +9,7 @@
|
||||
className: 'contact',
|
||||
templateName: 'contact',
|
||||
events: {
|
||||
'click': 'showIdentity'
|
||||
click: 'showIdentity',
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
@@ -25,7 +22,7 @@
|
||||
return {
|
||||
title: i18n('me'),
|
||||
number: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar()
|
||||
avatar: this.model.getAvatar(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +33,7 @@
|
||||
avatar: this.model.getAvatar(),
|
||||
profileName: this.model.getProfileName(),
|
||||
isVerified: this.model.isVerified(),
|
||||
verified: i18n('verified')
|
||||
verified: i18n('verified'),
|
||||
};
|
||||
},
|
||||
showIdentity: function() {
|
||||
@@ -44,10 +41,10 @@
|
||||
return;
|
||||
}
|
||||
var view = new Whisper.KeyVerificationPanelView({
|
||||
model: this.model
|
||||
model: this.model,
|
||||
});
|
||||
this.listenBack(view);
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -13,27 +10,43 @@
|
||||
},
|
||||
templateName: 'conversation-preview',
|
||||
events: {
|
||||
'click': 'select'
|
||||
click: 'select',
|
||||
},
|
||||
initialize: function() {
|
||||
// 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, 'opened', this.markSelected); // auto update
|
||||
|
||||
var updateLastMessage = _.debounce(this.model.updateLastMessage.bind(this.model), 1000);
|
||||
this.listenTo(this.model.messageCollection, 'add remove', updateLastMessage);
|
||||
var updateLastMessage = _.debounce(
|
||||
this.model.updateLastMessage.bind(this.model),
|
||||
1000
|
||||
);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'add remove',
|
||||
updateLastMessage
|
||||
);
|
||||
this.listenTo(this.model, 'newmessage', updateLastMessage);
|
||||
|
||||
extension.windows.onClosed(function() {
|
||||
extension.windows.onClosed(
|
||||
function() {
|
||||
this.stopListening();
|
||||
}.bind(this));
|
||||
this.timeStampView = new Whisper.TimestampView({brief: true});
|
||||
}.bind(this)
|
||||
);
|
||||
this.timeStampView = new Whisper.TimestampView({ brief: true });
|
||||
this.model.updateLastMessage();
|
||||
},
|
||||
|
||||
markSelected: function() {
|
||||
this.$el.addClass('selected').siblings('.selected').removeClass('selected');
|
||||
this.$el
|
||||
.addClass('selected')
|
||||
.siblings('.selected')
|
||||
.removeClass('selected');
|
||||
},
|
||||
|
||||
select: function(e) {
|
||||
@@ -43,15 +56,19 @@
|
||||
|
||||
render: function() {
|
||||
this.$el.html(
|
||||
Mustache.render(_.result(this,'template', ''), {
|
||||
Mustache.render(
|
||||
_.result(this, 'template', ''),
|
||||
{
|
||||
title: this.model.getTitle(),
|
||||
last_message: this.model.get('lastMessage'),
|
||||
last_message_timestamp: this.model.get('timestamp'),
|
||||
number: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar(),
|
||||
profileName: this.model.getProfileName(),
|
||||
unreadCount: this.model.get('unreadCount')
|
||||
}, this.render_partials())
|
||||
unreadCount: this.model.get('unreadCount'),
|
||||
},
|
||||
this.render_partials()
|
||||
)
|
||||
);
|
||||
this.timeStampView.setElement(this.$('.last-timestamp'));
|
||||
this.timeStampView.update();
|
||||
@@ -67,7 +84,6 @@
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -56,6 +53,6 @@
|
||||
if ($el && $el.length > 0) {
|
||||
$el.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -3,13 +3,12 @@
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const isSearchable = conversation =>
|
||||
conversation.isSearchable();
|
||||
const isSearchable = conversation => conversation.isSearchable();
|
||||
|
||||
Whisper.NewContactView = Whisper.View.extend({
|
||||
templateName: 'new-contact',
|
||||
@@ -46,7 +45,9 @@
|
||||
// View to display the matched contacts from typeahead
|
||||
this.typeahead_view = new Whisper.ConversationListView({
|
||||
collection: new Whisper.ConversationCollection([], {
|
||||
comparator(m) { return m.getTitle().toLowerCase(); },
|
||||
comparator(m) {
|
||||
return m.getTitle().toLowerCase();
|
||||
},
|
||||
}),
|
||||
});
|
||||
this.$el.append(this.typeahead_view.el);
|
||||
@@ -75,8 +76,11 @@
|
||||
/* eslint-disable more/no-then */
|
||||
this.pending = this.pending.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 */
|
||||
this.trigger('show');
|
||||
} else {
|
||||
@@ -105,8 +109,10 @@
|
||||
}
|
||||
|
||||
const newConversationId = this.new_contact_view.model.id;
|
||||
const conversation =
|
||||
await ConversationController.getOrCreateAndWait(newConversationId, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
newConversationId,
|
||||
'private'
|
||||
);
|
||||
this.trigger('open', conversation);
|
||||
this.initNewContact();
|
||||
this.resetTypeahead();
|
||||
@@ -129,7 +135,9 @@
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.typeahead.fetchAlphabetical().then(() => {
|
||||
if (this.typeahead.length > 0) {
|
||||
this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable));
|
||||
this.typeahead_view.collection.reset(
|
||||
this.typeahead.filter(isSearchable)
|
||||
);
|
||||
} else {
|
||||
this.showHints();
|
||||
}
|
||||
@@ -163,4 +171,4 @@
|
||||
return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -13,7 +13,7 @@
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -120,20 +120,32 @@
|
||||
this.listenTo(this.model, 'destroy', this.stopListening);
|
||||
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
|
||||
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, 'delivered', this.updateMessage);
|
||||
this.listenTo(this.model, 'read', this.updateMessage);
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
this.listenTo(this.model, 'expired', this.onExpired);
|
||||
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.model.messageCollection,
|
||||
'scroll-to-message',
|
||||
this.scrollToMessage
|
||||
);
|
||||
this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'reply',
|
||||
this.setQuoteMessage
|
||||
);
|
||||
|
||||
this.lazyUpdateVerified = _.debounce(
|
||||
this.model.updateVerified.bind(this.model),
|
||||
@@ -247,7 +259,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
||||
if (this.isHidden() && this.lastActivity < oneHourAgo) {
|
||||
this.unload('inactivity');
|
||||
} else if (this.view.atBottom()) {
|
||||
@@ -301,7 +313,7 @@
|
||||
|
||||
this.remove();
|
||||
|
||||
this.model.messageCollection.forEach((model) => {
|
||||
this.model.messageCollection.forEach(model => {
|
||||
model.trigger('unload');
|
||||
});
|
||||
this.model.messageCollection.reset([]);
|
||||
@@ -333,19 +345,21 @@
|
||||
);
|
||||
|
||||
this.model.messageCollection.remove(models);
|
||||
_.forEach(models, (model) => {
|
||||
_.forEach(models, model => {
|
||||
model.trigger('unload');
|
||||
});
|
||||
},
|
||||
|
||||
markAllAsVerifiedDefault(unverified) {
|
||||
return Promise.all(unverified.map((contact) => {
|
||||
return Promise.all(
|
||||
unverified.map(contact => {
|
||||
if (contact.isUnverified()) {
|
||||
return contact.setVerifiedDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
markAllAsApproved(untrusted) {
|
||||
@@ -404,7 +418,10 @@
|
||||
}
|
||||
},
|
||||
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();
|
||||
} else {
|
||||
this.$('.capture-audio').show();
|
||||
@@ -495,11 +512,14 @@
|
||||
|
||||
const statusPromise = this.throttledGetProfiles();
|
||||
// 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.statusFetch = null;
|
||||
console.log('done with status fetch');
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
// 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
|
||||
@@ -587,20 +607,25 @@
|
||||
|
||||
const conversationId = this.model.get('id');
|
||||
const WhisperMessageCollection = Whisper.MessageCollection;
|
||||
const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments({
|
||||
const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments(
|
||||
{
|
||||
conversationId,
|
||||
count: DEFAULT_MEDIA_FETCH_COUNT,
|
||||
WhisperMessageCollection,
|
||||
});
|
||||
const documents = await Signal.Backbone.Conversation.fetchFileAttachments({
|
||||
}
|
||||
);
|
||||
const documents = await Signal.Backbone.Conversation.fetchFileAttachments(
|
||||
{
|
||||
conversationId,
|
||||
count: DEFAULT_DOCUMENTS_FETCH_COUNT,
|
||||
WhisperMessageCollection,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// NOTE: Could we show grid previews from disk as well?
|
||||
const loadMessages = Signal.Components.Types.Message
|
||||
.loadWithObjectURL(Signal.Migrations.loadMessage);
|
||||
const loadMessages = Signal.Components.Types.Message.loadWithObjectURL(
|
||||
Signal.Migrations.loadMessage
|
||||
);
|
||||
const media = await loadMessages(rawMedia);
|
||||
|
||||
const { getAbsoluteAttachmentPath } = Signal.Migrations;
|
||||
@@ -624,13 +649,15 @@
|
||||
|
||||
case 'media': {
|
||||
const mediaWithObjectURL = media.map(mediaMessage =>
|
||||
Object.assign(
|
||||
{},
|
||||
mediaMessage,
|
||||
{ objectURL: getAbsoluteAttachmentPath(mediaMessage.attachments[0].path) }
|
||||
));
|
||||
const selectedIndex = media.findIndex(mediaMessage =>
|
||||
mediaMessage.id === message.id);
|
||||
Object.assign({}, mediaMessage, {
|
||||
objectURL: getAbsoluteAttachmentPath(
|
||||
mediaMessage.attachments[0].path
|
||||
),
|
||||
})
|
||||
);
|
||||
const selectedIndex = media.findIndex(
|
||||
mediaMessage => mediaMessage.id === message.id
|
||||
);
|
||||
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
||||
Component: Signal.Components.LightboxGallery,
|
||||
props: {
|
||||
@@ -684,7 +711,7 @@
|
||||
|
||||
// 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.
|
||||
this.model.messageCollection.forEach((model) => {
|
||||
this.model.messageCollection.forEach(model => {
|
||||
if (!model.get('unread')) {
|
||||
return;
|
||||
}
|
||||
@@ -744,7 +771,7 @@
|
||||
const delta = endingHeight - startingHeight;
|
||||
const height = this.view.outerHeight;
|
||||
|
||||
const newScrollPosition = (this.view.scrollPosition + delta) - height;
|
||||
const newScrollPosition = this.view.scrollPosition + delta - height;
|
||||
this.view.$el.scrollTop(newScrollPosition);
|
||||
}, 1);
|
||||
},
|
||||
@@ -759,15 +786,17 @@
|
||||
// Avoiding await, since we want to capture the promise and make it available via
|
||||
// this.inProgressFetch
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.inProgressFetch = this.model.fetchContacts()
|
||||
this.inProgressFetch = this.model
|
||||
.fetchContacts()
|
||||
.then(() => this.model.fetchMessages())
|
||||
.then(() => {
|
||||
this.$('.bar-container').hide();
|
||||
this.model.messageCollection.where({ unread: 1 }).forEach((m) => {
|
||||
this.model.messageCollection.where({ unread: 1 }).forEach(m => {
|
||||
m.fetch();
|
||||
});
|
||||
this.inProgressFetch = null;
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(
|
||||
'fetchMessages error:',
|
||||
error && error.stack ? error.stack : error
|
||||
@@ -820,8 +849,10 @@
|
||||
// The conversation is visible, but window is not focused
|
||||
if (!this.lastSeenIndicator) {
|
||||
this.resetLastSeenIndicator({ scroll: false });
|
||||
} else if (this.view.atBottom() &&
|
||||
this.model.get('unreadCount') === this.lastSeenIndicator.getCount()) {
|
||||
} else if (
|
||||
this.view.atBottom() &&
|
||||
this.model.get('unreadCount') === this.lastSeenIndicator.getCount()
|
||||
) {
|
||||
// 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.
|
||||
// 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'
|
||||
? '.bottom-bar'
|
||||
: '.send';
|
||||
const selector =
|
||||
storage.get('theme-setting') === 'ios' ? '.bottom-bar' : '.send';
|
||||
|
||||
this.$(selector).prepend(this.quoteView.el);
|
||||
this.updateMessageFieldSize({});
|
||||
@@ -1275,7 +1305,7 @@
|
||||
},
|
||||
|
||||
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 val = emoji.map.colons[idx];
|
||||
if (val) {
|
||||
@@ -1310,7 +1340,12 @@
|
||||
updateMessageFieldSize(event) {
|
||||
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
|
||||
event.preventDefault();
|
||||
this.$('.bottom-bar form').submit();
|
||||
@@ -1329,7 +1364,8 @@
|
||||
? this.quoteView.$el.outerHeight(includeMargin)
|
||||
: 0;
|
||||
|
||||
const height = this.$messageField.outerHeight() +
|
||||
const height =
|
||||
this.$messageField.outerHeight() +
|
||||
$attachmentPreviews.outerHeight() +
|
||||
this.$emojiPanelContainer.outerHeight() +
|
||||
quoteHeight +
|
||||
@@ -1350,8 +1386,10 @@
|
||||
},
|
||||
|
||||
isHidden() {
|
||||
return this.$el.css('display') === 'none' ||
|
||||
this.$('.panel').css('display') === 'none';
|
||||
return (
|
||||
this.$el.css('display') === 'none' ||
|
||||
this.$('.panel').css('display') === 'none'
|
||||
);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -2,7 +2,7 @@
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -27,7 +27,7 @@
|
||||
this.$('textarea').val(i18n('loading'));
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
window.log.fetch().then((text) => {
|
||||
window.log.fetch().then(text => {
|
||||
this.$('textarea').val(text);
|
||||
});
|
||||
},
|
||||
@@ -63,7 +63,9 @@
|
||||
});
|
||||
this.$('.loading').removeClass('loading');
|
||||
view.render();
|
||||
this.$('.link').focus().select();
|
||||
this.$('.link')
|
||||
.focus()
|
||||
.select();
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -11,6 +8,6 @@
|
||||
templateName: 'generic-error',
|
||||
render_attributes: function() {
|
||||
return this.model;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -7,7 +7,7 @@
|
||||
/* global Signal: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -29,7 +29,7 @@
|
||||
});
|
||||
|
||||
function makeImageThumbnail(size, objectUrl) {
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = document.createElement('img');
|
||||
img.onerror = reject;
|
||||
img.onload = () => {
|
||||
@@ -60,18 +60,20 @@
|
||||
resolve(blob);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function makeVideoScreenshot(objectUrl) {
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
function capture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
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'));
|
||||
|
||||
@@ -81,7 +83,7 @@
|
||||
}
|
||||
|
||||
video.addEventListener('canplay', capture);
|
||||
video.addEventListener('error', (error) => {
|
||||
video.addEventListener('error', error => {
|
||||
console.log(
|
||||
'makeVideoThumbnail error',
|
||||
Signal.Types.Errors.toLogFormat(error)
|
||||
@@ -90,7 +92,7 @@
|
||||
});
|
||||
|
||||
video.src = objectUrl;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function blobToArrayBuffer(blob) {
|
||||
@@ -123,7 +125,7 @@
|
||||
className: 'file-input',
|
||||
initialize(options) {
|
||||
this.$input = this.$('input[type=file]');
|
||||
this.$input.click((e) => {
|
||||
this.$input.click(e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
this.thumb = new Whisper.AttachmentPreviewView();
|
||||
@@ -146,15 +148,18 @@
|
||||
e.preventDefault();
|
||||
// hack
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
entry.file((file) => {
|
||||
entry.file(file => {
|
||||
this.file = file;
|
||||
this.previewImages();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.$input.click();
|
||||
}
|
||||
@@ -178,14 +183,16 @@
|
||||
},
|
||||
|
||||
autoScale(file) {
|
||||
if (file.type.split('/')[0] !== 'image' ||
|
||||
if (
|
||||
file.type.split('/')[0] !== 'image' ||
|
||||
file.type === 'image/gif' ||
|
||||
file.type === 'image/tiff') {
|
||||
file.type === 'image/tiff'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = document.createElement('img');
|
||||
img.onerror = reject;
|
||||
@@ -195,13 +202,19 @@
|
||||
const maxSize = 6000 * 1024;
|
||||
const maxHeight = 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);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = loadImage.scale(img, {
|
||||
canvas: true, maxWidth, maxHeight,
|
||||
canvas: true,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
});
|
||||
|
||||
let quality = 0.95;
|
||||
@@ -209,8 +222,10 @@
|
||||
let blob;
|
||||
do {
|
||||
i -= 1;
|
||||
blob = window.dataURLToBlobSync(canvas.toDataURL('image/jpeg', quality));
|
||||
quality = (quality * maxSize) / blob.size;
|
||||
blob = window.dataURLToBlobSync(
|
||||
canvas.toDataURL('image/jpeg', quality)
|
||||
);
|
||||
quality = quality * maxSize / blob.size;
|
||||
// NOTE: During testing with a large image, we observed the
|
||||
// `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
|
||||
@@ -222,7 +237,7 @@
|
||||
resolve(blob);
|
||||
};
|
||||
img.src = url;
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
async previewImages() {
|
||||
@@ -271,21 +286,25 @@
|
||||
|
||||
const blob = await this.autoScale(file);
|
||||
let limitKb = 1000000;
|
||||
const blobType = file.type === 'image/gif'
|
||||
? 'gif'
|
||||
: contentType.split('/')[0];
|
||||
const blobType =
|
||||
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
|
||||
|
||||
switch (blobType) {
|
||||
case 'image':
|
||||
limitKb = 6000; break;
|
||||
limitKb = 6000;
|
||||
break;
|
||||
case 'gif':
|
||||
limitKb = 25000; break;
|
||||
limitKb = 25000;
|
||||
break;
|
||||
case 'audio':
|
||||
limitKb = 100000; break;
|
||||
limitKb = 100000;
|
||||
break;
|
||||
case 'video':
|
||||
limitKb = 100000; break;
|
||||
limitKb = 100000;
|
||||
break;
|
||||
default:
|
||||
limitKb = 100000; break;
|
||||
limitKb = 100000;
|
||||
break;
|
||||
}
|
||||
if ((blob.size / 1024).toFixed(4) >= limitKb) {
|
||||
const units = ['kB', 'MB', 'GB'];
|
||||
@@ -310,7 +329,9 @@
|
||||
},
|
||||
|
||||
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)));
|
||||
this.clearForm();
|
||||
return promise;
|
||||
@@ -325,7 +346,7 @@
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const setFlags = flags => (attachment) => {
|
||||
const setFlags = flags => attachment => {
|
||||
const newAttachment = Object.assign({}, attachment);
|
||||
if (flags) {
|
||||
newAttachment.flags = flags;
|
||||
@@ -345,9 +366,11 @@
|
||||
// Scale and crop an image to 256px square
|
||||
const size = 256;
|
||||
const file = this.file || this.$input.prop('files')[0];
|
||||
if (file === undefined ||
|
||||
if (
|
||||
file === undefined ||
|
||||
file.type.split('/')[0] !== 'image' ||
|
||||
file.type === 'image/gif') {
|
||||
file.type === 'image/gif'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -362,9 +385,9 @@
|
||||
|
||||
// File -> Promise Attachment
|
||||
readFile(file) {
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.onload = (e) => {
|
||||
FR.onload = e => {
|
||||
resolve({
|
||||
data: e.target.result,
|
||||
contentType: file.type,
|
||||
@@ -375,7 +398,7 @@
|
||||
FR.onerror = reject;
|
||||
FR.onabort = reject;
|
||||
FR.readAsArrayBuffer(file);
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
@@ -390,9 +413,14 @@
|
||||
},
|
||||
|
||||
deleteFiles(e) {
|
||||
if (e) { e.stopPropagation(); }
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
this.clearForm();
|
||||
this.$input.wrap('<form>').parent('form').trigger('reset');
|
||||
this.$input
|
||||
.wrap('<form>')
|
||||
.parent('form')
|
||||
.trigger('reset');
|
||||
this.$input.unwrap();
|
||||
this.file = null;
|
||||
this.$input.trigger('change');
|
||||
@@ -450,4 +478,4 @@
|
||||
Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail;
|
||||
Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail;
|
||||
Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot;
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -18,8 +15,8 @@
|
||||
collection: this.model,
|
||||
className: 'members',
|
||||
toInclude: {
|
||||
listenBack: options.listenBack
|
||||
}
|
||||
listenBack: options.listenBack,
|
||||
},
|
||||
});
|
||||
this.member_list_view.render();
|
||||
|
||||
@@ -33,8 +30,8 @@
|
||||
|
||||
return {
|
||||
members: i18n('groupMembers'),
|
||||
summary: summary
|
||||
summary: summary,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.GroupUpdateView = Backbone.View.extend({
|
||||
tagName: "div",
|
||||
className: "group-update",
|
||||
tagName: 'div',
|
||||
className: 'group-update',
|
||||
render: function() {
|
||||
//TODO l10n
|
||||
if (this.model.left) {
|
||||
@@ -27,7 +24,6 @@
|
||||
this.$el.text(messages.join(' '));
|
||||
|
||||
return this;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -12,6 +9,6 @@
|
||||
},
|
||||
render_attributes: function() {
|
||||
return { content: this.content };
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -16,16 +13,18 @@
|
||||
},
|
||||
getSVGUrl: function() {
|
||||
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);
|
||||
},
|
||||
getDataUrl: function() {
|
||||
var svgurl = this.getSVGUrl();
|
||||
return new Promise(function(resolve) {
|
||||
var img = document.createElement('img');
|
||||
img.onload = function () {
|
||||
img.onload = function() {
|
||||
var canvas = loadImage.scale(img, {
|
||||
canvas: true, maxWidth: 100, maxHeight: 100
|
||||
canvas: true,
|
||||
maxWidth: 100,
|
||||
maxHeight: 100,
|
||||
});
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
@@ -35,25 +34,24 @@
|
||||
|
||||
img.src = svgurl;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var COLORS = {
|
||||
red : '#EF5350',
|
||||
pink : '#EC407A',
|
||||
purple : '#AB47BC',
|
||||
deep_purple : '#7E57C2',
|
||||
indigo : '#5C6BC0',
|
||||
blue : '#2196F3',
|
||||
light_blue : '#03A9F4',
|
||||
cyan : '#00BCD4',
|
||||
teal : '#009688',
|
||||
green : '#4CAF50',
|
||||
light_green : '#7CB342',
|
||||
orange : '#FF9800',
|
||||
deep_orange : '#FF5722',
|
||||
amber : '#FFB300',
|
||||
blue_grey : '#607D8B'
|
||||
red: '#EF5350',
|
||||
pink: '#EC407A',
|
||||
purple: '#AB47BC',
|
||||
deep_purple: '#7E57C2',
|
||||
indigo: '#5C6BC0',
|
||||
blue: '#2196F3',
|
||||
light_blue: '#03A9F4',
|
||||
cyan: '#00BCD4',
|
||||
teal: '#009688',
|
||||
green: '#4CAF50',
|
||||
light_green: '#7CB342',
|
||||
orange: '#FF9800',
|
||||
deep_orange: '#FF5722',
|
||||
amber: '#FFB300',
|
||||
blue_grey: '#607D8B',
|
||||
};
|
||||
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -18,11 +15,11 @@
|
||||
events: {
|
||||
'click .show-safety-number': 'showSafetyNumber',
|
||||
'click .send-anyway': 'sendAnyway',
|
||||
'click .cancel': 'cancel'
|
||||
'click .cancel': 'cancel',
|
||||
},
|
||||
showSafetyNumber: function() {
|
||||
var view = new Whisper.KeyVerificationPanelView({
|
||||
model: this.model
|
||||
model: this.model,
|
||||
});
|
||||
this.listenBack(view);
|
||||
},
|
||||
@@ -39,13 +36,16 @@
|
||||
send = i18n('resend');
|
||||
}
|
||||
|
||||
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]);
|
||||
var errorExplanation = i18n('identityKeyErrorOnSend', [
|
||||
this.model.getTitle(),
|
||||
this.model.getTitle(),
|
||||
]);
|
||||
return {
|
||||
errorExplanation : errorExplanation,
|
||||
showSafetyNumber : i18n('showSafetyNumber'),
|
||||
sendAnyway : send,
|
||||
cancel : i18n('cancel')
|
||||
errorExplanation: errorExplanation,
|
||||
showSafetyNumber: i18n('showSafetyNumber'),
|
||||
sendAnyway: send,
|
||||
cancel: i18n('cancel'),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -36,7 +33,7 @@
|
||||
},
|
||||
reset: function() {
|
||||
return Whisper.Database.clear();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Whisper.ImportView = Whisper.View.extend({
|
||||
@@ -102,16 +99,19 @@
|
||||
this.trigger('cancel');
|
||||
},
|
||||
onImport: function() {
|
||||
window.Signal.Backup.getDirectoryForImport().then(function(directory) {
|
||||
window.Signal.Backup.getDirectoryForImport().then(
|
||||
function(directory) {
|
||||
this.doImport(directory);
|
||||
}.bind(this), function(error) {
|
||||
}.bind(this),
|
||||
function(error) {
|
||||
if (error.name !== 'ChooseError') {
|
||||
console.log(
|
||||
'Error choosing directory:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
onRegister: function() {
|
||||
// AppView listens for this, and opens up InstallView to the QR code step to
|
||||
@@ -127,15 +127,19 @@
|
||||
this.render();
|
||||
|
||||
// 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
|
||||
return Whisper.Import.reset();
|
||||
}).then(function() {
|
||||
})
|
||||
.then(function() {
|
||||
return Promise.all([
|
||||
Whisper.Import.start(),
|
||||
window.Signal.Backup.importFromDirectory(directory)
|
||||
window.Signal.Backup.importFromDirectory(directory),
|
||||
]);
|
||||
}).then(function(results) {
|
||||
})
|
||||
.then(
|
||||
function(results) {
|
||||
var importResult = results[1];
|
||||
|
||||
// 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
|
||||
// normal link to finish the process.
|
||||
return this.finishLightImport(directory);
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.log('Error importing:', error && error.stack ? error.stack : error);
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(
|
||||
function(error) {
|
||||
console.log(
|
||||
'Error importing:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
this.error = error || new Error('Something went wrong!');
|
||||
this.state = null;
|
||||
this.render();
|
||||
|
||||
return Whisper.Import.reset();
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
finishLightImport: function(directory) {
|
||||
ConversationController.reset();
|
||||
|
||||
return ConversationController.load().then(function() {
|
||||
return ConversationController.load()
|
||||
.then(function() {
|
||||
return Promise.all([
|
||||
Whisper.Import.saveLocation(directory),
|
||||
Whisper.Import.complete(),
|
||||
]);
|
||||
}).then(function() {
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
this.state = State.LIGHT_COMPLETE;
|
||||
this.render();
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
finishFullImport: function(directory) {
|
||||
// Catching in-memory cache up with what's in indexeddb now...
|
||||
// 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.
|
||||
return storage.fetch()
|
||||
return storage
|
||||
.fetch()
|
||||
.then(function() {
|
||||
return Promise.all([
|
||||
// Clearing any migration-related state inherited from the Chrome App
|
||||
@@ -183,12 +199,15 @@
|
||||
storage.remove('migrationStorageLocation'),
|
||||
|
||||
Whisper.Import.saveLocation(directory),
|
||||
Whisper.Import.complete()
|
||||
Whisper.Import.complete(),
|
||||
]);
|
||||
}).then(function() {
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
this.state = State.COMPLETE;
|
||||
this.render();
|
||||
}.bind(this));
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -5,7 +5,7 @@
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -15,7 +15,10 @@
|
||||
open(conversation) {
|
||||
const id = `conversation-${conversation.cid}`;
|
||||
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();
|
||||
});
|
||||
let $el = this.$(`#${id}`);
|
||||
@@ -65,7 +68,6 @@
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Whisper.AppLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'app-loading-screen',
|
||||
className: 'app-loading-screen',
|
||||
@@ -147,7 +149,8 @@
|
||||
);
|
||||
|
||||
this.networkStatusView = new Whisper.NetworkStatusView();
|
||||
this.$el.find('.network-status-container')
|
||||
this.$el
|
||||
.find('.network-status-container')
|
||||
.append(this.networkStatusView.render().el);
|
||||
|
||||
extension.windows.onClosed(() => {
|
||||
@@ -194,7 +197,8 @@
|
||||
default:
|
||||
console.log(
|
||||
'Whisper.InboxView::startConnectionListener:',
|
||||
'Unknown web socket status:', status
|
||||
'Unknown web socket status:',
|
||||
status
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -254,7 +258,9 @@
|
||||
openConversation(e, conversation) {
|
||||
this.searchView.hideHints();
|
||||
if (conversation) {
|
||||
this.conversation_stack.open(ConversationController.get(conversation.id));
|
||||
this.conversation_stack.open(
|
||||
ConversationController.get(conversation.id)
|
||||
);
|
||||
this.focusConversation();
|
||||
}
|
||||
},
|
||||
@@ -279,4 +285,4 @@
|
||||
};
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -34,23 +31,24 @@
|
||||
this.on('disconnected', this.reconnect);
|
||||
|
||||
// Keep data around if it's a re-link, or the middle of a light import
|
||||
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
|
||||
this.shouldRetainData =
|
||||
Whisper.Registration.everDone() || options.hasExistingData;
|
||||
},
|
||||
render_attributes: function() {
|
||||
var errorMessage;
|
||||
|
||||
if (this.error) {
|
||||
if (this.error.name === 'HTTPError'
|
||||
&& this.error.code == TOO_MANY_DEVICES) {
|
||||
|
||||
if (
|
||||
this.error.name === 'HTTPError' &&
|
||||
this.error.code == TOO_MANY_DEVICES
|
||||
) {
|
||||
errorMessage = i18n('installTooManyDevices');
|
||||
}
|
||||
else if (this.error.name === 'HTTPError'
|
||||
&& this.error.code == CONNECTION_ERROR) {
|
||||
|
||||
} else if (
|
||||
this.error.name === 'HTTPError' &&
|
||||
this.error.code == CONNECTION_ERROR
|
||||
) {
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
}
|
||||
else if (this.error.message === 'websocket closed') {
|
||||
} else if (this.error.message === 'websocket closed') {
|
||||
// AccountManager.registerSecondDevice uses this specific
|
||||
// 'websocket closed' error message
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
@@ -95,10 +93,12 @@
|
||||
|
||||
var accountManager = getAccountManager();
|
||||
|
||||
accountManager.registerSecondDevice(
|
||||
accountManager
|
||||
.registerSecondDevice(
|
||||
this.setProvisioningUrl.bind(this),
|
||||
this.confirmNumber.bind(this)
|
||||
).catch(this.handleDisconnect.bind(this));
|
||||
)
|
||||
.catch(this.handleDisconnect.bind(this));
|
||||
},
|
||||
handleDisconnect: function(e) {
|
||||
console.log('provisioning failed', e.stack);
|
||||
@@ -108,9 +108,10 @@
|
||||
|
||||
if (e.message === 'websocket closed') {
|
||||
this.trigger('disconnected');
|
||||
} else if (e.name !== 'HTTPError'
|
||||
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
|
||||
|
||||
} else if (
|
||||
e.name !== 'HTTPError' ||
|
||||
(e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
@@ -155,13 +156,15 @@
|
||||
this.selectStep(Steps.ENTER_NAME);
|
||||
this.setDeviceNameDefault();
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
this.$('#link-phone').submit(function(e) {
|
||||
return new Promise(
|
||||
function(resolve, reject) {
|
||||
this.$('#link-phone').submit(
|
||||
function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
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) {
|
||||
this.$(DEVICE_NAME_SELECTOR).focus();
|
||||
return;
|
||||
@@ -189,8 +192,10 @@
|
||||
);
|
||||
finish();
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -17,15 +14,15 @@
|
||||
this.theirKey = options.newKey;
|
||||
}
|
||||
|
||||
this.loadKeys().then(function() {
|
||||
this.loadKeys().then(
|
||||
function() {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
loadKeys: function() {
|
||||
return Promise.all([
|
||||
this.loadTheirKey(),
|
||||
this.loadOurKey(),
|
||||
]).then(this.generateSecurityNumber.bind(this))
|
||||
return Promise.all([this.loadTheirKey(), this.loadOurKey()])
|
||||
.then(this.generateSecurityNumber.bind(this))
|
||||
.then(this.render.bind(this));
|
||||
//.then(this.makeQRCode.bind(this));
|
||||
},
|
||||
@@ -37,32 +34,37 @@
|
||||
);
|
||||
},
|
||||
loadTheirKey: function() {
|
||||
return textsecure.storage.protocol.loadIdentityKey(
|
||||
this.model.id
|
||||
).then(function(theirKey) {
|
||||
return textsecure.storage.protocol.loadIdentityKey(this.model.id).then(
|
||||
function(theirKey) {
|
||||
this.theirKey = theirKey;
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
loadOurKey: function() {
|
||||
return textsecure.storage.protocol.loadIdentityKey(
|
||||
this.ourNumber
|
||||
).then(function(ourKey) {
|
||||
return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then(
|
||||
function(ourKey) {
|
||||
this.ourKey = ourKey;
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
generateSecurityNumber: function() {
|
||||
return new libsignal.FingerprintGenerator(5200).createFor(
|
||||
this.ourNumber, this.ourKey, this.model.id, this.theirKey
|
||||
).then(function(securityNumber) {
|
||||
return new libsignal.FingerprintGenerator(5200)
|
||||
.createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
|
||||
.then(
|
||||
function(securityNumber) {
|
||||
this.securityNumber = securityNumber;
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
onSafetyNumberChanged: function() {
|
||||
this.model.getProfiles().then(this.loadKeys.bind(this));
|
||||
|
||||
var dialog = new Whisper.ConfirmationDialogView({
|
||||
message: i18n('changedRightAfterVerify', [this.model.getTitle(), this.model.getTitle()]),
|
||||
hideCancel: true
|
||||
message: i18n('changedRightAfterVerify', [
|
||||
this.model.getTitle(),
|
||||
this.model.getTitle(),
|
||||
]),
|
||||
hideCancel: true,
|
||||
});
|
||||
|
||||
dialog.$el.insertBefore(this.el);
|
||||
@@ -70,7 +72,10 @@
|
||||
},
|
||||
toggleVerified: function() {
|
||||
this.$('button.verify').attr('disabled', true);
|
||||
this.model.toggleVerified().catch(function(result) {
|
||||
this.model
|
||||
.toggleVerified()
|
||||
.catch(
|
||||
function(result) {
|
||||
if (result instanceof Error) {
|
||||
if (result.name === 'OutgoingIdentityKeyError') {
|
||||
this.onSafetyNumberChanged();
|
||||
@@ -89,33 +94,42 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
}.bind(this)).then(function() {
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function() {
|
||||
this.$('button.verify').removeAttr('disabled');
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
render_attributes: function() {
|
||||
var s = this.securityNumber;
|
||||
var chunks = [];
|
||||
for (var i = 0; i < s.length; i += 5) {
|
||||
chunks.push(s.substring(i, i+5));
|
||||
chunks.push(s.substring(i, i + 5));
|
||||
}
|
||||
var name = this.model.getTitle();
|
||||
var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name);
|
||||
var isVerified = this.model.isVerified();
|
||||
var verifyButton = isVerified ? i18n('unverify') : i18n('verify');
|
||||
var verifiedStatus = isVerified ? i18n('isVerified', name) : i18n('isNotVerified', name);
|
||||
var verifiedStatus = isVerified
|
||||
? i18n('isVerified', name)
|
||||
: i18n('isNotVerified', name);
|
||||
|
||||
return {
|
||||
learnMore : i18n('learnMore'),
|
||||
theirKeyUnknown : i18n('theirIdentityUnknown'),
|
||||
yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()),
|
||||
verifyHelp : i18n('verifyHelp', this.model.getTitle()),
|
||||
verifyButton : verifyButton,
|
||||
hasTheirKey : this.theirKey !== undefined,
|
||||
chunks : chunks,
|
||||
isVerified : isVerified,
|
||||
verifiedStatus : verifiedStatus
|
||||
learnMore: i18n('learnMore'),
|
||||
theirKeyUnknown: i18n('theirIdentityUnknown'),
|
||||
yourSafetyNumberWith: i18n(
|
||||
'yourSafetyNumberWith',
|
||||
this.model.getTitle()
|
||||
),
|
||||
verifyHelp: i18n('verifyHelp', this.model.getTitle()),
|
||||
verifyButton: verifyButton,
|
||||
hasTheirKey: this.theirKey !== undefined,
|
||||
chunks: chunks,
|
||||
isVerified: isVerified,
|
||||
verifiedStatus: verifiedStatus,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -25,12 +22,14 @@
|
||||
},
|
||||
|
||||
render_attributes: function() {
|
||||
var unreadMessages = this.count === 1 ? i18n('unreadMessage')
|
||||
var unreadMessages =
|
||||
this.count === 1
|
||||
? i18n('unreadMessage')
|
||||
: i18n('unreadMessages', [this.count]);
|
||||
|
||||
return {
|
||||
unreadMessages: unreadMessages
|
||||
unreadMessages: unreadMessages,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -20,7 +17,7 @@
|
||||
|
||||
addOne: function(model) {
|
||||
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);
|
||||
this.$el.append(view.render().el);
|
||||
this.$el.trigger('add');
|
||||
@@ -35,6 +32,6 @@
|
||||
render: function() {
|
||||
this.addAll();
|
||||
return this;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -25,14 +22,14 @@
|
||||
});
|
||||
},
|
||||
events: {
|
||||
'click': 'onClick'
|
||||
click: 'onClick',
|
||||
},
|
||||
onClick: function() {
|
||||
if (this.outgoingKeyError) {
|
||||
var view = new Whisper.IdentityKeySendErrorPanelView({
|
||||
model: this.model,
|
||||
listenBack: this.listenBack,
|
||||
resetPanel: this.resetPanel
|
||||
resetPanel: this.resetPanel,
|
||||
});
|
||||
|
||||
this.listenTo(view, 'send-anyway', this.onSendAnyway);
|
||||
@@ -44,19 +41,32 @@
|
||||
}
|
||||
},
|
||||
forceSend: function() {
|
||||
this.model.updateVerified().then(function() {
|
||||
this.model
|
||||
.updateVerified()
|
||||
.then(
|
||||
function() {
|
||||
if (this.model.isUnverified()) {
|
||||
return this.model.setVerifiedDefault();
|
||||
}
|
||||
}.bind(this)).then(function() {
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function() {
|
||||
return this.model.isUntrusted();
|
||||
}.bind(this)).then(function(untrusted) {
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function(untrusted) {
|
||||
if (untrusted) {
|
||||
return this.model.setApproved();
|
||||
}
|
||||
}.bind(this)).then(function() {
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function() {
|
||||
this.message.resend(this.outgoingKeyError.number);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
onSendAnyway: function() {
|
||||
if (this.outgoingKeyError) {
|
||||
@@ -67,14 +77,14 @@
|
||||
var showButton = Boolean(this.outgoingKeyError);
|
||||
|
||||
return {
|
||||
status : this.message.getStatus(this.model.id),
|
||||
name : this.model.getTitle(),
|
||||
avatar : this.model.getAvatar(),
|
||||
errors : this.errors,
|
||||
showErrorButton : showButton,
|
||||
errorButtonLabel : i18n('view')
|
||||
status: this.message.getStatus(this.model.id),
|
||||
name: this.model.getTitle(),
|
||||
avatar: this.model.getAvatar(),
|
||||
errors: this.errors,
|
||||
showErrorButton: showButton,
|
||||
errorButtonLabel: i18n('view'),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.MessageDetailView = Whisper.View.extend({
|
||||
@@ -84,14 +94,14 @@
|
||||
this.listenBack = options.listenBack;
|
||||
this.resetPanel = options.resetPanel;
|
||||
|
||||
this.view = new Whisper.MessageView({model: this.model});
|
||||
this.view = new Whisper.MessageView({ model: this.model });
|
||||
this.view.render();
|
||||
this.conversation = options.conversation;
|
||||
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
events: {
|
||||
'click button.delete': 'onDelete'
|
||||
'click button.delete': 'onDelete',
|
||||
},
|
||||
onDelete: function() {
|
||||
var dialog = new Whisper.ConfirmationDialogView({
|
||||
@@ -100,7 +110,7 @@
|
||||
resolve: function() {
|
||||
this.model.destroy();
|
||||
this.resetPanel();
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
});
|
||||
|
||||
this.$el.prepend(dialog.el);
|
||||
@@ -110,7 +120,7 @@
|
||||
// Return the set of models to be rendered in this view
|
||||
var ids;
|
||||
if (this.model.isIncoming()) {
|
||||
ids = [ this.model.get('source') ];
|
||||
ids = [this.model.get('source')];
|
||||
} else if (this.model.isOutgoing()) {
|
||||
ids = this.model.get('recipients');
|
||||
if (!ids) {
|
||||
@@ -119,9 +129,11 @@
|
||||
ids = this.conversation.getRecipients();
|
||||
}
|
||||
}
|
||||
return Promise.all(ids.map(function(number) {
|
||||
return Promise.all(
|
||||
ids.map(function(number) {
|
||||
return ConversationController.getOrCreateAndWait(number, 'private');
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
renderContact: function(contact) {
|
||||
var view = new ContactView({
|
||||
@@ -129,40 +141,50 @@
|
||||
errors: this.grouped[contact.id],
|
||||
listenBack: this.listenBack,
|
||||
resetPanel: this.resetPanel,
|
||||
message: this.model
|
||||
message: this.model,
|
||||
}).render();
|
||||
this.$('.contacts').append(view.el);
|
||||
},
|
||||
render: function() {
|
||||
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) {
|
||||
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(
|
||||
error
|
||||
) {
|
||||
return Boolean(error.number);
|
||||
});
|
||||
|
||||
this.$el.html(Mustache.render(_.result(this, 'template', ''), {
|
||||
sent_at : moment(this.model.get('sent_at')).format('LLLL'),
|
||||
received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null,
|
||||
tofrom : this.model.isIncoming() ? i18n('from') : i18n('to'),
|
||||
errors : errorsWithoutNumber,
|
||||
title : i18n('messageDetail'),
|
||||
sent : i18n('sent'),
|
||||
received : i18n('received'),
|
||||
errorLabel : i18n('error'),
|
||||
deleteLabel : i18n('deleteMessage'),
|
||||
retryDescription: i18n('retryDescription')
|
||||
}));
|
||||
this.$el.html(
|
||||
Mustache.render(_.result(this, 'template', ''), {
|
||||
sent_at: moment(this.model.get('sent_at')).format('LLLL'),
|
||||
received_at: this.model.isIncoming()
|
||||
? moment(this.model.get('received_at')).format('LLLL')
|
||||
: null,
|
||||
tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'),
|
||||
errors: errorsWithoutNumber,
|
||||
title: i18n('messageDetail'),
|
||||
sent: i18n('sent'),
|
||||
received: i18n('received'),
|
||||
errorLabel: i18n('error'),
|
||||
deleteLabel: i18n('deleteMessage'),
|
||||
retryDescription: i18n('retryDescription'),
|
||||
})
|
||||
);
|
||||
this.view.$el.prependTo(this.$('.message-container'));
|
||||
|
||||
this.grouped = _.groupBy(this.model.get('errors'), 'number');
|
||||
|
||||
this.getContacts().then(function(contacts) {
|
||||
_.sortBy(contacts, function(c) {
|
||||
this.getContacts().then(
|
||||
function(contacts) {
|
||||
_.sortBy(
|
||||
contacts,
|
||||
function(c) {
|
||||
var prefix = this.grouped[c.id] ? '0' : '1';
|
||||
// this prefix ensures that contacts with errors are listed first;
|
||||
// otherwise it's alphabetical
|
||||
return prefix + c.getTitle();
|
||||
}.bind(this)).forEach(this.renderContact.bind(this));
|
||||
}.bind(this));
|
||||
}
|
||||
}.bind(this)
|
||||
).forEach(this.renderContact.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -10,14 +7,17 @@
|
||||
className: 'message-list',
|
||||
itemView: Whisper.MessageView,
|
||||
events: {
|
||||
'scroll': 'onScroll',
|
||||
scroll: 'onScroll',
|
||||
},
|
||||
initialize: function() {
|
||||
Whisper.ListView.prototype.initialize.call(this);
|
||||
|
||||
this.triggerLazyScroll = _.debounce(function() {
|
||||
this.triggerLazyScroll = _.debounce(
|
||||
function() {
|
||||
this.$el.trigger('lazyScroll');
|
||||
}.bind(this), 500);
|
||||
}.bind(this),
|
||||
500
|
||||
);
|
||||
},
|
||||
onScroll: function() {
|
||||
this.measureScrollPosition();
|
||||
@@ -36,7 +36,8 @@
|
||||
return this.bottomOffset < 30;
|
||||
},
|
||||
measureScrollPosition: function() {
|
||||
if (this.el.scrollHeight === 0) { // hidden
|
||||
if (this.el.scrollHeight === 0) {
|
||||
// hidden
|
||||
return;
|
||||
}
|
||||
this.outerHeight = this.$el.outerHeight();
|
||||
@@ -64,13 +65,13 @@
|
||||
addOne: function(model) {
|
||||
var view;
|
||||
if (model.isExpirationTimerUpdate()) {
|
||||
view = new Whisper.ExpirationTimerUpdateView({model: model}).render();
|
||||
view = new Whisper.ExpirationTimerUpdateView({ model: model }).render();
|
||||
} 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') {
|
||||
view = new Whisper.VerifiedChangeView({model: model}).render();
|
||||
view = new Whisper.VerifiedChangeView({ model: model }).render();
|
||||
} else {
|
||||
view = new this.itemView({model: model}).render();
|
||||
view = new this.itemView({ model: model }).render();
|
||||
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
|
||||
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
/* global $: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const { Signal } = window;
|
||||
@@ -71,7 +71,10 @@
|
||||
const elapsed = (totalTime - remainingTime) / totalTime;
|
||||
this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`);
|
||||
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;
|
||||
},
|
||||
@@ -195,9 +198,17 @@
|
||||
this.listenTo(this.model, 'change:body', this.render);
|
||||
this.listenTo(this.model, 'change:delivered', this.renderDelivered);
|
||||
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: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, 'unload', this.onUnload);
|
||||
this.listenTo(this.model, 'expired', this.onExpired);
|
||||
@@ -225,7 +236,7 @@
|
||||
this.model.get('errors'),
|
||||
this.model.isReplayableError.bind(this.model)
|
||||
);
|
||||
_.map(retrys, 'number').forEach((number) => {
|
||||
_.map(retrys, 'number').forEach(number => {
|
||||
this.model.resend(number);
|
||||
});
|
||||
},
|
||||
@@ -251,7 +262,7 @@
|
||||
},
|
||||
onExpired() {
|
||||
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]) {
|
||||
this.remove();
|
||||
}
|
||||
@@ -284,8 +295,9 @@
|
||||
// as our tests rely on `onUnload` synchronously removing the view from
|
||||
// the DOM.
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.loadAttachmentViews()
|
||||
.then(views => views.forEach(view => view.unload()));
|
||||
this.loadAttachmentViews().then(views =>
|
||||
views.forEach(view => view.unload())
|
||||
);
|
||||
|
||||
// No need to handle this one, since it listens to 'unload' itself:
|
||||
// this.timerView
|
||||
@@ -321,7 +333,9 @@
|
||||
}
|
||||
},
|
||||
renderDelivered() {
|
||||
if (this.model.get('delivered')) { this.$el.addClass('delivered'); }
|
||||
if (this.model.get('delivered')) {
|
||||
this.$el.addClass('delivered');
|
||||
}
|
||||
},
|
||||
renderRead() {
|
||||
if (!_.isEmpty(this.model.get('read_by'))) {
|
||||
@@ -345,7 +359,9 @@
|
||||
}
|
||||
if (_.size(errors) > 0) {
|
||||
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.render().$el.appendTo(this.$('.bubble'));
|
||||
@@ -354,7 +370,9 @@
|
||||
if (!el || el.length === 0) {
|
||||
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();
|
||||
@@ -461,18 +479,24 @@
|
||||
const hasAttachments = attachments && attachments.length > 0;
|
||||
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'),
|
||||
hasBody,
|
||||
timestamp: this.model.get('sent_at'),
|
||||
sender: (contact && contact.getTitle()) || '',
|
||||
avatar: (contact && contact.getAvatar()),
|
||||
profileName: (contact && contact.getProfileName()),
|
||||
avatar: contact && contact.getAvatar(),
|
||||
profileName: contact && contact.getProfileName(),
|
||||
innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail',
|
||||
hoverIcon: !hasErrors,
|
||||
hasAttachments,
|
||||
reply: i18n('replyToMessage'),
|
||||
}, this.render_partials()));
|
||||
},
|
||||
this.render_partials()
|
||||
)
|
||||
);
|
||||
this.timeStampView.setElement(this.$('.timestamp'));
|
||||
this.timeStampView.update();
|
||||
|
||||
@@ -498,7 +522,9 @@
|
||||
// as our code / Backbone seems to rely on `render` synchronously returning
|
||||
// `this` instead of `Promise MessageView` (this):
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.loadAttachmentViews().then(views => this.renderAttachmentViews(views));
|
||||
this.loadAttachmentViews().then(views =>
|
||||
this.renderAttachmentViews(views)
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
@@ -523,8 +549,10 @@
|
||||
}
|
||||
|
||||
const attachments = this.model.get('attachments') || [];
|
||||
const loadedAttachmentViews = Promise.all(attachments.map(attachment =>
|
||||
new Promise(async (resolve) => {
|
||||
const loadedAttachmentViews = Promise.all(
|
||||
attachments.map(
|
||||
attachment =>
|
||||
new Promise(async resolve => {
|
||||
const attachmentWithData = await loadAttachmentData(attachment);
|
||||
const view = new Whisper.AttachmentView({
|
||||
model: attachmentWithData,
|
||||
@@ -538,7 +566,9 @@
|
||||
});
|
||||
|
||||
view.render();
|
||||
})));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Memoize attachment views to avoid double loading:
|
||||
this.loadedAttachmentViews = loadedAttachmentViews;
|
||||
@@ -550,8 +580,10 @@
|
||||
},
|
||||
renderAttachmentView(view) {
|
||||
if (!view.updated) {
|
||||
throw new Error('Invariant violation:' +
|
||||
' Cannot render an attachment view that isn’t ready');
|
||||
throw new Error(
|
||||
'Invariant violation:' +
|
||||
' Cannot render an attachment view that isn’t ready'
|
||||
);
|
||||
}
|
||||
|
||||
const parent = this.$('.attachments')[0];
|
||||
@@ -570,4 +602,4 @@
|
||||
this.trigger('afterChangeHeight');
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -10,9 +10,11 @@
|
||||
this.$el.hide();
|
||||
|
||||
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
|
||||
extension.windows.onClosed(function () {
|
||||
extension.windows.onClosed(
|
||||
function() {
|
||||
clearInterval(this.renderIntervalHandle);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
|
||||
|
||||
@@ -34,10 +36,13 @@
|
||||
setSocketReconnectInterval: function(millis) {
|
||||
this.socketReconnectWaitDuration = moment.duration(millis);
|
||||
},
|
||||
navigatorOnLine: function() { return navigator.onLine; },
|
||||
getSocketStatus: function() { return window.getSocketStatus(); },
|
||||
navigatorOnLine: function() {
|
||||
return navigator.onLine;
|
||||
},
|
||||
getSocketStatus: function() {
|
||||
return window.getSocketStatus();
|
||||
},
|
||||
getNetworkStatus: function() {
|
||||
|
||||
var message = '';
|
||||
var instructions = '';
|
||||
var hasInterruption = false;
|
||||
@@ -45,7 +50,7 @@
|
||||
var buttonClass = null;
|
||||
|
||||
var socketStatus = this.getSocketStatus();
|
||||
switch(socketStatus) {
|
||||
switch (socketStatus) {
|
||||
case WebSocket.CONNECTING:
|
||||
message = i18n('connecting');
|
||||
this.setSocketReconnectInterval(null);
|
||||
@@ -65,11 +70,16 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) {
|
||||
if (
|
||||
socketStatus == WebSocket.CONNECTING &&
|
||||
!this.withinConnectingGracePeriod
|
||||
) {
|
||||
hasInterruption = true;
|
||||
}
|
||||
if (this.socketReconnectWaitDuration.asSeconds() > 0) {
|
||||
instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]);
|
||||
instructions = i18n('attemptingReconnection', [
|
||||
this.socketReconnectWaitDuration.asSeconds(),
|
||||
]);
|
||||
}
|
||||
if (!this.navigatorOnLine()) {
|
||||
hasInterruption = true;
|
||||
@@ -88,7 +98,7 @@
|
||||
instructions: instructions,
|
||||
hasInterruption: hasInterruption,
|
||||
action: action,
|
||||
buttonClass: buttonClass
|
||||
buttonClass: buttonClass,
|
||||
};
|
||||
},
|
||||
update: function() {
|
||||
@@ -102,13 +112,9 @@
|
||||
this.render();
|
||||
if (this.model.attributes.hasInterruption) {
|
||||
this.$el.slideDown();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.$el.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
})();
|
||||
|
@@ -1,34 +1,33 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.NewGroupUpdateView = Whisper.View.extend({
|
||||
tagName: "div",
|
||||
tagName: 'div',
|
||||
className: 'new-group-update',
|
||||
templateName: 'new-group-update',
|
||||
initialize: function(options) {
|
||||
this.render();
|
||||
this.avatarInput = new Whisper.FileInputView({
|
||||
el: this.$('.group-avatar'),
|
||||
window: options.window
|
||||
window: options.window,
|
||||
});
|
||||
|
||||
this.recipients_view = new Whisper.RecipientsInputView();
|
||||
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)) {
|
||||
this.recipients_view.typeahead.remove(model);
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
});
|
||||
this.recipients_view.$el.insertBefore(this.$('.container'));
|
||||
|
||||
this.member_list_view = new Whisper.ContactListView({
|
||||
collection: this.model.contactCollection,
|
||||
className: 'members'
|
||||
className: 'members',
|
||||
});
|
||||
this.member_list_view.render();
|
||||
this.$('.scrollable').append(this.member_list_view.el);
|
||||
@@ -51,17 +50,21 @@
|
||||
render_attributes: function() {
|
||||
return {
|
||||
name: this.model.getTitle(),
|
||||
avatar: this.model.getAvatar()
|
||||
avatar: this.model.getAvatar(),
|
||||
};
|
||||
},
|
||||
send: function() {
|
||||
return this.avatarInput.getThumbnail().then(function(avatarFile) {
|
||||
return this.avatarInput.getThumbnail().then(
|
||||
function(avatarFile) {
|
||||
var now = Date.now();
|
||||
var attrs = {
|
||||
timestamp: now,
|
||||
active_at: now,
|
||||
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) {
|
||||
attrs.avatar = avatarFile;
|
||||
@@ -76,7 +79,8 @@
|
||||
|
||||
this.model.updateGroup(group_update);
|
||||
this.goBack();
|
||||
}.bind(this));
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -13,12 +10,14 @@
|
||||
this.$('input.number').intlTelInput();
|
||||
},
|
||||
events: {
|
||||
'change': 'validateNumber',
|
||||
'keyup': 'validateNumber'
|
||||
change: 'validateNumber',
|
||||
keyup: 'validateNumber',
|
||||
},
|
||||
validateNumber: function() {
|
||||
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 parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
|
||||
@@ -31,6 +30,6 @@
|
||||
input.trigger('validation');
|
||||
|
||||
return parsedNumber.e164;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -4,7 +4,7 @@
|
||||
/* global ReactDOM: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -44,4 +44,4 @@
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -10,21 +7,21 @@
|
||||
'name',
|
||||
'e164_number',
|
||||
'national_number',
|
||||
'international_number'
|
||||
'international_number',
|
||||
],
|
||||
database: Whisper.Database,
|
||||
storeName: 'conversations',
|
||||
model: Whisper.Conversation,
|
||||
fetchContacts: function() {
|
||||
return this.fetch({ reset: true, conditions: { type: 'private' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ContactPillView = Whisper.View.extend({
|
||||
tagName: 'span',
|
||||
className: 'recipient',
|
||||
events: {
|
||||
'click .remove': 'removeModel'
|
||||
'click .remove': 'removeModel',
|
||||
},
|
||||
templateName: 'contact_pill',
|
||||
initialize: function() {
|
||||
@@ -34,16 +31,16 @@
|
||||
}
|
||||
},
|
||||
removeModel: function() {
|
||||
this.$el.trigger('remove', {modelId: this.model.id});
|
||||
this.$el.trigger('remove', { modelId: this.model.id });
|
||||
this.remove();
|
||||
},
|
||||
render_attributes: function() {
|
||||
return { name: this.model.getTitle() };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.RecipientListView = Whisper.ListView.extend({
|
||||
itemView: Whisper.ContactPillView
|
||||
itemView: Whisper.ContactPillView,
|
||||
});
|
||||
|
||||
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
|
||||
@@ -52,7 +49,7 @@
|
||||
});
|
||||
|
||||
Whisper.SuggestionListView = Whisper.ConversationListView.extend({
|
||||
itemView: Whisper.SuggestionView
|
||||
itemView: Whisper.SuggestionView,
|
||||
});
|
||||
|
||||
Whisper.RecipientsInputView = Whisper.View.extend({
|
||||
@@ -68,13 +65,13 @@
|
||||
|
||||
// Collection of recipients selected for the new message
|
||||
this.recipients = new Whisper.ConversationCollection([], {
|
||||
comparator: false
|
||||
comparator: false,
|
||||
});
|
||||
|
||||
// View to display the selected recipients
|
||||
this.recipients_view = new Whisper.RecipientListView({
|
||||
collection: this.recipients,
|
||||
el: this.$('.recipients')
|
||||
el: this.$('.recipients'),
|
||||
});
|
||||
|
||||
// Collection of contacts to match user input against
|
||||
@@ -83,18 +80,19 @@
|
||||
|
||||
// View to display the matched contacts from typeahead
|
||||
this.typeahead_view = new Whisper.SuggestionListView({
|
||||
collection : new Whisper.ConversationCollection([], {
|
||||
comparator: function(m) { return m.getTitle().toLowerCase(); }
|
||||
})
|
||||
collection: new Whisper.ConversationCollection([], {
|
||||
comparator: function(m) {
|
||||
return m.getTitle().toLowerCase();
|
||||
},
|
||||
}),
|
||||
});
|
||||
this.$('.contacts').append(this.typeahead_view.el);
|
||||
this.initNewContact();
|
||||
this.listenTo(this.typeahead, 'reset', this.filterContacts);
|
||||
|
||||
},
|
||||
|
||||
render_attributes: function() {
|
||||
return { placeholder: this.placeholder || "name or phone number" };
|
||||
return { placeholder: this.placeholder || 'name or phone number' };
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -113,9 +111,7 @@
|
||||
} else {
|
||||
this.new_contact_view.$el.hide();
|
||||
}
|
||||
this.typeahead_view.collection.reset(
|
||||
this.typeahead.typeahead(query)
|
||||
);
|
||||
this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
|
||||
} else {
|
||||
this.resetTypeahead();
|
||||
}
|
||||
@@ -131,8 +127,8 @@
|
||||
el: this.$new_contact,
|
||||
model: ConversationController.create({
|
||||
type: 'private',
|
||||
newContact: true
|
||||
})
|
||||
newContact: true,
|
||||
}),
|
||||
}).render();
|
||||
},
|
||||
|
||||
@@ -176,10 +172,8 @@
|
||||
this.typeahead_view.collection.reset([]);
|
||||
},
|
||||
|
||||
|
||||
maybeNumber: function(number) {
|
||||
return number.match(/^\+?[0-9]*$/);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -16,7 +13,7 @@
|
||||
events: {
|
||||
'click .close': 'close',
|
||||
'click .finish': 'finish',
|
||||
'close': 'close'
|
||||
close: 'close',
|
||||
},
|
||||
updateTime: function() {
|
||||
var duration = moment.duration(Date.now() - this.startTime, 'ms');
|
||||
@@ -62,19 +59,23 @@
|
||||
this.input = this.context.createGain();
|
||||
this.recorder = new WebAudioRecorder(this.input, {
|
||||
encoding: 'mp3',
|
||||
workerDir: 'js/' // must end with slash
|
||||
workerDir: 'js/', // must end with slash
|
||||
});
|
||||
this.recorder.onComplete = this.handleBlob.bind(this);
|
||||
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.connect(this.input);
|
||||
}.bind(this), this.onError.bind(this));
|
||||
}.bind(this),
|
||||
this.onError.bind(this)
|
||||
);
|
||||
this.recorder.startRecording();
|
||||
},
|
||||
onError: function(error) {
|
||||
console.log(error.stack);
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -32,8 +29,8 @@
|
||||
|
||||
return {
|
||||
cssClass: cssClass,
|
||||
moreBelow: moreBelow
|
||||
moreBelow: moreBelow,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
const { Database } = window.Whisper;
|
||||
@@ -20,7 +20,7 @@
|
||||
this.populate();
|
||||
},
|
||||
events: {
|
||||
'change': 'change'
|
||||
change: 'change',
|
||||
},
|
||||
change: function(e) {
|
||||
var value = e.target.checked;
|
||||
@@ -43,7 +43,7 @@
|
||||
this.populate();
|
||||
},
|
||||
events: {
|
||||
'change': 'change'
|
||||
change: 'change',
|
||||
},
|
||||
change: function(e) {
|
||||
var value = this.$(e.target).val();
|
||||
@@ -67,26 +67,26 @@
|
||||
new RadioButtonGroupView({
|
||||
el: this.$('.notification-settings'),
|
||||
defaultValue: 'message',
|
||||
name: 'notification-setting'
|
||||
name: 'notification-setting',
|
||||
});
|
||||
new RadioButtonGroupView({
|
||||
el: this.$('.theme-settings'),
|
||||
defaultValue: 'android',
|
||||
name: 'theme-setting',
|
||||
event: 'change-theme'
|
||||
event: 'change-theme',
|
||||
});
|
||||
if (Settings.isAudioNotificationSupported()) {
|
||||
new CheckboxView({
|
||||
el: this.$('.audio-notification-setting'),
|
||||
defaultValue: false,
|
||||
name: 'audio-notification'
|
||||
name: 'audio-notification',
|
||||
});
|
||||
}
|
||||
new CheckboxView({
|
||||
el: this.$('.menu-bar-setting'),
|
||||
defaultValue: false,
|
||||
name: 'hide-menu-bar',
|
||||
event: 'change-hide-menu'
|
||||
event: 'change-hide-menu',
|
||||
});
|
||||
if (textsecure.storage.user.getDeviceId() != '1') {
|
||||
var syncView = new SyncView().render();
|
||||
@@ -160,10 +160,7 @@
|
||||
},
|
||||
async clearAllData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
Logs.deleteAll(),
|
||||
Database.drop(),
|
||||
]);
|
||||
await Promise.all([Logs.deleteAll(), Database.drop()]);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Something went wrong deleting all data:',
|
||||
@@ -193,7 +190,7 @@
|
||||
templateName: 'syncSettings',
|
||||
className: 'syncSettings',
|
||||
events: {
|
||||
'click .sync': 'sync'
|
||||
'click .sync': 'sync',
|
||||
},
|
||||
enable: function() {
|
||||
this.$('.sync').text(i18n('syncNow'));
|
||||
@@ -223,7 +220,7 @@
|
||||
syncRequest.addEventListener('success', this.onsuccess.bind(this));
|
||||
syncRequest.addEventListener('timeout', this.ontimeout.bind(this));
|
||||
} else {
|
||||
console.log("Tried to sync from device 1");
|
||||
console.log('Tried to sync from device 1');
|
||||
}
|
||||
},
|
||||
render_attributes: function() {
|
||||
@@ -231,7 +228,7 @@
|
||||
sync: i18n('sync'),
|
||||
syncNow: i18n('syncNow'),
|
||||
syncExplanation: i18n('syncExplanation'),
|
||||
syncFailed: i18n('syncFailed')
|
||||
syncFailed: i18n('syncFailed'),
|
||||
};
|
||||
var date = storage.get('synced_at');
|
||||
if (date) {
|
||||
@@ -241,6 +238,6 @@
|
||||
attrs.syncTime = date.toLocaleTimeString();
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -17,7 +14,9 @@
|
||||
if (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();
|
||||
},
|
||||
events: {
|
||||
@@ -29,24 +28,37 @@
|
||||
},
|
||||
verifyCode: function(e) {
|
||||
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');
|
||||
}.bind(this)).catch(this.log.bind(this));
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(this.log.bind(this));
|
||||
},
|
||||
log: function (s) {
|
||||
log: function(s) {
|
||||
console.log(s);
|
||||
this.$('#status').text(s);
|
||||
},
|
||||
validateCode: function() {
|
||||
var verificationCode = $('#code').val().replace(/\D/g, '');
|
||||
var verificationCode = $('#code')
|
||||
.val()
|
||||
.replace(/\D/g, '');
|
||||
if (verificationCode.length == 6) {
|
||||
return verificationCode;
|
||||
}
|
||||
},
|
||||
displayError: function(error) {
|
||||
this.$('#error').hide().text(error).addClass('in').fadeIn();
|
||||
this.$('#error')
|
||||
.hide()
|
||||
.text(error)
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
},
|
||||
onValidation: function() {
|
||||
if (this.$('#number-container').hasClass('valid')) {
|
||||
@@ -67,8 +79,12 @@
|
||||
this.$('#error').hide();
|
||||
var number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this));
|
||||
this.$('#step2').addClass('in').fadeIn();
|
||||
this.accountManager
|
||||
.requestVoiceVerification(number)
|
||||
.catch(this.displayError.bind(this));
|
||||
this.$('#step2')
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
} else {
|
||||
this.$('#number-container').addClass('invalid');
|
||||
}
|
||||
@@ -78,11 +94,15 @@
|
||||
$('#error').hide();
|
||||
var number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
this.accountManager.requestSMSVerification(number).catch(this.displayError.bind(this));
|
||||
this.$('#step2').addClass('in').fadeIn();
|
||||
this.accountManager
|
||||
.requestSMSVerification(number)
|
||||
.catch(this.displayError.bind(this));
|
||||
this.$('#step2')
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
} else {
|
||||
this.$('#number-container').addClass('invalid');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -13,7 +10,7 @@
|
||||
this.clearTimeout();
|
||||
var millis_now = Date.now();
|
||||
var millis = this.$el.data('timestamp');
|
||||
if (millis === "") {
|
||||
if (millis === '') {
|
||||
return;
|
||||
}
|
||||
if (millis >= millis_now) {
|
||||
@@ -27,7 +24,9 @@
|
||||
|
||||
var millis_since = millis_now - millis;
|
||||
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);
|
||||
}
|
||||
},
|
||||
@@ -47,42 +46,54 @@
|
||||
this.delay = null;
|
||||
return timestamp.format(this._format.M);
|
||||
} else if (timediff.days() > 0) {
|
||||
this.delay = moment(timestamp).add(timediff.days() + 1,'d').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(timediff.days() + 1, 'd')
|
||||
.diff(now);
|
||||
return timestamp.format(this._format.d);
|
||||
} else if (timediff.hours() > 1) {
|
||||
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(timediff.hours() + 1, 'h')
|
||||
.diff(now);
|
||||
return this.relativeTime(timediff.hours(), 'h');
|
||||
} else if (timediff.hours() === 1) {
|
||||
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(timediff.hours() + 1, 'h')
|
||||
.diff(now);
|
||||
return this.relativeTime(timediff.hours(), 'h');
|
||||
} else if (timediff.minutes() > 1) {
|
||||
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(timediff.minutes() + 1, 'm')
|
||||
.diff(now);
|
||||
return this.relativeTime(timediff.minutes(), 'm');
|
||||
} else if (timediff.minutes() === 1) {
|
||||
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(timediff.minutes() + 1, 'm')
|
||||
.diff(now);
|
||||
return this.relativeTime(timediff.minutes(), 'm');
|
||||
} else {
|
||||
this.delay = moment(timestamp).add(1,'m').diff(now);
|
||||
this.delay = moment(timestamp)
|
||||
.add(1, 'm')
|
||||
.diff(now);
|
||||
return this.relativeTime(timediff.seconds(), 's');
|
||||
}
|
||||
},
|
||||
relativeTime : function (number, string) {
|
||||
relativeTime: function(number, string) {
|
||||
return moment.duration(number, string).humanize();
|
||||
},
|
||||
_format: {
|
||||
y: "ll",
|
||||
M: i18n('timestampFormat_M') || "MMM D",
|
||||
d: "ddd"
|
||||
}
|
||||
y: 'll',
|
||||
M: i18n('timestampFormat_M') || 'MMM D',
|
||||
d: 'ddd',
|
||||
},
|
||||
});
|
||||
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
|
||||
relativeTime : function (number, string, isFuture) {
|
||||
relativeTime: function(number, string, isFuture) {
|
||||
return moment.duration(-1 * number, string).humanize(string !== 's');
|
||||
},
|
||||
_format: {
|
||||
y: "lll",
|
||||
M: (i18n('timestampFormat_M') || "MMM D") + ' LT',
|
||||
d: "ddd LT"
|
||||
}
|
||||
y: 'lll',
|
||||
M: (i18n('timestampFormat_M') || 'MMM D') + ' LT',
|
||||
d: 'ddd LT',
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -17,12 +14,14 @@
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(Mustache.render(
|
||||
this.$el.html(
|
||||
Mustache.render(
|
||||
_.result(this, 'template', ''),
|
||||
_.result(this, 'render_attributes', '')
|
||||
));
|
||||
)
|
||||
);
|
||||
this.$el.show();
|
||||
setTimeout(this.close.bind(this), 2000);
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* Whisper.View
|
||||
*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.View = Backbone.View.extend({
|
||||
Whisper.View = Backbone.View.extend(
|
||||
{
|
||||
constructor: function() {
|
||||
Backbone.View.apply(this, arguments);
|
||||
Mustache.parse(_.result(this, 'template'));
|
||||
@@ -48,24 +47,28 @@
|
||||
return this;
|
||||
},
|
||||
confirm: function(message, okText) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise(
|
||||
function(resolve, reject) {
|
||||
var dialog = new Whisper.ConfirmationDialogView({
|
||||
message: message,
|
||||
okText: okText,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
reject: reject,
|
||||
});
|
||||
this.$el.append(dialog.el);
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
i18n_with_links: function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
for (var i=1; i < args.length; ++i) {
|
||||
args[i] = 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
|
||||
for (var i = 1; i < args.length; ++i) {
|
||||
args[i] =
|
||||
'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
|
||||
}
|
||||
return i18n(args[0], args.slice(1));
|
||||
}
|
||||
},{
|
||||
},
|
||||
},
|
||||
{
|
||||
// Class attributes
|
||||
Templates: (function() {
|
||||
var templates = {};
|
||||
@@ -75,6 +78,7 @@
|
||||
templates[id] = $el.html();
|
||||
});
|
||||
return templates;
|
||||
}())
|
||||
});
|
||||
})(),
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
@@ -1,8 +1,4 @@
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
|
||||
;(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
@@ -11,7 +7,7 @@
|
||||
var events;
|
||||
function checkTime() {
|
||||
var currentTime = Date.now();
|
||||
if (currentTime > (lastTime + interval * 2)) {
|
||||
if (currentTime > lastTime + interval * 2) {
|
||||
events.trigger('timetravel');
|
||||
}
|
||||
lastTime = currentTime;
|
||||
@@ -22,6 +18,6 @@
|
||||
events = _events;
|
||||
lastTime = Date.now();
|
||||
setInterval(checkTime, interval);
|
||||
}
|
||||
},
|
||||
};
|
||||
}());
|
||||
})();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user