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
/*.js
# typescript-generated files
# Generated files
js/components.js
js/libtextsecure.js
js/libsignal-protocol-worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
# Third-party files
js/jquery.js
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
# TypeScript generated files
ts/**/*.js
# ES2015+ files

View File

@@ -2,37 +2,24 @@
module.exports = {
settings: {
'import/core-modules': [
'electron'
]
'import/core-modules': ['electron'],
},
extends: [
'airbnb-base',
],
extends: ['airbnb-base', 'prettier'],
plugins: [
'mocha',
'more',
],
plugins: ['mocha', 'more'],
rules: {
'comma-dangle': ['error', {
'comma-dangle': [
'error',
{
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
functions: 'never',
}],
// putting params on their own line helps stay within line length limit
'function-paren-newline': ['error', 'multiline'],
// 90 characters allows three+ side-by-side screens on a standard-size monitor
'max-len': ['error', {
code: 90,
ignoreUrls: true,
}],
},
],
// prevents us from accidentally checking in exclusive tests (`.only`):
'mocha/no-exclusive-tests': 'error',
@@ -52,6 +39,26 @@ module.exports = {
// consistently place operators at end of line except ternaries
'operator-linebreak': 'error',
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
}
quotes: [
'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false },
],
// Prettier overrides:
'arrow-parens': 'off',
'function-paren-newline': 'off',
'max-len': [
'error',
{
// Prettier generally limits line length to 80 but sometimes goes over.
// The `max-len` plugin 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
js/components.js
libtextsecure/components.js
js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js
stylesheets/*.css
test/test.js
libtextsecure/test/test.js
# React / TypeScript
ts/**/*.js

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,198 +1,215 @@
/*global $, Whisper, Backbone, textsecure, extension*/
/*
* vim: ts=4:sw=4:expandtab
*/
// This script should only be included in background.html
(function () {
'use strict';
(function() {
'use strict';
window.Whisper = window.Whisper || {};
window.Whisper = window.Whisper || {};
var conversations = new Whisper.ConversationCollection();
var inboxCollection = new (Backbone.Collection.extend({
initialize: function() {
this.on('change:timestamp change:name change:number', this.sort);
var conversations = new Whisper.ConversationCollection();
var inboxCollection = new (Backbone.Collection.extend({
initialize: function() {
this.on('change:timestamp change:name change:number', this.sort);
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', function() {
this.reset([]);
});
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', function() {
this.reset([]);
});
this.on('add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000)
);
this.startPruning();
this.on(
'add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000)
);
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) {
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;
}
0
);
storage.put('unreadCount', newUnreadCount);
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;
},
0
);
storage.put("unreadCount", newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title =
window.config.title + ' (' + newUnreadCount + ')';
} else {
window.setBadgeCount(0);
window.document.title = window.config.title;
}
window.updateTrayIcon(newUnreadCount);
},
startPruning: function() {
var halfHour = 30 * 60 * 1000;
this.interval = setInterval(
function() {
this.forEach(function(conversation) {
conversation.trigger('prune');
});
}.bind(this),
halfHour
);
},
}))();
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = window.config.title + " (" + newUnreadCount + ")";
} else {
window.setBadgeCount(0);
window.document.title = window.config.title;
}
window.updateTrayIcon(newUnreadCount);
},
startPruning: function() {
var halfHour = 30 * 60 * 1000;
this.interval = setInterval(function() {
this.forEach(function(conversation) {
conversation.trigger('prune');
});
}.bind(this), halfHour);
window.getInboxCollection = function() {
return inboxCollection;
};
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);
}
}))();
window.getInboxCollection = function() {
return inboxCollection;
};
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;
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;
},
};
})();

View File

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

View File

@@ -1,79 +1,102 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.DeliveryReceipts = new (Backbone.Collection.extend({
forMessage: function(conversation, message) {
var recipients;
if (conversation.isPrivate()) {
recipients = [ conversation.id ];
} else {
recipients = conversation.get('members') || [];
}
var receipts = this.filter(function(receipt) {
return (receipt.get('timestamp') === message.get('sent_at')) &&
(recipients.indexOf(receipt.get('source')) > -1);
Whisper.DeliveryReceipts = new (Backbone.Collection.extend({
forMessage: function(conversation, message) {
var recipients;
if (conversation.isPrivate()) {
recipients = [conversation.id];
} else {
recipients = conversation.get('members') || [];
}
var receipts = this.filter(function(receipt) {
return (
receipt.get('timestamp') === message.get('sent_at') &&
recipients.indexOf(receipt.get('source')) > -1
);
});
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;
},
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')));
});
});
}).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',
});
})
.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'),
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)).catch(function(error) {
console.log(
'DeliveryReceipts.onReceipt error:',
error && error.stack ? error.stack : error
);
});
}
}))();
}.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('timestamp')
);
}
}.bind(this)
)
.catch(function(error) {
console.log(
'DeliveryReceipts.onReceipt error:',
error && error.stack ? error.stack : error
);
});
},
}))();
})();

View File

@@ -1,99 +1,91 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.emoji_util = window.emoji_util || {};
;(function() {
'use strict';
window.emoji_util = window.emoji_util || {};
// EmojiConverter overrides
EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) {
var match = regex.exec(str);
var count = 0;
// EmojiConverter overrides
EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) {
var match = regex.exec(str);
var count = 0;
if (!regex.global) {
return match ? 1 : 0;
}
if (!regex.global) {
return match ? 1 : 0;
}
while (match) {
count += 1;
match = regex.exec(str);
}
while (match) {
count += 1;
match = regex.exec(str);
}
return count;
};
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) {
var self = this;
var noEmoji = str.replace(self.rx_unified, '').trim();
return noEmoji.length > 0;
};
EmojiConvertor.prototype.getSizeClass = function(str) {
var self = this;
EmojiConvertor.prototype.getSizeClass = function(str) {
var self = this;
if (self.hasNormalCharacters(str)) {
return '';
}
if (self.hasNormalCharacters(str)) {
return '';
}
var emojiCount = self.getCountOfAllMatches(str, self.rx_unified);
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);
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 imgClass = /(<img [^>]+ class="emoji)(")/g;
EmojiConvertor.prototype.addClass = function(text, sizeClass) {
if (!sizeClass) {
return text;
}
var imgClass = /(<img [^>]+ class="emoji)(")/g;
EmojiConvertor.prototype.addClass = function(text, sizeClass) {
if (!sizeClass) {
return text;
}
return text.replace(imgClass, function(match, before, after) {
return before + ' ' + sizeClass + after;
});
};
return text.replace(imgClass, function(match, before, after) {
return before + ' ' + sizeClass + after;
});
};
var imgTitle = /(<img [^>]+ class="emoji[^>]+ title=")([^:">]+)(")/g;
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.ensureTitlesHaveColons = function(text) {
return text.replace(imgTitle, function(match, before, title, after) {
return before + ':' + title + ':' + after;
});
};
EmojiConvertor.prototype.signalReplace = function(str) {
var sizeClass = this.getSizeClass(str);
EmojiConvertor.prototype.signalReplace = function(str) {
var sizeClass = this.getSizeClass(str);
var text = this.replace_unified(str);
text = this.addClass(text, sizeClass);
var text = this.replace_unified(str);
text = this.addClass(text, sizeClass);
return this.ensureTitlesHaveColons(text);
};
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();
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_util.parse = function($el) {
if (!$el || !$el.length) {
return;
}
$el.html(emoji.signalReplace($el.html()));
};
window.emoji_util.parse = function($el) {
if (!$el || !$el.length) {
return;
}
$el.html(emoji.signalReplace($el.html()));
};
})();

View File

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

View File

@@ -1,115 +1,121 @@
(function() {
'use strict';
window.Whisper = window.Whisper || {};
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict';
window.Whisper = window.Whisper || {};
function destroyExpiredMessages() {
// 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('_'));
function destroyExpiredMessages() {
// 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();
});
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('');
}
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;
}
}))([
[ 0, 'seconds' ],
[ 5, 'seconds' ],
[ 10, 'seconds' ],
[ 30, 'seconds' ],
[ 1, 'minute' ],
[ 5, 'minutes' ],
[ 30, 'minutes' ],
[ 1, 'hour' ],
[ 6, 'hours' ],
[ 12, 'hours' ],
[ 1, 'day' ],
[ 1, 'week' ],
// 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(
'_'
)
);
},
});
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) {
var duration = moment.duration(o[0], o[1]); // 5, 'seconds'
return {
time: o[0],
unit: o[1],
seconds: duration.asSeconds()
seconds: duration.asSeconds(),
};
}));
})
);
})();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,26 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
storage.isBlocked = function(number) {
var numbers = storage.get('blocked', []);
(function() {
'use strict';
storage.isBlocked = function(number) {
var numbers = storage.get('blocked', []);
return _.include(numbers, number);
};
storage.addBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (_.include(numbers, number)) {
return;
}
return _.include(numbers, number);
};
storage.addBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (_.include(numbers, number)) {
return;
}
console.log('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number));
};
storage.removeBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (!_.include(numbers, number)) {
return;
}
console.log('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number));
};
storage.removeBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (!_.include(numbers, number)) {
return;
}
console.log('removing', number, 'from blocked list');
storage.put('blocked', _.without(numbers, number));
};
console.log('removing', number, 'from blocked list');
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-next-line func-names
(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -32,10 +32,13 @@
this.on('unload', this.unload);
this.setToExpire();
this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
this.VOICE_FLAG =
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
},
idForLogging() {
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`;
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
'sent_at'
)}`;
},
defaults() {
return {
@@ -56,12 +59,13 @@
return !!(this.get('flags') & flag);
},
isExpirationTimerUpdate() {
const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag);
},
isGroupUpdate() {
return !!(this.get('group_update'));
return !!this.get('group_update');
},
isIncoming() {
return this.get('type') === 'incoming';
@@ -79,14 +83,14 @@
if (options.parse === void 0) options.parse = true;
const model = this;
const success = options.success;
options.success = function (resp) {
options.success = function(resp) {
model.attributes = {}; // this is the only changed line
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
const error = options.error;
options.error = function (resp) {
options.error = function(resp) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
@@ -116,7 +120,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name));
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this));
const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
);
if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else {
@@ -186,7 +193,7 @@
}
const quote = this.get('quote');
const attachments = (quote && quote.attachments) || [];
attachments.forEach((attachment) => {
attachments.forEach(attachment => {
if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
URL.revokeObjectURL(attachment.thumbnail.objectUrl);
// eslint-disable-next-line no-param-reassign
@@ -235,8 +242,8 @@
const thumbnailWithObjectUrl = !objectUrl
? null
: Object.assign({}, attachment.thumbnail || {}, {
objectUrl,
});
objectUrl,
});
return Object.assign({}, attachment, {
// eslint-disable-next-line no-bitwise
@@ -269,7 +276,8 @@
return {
attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)),
this.processAttachment(attachment, objectUrl)
),
authorColor,
authorProfileName,
authorTitle,
@@ -342,59 +350,63 @@
send(promise) {
this.trigger('pending');
return promise.then((result) => {
const now = Date.now();
this.trigger('done');
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());
return promise
.then(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
} 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());
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 });
}
promises = promises.concat(_.map(result.errors, (error) => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
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 {
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(() => {
this.trigger('send-error', this.get('errors'));
return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors'));
});
});
});
},
someRecipientsFailed() {
@@ -423,14 +435,16 @@
if (this.get('synced') || !dataMessage) {
return Promise.resolve();
}
return textsecure.messaging.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
this.get('expirationStartTimestamp')
).then(() => {
this.save({ synced: true, dataMessage: null });
});
return textsecure.messaging
.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
this.get('expirationStartTimestamp')
)
.then(() => {
this.save({ synced: true, dataMessage: null });
});
});
},
@@ -440,17 +454,19 @@
if (!(errors instanceof Array)) {
errors = [errors];
}
errors.forEach((e) => {
errors.forEach(e => {
console.log(
'Message.saveErrors:',
e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e
);
});
errors = errors.map((e) => {
if (e.constructor === Error ||
e.constructor === TypeError ||
e.constructor === ReferenceError) {
errors = errors.map(e => {
if (
e.constructor === Error ||
e.constructor === TypeError ||
e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
}
return e;
@@ -463,32 +479,36 @@
hasNetworkError() {
const error = _.find(
this.get('errors'),
e => (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError')
e =>
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError'
);
return !!error;
},
removeOutgoingErrors(number) {
const errors = _.partition(
this.get('errors'),
e => e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
e =>
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
this.set({ errors: errors[1] });
return errors[0][0];
},
isReplayableError(e) {
return (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError');
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
},
resend(number) {
const error = this.removeOutgoingErrors(number);
@@ -513,236 +533,282 @@
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId);
return conversation.queueJob(() => new Promise((resolve) => {
const now = new Date().getTime();
let attributes = { type: 'private' };
if (dataMessage.group) {
let groupUpdate = null;
attributes = {
type: 'group',
groupId: dataMessage.group.id,
};
if (dataMessage.group.type === GROUP_TYPES.UPDATE) {
attributes = {
type: 'group',
groupId: dataMessage.group.id,
name: dataMessage.group.name,
avatar: dataMessage.group.avatar,
members: _.union(dataMessage.group.members, conversation.get('members')),
};
groupUpdate = conversation.changedAttributes(_.pick(
dataMessage.group,
'name',
'avatar'
)) || {};
const difference = _.difference(
attributes.members,
conversation.get('members')
);
if (difference.length > 0) {
groupUpdate.joined = difference;
return conversation.queueJob(
() =>
new Promise(resolve => {
const now = new Date().getTime();
let attributes = { type: 'private' };
if (dataMessage.group) {
let groupUpdate = null;
attributes = {
type: 'group',
groupId: dataMessage.group.id,
};
if (dataMessage.group.type === GROUP_TYPES.UPDATE) {
attributes = {
type: 'group',
groupId: dataMessage.group.id,
name: dataMessage.group.name,
avatar: dataMessage.group.avatar,
members: _.union(
dataMessage.group.members,
conversation.get('members')
),
};
groupUpdate =
conversation.changedAttributes(
_.pick(dataMessage.group, 'name', 'avatar')
) || {};
const difference = _.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')) {
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 });
}
}
message.set({
attachments: dataMessage.attachments,
body: dataMessage.body,
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')
message.set({
attachments: dataMessage.attachments,
body: dataMessage.body,
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,
})
);
}
} else if (conversation.get('expireTimer')) {
conversation.updateExpirationTimer(
null, source,
message.get('received_at')
);
}
}
if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) {
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) {
message.set('expirationStartTimestamp', readSync.get('read_at'));
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 });
}
}
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);
// 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`'
);
}
// 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);
if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) {
if (
dataMessage.expireTimer !== conversation.get('expireTimer')
) {
conversation.updateExpirationTimer(
dataMessage.expireTimer,
source,
message.get('received_at')
);
}
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
}, () => {
try {
console.log(
'handleDataMessage: Message',
message.idForLogging(),
'was deleted'
} else if (conversation.get('expireTimer')) {
conversation.updateExpirationTimer(
null,
source,
message.get('received_at')
);
confirm();
return resolve();
} catch (e) {
return handleError(e);
}
});
}, handleError);
}, handleError);
}));
}
if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) {
if (
message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
}
}
if (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) {
this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
this.set('expirationStartTimestamp', readAt || Date.now());
}
Whisper.Notifications.remove(Whisper.Notifications.where({
messageId: this.id,
}));
Whisper.Notifications.remove(
Whisper.Notifications.where({
messageId: this.id,
})
);
return new Promise((resolve, reject) => {
this.save().then(resolve, reject);
});
@@ -760,7 +826,7 @@
const now = Date.now();
const start = this.get('expirationStartTimestamp');
const delta = this.get('expireTimer') * 1000;
let msFromNow = (start + delta) - now;
let msFromNow = start + delta - now;
if (msFromNow < 0) {
msFromNow = 0;
}
@@ -784,7 +850,6 @@
console.log('message', this.get('sent_at'), 'expires at', expiresAt);
}
},
});
Whisper.MessageCollection = Backbone.Collection.extend({
@@ -804,19 +869,29 @@
}
},
destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => {
m.destroy().then(resolve).fail(reject);
})));
return Promise.all(
this.models.map(
m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
},
fetchSentAt(timestamp) {
return new Promise((resolve => this.fetch({
index: {
// 'receipt' index on sent_at
name: 'receipt',
only: timestamp,
},
}).always(resolve)));
return new Promise(resolve =>
this.fetch({
index: {
// 'receipt' index on sent_at
name: 'receipt',
only: timestamp,
},
}).always(resolve)
);
},
getLoadedUnreadCount() {
@@ -841,7 +916,7 @@
if (unreadCount > 0) {
startingLoadedUnread = this.getLoadedUnreadCount();
}
return new Promise((resolve) => {
return new Promise(resolve => {
let upper;
if (this.length === 0) {
// fetch the most recent messages first
@@ -893,4 +968,4 @@
});
},
});
}());
})();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
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');
exports.isAudioNotificationSupported = () =>
!OS.isLinux();
exports.isAudioNotificationSupported = () => !OS.isLinux();

View File

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

View File

@@ -1,141 +1,143 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
const { Settings } = window.Signal.Types;
;(function() {
'use strict';
window.Whisper = window.Whisper || {};
const { Settings } = window.Signal.Types;
var SETTINGS = {
OFF: 'off',
COUNT: 'count',
NAME: 'name',
MESSAGE: 'message',
};
var SETTINGS = {
OFF : 'off',
COUNT : 'count',
NAME : 'name',
MESSAGE : 'message'
};
Whisper.Notifications = new (Backbone.Collection.extend({
initialize: function() {
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,
});
Whisper.Notifications = new (Backbone.Collection.extend({
initialize: function() {
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) {
return;
}
if (!isEnabled) {
return;
}
const hasNotifications = numNotifications > 0;
if (!hasNotifications) {
return;
}
const hasNotifications = numNotifications > 0;
if (!hasNotifications) {
return;
}
const isNotificationOmitted = isFocused;
if (isNotificationOmitted) {
this.clear();
return;
}
const isNotificationOmitted = isFocused;
if (isNotificationOmitted) {
this.clear();
return;
}
var setting = storage.get('notification-setting') || 'message';
if (setting === SETTINGS.OFF) {
return;
}
var setting = storage.get('notification-setting') || 'message';
if (setting === SETTINGS.OFF) {
return;
}
window.drawAttention();
window.drawAttention();
var title;
var message;
var iconUrl;
var title;
var message;
var iconUrl;
// NOTE: i18n has more complex rules for pluralization than just
// distinguishing between zero (0) and other (non-zero),
// 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
// distinguishing between zero (0) and other (non-zero),
// 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(' ');
var last = this.last();
switch (this.getSetting()) {
case SETTINGS.COUNT:
title = 'Signal';
message = newMessageCount;
break;
case SETTINGS.NAME:
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();
switch (this.getSetting()) {
case SETTINGS.COUNT:
title = 'Signal';
message = newMessageCount;
break;
case SETTINGS.NAME:
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;
}
if (window.config.polyfillNotifications) {
window.nodeNotifier.notify({
title: title,
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,
});
if (window.config.polyfillNotifications) {
window.nodeNotifier.notify({
title: title,
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')
);
}
notification.onclick = this.onClick.bind(this, last.get('conversationId'));
}
// We don't want to notify the user about these same messages again
this.clear();
},
getSetting: function() {
return storage.get('notification-setting') || SETTINGS.MESSAGE;
},
onRemove: function() {
console.log('remove notification');
},
clear: function() {
console.log('remove all notifications');
this.reset([]);
},
enable: function() {
const needUpdate = !this.isEnabled;
this.isEnabled = true;
if (needUpdate) {
this.update();
}
},
disable: function() {
this.isEnabled = false;
},
}))();
// We don't want to notify the user about these same messages again
this.clear();
},
getSetting: function() {
return storage.get('notification-setting') || SETTINGS.MESSAGE;
},
onRemove: function() {
console.log('remove notification');
},
clear: function() {
console.log('remove all notifications');
this.reset([]);
},
enable: function() {
const needUpdate = !this.isEnabled;
this.isEnabled = true;
if (needUpdate) {
this.update();
}
},
disable: function() {
this.isEnabled = false;
},
}))();
})();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,84 +1,86 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
var timeout;
var scheduledTime;
;(function () {
'use strict';
window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
var timeout;
var scheduledTime;
function scheduleNextRotation() {
var now = Date.now();
var nextTime = now + ROTATION_INTERVAL;
storage.put('nextSignedKeyRotationTime', nextTime);
}
function scheduleNextRotation() {
var now = Date.now();
var nextTime = now + ROTATION_INTERVAL;
storage.put('nextSignedKeyRotationTime', nextTime);
function run() {
console.log('Rotating signed prekey...');
getAccountManager()
.rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000);
});
scheduleNextRotation();
setTimeoutForNextRun();
}
function runWhenOnline() {
if (navigator.onLine) {
run();
} else {
console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() {
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
}
}
function setTimeoutForNextRun() {
var now = Date.now();
var time = storage.get('nextSignedKeyRotationTime', now);
if (scheduledTime !== time || !timeout) {
console.log(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
}
function run() {
console.log('Rotating signed prekey...');
getAccountManager().rotateSignedPreKey().catch(function() {
console.log('rotateSignedPrekey() failed. Trying again in five seconds');
setTimeout(runWhenOnline, 5000);
});
scheduleNextRotation();
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();
}
}
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);
events.on('timetravel', function() {
if (Whisper.Registration.isDone()) {
setTimeoutForNextRun();
}
}
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 remote = electron.remote;
var app = remote.app;
@@ -31,7 +31,7 @@
'shouldn',
'wasn',
'weren',
'wouldn'
'wouldn',
];
function setupLinux(locale) {
@@ -39,7 +39,12 @@
// apt-get install hunspell-<locale> can be run for easy access to other dictionaries
var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
console.log('Detected Linux. Setting up spell check with locale', locale, 'and dictionary location', location);
console.log(
'Detected Linux. Setting up spell check with locale',
locale,
'and dictionary location',
location
);
spellchecker.setDictionary(locale, location);
} else {
console.log('Detected Linux. Using default en_US spell check dictionary');
@@ -50,10 +55,17 @@
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
var location = process.env.HUNSPELL_DICTIONARIES;
console.log('Detected Windows 7 or below. Setting up spell-check with locale', locale, 'and dictionary location', location);
console.log(
'Detected Windows 7 or below. Setting up spell-check with locale',
locale,
'and dictionary location',
location
);
spellchecker.setDictionary(locale, location);
} else {
console.log('Detected Windows 7 or below. Using default en_US spell check dictionary');
console.log(
'Detected Windows 7 or below. Using default en_US spell check dictionary'
);
}
}
@@ -69,14 +81,17 @@
if (process.platform === 'linux') {
setupLinux(locale);
} else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) {
} else if (
process.platform === 'windows' &&
semver.lt(os.release(), '8.0.0')
) {
setupWin7AndEarlier(locale);
} else {
// OSX and Windows 8+ have OS-level spellcheck APIs
console.log('Using OS-level spell check API with locale', process.env.LANG);
}
var simpleChecker = window.spellChecker = {
var simpleChecker = (window.spellChecker = {
spellCheck: function(text) {
return !this.isMisspelled(text);
},
@@ -101,8 +116,8 @@
},
add: function(text) {
spellchecker.add(text);
}
};
},
});
webFrame.setSpellCheckProvider(
'en-US',
@@ -120,7 +135,8 @@
var selectedText = window.getSelection().toString();
var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
var spellingSuggestions = isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
var spellingSuggestions =
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
var menu = buildEditorContextMenu({
isMisspelled: isMisspelled,
spellingSuggestions: spellingSuggestions,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,59 +1,57 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'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.
*/
Whisper.IdenticonSVGView = Whisper.View.extend({
templateName: 'identicon-svg',
initialize: function(options) {
this.render_attributes = options;
this.render_attributes.color = COLORS[this.render_attributes.color];
},
getSVGUrl: function() {
var html = this.render().$el.html();
var svg = new Blob([html], {type: 'image/svg+xml;charset=utf-8'});
return URL.createObjectURL(svg);
},
getDataUrl: function() {
var svgurl = this.getSVGUrl();
return new Promise(function(resolve) {
var img = document.createElement('img');
img.onload = function () {
var canvas = loadImage.scale(img, {
canvas: true, maxWidth: 100, maxHeight: 100
});
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(svgurl);
resolve(canvas.toDataURL('image/png'));
};
Whisper.IdenticonSVGView = Whisper.View.extend({
templateName: 'identicon-svg',
initialize: function(options) {
this.render_attributes = options;
this.render_attributes.color = COLORS[this.render_attributes.color];
},
getSVGUrl: function() {
var html = this.render().$el.html();
var svg = new Blob([html], { type: 'image/svg+xml;charset=utf-8' });
return URL.createObjectURL(svg);
},
getDataUrl: function() {
var svgurl = this.getSVGUrl();
return new Promise(function(resolve) {
var img = document.createElement('img');
img.onload = function() {
var canvas = loadImage.scale(img, {
canvas: true,
maxWidth: 100,
maxHeight: 100,
});
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(svgurl);
resolve(canvas.toDataURL('image/png'));
};
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'
};
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',
};
})();

View File

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

View File

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

View File

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

View File

@@ -1,196 +1,201 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var Steps = {
INSTALL_SIGNAL: 2,
SCAN_QR_CODE: 3,
ENTER_NAME: 4,
PROGRESS_BAR: 5,
TOO_MANY_DEVICES: 'TooManyDevices',
NETWORK_ERROR: 'NetworkError',
};
var Steps = {
INSTALL_SIGNAL: 2,
SCAN_QR_CODE: 3,
ENTER_NAME: 4,
PROGRESS_BAR: 5,
TOO_MANY_DEVICES: 'TooManyDevices',
NETWORK_ERROR: 'NetworkError',
};
var DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411;
var DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411;
Whisper.InstallView = Whisper.View.extend({
templateName: 'link-flow-template',
className: 'main full-screen-flow',
events: {
'click .try-again': 'connect',
'click .finish': 'finishLinking',
// the actual next step happens in confirmNumber() on submit form #link-phone
},
initialize: function(options) {
options = options || {};
Whisper.InstallView = Whisper.View.extend({
templateName: 'link-flow-template',
className: 'main full-screen-flow',
events: {
'click .try-again': 'connect',
'click .finish': 'finishLinking',
// the actual next step happens in confirmNumber() on submit form #link-phone
},
initialize: function(options) {
options = options || {};
this.selectStep(Steps.SCAN_QR_CODE);
this.connect();
this.on('disconnected', this.reconnect);
this.selectStep(Steps.SCAN_QR_CODE);
this.connect();
this.on('disconnected', this.reconnect);
// Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
},
render_attributes: function() {
var errorMessage;
// Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData =
Whisper.Registration.everDone() || options.hasExistingData;
},
render_attributes: function() {
var errorMessage;
if (this.error) {
if (this.error.name === 'HTTPError'
&& this.error.code == TOO_MANY_DEVICES) {
if (this.error) {
if (
this.error.name === 'HTTPError' &&
this.error.code == TOO_MANY_DEVICES
) {
errorMessage = i18n('installTooManyDevices');
} else if (
this.error.name === 'HTTPError' &&
this.error.code == CONNECTION_ERROR
) {
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');
}
else if (this.error.name === 'HTTPError'
&& this.error.code == CONNECTION_ERROR) {
return {
isError: true,
errorHeader: 'Something went wrong!',
errorMessage,
errorButton: 'Try again',
};
}
errorMessage = i18n('installConnectionFailed');
}
else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message
errorMessage = i18n('installConnectionFailed');
}
return {
isStep3: this.step === Steps.SCAN_QR_CODE,
linkYourPhone: i18n('linkYourPhone'),
signalSettings: i18n('signalSettings'),
linkedDevices: i18n('linkedDevices'),
androidFinalStep: i18n('plusButton'),
appleFinalStep: i18n('linkNewDevice'),
return {
isError: true,
errorHeader: 'Something went wrong!',
errorMessage,
errorButton: 'Try again',
};
}
isStep4: this.step === Steps.ENTER_NAME,
chooseName: i18n('chooseDeviceName'),
finishLinkingPhoneButton: i18n('finishLinkingPhone'),
return {
isStep3: this.step === Steps.SCAN_QR_CODE,
linkYourPhone: i18n('linkYourPhone'),
signalSettings: i18n('signalSettings'),
linkedDevices: i18n('linkedDevices'),
androidFinalStep: i18n('plusButton'),
appleFinalStep: i18n('linkNewDevice'),
isStep5: this.step === Steps.PROGRESS_BAR,
syncing: i18n('initialSync'),
};
},
selectStep: function(step) {
this.step = step;
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,
chooseName: i18n('chooseDeviceName'),
finishLinkingPhoneButton: i18n('finishLinkingPhone'),
var accountManager = getAccountManager();
isStep5: this.step === Steps.PROGRESS_BAR,
syncing: i18n('initialSync'),
};
},
selectStep: function(step) {
this.step = step;
this.render();
},
connect: function() {
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
accountManager
.registerSecondDevice(
this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this)
)
.catch(this.handleDisconnect.bind(this));
},
handleDisconnect: function(e) {
console.log('provisioning failed', e.stack);
var accountManager = getAccountManager();
this.error = e;
this.render();
accountManager.registerSecondDevice(
this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this)
).catch(this.handleDisconnect.bind(this));
},
handleDisconnect: function(e) {
console.log('provisioning failed', e.stack);
if (e.message === 'websocket closed') {
this.trigger('disconnected');
} else if (
e.name !== 'HTTPError' ||
(e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
) {
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.render();
this.$('#qr .container').hide();
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.trigger('disconnected');
} else if (e.name !== 'HTTPError'
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname);
this.$(DEVICE_NAME_SELECTOR).focus();
},
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;
}
},
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!');
window.removeSetupMenuItems();
this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault();
return new Promise(
function(resolve, reject) {
this.$('#link-phone').submit(
function(e) {
e.stopPropagation();
e.preventDefault();
var name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g, ''); // strip unicode null
if (name.trim().length === 0) {
this.$(DEVICE_NAME_SELECTOR).focus();
return;
}
}
this.$('#qr .container').hide();
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.selectStep(Steps.PROGRESS_BAR);
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname);
this.$(DEVICE_NAME_SELECTOR).focus();
},
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;
var finish = function() {
resolve(name);
};
window.removeSetupMenuItems();
this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault();
// 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();
}
return new Promise(function(resolve, reject) {
this.$('#link-phone').submit(function(e) {
e.stopPropagation();
e.preventDefault();
var name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g,''); // strip unicode null
if (name.trim().length === 0) {
this.$(DEVICE_NAME_SELECTOR).focus();
return;
}
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));
},
});
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.KeyVerificationPanelView = Whisper.View.extend({
className: 'key-verification panel',
templateName: 'key-verification',
events: {
'click button.verify': 'toggleVerified',
},
initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber();
if (options.newKey) {
this.theirKey = options.newKey;
Whisper.KeyVerificationPanelView = Whisper.View.extend({
className: 'key-verification panel',
templateName: 'key-verification',
events: {
'click button.verify': 'toggleVerified',
},
initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber();
if (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() {
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);
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
};
}
});
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'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({
className: 'last-seen-indicator-view',
templateName: 'last-seen-indicator-view',
initialize: function(options) {
options = options || {};
this.count = options.count || 0;
},
Whisper.LastSeenIndicatorView = Whisper.View.extend({
className: 'last-seen-indicator-view',
templateName: 'last-seen-indicator-view',
initialize: function(options) {
options = options || {};
this.count = options.count || 0;
},
increment: function(count) {
this.count += count;
this.render();
},
increment: function(count) {
this.count += count;
this.render();
},
getCount: function() {
return this.count;
},
getCount: function() {
return this.count;
},
render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]);
render_attributes: function() {
var unreadMessages =
this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]);
return {
unreadMessages: unreadMessages
};
}
});
return {
unreadMessages: unreadMessages,
};
},
});
})();

View File

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

View File

@@ -1,168 +1,190 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var ContactView = Whisper.View.extend({
className: 'contact-detail',
templateName: 'contact-detail',
initialize: function(options) {
this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel;
this.message = options.message;
var ContactView = Whisper.View.extend({
className: 'contact-detail',
templateName: 'contact-detail',
initialize: function(options) {
this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel;
this.message = options.message;
var newIdentity = i18n('newIdentity');
this.errors = _.map(options.errors, function(error) {
if (error.name === 'OutgoingIdentityKeyError') {
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')
};
var newIdentity = i18n('newIdentity');
this.errors = _.map(options.errors, function(error) {
if (error.name === 'OutgoingIdentityKeyError') {
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,
});
Whisper.MessageDetailView = Whisper.View.extend({
className: 'message-detail panel',
templateName: 'message-detail',
initialize: function(options) {
this.listenBack = options.listenBack;
this.resetPanel = options.resetPanel;
this.listenTo(view, 'send-anyway', this.onSendAnyway);
this.view = new Whisper.MessageView({model: this.model});
this.view.render();
this.conversation = options.conversation;
view.render();
this.listenTo(this.model, 'change', this.render);
},
events: {
'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();
}
this.listenBack(view);
view.$('.cancel').focus();
}
},
forceSend: function() {
this.model
.updateVerified()
.then(
function() {
if (this.model.isUnverified()) {
return this.model.setVerifiedDefault();
}
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);
});
}.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);
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'));
return {
status: this.message.getStatus(this.model.id),
name: this.model.getTitle(),
avatar: this.model.getAvatar(),
errors: this.errors,
showErrorButton: showButton,
errorButtonLabel: i18n('view'),
};
},
});
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) {
_.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));
this.view = new Whisper.MessageView({ model: this.model });
this.view.render();
this.conversation = options.conversation;
this.listenTo(this.model, 'change', this.render);
},
events: {
'click button.delete': 'onDelete',
},
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.MessageListView = Whisper.ListView.extend({
tagName: 'ul',
className: 'message-list',
itemView: Whisper.MessageView,
events: {
'scroll': 'onScroll',
},
initialize: function() {
Whisper.ListView.prototype.initialize.call(this);
Whisper.MessageListView = Whisper.ListView.extend({
tagName: 'ul',
className: 'message-list',
itemView: Whisper.MessageView,
events: {
scroll: 'onScroll',
},
initialize: function() {
Whisper.ListView.prototype.initialize.call(this);
this.triggerLazyScroll = _.debounce(function() {
this.$el.trigger('lazyScroll');
}.bind(this), 500);
},
onScroll: function() {
this.measureScrollPosition();
if (this.$el.scrollTop() === 0) {
this.$el.trigger('loadMore');
}
if (this.atBottom()) {
this.$el.trigger('atBottom');
} else if (this.bottomOffset > this.outerHeight) {
this.$el.trigger('farFromBottom');
}
this.triggerLazyScroll = _.debounce(
function() {
this.$el.trigger('lazyScroll');
}.bind(this),
500
);
},
onScroll: function() {
this.measureScrollPosition();
if (this.$el.scrollTop() === 0) {
this.$el.trigger('loadMore');
}
if (this.atBottom()) {
this.$el.trigger('atBottom');
} else if (this.bottomOffset > this.outerHeight) {
this.$el.trigger('farFromBottom');
}
this.triggerLazyScroll();
},
atBottom: function() {
return this.bottomOffset < 30;
},
measureScrollPosition: function() {
if (this.el.scrollHeight === 0) { // hidden
return;
}
this.outerHeight = this.$el.outerHeight();
this.scrollPosition = this.$el.scrollTop() + this.outerHeight;
this.scrollHeight = this.el.scrollHeight;
this.bottomOffset = this.scrollHeight - this.scrollPosition;
},
resetScrollPosition: function() {
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
},
scrollToBottomIfNeeded: function() {
// This is counter-intuitive. Our current bottomOffset is reflective of what
// we last measured, not necessarily the current state. And this is called
// after we just made a change to the DOM: inserting a message, or an image
// finished loading. So if we were near the bottom before, we _need_ to be
// at the bottom again. So we scroll to the bottom.
if (this.atBottom()) {
this.scrollToBottom();
}
},
scrollToBottom: function() {
this.$el.scrollTop(this.el.scrollHeight);
this.measureScrollPosition();
},
addOne: function(model) {
var view;
if (model.isExpirationTimerUpdate()) {
view = new Whisper.ExpirationTimerUpdateView({model: model}).render();
} else if (model.get('type') === 'keychange') {
view = new Whisper.KeyChangeView({model: model}).render();
} else if (model.get('type') === 'verified-change') {
view = new Whisper.VerifiedChangeView({model: model}).render();
} else {
view = new this.itemView({model: model}).render();
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
}
this.triggerLazyScroll();
},
atBottom: function() {
return this.bottomOffset < 30;
},
measureScrollPosition: function() {
if (this.el.scrollHeight === 0) {
// hidden
return;
}
this.outerHeight = this.$el.outerHeight();
this.scrollPosition = this.$el.scrollTop() + this.outerHeight;
this.scrollHeight = this.el.scrollHeight;
this.bottomOffset = this.scrollHeight - this.scrollPosition;
},
resetScrollPosition: function() {
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
},
scrollToBottomIfNeeded: function() {
// This is counter-intuitive. Our current bottomOffset is reflective of what
// we last measured, not necessarily the current state. And this is called
// after we just made a change to the DOM: inserting a message, or an image
// finished loading. So if we were near the bottom before, we _need_ to be
// at the bottom again. So we scroll to the bottom.
if (this.atBottom()) {
this.scrollToBottom();
}
},
scrollToBottom: function() {
this.$el.scrollTop(this.el.scrollHeight);
this.measureScrollPosition();
},
addOne: function(model) {
var view;
if (model.isExpirationTimerUpdate()) {
view = new Whisper.ExpirationTimerUpdateView({ model: model }).render();
} else if (model.get('type') === 'keychange') {
view = new Whisper.KeyChangeView({ model: model }).render();
} else if (model.get('type') === 'verified-change') {
view = new Whisper.VerifiedChangeView({ model: model }).render();
} else {
view = new this.itemView({ model: model }).render();
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
}
var index = this.collection.indexOf(model);
this.measureScrollPosition();
var index = this.collection.indexOf(model);
this.measureScrollPosition();
if (model.get('unread') && !this.atBottom()) {
this.$el.trigger('newOffscreenMessage');
}
if (model.get('unread') && !this.atBottom()) {
this.$el.trigger('newOffscreenMessage');
}
if (index === this.collection.length - 1) {
// add to the bottom.
this.$el.append(view.el);
} else if (index === 0) {
// add to top
this.$el.prepend(view.el);
} else {
// insert
var next = this.$('#' + this.collection.at(index + 1).id);
var prev = this.$('#' + this.collection.at(index - 1).id);
if (next.length > 0) {
view.$el.insertBefore(next);
} else if (prev.length > 0) {
view.$el.insertAfter(prev);
} else {
// scan for the right spot
var elements = this.$el.children();
if (elements.length > 0) {
for (var i = 0; i < elements.length; ++i) {
var m = this.collection.get(elements[i].id);
var m_index = this.collection.indexOf(m);
if (m_index > index) {
view.$el.insertBefore(elements[i]);
break;
}
}
} else {
this.$el.append(view.el);
}
}
if (index === this.collection.length - 1) {
// add to the bottom.
this.$el.append(view.el);
} else if (index === 0) {
// add to top
this.$el.prepend(view.el);
} else {
// insert
var next = this.$('#' + this.collection.at(index + 1).id);
var prev = this.$('#' + this.collection.at(index - 1).id);
if (next.length > 0) {
view.$el.insertBefore(next);
} else if (prev.length > 0) {
view.$el.insertAfter(prev);
} else {
// scan for the right spot
var elements = this.$el.children();
if (elements.length > 0) {
for (var i = 0; i < elements.length; ++i) {
var m = this.collection.get(elements[i].id);
var m_index = this.collection.indexOf(m);
if (m_index > index) {
view.$el.insertBefore(elements[i]);
break;
}
}
this.scrollToBottomIfNeeded();
},
});
} else {
this.$el.append(view.el);
}
}
}
this.scrollToBottomIfNeeded();
},
});
})();

View File

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

View File

@@ -1,114 +1,120 @@
(function () {
'use strict';
(function() {
'use strict';
window.Whisper = window.Whisper || {};
window.Whisper = window.Whisper || {};
Whisper.NetworkStatusView = Whisper.View.extend({
className: 'network-status',
templateName: 'networkStatus',
initialize: function() {
this.$el.hide();
Whisper.NetworkStatusView = Whisper.View.extend({
className: 'network-status',
templateName: 'networkStatus',
initialize: function() {
this.$el.hide();
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
extension.windows.onClosed(function () {
clearInterval(this.renderIntervalHandle);
}.bind(this));
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
extension.windows.onClosed(
function() {
clearInterval(this.renderIntervalHandle);
}.bind(this)
);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
this.withinConnectingGracePeriod = true;
this.setSocketReconnectInterval(null);
this.withinConnectingGracePeriod = true;
this.setSocketReconnectInterval(null);
window.addEventListener('online', this.update.bind(this));
window.addEventListener('offline', this.update.bind(this));
window.addEventListener('online', this.update.bind(this));
window.addEventListener('offline', this.update.bind(this));
this.model = new Backbone.Model();
this.listenTo(this.model, 'change', this.onChange);
},
onReconnectTimer: function() {
this.setSocketReconnectInterval(60000);
},
finishConnectingGracePeriod: function() {
this.withinConnectingGracePeriod = false;
},
setSocketReconnectInterval: function(millis) {
this.socketReconnectWaitDuration = moment.duration(millis);
},
navigatorOnLine: function() { return navigator.onLine; },
getSocketStatus: function() { return window.getSocketStatus(); },
getNetworkStatus: function() {
var message = '';
var instructions = '';
var hasInterruption = false;
var action = null;
var buttonClass = null;
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();
}
}
});
this.model = new Backbone.Model();
this.listenTo(this.model, 'change', this.onChange);
},
onReconnectTimer: function() {
this.setSocketReconnectInterval(60000);
},
finishConnectingGracePeriod: function() {
this.withinConnectingGracePeriod = false;
},
setSocketReconnectInterval: function(millis) {
this.socketReconnectWaitDuration = moment.duration(millis);
},
navigatorOnLine: function() {
return navigator.onLine;
},
getSocketStatus: function() {
return window.getSocketStatus();
},
getNetworkStatus: function() {
var message = '';
var instructions = '';
var hasInterruption = false;
var action = null;
var buttonClass = null;
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.NewGroupUpdateView = Whisper.View.extend({
tagName: "div",
className: 'new-group-update',
templateName: 'new-group-update',
initialize: function(options) {
this.render();
this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'),
window: options.window
});
Whisper.NewGroupUpdateView = Whisper.View.extend({
tagName: 'div',
className: 'new-group-update',
templateName: 'new-group-update',
initialize: function(options) {
this.render();
this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'),
window: options.window,
});
this.recipients_view = new Whisper.RecipientsInputView();
this.listenTo(this.recipients_view.typeahead, 'sync', function() {
this.model.contactCollection.models.forEach(function(model) {
if (this.recipients_view.typeahead.get(model)) {
this.recipients_view.typeahead.remove(model);
}
}.bind(this));
});
this.recipients_view.$el.insertBefore(this.$('.container'));
this.recipients_view = new Whisper.RecipientsInputView();
this.listenTo(this.recipients_view.typeahead, 'sync', function() {
this.model.contactCollection.models.forEach(
function(model) {
if (this.recipients_view.typeahead.get(model)) {
this.recipients_view.typeahead.remove(model);
}
}.bind(this)
);
});
this.recipients_view.$el.insertBefore(this.$('.container'));
this.member_list_view = new Whisper.ContactListView({
collection: this.model.contactCollection,
className: 'members'
});
this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el);
},
events: {
'click .back': 'goBack',
'click .send': 'send',
'focusin input.search': 'showResults',
'focusout input.search': 'hideResults',
},
hideResults: function() {
this.$('.results').hide();
},
showResults: function() {
this.$('.results').show();
},
goBack: function() {
this.trigger('back');
},
render_attributes: function() {
return {
name: this.model.getTitle(),
avatar: this.model.getAvatar()
};
},
send: function() {
return this.avatarInput.getThumbnail().then(function(avatarFile) {
var now = Date.now();
var attrs = {
timestamp: now,
active_at: now,
name: this.$('.name').val(),
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id'))
};
if (avatarFile) {
attrs.avatar = avatarFile;
}
this.model.set(attrs);
var group_update = this.model.changed;
this.model.save();
this.member_list_view = new Whisper.ContactListView({
collection: this.model.contactCollection,
className: 'members',
});
this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el);
},
events: {
'click .back': 'goBack',
'click .send': 'send',
'focusin input.search': 'showResults',
'focusout input.search': 'hideResults',
},
hideResults: function() {
this.$('.results').hide();
},
showResults: function() {
this.$('.results').show();
},
goBack: function() {
this.trigger('back');
},
render_attributes: function() {
return {
name: this.model.getTitle(),
avatar: this.model.getAvatar(),
};
},
send: function() {
return this.avatarInput.getThumbnail().then(
function(avatarFile) {
var now = Date.now();
var attrs = {
timestamp: now,
active_at: now,
name: this.$('.name').val(),
members: _.union(
this.model.get('members'),
this.recipients_view.recipients.pluck('id')
),
};
if (avatarFile) {
attrs.avatar = avatarFile;
}
this.model.set(attrs);
var group_update = this.model.changed;
this.model.save();
if (group_update.avatar) {
this.model.trigger('change:avatar');
}
if (group_update.avatar) {
this.model.trigger('change:avatar');
}
this.model.updateGroup(group_update);
this.goBack();
}.bind(this));
}
});
this.model.updateGroup(group_update);
this.goBack();
}.bind(this)
);
},
});
})();

View File

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

View File

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

View File

@@ -1,185 +1,179 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var ContactsTypeahead = Backbone.TypeaheadCollection.extend({
typeaheadAttributes: [
'name',
'e164_number',
'national_number',
'international_number'
],
database: Whisper.Database,
storeName: 'conversations',
model: Whisper.Conversation,
fetchContacts: function() {
return this.fetch({ reset: true, conditions: { type: 'private' } });
var ContactsTypeahead = Backbone.TypeaheadCollection.extend({
typeaheadAttributes: [
'name',
'e164_number',
'national_number',
'international_number',
],
database: Whisper.Database,
storeName: 'conversations',
model: Whisper.Conversation,
fetchContacts: function() {
return this.fetch({ reset: true, conditions: { type: 'private' } });
},
});
Whisper.ContactPillView = Whisper.View.extend({
tagName: 'span',
className: 'recipient',
events: {
'click .remove': 'removeModel',
},
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({
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() };
}
});
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();
},
Whisper.RecipientListView = Whisper.ListView.extend({
itemView: Whisper.ContactPillView
});
addNewRecipient: function() {
this.recipients.add(this.new_contact_view.model);
this.initNewContact();
this.resetTypeahead();
},
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
className: 'contact-details contact',
templateName: 'contact_name_and_number',
});
addRecipient: function(e, conversation) {
this.recipients.add(this.typeahead.remove(conversation.id));
this.resetTypeahead();
},
Whisper.SuggestionListView = Whisper.ConversationListView.extend({
itemView: Whisper.SuggestionView
});
removeRecipient: function(e, data) {
var model = this.recipients.remove(data.modelId);
if (!model.get('newContact')) {
this.typeahead.add(model);
}
this.filterContacts();
},
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');
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();
},
// 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();
}
},
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]*$/);
}
});
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]*$/);
},
});
})();

View File

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

View File

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

View File

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

View File

@@ -1,88 +1,108 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.StandaloneRegistrationView = Whisper.View.extend({
templateName: 'standalone',
className: 'full-screen-flow',
initialize: function() {
this.accountManager = getAccountManager();
Whisper.StandaloneRegistrationView = Whisper.View.extend({
templateName: 'standalone',
className: 'full-screen-flow',
initialize: function() {
this.accountManager = getAccountManager();
this.render();
this.render();
var number = textsecure.storage.user.getNumber();
if (number) {
this.$('input.number').val(number);
}
this.phoneView = new Whisper.PhoneInputView({el: this.$('#phone-number-input')});
this.$('#error').hide();
},
events: {
'validation input.number': 'onValidation',
'click #request-voice': 'requestVoice',
'click #request-sms': 'requestSMSVerification',
'change #code': 'onChangeCode',
'click #verifyCode': 'verifyCode',
},
verifyCode: function(e) {
var number = this.phoneView.validateNumber();
var verificationCode = $('#code').val().replace(/\D+/g, '');
var number = textsecure.storage.user.getNumber();
if (number) {
this.$('input.number').val(number);
}
this.phoneView = new Whisper.PhoneInputView({
el: this.$('#phone-number-input'),
});
this.$('#error').hide();
},
events: {
'validation input.number': 'onValidation',
'click #request-voice': 'requestVoice',
'click #request-sms': 'requestSMSVerification',
'change #code': 'onChangeCode',
'click #verifyCode': 'verifyCode',
},
verifyCode: function(e) {
var number = this.phoneView.validateNumber();
var verificationCode = $('#code')
.val()
.replace(/\D+/g, '');
this.accountManager.registerSingleDevice(number, verificationCode).then(function() {
this.$el.trigger('openInbox');
}.bind(this)).catch(this.log.bind(this));
},
log: function (s) {
console.log(s);
this.$('#status').text(s);
},
validateCode: function() {
var verificationCode = $('#code').val().replace(/\D/g, '');
if (verificationCode.length == 6) {
return verificationCode;
}
},
displayError: function(error) {
this.$('#error').hide().text(error).addClass('in').fadeIn();
},
onValidation: function() {
if (this.$('#number-container').hasClass('valid')) {
this.$('#request-sms, #request-voice').removeAttr('disabled');
} else {
this.$('#request-sms, #request-voice').prop('disabled', 'disabled');
}
},
onChangeCode: function() {
if (!this.validateCode()) {
this.$('#code').addClass('invalid');
} else {
this.$('#code').removeClass('invalid');
}
},
requestVoice: function() {
window.removeSetupMenuItems();
this.$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this));
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');
}
}
});
this.accountManager
.registerSingleDevice(number, verificationCode)
.then(
function() {
this.$el.trigger('openInbox');
}.bind(this)
)
.catch(this.log.bind(this));
},
log: function(s) {
console.log(s);
this.$('#status').text(s);
},
validateCode: function() {
var verificationCode = $('#code')
.val()
.replace(/\D/g, '');
if (verificationCode.length == 6) {
return verificationCode;
}
},
displayError: function(error) {
this.$('#error')
.hide()
.text(error)
.addClass('in')
.fadeIn();
},
onValidation: function() {
if (this.$('#number-container').hasClass('valid')) {
this.$('#request-sms, #request-voice').removeAttr('disabled');
} else {
this.$('#request-sms, #request-voice').prop('disabled', 'disabled');
}
},
onChangeCode: function() {
if (!this.validateCode()) {
this.$('#code').addClass('invalid');
} else {
this.$('#code').removeClass('invalid');
}
},
requestVoice: function() {
window.removeSetupMenuItems();
this.$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
this.accountManager
.requestVoiceVerification(number)
.catch(this.displayError.bind(this));
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.TimestampView = Whisper.View.extend({
initialize: function(options) {
extension.windows.onClosed(this.clearTimeout.bind(this));
},
update: function() {
this.clearTimeout();
var millis_now = Date.now();
var millis = this.$el.data('timestamp');
if (millis === "") {
return;
}
if (millis >= millis_now) {
millis = millis_now;
}
var result = this.getRelativeTimeSpanString(millis);
this.$el.text(result);
Whisper.TimestampView = Whisper.View.extend({
initialize: function(options) {
extension.windows.onClosed(this.clearTimeout.bind(this));
},
update: function() {
this.clearTimeout();
var millis_now = Date.now();
var millis = this.$el.data('timestamp');
if (millis === '') {
return;
}
if (millis >= millis_now) {
millis = millis_now;
}
var result = this.getRelativeTimeSpanString(millis);
this.$el.text(result);
var timestamp = moment(millis);
this.$el.attr('title', timestamp.format('llll'));
var timestamp = moment(millis);
this.$el.attr('title', timestamp.format('llll'));
var millis_since = millis_now - millis;
if (this.delay) {
if (this.delay < 0) { this.delay = 1000; }
this.timeout = setTimeout(this.update.bind(this), this.delay);
}
},
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"
var millis_since = millis_now - millis;
if (this.delay) {
if (this.delay < 0) {
this.delay = 1000;
}
});
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"
}
});
this.timeout = setTimeout(this.update.bind(this), this.delay);
}
},
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',
},
});
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ToastView = Whisper.View.extend({
className: 'toast',
templateName: 'toast',
initialize: function() {
this.$el.hide();
},
Whisper.ToastView = Whisper.View.extend({
className: 'toast',
templateName: 'toast',
initialize: function() {
this.$el.hide();
},
close: function() {
this.$el.fadeOut(this.remove.bind(this));
},
close: function() {
this.$el.fadeOut(this.remove.bind(this));
},
render: function() {
this.$el.html(Mustache.render(
_.result(this, 'template', ''),
_.result(this, 'render_attributes', '')
));
this.$el.show();
setTimeout(this.close.bind(this), 2000);
}
});
render: function() {
this.$el.html(
Mustache.render(
_.result(this, 'template', ''),
_.result(this, 'render_attributes', '')
)
);
this.$el.show();
setTimeout(this.close.bind(this), 2000);
},
});
})();

View File

@@ -1,6 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*
* Whisper.View
*
* This is the base for most of our views. The Backbone view is extended
@@ -19,62 +17,68 @@
* 4. Provides some common functionality, e.g. confirmation dialog
*
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.View = Backbone.View.extend({
constructor: function() {
Backbone.View.apply(this, arguments);
Mustache.parse(_.result(this, 'template'));
},
render_attributes: function() {
return _.result(this.model, 'attributes', {});
},
render_partials: function() {
return Whisper.View.Templates;
},
template: function() {
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));
Whisper.View = Backbone.View.extend(
{
constructor: function() {
Backbone.View.apply(this, arguments);
Mustache.parse(_.result(this, 'template'));
},
render_attributes: function() {
return _.result(this.model, 'attributes', {});
},
render_partials: function() {
return Whisper.View.Templates;
},
template: function() {
if (this.templateName) {
return Whisper.View.Templates[this.templateName];
}
},{
// 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 '';
},
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,
});
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 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
;(function () {
'use strict';
window.Whisper = window.Whisper || {};
var lastTime;
var interval = 1000;
var events;
function checkTime() {
var currentTime = Date.now();
if (currentTime > (lastTime + interval * 2)) {
events.trigger('timetravel');
}
lastTime = currentTime;
var lastTime;
var interval = 1000;
var events;
function checkTime() {
var currentTime = Date.now();
if (currentTime > lastTime + interval * 2) {
events.trigger('timetravel');
}
lastTime = currentTime;
}
Whisper.WallClockListener = {
init: function(_events) {
events = _events;
lastTime = Date.now();
setInterval(checkTime, interval);
}
};
}());
Whisper.WallClockListener = {
init: function(_events) {
events = _events;
lastTime = Date.now();
setInterval(checkTime, interval);
},
};
})();

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