Prettier (All The Things) (#2303)

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

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

## Sublime Text Plugin

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

## Changes

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

View File

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

View File

@@ -2,37 +2,24 @@
module.exports = { module.exports = {
settings: { settings: {
'import/core-modules': [ 'import/core-modules': ['electron'],
'electron'
]
}, },
extends: [ extends: ['airbnb-base', 'prettier'],
'airbnb-base',
],
plugins: [ plugins: ['mocha', 'more'],
'mocha',
'more',
],
rules: { rules: {
'comma-dangle': ['error', { 'comma-dangle': [
'error',
{
arrays: 'always-multiline', arrays: 'always-multiline',
objects: 'always-multiline', objects: 'always-multiline',
imports: 'always-multiline', imports: 'always-multiline',
exports: 'always-multiline', exports: 'always-multiline',
functions: 'never', functions: 'never',
}], },
],
// putting params on their own line helps stay within line length limit
'function-paren-newline': ['error', 'multiline'],
// 90 characters allows three+ side-by-side screens on a standard-size monitor
'max-len': ['error', {
code: 90,
ignoreUrls: true,
}],
// prevents us from accidentally checking in exclusive tests (`.only`): // prevents us from accidentally checking in exclusive tests (`.only`):
'mocha/no-exclusive-tests': 'error', 'mocha/no-exclusive-tests': 'error',
@@ -52,6 +39,26 @@ module.exports = {
// consistently place operators at end of line except ternaries // consistently place operators at end of line except ternaries
'operator-linebreak': 'error', 'operator-linebreak': 'error',
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], quotes: [
} 'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false },
],
// Prettier overrides:
'arrow-parens': 'off',
'function-paren-newline': 'off',
'max-len': [
'error',
{
// Prettier generally limits line length to 80 but sometimes goes over.
// The `max-len` plugin doesnt let us omit `code` so we set it to a
// high value as a buffer to let Prettier control the line length:
code: 999,
// We still want to limit comments as before:
comments: 90,
ignoreUrls: true,
},
],
},
}; };

4
.gitignore vendored
View File

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

18
.prettierignore Normal file
View File

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

4
.prettierrc.js Normal file
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,99 +1,91 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.emoji_util = window.emoji_util || {};
;(function() { // EmojiConverter overrides
'use strict'; EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) {
window.emoji_util = window.emoji_util || {}; var match = regex.exec(str);
var count = 0;
// EmojiConverter overrides if (!regex.global) {
EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) { return match ? 1 : 0;
var match = regex.exec(str); }
var count = 0;
if (!regex.global) { while (match) {
return match ? 1 : 0; count += 1;
} match = regex.exec(str);
}
while (match) { return count;
count += 1; };
match = regex.exec(str);
}
return count; EmojiConvertor.prototype.hasNormalCharacters = function(str) {
}; var self = this;
var noEmoji = str.replace(self.rx_unified, '').trim();
return noEmoji.length > 0;
};
EmojiConvertor.prototype.hasNormalCharacters = function(str) { EmojiConvertor.prototype.getSizeClass = function(str) {
var self = this; var self = this;
var noEmoji = str.replace(self.rx_unified, '').trim();
return noEmoji.length > 0;
};
EmojiConvertor.prototype.getSizeClass = function(str) { if (self.hasNormalCharacters(str)) {
var self = this; return '';
}
if (self.hasNormalCharacters(str)) { var emojiCount = self.getCountOfAllMatches(str, self.rx_unified);
return ''; if (emojiCount > 8) {
} return '';
} else if (emojiCount > 6) {
return 'small';
} else if (emojiCount > 4) {
return 'medium';
} else if (emojiCount > 2) {
return 'large';
} else {
return 'jumbo';
}
};
var emojiCount = self.getCountOfAllMatches(str, self.rx_unified); var imgClass = /(<img [^>]+ class="emoji)(")/g;
if (emojiCount > 8) { EmojiConvertor.prototype.addClass = function(text, sizeClass) {
return ''; if (!sizeClass) {
} return text;
else if (emojiCount > 6) { }
return 'small';
}
else if (emojiCount > 4) {
return 'medium';
}
else if (emojiCount > 2) {
return 'large';
}
else {
return 'jumbo';
}
};
var imgClass = /(<img [^>]+ class="emoji)(")/g; return text.replace(imgClass, function(match, before, after) {
EmojiConvertor.prototype.addClass = function(text, sizeClass) { return before + ' ' + sizeClass + after;
if (!sizeClass) { });
return text; };
}
return text.replace(imgClass, function(match, before, after) { var imgTitle = /(<img [^>]+ class="emoji[^>]+ title=")([^:">]+)(")/g;
return before + ' ' + sizeClass + after; EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) {
}); return text.replace(imgTitle, function(match, before, title, after) {
}; return before + ':' + title + ':' + after;
});
};
var imgTitle = /(<img [^>]+ class="emoji[^>]+ title=")([^:">]+)(")/g; EmojiConvertor.prototype.signalReplace = function(str) {
EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) { var sizeClass = this.getSizeClass(str);
return text.replace(imgTitle, function(match, before, title, after) {
return before + ':' + title + ':' + after;
});
};
EmojiConvertor.prototype.signalReplace = function(str) { var text = this.replace_unified(str);
var sizeClass = this.getSizeClass(str); text = this.addClass(text, sizeClass);
var text = this.replace_unified(str); return this.ensureTitlesHaveColons(text);
text = this.addClass(text, sizeClass); };
return this.ensureTitlesHaveColons(text); window.emoji = new EmojiConvertor();
}; emoji.init_colons();
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
window.emoji = new EmojiConvertor(); window.emoji_util.parse = function($el) {
emoji.init_colons(); if (!$el || !$el.length) {
emoji.img_sets.apple.path = 'node_modules/emoji-datasource-apple/img/apple/64/'; return;
emoji.include_title = true; }
emoji.replace_mode = 'img';
emoji.supports_css = false; // needed to avoid spans with background-image
window.emoji_util.parse = function($el) {
if (!$el || !$el.length) {
return;
}
$el.html(emoji.signalReplace($el.html()));
};
$el.html(emoji.signalReplace($el.html()));
};
})(); })();

View File

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

View File

@@ -1,115 +1,121 @@
(function() {
'use strict';
window.Whisper = window.Whisper || {};
/* function destroyExpiredMessages() {
* vim: ts=4:sw=4:expandtab // Load messages that have expired and destroy them
*/ var expired = new Whisper.MessageCollection();
;(function() { expired.on('add', function(message) {
'use strict'; console.log('message', message.get('sent_at'), 'expired');
window.Whisper = window.Whisper || {}; var conversation = message.getConversation();
if (conversation) {
function destroyExpiredMessages() { conversation.trigger('expired', message);
// Load messages that have expired and destroy them
var expired = new Whisper.MessageCollection();
expired.on('add', function(message) {
console.log('message', message.get('sent_at'), 'expired');
var conversation = message.getConversation();
if (conversation) {
conversation.trigger('expired', message);
}
// We delete after the trigger to allow the conversation time to process
// the expiration before the message is removed from the database.
message.destroy();
});
expired.on('reset', throttledCheckExpiringMessages);
expired.fetchExpired();
}
var timeout;
function checkExpiringMessages() {
// Look up the next expiring message and set a timer to destroy it
var expiring = new Whisper.MessageCollection();
expiring.once('add', function(next) {
var expires_at = next.get('expires_at');
console.log('next message expires', new Date(expires_at).toISOString());
var wait = expires_at - Date.now();
// In the past
if (wait < 0) { wait = 0; }
// Too far in the future, since it's limited to a 32-bit value
if (wait > 2147483647) { wait = 2147483647; }
clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait);
});
expiring.fetchNextExpiring();
}
var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000);
Whisper.ExpiringMessagesListener = {
init: function(events) {
checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages);
},
update: throttledCheckExpiringMessages
};
var TimerOption = Backbone.Model.extend({
getName: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'),
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize();
},
getAbbreviated: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'), 'abbreviated'
].join('_'));
} }
// We delete after the trigger to allow the conversation time to process
// the expiration before the message is removed from the database.
message.destroy();
}); });
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ expired.on('reset', throttledCheckExpiringMessages);
model: TimerOption,
getName: function(seconds) { expired.fetchExpired();
if (!seconds) { }
seconds = 0;
} var timeout;
var o = this.findWhere({seconds: seconds}); function checkExpiringMessages() {
if (o) { return o.getName(); } // Look up the next expiring message and set a timer to destroy it
else { var expiring = new Whisper.MessageCollection();
return [seconds, 'seconds'].join(' '); expiring.once('add', function(next) {
} var expires_at = next.get('expires_at');
}, console.log('next message expires', new Date(expires_at).toISOString());
getAbbreviated: function(seconds) {
if (!seconds) { var wait = expires_at - Date.now();
seconds = 0;
} // In the past
var o = this.findWhere({seconds: seconds}); if (wait < 0) {
if (o) { return o.getAbbreviated(); } wait = 0;
else {
return [seconds, 's'].join('');
}
} }
}))([
[ 0, 'seconds' ], // Too far in the future, since it's limited to a 32-bit value
[ 5, 'seconds' ], if (wait > 2147483647) {
[ 10, 'seconds' ], wait = 2147483647;
[ 30, 'seconds' ], }
[ 1, 'minute' ],
[ 5, 'minutes' ], clearTimeout(timeout);
[ 30, 'minutes' ], timeout = setTimeout(destroyExpiredMessages, wait);
[ 1, 'hour' ], });
[ 6, 'hours' ], expiring.fetchNextExpiring();
[ 12, 'hours' ], }
[ 1, 'day' ], var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000);
[ 1, 'week' ],
Whisper.ExpiringMessagesListener = {
init: function(events) {
checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages);
},
update: throttledCheckExpiringMessages,
};
var TimerOption = Backbone.Model.extend({
getName: function() {
return (
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
moment.duration(this.get('time'), this.get('unit')).humanize()
);
},
getAbbreviated: function() {
return i18n(
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
'_'
)
);
},
});
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption,
getName: function(seconds) {
if (!seconds) {
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) {
return o.getName();
} else {
return [seconds, 'seconds'].join(' ');
}
},
getAbbreviated: function(seconds) {
if (!seconds) {
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) {
return o.getAbbreviated();
} else {
return [seconds, 's'].join('');
}
},
}))(
[
[0, 'seconds'],
[5, 'seconds'],
[10, 'seconds'],
[30, 'seconds'],
[1, 'minute'],
[5, 'minutes'],
[30, 'minutes'],
[1, 'hour'],
[6, 'hours'],
[12, 'hours'],
[1, 'day'],
[1, 'week'],
].map(function(o) { ].map(function(o) {
var duration = moment.duration(o[0], o[1]); // 5, 'seconds' var duration = moment.duration(o[0], o[1]); // 5, 'seconds'
return { return {
time: o[0], time: o[0],
unit: o[1], unit: o[1],
seconds: duration.asSeconds() seconds: duration.asSeconds(),
}; };
})); })
);
})(); })();

View File

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

View File

@@ -1,28 +1,28 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
;(function () { Whisper.KeyChangeListener = {
'use strict'; init: function(signalProtocolStore) {
window.Whisper = window.Whisper || {}; if (!(signalProtocolStore instanceof SignalProtocolStore)) {
throw new Error('KeyChangeListener requires a SignalProtocolStore');
}
Whisper.KeyChangeListener = { signalProtocolStore.on('keychange', function(id) {
init: function(signalProtocolStore) { ConversationController.getOrCreateAndWait(id, 'private').then(function(
if (!(signalProtocolStore instanceof SignalProtocolStore)) { conversation
throw new Error('KeyChangeListener requires a SignalProtocolStore'); ) {
} conversation.addKeyChange(id);
signalProtocolStore.on('keychange', function(id) { ConversationController.getAllGroupsInvolvingId(id).then(function(
ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { groups
conversation.addKeyChange(id); ) {
_.forEach(groups, function(group) {
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) { group.addKeyChange(id);
_.forEach(groups, function(group) {
group.addKeyChange(id);
});
}); });
}); });
}); });
} });
}; },
}()); };
})();

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -32,10 +32,13 @@
this.on('unload', this.unload); this.on('unload', this.unload);
this.setToExpire(); this.setToExpire();
this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; this.VOICE_FLAG =
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
}, },
idForLogging() { idForLogging() {
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`; return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
'sent_at'
)}`;
}, },
defaults() { defaults() {
return { return {
@@ -56,12 +59,13 @@
return !!(this.get('flags') & flag); return !!(this.get('flags') & flag);
}, },
isExpirationTimerUpdate() { isExpirationTimerUpdate() {
const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag); return !!(this.get('flags') & flag);
}, },
isGroupUpdate() { isGroupUpdate() {
return !!(this.get('group_update')); return !!this.get('group_update');
}, },
isIncoming() { isIncoming() {
return this.get('type') === 'incoming'; return this.get('type') === 'incoming';
@@ -79,14 +83,14 @@
if (options.parse === void 0) options.parse = true; if (options.parse === void 0) options.parse = true;
const model = this; const model = this;
const success = options.success; const success = options.success;
options.success = function (resp) { options.success = function(resp) {
model.attributes = {}; // this is the only changed line model.attributes = {}; // this is the only changed line
if (!model.set(model.parse(resp, options), options)) return false; if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options); if (success) success(model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
const error = options.error; const error = options.error;
options.error = function (resp) { options.error = function(resp) {
if (error) error(model, resp, options); if (error) error(model, resp, options);
model.trigger('error', model, resp, options); model.trigger('error', model, resp, options);
}; };
@@ -116,7 +120,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name)); messages.push(i18n('titleIsNow', groupUpdate.name));
} }
if (groupUpdate.joined && groupUpdate.joined.length) { if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this)); const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
);
if (names.length > 1) { if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', '))); messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else { } else {
@@ -186,7 +193,7 @@
} }
const quote = this.get('quote'); const quote = this.get('quote');
const attachments = (quote && quote.attachments) || []; const attachments = (quote && quote.attachments) || [];
attachments.forEach((attachment) => { attachments.forEach(attachment => {
if (attachment.thumbnail && attachment.thumbnail.objectUrl) { if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
URL.revokeObjectURL(attachment.thumbnail.objectUrl); URL.revokeObjectURL(attachment.thumbnail.objectUrl);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -235,8 +242,8 @@
const thumbnailWithObjectUrl = !objectUrl const thumbnailWithObjectUrl = !objectUrl
? null ? null
: Object.assign({}, attachment.thumbnail || {}, { : Object.assign({}, attachment.thumbnail || {}, {
objectUrl, objectUrl,
}); });
return Object.assign({}, attachment, { return Object.assign({}, attachment, {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
@@ -269,7 +276,8 @@
return { return {
attachments: (quote.attachments || []).map(attachment => attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)), this.processAttachment(attachment, objectUrl)
),
authorColor, authorColor,
authorProfileName, authorProfileName,
authorTitle, authorTitle,
@@ -342,59 +350,63 @@
send(promise) { send(promise) {
this.trigger('pending'); this.trigger('pending');
return promise.then((result) => { return promise
const now = Date.now(); .then(result => {
this.trigger('done'); const now = Date.now();
if (result.dataMessage) { this.trigger('done');
this.set({ dataMessage: result.dataMessage }); if (result.dataMessage) {
} this.set({ dataMessage: result.dataMessage });
const sentTo = this.get('sent_to') || [];
this.save({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
this.sendSyncMessage();
}).catch((result) => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
let promises = [];
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles());
} }
} else { const sentTo = this.get('sent_to') || [];
this.saveErrors(result.errors); this.save({
if (result.successfulNumbers.length > 0) { sent_to: _.union(sentTo, result.successfulNumbers),
const sentTo = this.get('sent_to') || []; sent: true,
this.set({ expirationStartTimestamp: now,
sent_to: _.union(sentTo, result.successfulNumbers), });
sent: true, this.sendSyncMessage();
expirationStartTimestamp: now, })
}); .catch(result => {
promises.push(this.sendSyncMessage()); const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
} }
promises = promises.concat(_.map(result.errors, (error) => {
if (error.name === 'OutgoingIdentityKeyError') { let promises = [];
const c = ConversationController.get(error.number);
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
})); } else {
} this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
promises.push(this.sendSyncMessage());
}
promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
promises.push(c.getProfiles());
}
})
);
}
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors')); this.trigger('send-error', this.get('errors'));
});
}); });
});
}, },
someRecipientsFailed() { someRecipientsFailed() {
@@ -423,14 +435,16 @@
if (this.get('synced') || !dataMessage) { if (this.get('synced') || !dataMessage) {
return Promise.resolve(); return Promise.resolve();
} }
return textsecure.messaging.sendSyncMessage( return textsecure.messaging
dataMessage, .sendSyncMessage(
this.get('sent_at'), dataMessage,
this.get('destination'), this.get('sent_at'),
this.get('expirationStartTimestamp') this.get('destination'),
).then(() => { this.get('expirationStartTimestamp')
this.save({ synced: true, dataMessage: null }); )
}); .then(() => {
this.save({ synced: true, dataMessage: null });
});
}); });
}, },
@@ -440,17 +454,19 @@
if (!(errors instanceof Array)) { if (!(errors instanceof Array)) {
errors = [errors]; errors = [errors];
} }
errors.forEach((e) => { errors.forEach(e => {
console.log( console.log(
'Message.saveErrors:', 'Message.saveErrors:',
e && e.reason ? e.reason : null, e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e e && e.stack ? e.stack : e
); );
}); });
errors = errors.map((e) => { errors = errors.map(e => {
if (e.constructor === Error || if (
e.constructor === TypeError || e.constructor === Error ||
e.constructor === ReferenceError) { e.constructor === TypeError ||
e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
} }
return e; return e;
@@ -463,32 +479,36 @@
hasNetworkError() { hasNetworkError() {
const error = _.find( const error = _.find(
this.get('errors'), this.get('errors'),
e => (e.name === 'MessageError' || e =>
e.name === 'OutgoingMessageError' || e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError') e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError'
); );
return !!error; return !!error;
}, },
removeOutgoingErrors(number) { removeOutgoingErrors(number) {
const errors = _.partition( const errors = _.partition(
this.get('errors'), this.get('errors'),
e => e.number === number && e =>
(e.name === 'MessageError' || e.number === number &&
e.name === 'OutgoingMessageError' || (e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SendMessageNetworkError' ||
e.name === 'OutgoingIdentityKeyError') e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
); );
this.set({ errors: errors[1] }); this.set({ errors: errors[1] });
return errors[0][0]; return errors[0][0];
}, },
isReplayableError(e) { isReplayableError(e) {
return (e.name === 'MessageError' || return (
e.name === 'OutgoingMessageError' || e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'OutgoingMessageError' ||
e.name === 'SignedPreKeyRotationError' || e.name === 'SendMessageNetworkError' ||
e.name === 'OutgoingIdentityKeyError'); e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
}, },
resend(number) { resend(number) {
const error = this.removeOutgoingErrors(number); const error = this.removeOutgoingErrors(number);
@@ -513,236 +533,282 @@
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
return conversation.queueJob(() => new Promise((resolve) => { return conversation.queueJob(
const now = new Date().getTime(); () =>
let attributes = { type: 'private' }; new Promise(resolve => {
if (dataMessage.group) { const now = new Date().getTime();
let groupUpdate = null; let attributes = { type: 'private' };
attributes = { if (dataMessage.group) {
type: 'group', let groupUpdate = null;
groupId: dataMessage.group.id, attributes = {
}; type: 'group',
if (dataMessage.group.type === GROUP_TYPES.UPDATE) { groupId: dataMessage.group.id,
attributes = { };
type: 'group', if (dataMessage.group.type === GROUP_TYPES.UPDATE) {
groupId: dataMessage.group.id, attributes = {
name: dataMessage.group.name, type: 'group',
avatar: dataMessage.group.avatar, groupId: dataMessage.group.id,
members: _.union(dataMessage.group.members, conversation.get('members')), name: dataMessage.group.name,
}; avatar: dataMessage.group.avatar,
groupUpdate = conversation.changedAttributes(_.pick( members: _.union(
dataMessage.group, dataMessage.group.members,
'name', conversation.get('members')
'avatar' ),
)) || {}; };
const difference = _.difference( groupUpdate =
attributes.members, conversation.changedAttributes(
conversation.get('members') _.pick(dataMessage.group, 'name', 'avatar')
); ) || {};
if (difference.length > 0) { const difference = _.difference(
groupUpdate.joined = difference; attributes.members,
conversation.get('members')
);
if (difference.length > 0) {
groupUpdate.joined = difference;
}
if (conversation.get('left')) {
console.log('re-added to a left group');
attributes.left = false;
}
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
if (source === textsecure.storage.user.getNumber()) {
attributes.left = true;
groupUpdate = { left: 'You' };
} else {
groupUpdate = { left: source };
}
attributes.members = _.without(
conversation.get('members'),
source
);
}
if (groupUpdate !== null) {
message.set({ group_update: groupUpdate });
}
} }
if (conversation.get('left')) { message.set({
console.log('re-added to a left group'); attachments: dataMessage.attachments,
attributes.left = false; body: dataMessage.body,
} conversationId: conversation.id,
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { decrypted_at: now,
if (source === textsecure.storage.user.getNumber()) { errors: [],
attributes.left = true; flags: dataMessage.flags,
groupUpdate = { left: 'You' }; hasAttachments: dataMessage.hasAttachments,
} else { hasFileAttachments: dataMessage.hasFileAttachments,
groupUpdate = { left: source }; hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
} quote: dataMessage.quote,
attributes.members = _.without(conversation.get('members'), source); schemaVersion: dataMessage.schemaVersion,
} });
if (type === 'outgoing') {
if (groupUpdate !== null) { const receipts = Whisper.DeliveryReceipts.forMessage(
message.set({ group_update: groupUpdate }); conversation,
} message
} );
message.set({ receipts.forEach(() =>
attachments: dataMessage.attachments, message.set({
body: dataMessage.body, delivered: (message.get('delivered') || 0) + 1,
conversationId: conversation.id, })
decrypted_at: now,
errors: [],
flags: dataMessage.flags,
hasAttachments: dataMessage.hasAttachments,
hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
});
if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message);
receipts.forEach(() => message.set({
delivered: (message.get('delivered') || 0) + 1,
}));
}
attributes.active_at = now;
conversation.set(attributes);
if (message.isExpirationTimerUpdate()) {
message.set({
expirationTimerUpdate: {
source,
expireTimer: dataMessage.expireTimer,
},
});
conversation.set({ expireTimer: dataMessage.expireTimer });
} else if (dataMessage.expireTimer) {
message.set({ expireTimer: dataMessage.expireTimer });
}
// NOTE: Remove once the above uses
// `Conversation::updateExpirationTimer`:
const { expireTimer } = dataMessage;
const shouldLogExpireTimerChange =
message.isExpirationTimerUpdate() || expireTimer;
if (shouldLogExpireTimerChange) {
console.log(
'Updating expireTimer for conversation',
conversation.idForLogging(),
'to',
expireTimer,
'via `handleDataMessage`'
);
}
if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) {
if (dataMessage.expireTimer !== conversation.get('expireTimer')) {
conversation.updateExpirationTimer(
dataMessage.expireTimer, source,
message.get('received_at')
); );
} }
} else if (conversation.get('expireTimer')) { attributes.active_at = now;
conversation.updateExpirationTimer( conversation.set(attributes);
null, source,
message.get('received_at') if (message.isExpirationTimerUpdate()) {
); message.set({
} expirationTimerUpdate: {
} source,
if (type === 'incoming') { expireTimer: dataMessage.expireTimer,
const readSync = Whisper.ReadSyncs.forMessage(message); },
if (readSync) { });
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) { conversation.set({ expireTimer: dataMessage.expireTimer });
message.set('expirationStartTimestamp', readSync.get('read_at')); } else if (dataMessage.expireTimer) {
message.set({ expireTimer: dataMessage.expireTimer });
} }
}
if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as
// read, as is done when we receive a read sync for a message we already
// know about.
Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1);
}
}
if (type === 'outgoing') { // NOTE: Remove once the above uses
const reads = Whisper.ReadReceipts.forMessage(conversation, message); // `Conversation::updateExpirationTimer`:
if (reads.length) { const { expireTimer } = dataMessage;
const readBy = reads.map(receipt => receipt.get('reader')); const shouldLogExpireTimerChange =
message.set({ message.isExpirationTimerUpdate() || expireTimer;
read_by: _.union(message.get('read_by'), readBy), if (shouldLogExpireTimerChange) {
}); console.log(
} 'Updating expireTimer for conversation',
conversation.idForLogging(),
message.set({ recipients: conversation.getRecipients() }); 'to',
} expireTimer,
'via `handleDataMessage`'
const conversationTimestamp = conversation.get('timestamp'); );
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toArrayBuffer();
if (source === textsecure.storage.user.getNumber()) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.set({ profileKey });
} else {
ConversationController.getOrCreateAndWait(
source,
'private'
).then((sender) => {
sender.setProfileKey(profileKey);
});
}
}
const handleError = (error) => {
const errorForLog = error && error.stack ? error.stack : error;
console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog);
return resolve();
};
message.save().then(() => {
conversation.save().then(() => {
try {
conversation.trigger('newmessage', message);
} catch (e) {
return handleError(e);
} }
// We fetch() here because, between the message.save() above and the previous
// line's trigger() call, we might have marked all messages unread in the
// database. This message might already be read!
const previousUnread = message.get('unread');
return message.fetch().then(() => {
try {
if (previousUnread !== message.get('unread')) {
console.log('Caught race condition on new message read state! ' +
'Manually starting timers.');
// We call markRead() even though the message is already marked read
// because we need to start expiration timers, etc.
message.markRead();
}
if (message.get('unread')) { if (!message.isEndSession() && !message.isGroupUpdate()) {
return conversation.notify(message).then(() => { if (dataMessage.expireTimer) {
confirm(); if (
return resolve(); dataMessage.expireTimer !== conversation.get('expireTimer')
}, handleError); ) {
conversation.updateExpirationTimer(
dataMessage.expireTimer,
source,
message.get('received_at')
);
} }
} else if (conversation.get('expireTimer')) {
confirm(); conversation.updateExpirationTimer(
return resolve(); null,
} catch (e) { source,
return handleError(e); message.get('received_at')
}
}, () => {
try {
console.log(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
); );
confirm();
return resolve();
} catch (e) {
return handleError(e);
} }
}); }
}, handleError); if (type === 'incoming') {
}, handleError); const readSync = Whisper.ReadSyncs.forMessage(message);
})); if (readSync) {
if (
message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
}
}
if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older
// messages as read, as is done when we receive a read sync for
// a message we already know about.
Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
}
}
if (type === 'outgoing') {
const reads = Whisper.ReadReceipts.forMessage(
conversation,
message
);
if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader'));
message.set({
read_by: _.union(message.get('read_by'), readBy),
});
}
message.set({ recipients: conversation.getRecipients() });
}
const conversationTimestamp = conversation.get('timestamp');
if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toArrayBuffer();
if (source === textsecure.storage.user.getNumber()) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.set({ profileKey });
} else {
ConversationController.getOrCreateAndWait(
source,
'private'
).then(sender => {
sender.setProfileKey(profileKey);
});
}
}
const handleError = error => {
const errorForLog = error && error.stack ? error.stack : error;
console.log(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
return resolve();
};
message.save().then(() => {
conversation.save().then(() => {
try {
conversation.trigger('newmessage', message);
} catch (e) {
return handleError(e);
}
// We fetch() here because, between the message.save() above and
// the previous line's trigger() call, we might have marked all
// messages unread in the database. This message might already
// be read!
const previousUnread = message.get('unread');
return message.fetch().then(
() => {
try {
if (previousUnread !== message.get('unread')) {
console.log(
'Caught race condition on new message read state! ' +
'Manually starting timers.'
);
// We call markRead() even though the message is already
// marked read because we need to start expiration
// timers, etc.
message.markRead();
}
if (message.get('unread')) {
return conversation.notify(message).then(() => {
confirm();
return resolve();
}, handleError);
}
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
},
() => {
try {
console.log(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
);
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
}
);
}, handleError);
}, handleError);
})
);
}, },
markRead(readAt) { markRead(readAt) {
this.unset('unread'); this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
this.set('expirationStartTimestamp', readAt || Date.now()); this.set('expirationStartTimestamp', readAt || Date.now());
} }
Whisper.Notifications.remove(Whisper.Notifications.where({ Whisper.Notifications.remove(
messageId: this.id, Whisper.Notifications.where({
})); messageId: this.id,
})
);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.save().then(resolve, reject); this.save().then(resolve, reject);
}); });
@@ -760,7 +826,7 @@
const now = Date.now(); const now = Date.now();
const start = this.get('expirationStartTimestamp'); const start = this.get('expirationStartTimestamp');
const delta = this.get('expireTimer') * 1000; const delta = this.get('expireTimer') * 1000;
let msFromNow = (start + delta) - now; let msFromNow = start + delta - now;
if (msFromNow < 0) { if (msFromNow < 0) {
msFromNow = 0; msFromNow = 0;
} }
@@ -784,7 +850,6 @@
console.log('message', this.get('sent_at'), 'expires at', expiresAt); console.log('message', this.get('sent_at'), 'expires at', expiresAt);
} }
}, },
}); });
Whisper.MessageCollection = Backbone.Collection.extend({ Whisper.MessageCollection = Backbone.Collection.extend({
@@ -804,19 +869,29 @@
} }
}, },
destroyAll() { destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => { return Promise.all(
m.destroy().then(resolve).fail(reject); this.models.map(
}))); m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
}, },
fetchSentAt(timestamp) { fetchSentAt(timestamp) {
return new Promise((resolve => this.fetch({ return new Promise(resolve =>
index: { this.fetch({
// 'receipt' index on sent_at index: {
name: 'receipt', // 'receipt' index on sent_at
only: timestamp, name: 'receipt',
}, only: timestamp,
}).always(resolve))); },
}).always(resolve)
);
}, },
getLoadedUnreadCount() { getLoadedUnreadCount() {
@@ -841,7 +916,7 @@
if (unreadCount > 0) { if (unreadCount > 0) {
startingLoadedUnread = this.getLoadedUnreadCount(); startingLoadedUnread = this.getLoadedUnreadCount();
} }
return new Promise((resolve) => { return new Promise(resolve => {
let upper; let upper;
if (this.length === 0) { if (this.length === 0) {
// fetch the most recent messages first // fetch the most recent messages first
@@ -893,4 +968,4 @@
}); });
}, },
}); });
}()); })();

View File

@@ -20,21 +20,25 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
); );
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadImage(fileOrBlobOrURL, (canvasOrError) => { loadImage(
if (canvasOrError.type === 'error') { fileOrBlobOrURL,
const error = new Error('autoOrientImage: Failed to process image'); canvasOrError => {
error.cause = canvasOrError; if (canvasOrError.type === 'error') {
reject(error); const error = new Error('autoOrientImage: Failed to process image');
return; error.cause = canvasOrError;
} reject(error);
return;
}
const canvas = canvasOrError; const canvas = canvasOrError;
const dataURL = canvas.toDataURL( const dataURL = canvas.toDataURL(
optionsWithDefaults.type, optionsWithDefaults.type,
optionsWithDefaults.quality optionsWithDefaults.quality
); );
resolve(dataURL); resolve(dataURL);
}, optionsWithDefaults); },
optionsWithDefaults
);
}); });
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,141 +1,143 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
const { Settings } = window.Signal.Types;
;(function() { var SETTINGS = {
'use strict'; OFF: 'off',
window.Whisper = window.Whisper || {}; COUNT: 'count',
const { Settings } = window.Signal.Types; NAME: 'name',
MESSAGE: 'message',
};
var SETTINGS = { Whisper.Notifications = new (Backbone.Collection.extend({
OFF : 'off', initialize: function() {
COUNT : 'count', this.isEnabled = false;
NAME : 'name', this.on('add', this.update);
MESSAGE : 'message' this.on('remove', this.onRemove);
}; },
onClick: function(conversationId) {
var conversation = ConversationController.get(conversationId);
this.trigger('click', conversation);
},
update: function() {
const { isEnabled } = this;
const isFocused = window.isFocused();
const isAudioNotificationEnabled =
storage.get('audio-notification') || false;
const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
const shouldPlayNotificationSound =
isAudioNotificationSupported && isAudioNotificationEnabled;
const numNotifications = this.length;
console.log('Update notifications:', {
isFocused,
isEnabled,
numNotifications,
shouldPlayNotificationSound,
});
Whisper.Notifications = new (Backbone.Collection.extend({ if (!isEnabled) {
initialize: function() { return;
this.isEnabled = false; }
this.on('add', this.update);
this.on('remove', this.onRemove);
},
onClick: function(conversationId) {
var conversation = ConversationController.get(conversationId);
this.trigger('click', conversation);
},
update: function() {
const {isEnabled} = this;
const isFocused = window.isFocused();
const isAudioNotificationEnabled = storage.get('audio-notification') || false;
const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
const shouldPlayNotificationSound = isAudioNotificationSupported &&
isAudioNotificationEnabled;
const numNotifications = this.length;
console.log(
'Update notifications:',
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound}
);
if (!isEnabled) { const hasNotifications = numNotifications > 0;
return; if (!hasNotifications) {
} return;
}
const hasNotifications = numNotifications > 0; const isNotificationOmitted = isFocused;
if (!hasNotifications) { if (isNotificationOmitted) {
return; this.clear();
} return;
}
const isNotificationOmitted = isFocused; var setting = storage.get('notification-setting') || 'message';
if (isNotificationOmitted) { if (setting === SETTINGS.OFF) {
this.clear(); return;
return; }
}
var setting = storage.get('notification-setting') || 'message'; window.drawAttention();
if (setting === SETTINGS.OFF) {
return;
}
window.drawAttention(); var title;
var message;
var iconUrl;
var title; // NOTE: i18n has more complex rules for pluralization than just
var message; // distinguishing between zero (0) and other (non-zero),
var iconUrl; // e.g. Russian:
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
var newMessageCount = [
numNotifications,
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'),
].join(' ');
// NOTE: i18n has more complex rules for pluralization than just var last = this.last();
// distinguishing between zero (0) and other (non-zero), switch (this.getSetting()) {
// e.g. Russian: case SETTINGS.COUNT:
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html title = 'Signal';
var newMessageCount = [ message = newMessageCount;
numNotifications, break;
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages') case SETTINGS.NAME:
].join(' '); title = newMessageCount;
message = 'Most recent from ' + last.get('title');
iconUrl = last.get('iconUrl');
break;
case SETTINGS.MESSAGE:
if (numNotifications === 1) {
title = last.get('title');
} else {
title = newMessageCount;
}
message = last.get('message');
iconUrl = last.get('iconUrl');
break;
}
var last = this.last(); if (window.config.polyfillNotifications) {
switch (this.getSetting()) { window.nodeNotifier.notify({
case SETTINGS.COUNT: title: title,
title = 'Signal'; message: message,
message = newMessageCount; sound: false,
break; });
case SETTINGS.NAME: window.nodeNotifier.on('click', function(notifierObject, options) {
title = newMessageCount; last.get('conversationId');
message = 'Most recent from ' + last.get('title'); });
iconUrl = last.get('iconUrl'); } else {
break; var notification = new Notification(title, {
case SETTINGS.MESSAGE: body: message,
if (numNotifications === 1) { icon: iconUrl,
title = last.get('title'); tag: 'signal',
} else { silent: !shouldPlayNotificationSound,
title = newMessageCount; });
}
message = last.get('message');
iconUrl = last.get('iconUrl');
break;
}
if (window.config.polyfillNotifications) { notification.onclick = this.onClick.bind(
window.nodeNotifier.notify({ this,
title: title, last.get('conversationId')
message: message, );
sound: false, }
});
window.nodeNotifier.on('click', function(notifierObject, options) {
last.get('conversationId');
});
} else {
var notification = new Notification(title, {
body : message,
icon : iconUrl,
tag : 'signal',
silent : !shouldPlayNotificationSound,
});
notification.onclick = this.onClick.bind(this, last.get('conversationId')); // We don't want to notify the user about these same messages again
} this.clear();
},
// We don't want to notify the user about these same messages again getSetting: function() {
this.clear(); return storage.get('notification-setting') || SETTINGS.MESSAGE;
}, },
getSetting: function() { onRemove: function() {
return storage.get('notification-setting') || SETTINGS.MESSAGE; console.log('remove notification');
}, },
onRemove: function() { clear: function() {
console.log('remove notification'); console.log('remove all notifications');
}, this.reset([]);
clear: function() { },
console.log('remove all notifications'); enable: function() {
this.reset([]); const needUpdate = !this.isEnabled;
}, this.isEnabled = true;
enable: function() { if (needUpdate) {
const needUpdate = !this.isEnabled; this.update();
this.isEnabled = true; }
if (needUpdate) { },
this.update(); disable: function() {
} this.isEnabled = false;
}, },
disable: function() { }))();
this.isEnabled = false;
},
}))();
})(); })();

View File

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

View File

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

View File

@@ -1,25 +1,24 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ Whisper.Registration = {
(function () { markEverDone: function() {
'use strict'; storage.put('chromiumRegistrationDoneEver', '');
Whisper.Registration = { },
markEverDone: function() { markDone: function() {
storage.put('chromiumRegistrationDoneEver', ''); this.markEverDone();
}, storage.put('chromiumRegistrationDone', '');
markDone: function () { },
this.markEverDone(); isDone: function() {
storage.put('chromiumRegistrationDone', ''); return storage.get('chromiumRegistrationDone') === '';
}, },
isDone: function () { everDone: function() {
return storage.get('chromiumRegistrationDone') === ''; return (
}, storage.get('chromiumRegistrationDoneEver') === '' ||
everDone: function() { storage.get('chromiumRegistrationDone') === ''
return storage.get('chromiumRegistrationDoneEver') === '' || );
storage.get('chromiumRegistrationDone') === ''; },
}, remove: function() {
remove: function() { storage.remove('chromiumRegistrationDone');
storage.remove('chromiumRegistrationDone'); },
} };
}; })();
}());

View File

@@ -1,4 +1,4 @@
(function () { (function() {
// Note: this is all the code required to customize Backbone's trigger() method to make // Note: this is all the code required to customize Backbone's trigger() method to make
// it resilient to exceptions thrown by event handlers. Indentation and code styles // it resilient to exceptions thrown by event handlers. Indentation and code styles
// were kept inline with the Backbone implementation for easier diffs. // were kept inline with the Backbone implementation for easier diffs.
@@ -49,17 +49,26 @@
// triggering events. Tries to keep the usual cases speedy (most internal // triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments). // Backbone events have 3 arguments).
var triggerEvents = function(events, name, args) { var triggerEvents = function(events, name, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; var ev,
i = -1,
l = events.length,
a1 = args[0],
a2 = args[1],
a3 = args[2];
var logError = function(error) { var logError = function(error) {
console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error); console.log(
'Model caught error triggering',
name,
'event:',
error && error.stack ? error.stack : error
);
}; };
switch (args.length) { switch (args.length) {
case 0: case 0:
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx); (ev = events[i]).callback.call(ev.ctx);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@@ -68,8 +77,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1); (ev = events[i]).callback.call(ev.ctx, a1);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@@ -78,8 +86,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1, a2); (ev = events[i]).callback.call(ev.ctx, a1, a2);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@@ -88,8 +95,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3); (ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@@ -98,8 +104,7 @@
while (++i < l) { while (++i < l) {
try { try {
(ev = events[i]).callback.apply(ev.ctx, args); (ev = events[i]).callback.apply(ev.ctx, args);
} } catch (error) {
catch (error) {
logError(error); logError(error);
} }
} }
@@ -122,10 +127,5 @@
return this; return this;
} }
Backbone.Model.prototype.trigger Backbone.Model.prototype.trigger = Backbone.View.prototype.trigger = Backbone.Collection.prototype.trigger = Backbone.Events.trigger = trigger;
= Backbone.View.prototype.trigger
= Backbone.Collection.prototype.trigger
= Backbone.Events.trigger
= trigger;
})(); })();

View File

@@ -1,84 +1,86 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
var timeout;
var scheduledTime;
;(function () { function scheduleNextRotation() {
'use strict'; var now = Date.now();
window.Whisper = window.Whisper || {}; var nextTime = now + ROTATION_INTERVAL;
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; storage.put('nextSignedKeyRotationTime', nextTime);
var timeout; }
var scheduledTime;
function scheduleNextRotation() { function run() {
var now = Date.now(); console.log('Rotating signed prekey...');
var nextTime = now + ROTATION_INTERVAL; getAccountManager()
storage.put('nextSignedKeyRotationTime', nextTime); .rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000);
});
scheduleNextRotation();
setTimeoutForNextRun();
}
function runWhenOnline() {
if (navigator.onLine) {
run();
} else {
console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() {
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
}
}
function setTimeoutForNextRun() {
var now = Date.now();
var time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) {
console.log(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
} }
function run() { scheduledTime = time;
console.log('Rotating signed prekey...'); var waitTime = time - now;
getAccountManager().rotateSignedPreKey().catch(function() { if (waitTime < 0) {
console.log('rotateSignedPrekey() failed. Trying again in five seconds'); waitTime = 0;
setTimeout(runWhenOnline, 5000); }
});
scheduleNextRotation(); clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
var initComplete;
Whisper.RotateSignedPreKeyListener = {
init: function(events, newVersion) {
if (initComplete) {
console.log('Rotate signed prekey listener: Already initialized');
return;
}
initComplete = true;
if (newVersion) {
runWhenOnline();
} else {
setTimeoutForNextRun(); setTimeoutForNextRun();
} }
function runWhenOnline() { events.on('timetravel', function() {
if (navigator.onLine) { if (Whisper.Registration.isDone()) {
run(); setTimeoutForNextRun();
} else {
console.log('We are offline; keys will be rotated when we are next online');
var listener = function() {
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
} }
} });
},
function setTimeoutForNextRun() { };
var now = Date.now(); })();
var time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) {
console.log(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
}
scheduledTime = time;
var waitTime = time - now;
if (waitTime < 0) {
waitTime = 0;
}
clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
var initComplete;
Whisper.RotateSignedPreKeyListener = {
init: function(events, newVersion) {
if (initComplete) {
console.log('Rotate signed prekey listener: Already initialized');
return;
}
initComplete = true;
if (newVersion) {
runWhenOnline();
} else {
setTimeoutForNextRun();
}
events.on('timetravel', function() {
if (Whisper.Registration.isDone()) {
setTimeoutForNextRun();
}
});
}
};
}());

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,80 +1,86 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
;(function() { var Item = Backbone.Model.extend({
'use strict'; database: Whisper.Database,
window.Whisper = window.Whisper || {}; storeName: 'items',
var Item = Backbone.Model.extend({ });
database: Whisper.Database, var ItemCollection = Backbone.Collection.extend({
storeName: 'items' model: Item,
}); storeName: 'items',
var ItemCollection = Backbone.Collection.extend({ database: Whisper.Database,
model: Item, });
storeName: 'items',
database: Whisper.Database,
});
var ready = false; var ready = false;
var items = new ItemCollection(); var items = new ItemCollection();
items.on('reset', function() { ready = true; }); items.on('reset', function() {
window.storage = { ready = true;
/***************************** });
*** Base Storage Routines *** window.storage = {
*****************************/ /*****************************
put: function(key, value) { *** Base Storage Routines ***
if (value === undefined) { *****************************/
throw new Error("Tried to store undefined"); put: function(key, value) {
} if (value === undefined) {
if (!ready) { throw new Error('Tried to store undefined');
console.log('Called storage.put before storage is ready. key:', key); }
} if (!ready) {
var item = items.add({id: key, value: value}, {merge: true}); console.log('Called storage.put before storage is ready. key:', key);
return new Promise(function(resolve, reject) { }
item.save().then(resolve, reject); var item = items.add({ id: key, value: value }, { merge: true });
}); return new Promise(function(resolve, reject) {
}, item.save().then(resolve, reject);
});
},
get: function(key, defaultValue) { get: function(key, defaultValue) {
var item = items.get("" + key); var item = items.get('' + key);
if (!item) { if (!item) {
return defaultValue; return defaultValue;
} }
return item.get('value'); return item.get('value');
}, },
remove: function(key) { remove: function(key) {
var item = items.get("" + key); var item = items.get('' + key);
if (item) { if (item) {
items.remove(item); items.remove(item);
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
item.destroy().then(resolve, reject); item.destroy().then(resolve, reject);
}); });
} }
return Promise.resolve(); return Promise.resolve();
}, },
onready: function(callback) { onready: function(callback) {
if (ready) { if (ready) {
callback(); callback();
} else { } else {
items.on('reset', callback); items.on('reset', callback);
} }
}, },
fetch: function() { fetch: function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
items.fetch({reset: true}) items
.fail(() => reject(new Error('Failed to fetch from storage.' + .fetch({ reset: true })
' This may be due to an unexpected database version.'))) .fail(() =>
.always(resolve); reject(
}); new Error(
}, 'Failed to fetch from storage.' +
' This may be due to an unexpected database version.'
)
)
)
.always(resolve);
});
},
reset: function() { reset: function() {
items.reset(); items.reset();
} },
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
window.textsecure.storage.impl = window.storage; window.textsecure.storage.impl = window.storage;
})(); })();

View File

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

View File

@@ -1,15 +1,12 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.AttachmentPreviewView = Whisper.View.extend({ Whisper.AttachmentPreviewView = Whisper.View.extend({
className: 'attachment-preview', className: 'attachment-preview',
templateName: 'attachment-preview', templateName: 'attachment-preview',
render_attributes: function() { render_attributes: function() {
return {source: this.src}; return { source: this.src };
} },
}); });
})(); })();

View File

@@ -9,7 +9,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
const FileView = Whisper.View.extend({ const FileView = Whisper.View.extend({
@@ -62,10 +62,7 @@
const VideoView = MediaView.extend({ tagName: 'video' }); const VideoView = MediaView.extend({ tagName: 'video' });
// Blacklist common file types known to be unsupported in Chrome // Blacklist common file types known to be unsupported in Chrome
const unsupportedFileTypes = [ const unsupportedFileTypes = ['audio/aiff', 'video/quicktime'];
'audio/aiff',
'video/quicktime',
];
Whisper.AttachmentView = Backbone.View.extend({ Whisper.AttachmentView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
@@ -122,8 +119,11 @@
Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
}, },
isVoiceMessage() { isVoiceMessage() {
// eslint-disable-next-line no-bitwise if (
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) { // eslint-disable-next-line no-bitwise
this.model.flags &
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
) {
return true; return true;
} }
@@ -241,4 +241,4 @@
this.trigger('update'); this.trigger('update');
}, },
}); });
}()); })();

View File

@@ -1,36 +1,33 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.BannerView = Whisper.View.extend({ Whisper.BannerView = Whisper.View.extend({
className: 'banner', className: 'banner',
templateName: 'banner', templateName: 'banner',
events: { events: {
'click .dismiss': 'onDismiss', 'click .dismiss': 'onDismiss',
'click .body': 'onClick', 'click .body': 'onClick',
}, },
initialize: function(options) { initialize: function(options) {
this.message = options.message; this.message = options.message;
this.callbacks = { this.callbacks = {
onDismiss: options.onDismiss, onDismiss: options.onDismiss,
onClick: options.onClick onClick: options.onClick,
}; };
this.render(); this.render();
}, },
render_attributes: function() { render_attributes: function() {
return { return {
message: this.message message: this.message,
}; };
}, },
onDismiss: function(e) { onDismiss: function(e) {
this.callbacks.onDismiss(); this.callbacks.onDismiss();
e.stopPropagation(); e.stopPropagation();
}, },
onClick: function() { onClick: function() {
this.callbacks.onClick(); this.callbacks.onClick();
} },
}); });
})(); })();

View File

@@ -1,57 +1,54 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ConfirmationDialogView = Whisper.View.extend({ Whisper.ConfirmationDialogView = Whisper.View.extend({
className: 'confirmation-dialog modal', className: 'confirmation-dialog modal',
templateName: 'confirmation-dialog', templateName: 'confirmation-dialog',
initialize: function(options) { initialize: function(options) {
this.message = options.message; this.message = options.message;
this.hideCancel = options.hideCancel; this.hideCancel = options.hideCancel;
this.resolve = options.resolve; this.resolve = options.resolve;
this.okText = options.okText || i18n('ok'); this.okText = options.okText || i18n('ok');
this.reject = options.reject; this.reject = options.reject;
this.cancelText = options.cancelText || i18n('cancel'); this.cancelText = options.cancelText || i18n('cancel');
this.render(); this.render();
}, },
events: { events: {
'keyup': 'onKeyup', keyup: 'onKeyup',
'click .ok': 'ok', 'click .ok': 'ok',
'click .cancel': 'cancel', 'click .cancel': 'cancel',
}, },
render_attributes: function() { render_attributes: function() {
return { return {
message: this.message, message: this.message,
showCancel: !this.hideCancel, showCancel: !this.hideCancel,
cancel: this.cancelText, cancel: this.cancelText,
ok: this.okText ok: this.okText,
}; };
}, },
ok: function() { ok: function() {
this.remove(); this.remove();
if (this.resolve) { if (this.resolve) {
this.resolve(); this.resolve();
} }
}, },
cancel: function() { cancel: function() {
this.remove(); this.remove();
if (this.reject) { if (this.reject) {
this.reject(); this.reject();
} }
}, },
onKeyup: function(event) { onKeyup: function(event) {
if (event.key === 'Escape' || event.key === 'Esc') { if (event.key === 'Escape' || event.key === 'Esc') {
this.cancel(); this.cancel();
} }
}, },
focusCancel: function() { focusCancel: function() {
this.$('.cancel').focus(); this.$('.cancel').focus();
} },
}); });
})(); })();

View File

@@ -1,53 +1,50 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ContactListView = Whisper.ListView.extend({ Whisper.ContactListView = Whisper.ListView.extend({
tagName: 'div', tagName: 'div',
itemView: Whisper.View.extend({ itemView: Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: 'contact', className: 'contact',
templateName: 'contact', templateName: 'contact',
events: { events: {
'click': 'showIdentity' click: 'showIdentity',
}, },
initialize: function(options) { initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
render_attributes: function() { render_attributes: function() {
if (this.model.id === this.ourNumber) { if (this.model.id === this.ourNumber) {
return { return {
title: i18n('me'), title: i18n('me'),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
} }
return { return {
class: 'clickable', class: 'clickable',
title: this.model.getTitle(), title: this.model.getTitle(),
number: this.model.getNumber(), number: this.model.getNumber(),
avatar: this.model.getAvatar(), avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
isVerified: this.model.isVerified(), isVerified: this.model.isVerified(),
verified: i18n('verified') verified: i18n('verified'),
}; };
}, },
showIdentity: function() { showIdentity: function() {
if (this.model.id === this.ourNumber) { if (this.model.id === this.ourNumber) {
return; return;
} }
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
} },
}) }),
}); });
})(); })();

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
/* global Whisper: false */ /* global Whisper: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@@ -27,7 +27,7 @@
this.$('textarea').val(i18n('loading')); this.$('textarea').val(i18n('loading'));
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
window.log.fetch().then((text) => { window.log.fetch().then(text => {
this.$('textarea').val(text); this.$('textarea').val(text);
}); });
}, },
@@ -63,7 +63,9 @@
}); });
this.$('.loading').removeClass('loading'); this.$('.loading').removeClass('loading');
view.render(); view.render();
this.$('.link').focus().select(); this.$('.link')
.focus()
.select();
}, },
}); });
}()); })();

View File

@@ -1,16 +1,13 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
var ErrorView = Whisper.View.extend({ var ErrorView = Whisper.View.extend({
className: 'error', className: 'error',
templateName: 'generic-error', templateName: 'generic-error',
render_attributes: function() { render_attributes: function() {
return this.model; return this.model;
} },
}); });
})(); })();

View File

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

View File

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

View File

@@ -1,33 +1,29 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.GroupUpdateView = Backbone.View.extend({ Whisper.GroupUpdateView = Backbone.View.extend({
tagName: "div", tagName: 'div',
className: "group-update", className: 'group-update',
render: function() { render: function() {
//TODO l10n //TODO l10n
if (this.model.left) { if (this.model.left) {
this.$el.text(this.model.left + ' left the group'); this.$el.text(this.model.left + ' left the group');
return this; return this;
} }
var messages = ['Updated the group.']; var messages = ['Updated the group.'];
if (this.model.name) { if (this.model.name) {
messages.push("Title is now '" + this.model.name + "'."); messages.push("Title is now '" + this.model.name + "'.");
} }
if (this.model.joined) { if (this.model.joined) {
messages.push(this.model.joined.join(', ') + ' joined the group'); messages.push(this.model.joined.join(', ') + ' joined the group');
} }
this.$el.text(messages.join(' ')); this.$el.text(messages.join(' '));
return this;
}
});
return this;
},
});
})(); })();

View File

@@ -1,17 +1,14 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.HintView = Whisper.View.extend({ Whisper.HintView = Whisper.View.extend({
templateName: 'hint', templateName: 'hint',
initialize: function(options) { initialize: function(options) {
this.content = options.content; this.content = options.content;
}, },
render_attributes: function() { render_attributes: function() {
return { content: this.content }; return { content: this.content };
} },
}); });
})(); })();

View File

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

View File

@@ -1,51 +1,51 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({ Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({
className: 'identity-key-send-error panel', className: 'identity-key-send-error panel',
templateName: 'identity-key-send-error', templateName: 'identity-key-send-error',
initialize: function(options) { initialize: function(options) {
this.listenBack = options.listenBack; this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel; this.resetPanel = options.resetPanel;
this.wasUnverified = this.model.isUnverified(); this.wasUnverified = this.model.isUnverified();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
events: { events: {
'click .show-safety-number': 'showSafetyNumber', 'click .show-safety-number': 'showSafetyNumber',
'click .send-anyway': 'sendAnyway', 'click .send-anyway': 'sendAnyway',
'click .cancel': 'cancel' 'click .cancel': 'cancel',
}, },
showSafetyNumber: function() { showSafetyNumber: function() {
var view = new Whisper.KeyVerificationPanelView({ var view = new Whisper.KeyVerificationPanelView({
model: this.model model: this.model,
}); });
this.listenBack(view); this.listenBack(view);
}, },
sendAnyway: function() { sendAnyway: function() {
this.resetPanel(); this.resetPanel();
this.trigger('send-anyway'); this.trigger('send-anyway');
}, },
cancel: function() { cancel: function() {
this.resetPanel(); this.resetPanel();
}, },
render_attributes: function() { render_attributes: function() {
var send = i18n('sendAnyway'); var send = i18n('sendAnyway');
if (this.wasUnverified && !this.model.isUnverified()) { if (this.wasUnverified && !this.model.isUnverified()) {
send = i18n('resend'); send = i18n('resend');
} }
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]); var errorExplanation = i18n('identityKeyErrorOnSend', [
return { this.model.getTitle(),
errorExplanation : errorExplanation, this.model.getTitle(),
showSafetyNumber : i18n('showSafetyNumber'), ]);
sendAnyway : send, return {
cancel : i18n('cancel') errorExplanation: errorExplanation,
}; showSafetyNumber: i18n('showSafetyNumber'),
} sendAnyway: send,
}); cancel: i18n('cancel'),
};
},
});
})(); })();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,35 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
var FIVE_SECONDS = 5 * 1000; var FIVE_SECONDS = 5 * 1000;
Whisper.LastSeenIndicatorView = Whisper.View.extend({ Whisper.LastSeenIndicatorView = Whisper.View.extend({
className: 'last-seen-indicator-view', className: 'last-seen-indicator-view',
templateName: 'last-seen-indicator-view', templateName: 'last-seen-indicator-view',
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment: function(count) {
this.count += count; this.count += count;
this.render(); this.render();
}, },
getCount: function() { getCount: function() {
return this.count; return this.count;
}, },
render_attributes: function() { render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage') var unreadMessages =
: i18n('unreadMessages', [this.count]); this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]);
return { return {
unreadMessages: unreadMessages unreadMessages: unreadMessages,
}; };
} },
}); });
})(); })();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,82 +1,86 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.NewGroupUpdateView = Whisper.View.extend({ Whisper.NewGroupUpdateView = Whisper.View.extend({
tagName: "div", tagName: 'div',
className: 'new-group-update', className: 'new-group-update',
templateName: 'new-group-update', templateName: 'new-group-update',
initialize: function(options) { initialize: function(options) {
this.render(); this.render();
this.avatarInput = new Whisper.FileInputView({ this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'), el: this.$('.group-avatar'),
window: options.window window: options.window,
}); });
this.recipients_view = new Whisper.RecipientsInputView(); this.recipients_view = new Whisper.RecipientsInputView();
this.listenTo(this.recipients_view.typeahead, 'sync', function() { this.listenTo(this.recipients_view.typeahead, 'sync', function() {
this.model.contactCollection.models.forEach(function(model) { this.model.contactCollection.models.forEach(
if (this.recipients_view.typeahead.get(model)) { function(model) {
this.recipients_view.typeahead.remove(model); if (this.recipients_view.typeahead.get(model)) {
} this.recipients_view.typeahead.remove(model);
}.bind(this)); }
}); }.bind(this)
this.recipients_view.$el.insertBefore(this.$('.container')); );
});
this.recipients_view.$el.insertBefore(this.$('.container'));
this.member_list_view = new Whisper.ContactListView({ this.member_list_view = new Whisper.ContactListView({
collection: this.model.contactCollection, collection: this.model.contactCollection,
className: 'members' className: 'members',
}); });
this.member_list_view.render(); this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el); this.$('.scrollable').append(this.member_list_view.el);
}, },
events: { events: {
'click .back': 'goBack', 'click .back': 'goBack',
'click .send': 'send', 'click .send': 'send',
'focusin input.search': 'showResults', 'focusin input.search': 'showResults',
'focusout input.search': 'hideResults', 'focusout input.search': 'hideResults',
}, },
hideResults: function() { hideResults: function() {
this.$('.results').hide(); this.$('.results').hide();
}, },
showResults: function() { showResults: function() {
this.$('.results').show(); this.$('.results').show();
}, },
goBack: function() { goBack: function() {
this.trigger('back'); this.trigger('back');
}, },
render_attributes: function() { render_attributes: function() {
return { return {
name: this.model.getTitle(), name: this.model.getTitle(),
avatar: this.model.getAvatar() avatar: this.model.getAvatar(),
}; };
}, },
send: function() { send: function() {
return this.avatarInput.getThumbnail().then(function(avatarFile) { return this.avatarInput.getThumbnail().then(
var now = Date.now(); function(avatarFile) {
var attrs = { var now = Date.now();
timestamp: now, var attrs = {
active_at: now, timestamp: now,
name: this.$('.name').val(), active_at: now,
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id')) name: this.$('.name').val(),
}; members: _.union(
if (avatarFile) { this.model.get('members'),
attrs.avatar = avatarFile; this.recipients_view.recipients.pluck('id')
} ),
this.model.set(attrs); };
var group_update = this.model.changed; if (avatarFile) {
this.model.save(); attrs.avatar = avatarFile;
}
this.model.set(attrs);
var group_update = this.model.changed;
this.model.save();
if (group_update.avatar) { if (group_update.avatar) {
this.model.trigger('change:avatar'); this.model.trigger('change:avatar');
} }
this.model.updateGroup(group_update); this.model.updateGroup(group_update);
this.goBack(); this.goBack();
}.bind(this)); }.bind(this)
} );
}); },
});
})(); })();

View File

@@ -1,36 +1,35 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.PhoneInputView = Whisper.View.extend({ Whisper.PhoneInputView = Whisper.View.extend({
tagName: 'div', tagName: 'div',
className: 'phone-input', className: 'phone-input',
templateName: 'phone-number', templateName: 'phone-number',
initialize: function() { initialize: function() {
this.$('input.number').intlTelInput(); this.$('input.number').intlTelInput();
}, },
events: { events: {
'change': 'validateNumber', change: 'validateNumber',
'keyup': 'validateNumber' keyup: 'validateNumber',
}, },
validateNumber: function() { validateNumber: function() {
var input = this.$('input.number'); var input = this.$('input.number');
var regionCode = this.$('li.active').attr('data-country-code').toUpperCase(); var regionCode = this.$('li.active')
var number = input.val(); .attr('data-country-code')
.toUpperCase();
var number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); var parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
if (parsedNumber.isValidNumber) { if (parsedNumber.isValidNumber) {
this.$('.number-container').removeClass('invalid'); this.$('.number-container').removeClass('invalid');
this.$('.number-container').addClass('valid'); this.$('.number-container').addClass('valid');
} else { } else {
this.$('.number-container').removeClass('valid'); this.$('.number-container').removeClass('valid');
} }
input.trigger('validation'); input.trigger('validation');
return parsedNumber.e164; return parsedNumber.e164;
} },
}); });
})(); })();

View File

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

View File

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

View File

@@ -1,80 +1,81 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.RecorderView = Whisper.View.extend({ Whisper.RecorderView = Whisper.View.extend({
className: 'recorder clearfix', className: 'recorder clearfix',
templateName: 'recorder', templateName: 'recorder',
initialize: function() { initialize: function() {
this.startTime = Date.now(); this.startTime = Date.now();
this.interval = setInterval(this.updateTime.bind(this), 1000); this.interval = setInterval(this.updateTime.bind(this), 1000);
this.start(); this.start();
}, },
events: { events: {
'click .close': 'close', 'click .close': 'close',
'click .finish': 'finish', 'click .finish': 'finish',
'close': 'close' close: 'close',
}, },
updateTime: function() { updateTime: function() {
var duration = moment.duration(Date.now() - this.startTime, 'ms'); var duration = moment.duration(Date.now() - this.startTime, 'ms');
var minutes = '' + Math.trunc(duration.asMinutes()); var minutes = '' + Math.trunc(duration.asMinutes());
var seconds = '' + duration.seconds(); var seconds = '' + duration.seconds();
if (seconds.length < 2) { if (seconds.length < 2) {
seconds = '0' + seconds; seconds = '0' + seconds;
} }
this.$('.time').text(minutes + ':' + seconds); this.$('.time').text(minutes + ':' + seconds);
}, },
close: function() { close: function() {
// Note: the 'close' event can be triggered by InboxView, when the user clicks // Note: the 'close' event can be triggered by InboxView, when the user clicks
// anywhere outside the recording pane. // anywhere outside the recording pane.
if (this.recorder.isRecording()) { if (this.recorder.isRecording()) {
this.recorder.cancelRecording(); this.recorder.cancelRecording();
} }
if (this.interval) { if (this.interval) {
clearInterval(this.interval); clearInterval(this.interval);
} }
if (this.source) { if (this.source) {
this.source.disconnect(); this.source.disconnect();
} }
if (this.context) { if (this.context) {
this.context.close().then(function() { this.context.close().then(function() {
console.log('audio context closed'); console.log('audio context closed');
}); });
} }
this.remove(); this.remove();
this.trigger('closed'); this.trigger('closed');
}, },
finish: function() { finish: function() {
this.recorder.finishRecording(); this.recorder.finishRecording();
this.close(); this.close();
}, },
handleBlob: function(recorder, blob) { handleBlob: function(recorder, blob) {
if (blob) { if (blob) {
this.trigger('send', blob); this.trigger('send', blob);
} }
}, },
start: function() { start: function() {
this.context = new AudioContext(); this.context = new AudioContext();
this.input = this.context.createGain(); this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, { this.recorder = new WebAudioRecorder(this.input, {
encoding: 'mp3', encoding: 'mp3',
workerDir: 'js/' // must end with slash workerDir: 'js/', // must end with slash
}); });
this.recorder.onComplete = this.handleBlob.bind(this); this.recorder.onComplete = this.handleBlob.bind(this);
this.recorder.onError = this.onError; this.recorder.onError = this.onError;
navigator.webkitGetUserMedia({ audio: true }, function(stream) { navigator.webkitGetUserMedia(
this.source = this.context.createMediaStreamSource(stream); { audio: true },
this.source.connect(this.input); function(stream) {
}.bind(this), this.onError.bind(this)); this.source = this.context.createMediaStreamSource(stream);
this.recorder.startRecording(); this.source.connect(this.input);
}, }.bind(this),
onError: function(error) { this.onError.bind(this)
console.log(error.stack); );
this.close(); this.recorder.startRecording();
} },
}); onError: function(error) {
console.log(error.stack);
this.close();
},
});
})(); })();

View File

@@ -1,39 +1,36 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ScrollDownButtonView = Whisper.View.extend({ Whisper.ScrollDownButtonView = Whisper.View.extend({
className: 'scroll-down-button-view', className: 'scroll-down-button-view',
templateName: 'scroll-down-button-view', templateName: 'scroll-down-button-view',
initialize: function(options) { initialize: function(options) {
options = options || {}; options = options || {};
this.count = options.count || 0; this.count = options.count || 0;
}, },
increment: function(count) { increment: function(count) {
count = count || 0; count = count || 0;
this.count += count; this.count += count;
this.render(); this.render();
}, },
render_attributes: function() { render_attributes: function() {
var cssClass = this.count > 0 ? 'new-messages' : ''; var cssClass = this.count > 0 ? 'new-messages' : '';
var moreBelow = i18n('scrollDown'); var moreBelow = i18n('scrollDown');
if (this.count > 1) { if (this.count > 1) {
moreBelow = i18n('messagesBelow'); moreBelow = i18n('messagesBelow');
} else if (this.count === 1) { } else if (this.count === 1) {
moreBelow = i18n('messageBelow'); moreBelow = i18n('messageBelow');
} }
return { return {
cssClass: cssClass, cssClass: cssClass,
moreBelow: moreBelow moreBelow: moreBelow,
}; };
} },
}); });
})(); })();

View File

@@ -5,127 +5,127 @@
/* eslint-disable */ /* eslint-disable */
(function () { (function() {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Database } = window.Whisper; const { Database } = window.Whisper;
const { OS, Logs } = window.Signal; const { OS, Logs } = window.Signal;
const { Settings } = window.Signal.Types; const { Settings } = window.Signal.Types;
var CheckboxView = Whisper.View.extend({ var CheckboxView = Whisper.View.extend({
initialize: function(options) { initialize: function(options) {
this.name = options.name; this.name = options.name;
this.defaultValue = options.defaultValue; this.defaultValue = options.defaultValue;
this.event = options.event; this.event = options.event;
this.populate(); this.populate();
}, },
events: { events: {
'change': 'change' change: 'change',
}, },
change: function(e) { change: function(e) {
var value = e.target.checked; var value = e.target.checked;
storage.put(this.name, value); storage.put(this.name, value);
console.log(this.name, 'changed to', value); console.log(this.name, 'changed to', value);
if (this.event) { if (this.event) {
this.$el.trigger(this.event); this.$el.trigger(this.event);
} }
}, },
populate: function() { populate: function() {
var value = storage.get(this.name, this.defaultValue); var value = storage.get(this.name, this.defaultValue);
this.$('input').prop('checked', !!value); this.$('input').prop('checked', !!value);
}, },
}); });
var RadioButtonGroupView = Whisper.View.extend({ var RadioButtonGroupView = Whisper.View.extend({
initialize: function(options) { initialize: function(options) {
this.name = options.name; this.name = options.name;
this.defaultValue = options.defaultValue; this.defaultValue = options.defaultValue;
this.event = options.event; this.event = options.event;
this.populate(); this.populate();
}, },
events: { events: {
'change': 'change' change: 'change',
}, },
change: function(e) { change: function(e) {
var value = this.$(e.target).val(); var value = this.$(e.target).val();
storage.put(this.name, value); storage.put(this.name, value);
console.log(this.name, 'changed to', value); console.log(this.name, 'changed to', value);
if (this.event) { if (this.event) {
this.$el.trigger(this.event); this.$el.trigger(this.event);
} }
}, },
populate: function() { populate: function() {
var value = storage.get(this.name, this.defaultValue); var value = storage.get(this.name, this.defaultValue);
this.$('#' + this.name + '-' + value).attr('checked', 'checked'); this.$('#' + this.name + '-' + value).attr('checked', 'checked');
}, },
}); });
Whisper.SettingsView = Whisper.View.extend({ Whisper.SettingsView = Whisper.View.extend({
className: 'settings modal expand', className: 'settings modal expand',
templateName: 'settings', templateName: 'settings',
initialize: function() { initialize: function() {
this.deviceName = textsecure.storage.user.getDeviceName(); this.deviceName = textsecure.storage.user.getDeviceName();
this.render(); this.render();
new RadioButtonGroupView({ new RadioButtonGroupView({
el: this.$('.notification-settings'), el: this.$('.notification-settings'),
defaultValue: 'message', defaultValue: 'message',
name: 'notification-setting' name: 'notification-setting',
}); });
new RadioButtonGroupView({ new RadioButtonGroupView({
el: this.$('.theme-settings'), el: this.$('.theme-settings'),
defaultValue: 'android', defaultValue: 'android',
name: 'theme-setting', name: 'theme-setting',
event: 'change-theme' event: 'change-theme',
}); });
if (Settings.isAudioNotificationSupported()) { if (Settings.isAudioNotificationSupported()) {
new CheckboxView({ new CheckboxView({
el: this.$('.audio-notification-setting'), el: this.$('.audio-notification-setting'),
defaultValue: false, defaultValue: false,
name: 'audio-notification' name: 'audio-notification',
}); });
} }
new CheckboxView({ new CheckboxView({
el: this.$('.menu-bar-setting'), el: this.$('.menu-bar-setting'),
defaultValue: false, defaultValue: false,
name: 'hide-menu-bar', name: 'hide-menu-bar',
event: 'change-hide-menu' event: 'change-hide-menu',
}); });
if (textsecure.storage.user.getDeviceId() != '1') { if (textsecure.storage.user.getDeviceId() != '1') {
var syncView = new SyncView().render(); var syncView = new SyncView().render();
this.$('.sync-setting').append(syncView.el); this.$('.sync-setting').append(syncView.el);
} }
}, },
events: { events: {
'click .close': 'remove', 'click .close': 'remove',
'click .clear-data': 'onClearData', 'click .clear-data': 'onClearData',
}, },
render_attributes: function() { render_attributes: function() {
return { return {
deviceNameLabel: i18n('deviceName'), deviceNameLabel: i18n('deviceName'),
deviceName: this.deviceName, deviceName: this.deviceName,
theme: i18n('theme'), theme: i18n('theme'),
notifications: i18n('notifications'), notifications: i18n('notifications'),
notificationSettingsDialog: i18n('notificationSettingsDialog'), notificationSettingsDialog: i18n('notificationSettingsDialog'),
settings: i18n('settings'), settings: i18n('settings'),
disableNotifications: i18n('disableNotifications'), disableNotifications: i18n('disableNotifications'),
nameAndMessage: i18n('nameAndMessage'), nameAndMessage: i18n('nameAndMessage'),
noNameOrMessage: i18n('noNameOrMessage'), noNameOrMessage: i18n('noNameOrMessage'),
nameOnly: i18n('nameOnly'), nameOnly: i18n('nameOnly'),
audioNotificationDescription: i18n('audioNotificationDescription'), audioNotificationDescription: i18n('audioNotificationDescription'),
isAudioNotificationSupported: Settings.isAudioNotificationSupported(), isAudioNotificationSupported: Settings.isAudioNotificationSupported(),
themeAndroidDark: i18n('themeAndroidDark'), themeAndroidDark: i18n('themeAndroidDark'),
hideMenuBar: i18n('hideMenuBar'), hideMenuBar: i18n('hideMenuBar'),
clearDataHeader: i18n('clearDataHeader'), clearDataHeader: i18n('clearDataHeader'),
clearDataButton: i18n('clearDataButton'), clearDataButton: i18n('clearDataButton'),
clearDataExplanation: i18n('clearDataExplanation'), clearDataExplanation: i18n('clearDataExplanation'),
}; };
}, },
onClearData: function() { onClearData: function() {
var clearDataView = new ClearDataView().render(); var clearDataView = new ClearDataView().render();
$('body').append(clearDataView.el); $('body').append(clearDataView.el);
}, },
}); });
/* jshint ignore:start */ /* jshint ignore:start */
/* eslint-enable */ /* eslint-enable */
const CLEAR_DATA_STEPS = { const CLEAR_DATA_STEPS = {
CHOICE: 1, CHOICE: 1,
@@ -160,10 +160,7 @@
}, },
async clearAllData() { async clearAllData() {
try { try {
await Promise.all([ await Promise.all([Logs.deleteAll(), Database.drop()]);
Logs.deleteAll(),
Database.drop(),
]);
} catch (error) { } catch (error) {
console.log( console.log(
'Something went wrong deleting all data:', 'Something went wrong deleting all data:',
@@ -186,61 +183,61 @@
}, },
}); });
/* eslint-disable */ /* eslint-disable */
/* jshint ignore:end */ /* jshint ignore:end */
var SyncView = Whisper.View.extend({ var SyncView = Whisper.View.extend({
templateName: 'syncSettings', templateName: 'syncSettings',
className: 'syncSettings', className: 'syncSettings',
events: { events: {
'click .sync': 'sync' 'click .sync': 'sync',
}, },
enable: function() { enable: function() {
this.$('.sync').text(i18n('syncNow')); this.$('.sync').text(i18n('syncNow'));
this.$('.sync').removeAttr('disabled'); this.$('.sync').removeAttr('disabled');
}, },
disable: function() { disable: function() {
this.$('.sync').attr('disabled', 'disabled'); this.$('.sync').attr('disabled', 'disabled');
this.$('.sync').text(i18n('syncing')); this.$('.sync').text(i18n('syncing'));
}, },
onsuccess: function() { onsuccess: function() {
storage.put('synced_at', Date.now()); storage.put('synced_at', Date.now());
console.log('sync successful'); console.log('sync successful');
this.enable(); this.enable();
this.render(); this.render();
}, },
ontimeout: function() { ontimeout: function() {
console.log('sync timed out'); console.log('sync timed out');
this.$('.synced_at').hide(); this.$('.synced_at').hide();
this.$('.sync_failed').show(); this.$('.sync_failed').show();
this.enable(); this.enable();
}, },
sync: function() { sync: function() {
this.$('.sync_failed').hide(); this.$('.sync_failed').hide();
if (textsecure.storage.user.getDeviceId() != '1') { if (textsecure.storage.user.getDeviceId() != '1') {
this.disable(); this.disable();
var syncRequest = window.getSyncRequest(); var syncRequest = window.getSyncRequest();
syncRequest.addEventListener('success', this.onsuccess.bind(this)); syncRequest.addEventListener('success', this.onsuccess.bind(this));
syncRequest.addEventListener('timeout', this.ontimeout.bind(this)); syncRequest.addEventListener('timeout', this.ontimeout.bind(this));
} else { } else {
console.log("Tried to sync from device 1"); console.log('Tried to sync from device 1');
} }
}, },
render_attributes: function() { render_attributes: function() {
var attrs = { var attrs = {
sync: i18n('sync'), sync: i18n('sync'),
syncNow: i18n('syncNow'), syncNow: i18n('syncNow'),
syncExplanation: i18n('syncExplanation'), syncExplanation: i18n('syncExplanation'),
syncFailed: i18n('syncFailed') syncFailed: i18n('syncFailed'),
}; };
var date = storage.get('synced_at'); var date = storage.get('synced_at');
if (date) { if (date) {
date = new Date(date); date = new Date(date);
attrs.lastSynced = i18n('lastSynced'); attrs.lastSynced = i18n('lastSynced');
attrs.syncDate = date.toLocaleDateString(); attrs.syncDate = date.toLocaleDateString();
attrs.syncTime = date.toLocaleTimeString(); attrs.syncTime = date.toLocaleTimeString();
} }
return attrs; return attrs;
} },
}); });
})(); })();

View File

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

View File

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

View File

@@ -1,28 +1,27 @@
/* (function() {
* vim: ts=4:sw=4:expandtab 'use strict';
*/ window.Whisper = window.Whisper || {};
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ToastView = Whisper.View.extend({ Whisper.ToastView = Whisper.View.extend({
className: 'toast', className: 'toast',
templateName: 'toast', templateName: 'toast',
initialize: function() { initialize: function() {
this.$el.hide(); this.$el.hide();
}, },
close: function() { close: function() {
this.$el.fadeOut(this.remove.bind(this)); this.$el.fadeOut(this.remove.bind(this));
}, },
render: function() { render: function() {
this.$el.html(Mustache.render( this.$el.html(
_.result(this, 'template', ''), Mustache.render(
_.result(this, 'render_attributes', '') _.result(this, 'template', ''),
)); _.result(this, 'render_attributes', '')
this.$el.show(); )
setTimeout(this.close.bind(this), 2000); );
} this.$el.show();
}); setTimeout(this.close.bind(this), 2000);
},
});
})(); })();

View File

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

View File

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

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