Format all source code using Prettier
This commit is contained in:
parent
b4dee3f30b
commit
1dd87ad197
149 changed files with 17847 additions and 15439 deletions
27
.eslintrc.js
27
.eslintrc.js
|
@ -2,29 +2,24 @@
|
|||
|
||||
module.exports = {
|
||||
settings: {
|
||||
'import/core-modules': [
|
||||
'electron'
|
||||
]
|
||||
'import/core-modules': ['electron'],
|
||||
},
|
||||
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'prettier',
|
||||
],
|
||||
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',
|
||||
}],
|
||||
},
|
||||
],
|
||||
|
||||
// prevents us from accidentally checking in exclusive tests (`.only`):
|
||||
'mocha/no-exclusive-tests': 'error',
|
||||
|
@ -44,7 +39,11 @@ 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',
|
||||
|
|
511
Gruntfile.js
511
Gruntfile.js
|
@ -13,11 +13,13 @@ module.exports = function(grunt) {
|
|||
|
||||
var libtextsecurecomponents = [];
|
||||
for (i in bower.concat.libtextsecure) {
|
||||
libtextsecurecomponents.push('components/' + bower.concat.libtextsecure[i] + '/**/*.js');
|
||||
libtextsecurecomponents.push(
|
||||
'components/' + bower.concat.libtextsecure[i] + '/**/*.js'
|
||||
);
|
||||
}
|
||||
|
||||
var importOnce = require("node-sass-import-once");
|
||||
grunt.loadNpmTasks("grunt-sass");
|
||||
var importOnce = require('node-sass-import-once');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
@ -34,15 +36,15 @@ module.exports = function(grunt) {
|
|||
src: [
|
||||
'components/mocha/mocha.js',
|
||||
'components/chai/chai.js',
|
||||
'test/_test.js'
|
||||
'test/_test.js',
|
||||
],
|
||||
dest: 'test/test.js',
|
||||
},
|
||||
//TODO: Move errors back down?
|
||||
libtextsecure: {
|
||||
options: {
|
||||
banner: ";(function() {\n",
|
||||
footer: "})();\n",
|
||||
banner: ';(function() {\n',
|
||||
footer: '})();\n',
|
||||
},
|
||||
src: [
|
||||
'libtextsecure/errors.js',
|
||||
|
@ -77,21 +79,21 @@ module.exports = function(grunt) {
|
|||
'components/mock-socket/dist/mock-socket.js',
|
||||
'components/mocha/mocha.js',
|
||||
'components/chai/chai.js',
|
||||
'libtextsecure/test/_test.js'
|
||||
'libtextsecure/test/_test.js',
|
||||
],
|
||||
dest: 'libtextsecure/test/test.js',
|
||||
}
|
||||
},
|
||||
},
|
||||
sass: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
importer: importOnce
|
||||
importer: importOnce,
|
||||
},
|
||||
dev: {
|
||||
files: {
|
||||
"stylesheets/manifest.css": "stylesheets/manifest.scss"
|
||||
}
|
||||
}
|
||||
'stylesheets/manifest.css': 'stylesheets/manifest.scss',
|
||||
},
|
||||
},
|
||||
},
|
||||
jshint: {
|
||||
files: [
|
||||
|
@ -117,7 +119,7 @@ module.exports = function(grunt) {
|
|||
'!js/models/messages.js',
|
||||
'!js/WebAudioRecorderMp3.js',
|
||||
'!libtextsecure/message_receiver.js',
|
||||
'_locales/**/*'
|
||||
'_locales/**/*',
|
||||
],
|
||||
options: { jshintrc: '.jshintrc' },
|
||||
},
|
||||
|
@ -130,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',
|
||||
]);
|
||||
};
|
||||
|
|
1418
js/background.js
1418
js/background.js
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,14 @@
|
|||
/*
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -4,195 +4,215 @@
|
|||
*/
|
||||
|
||||
// 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;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* global _: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const { getPlaceholderMigrations } = window.Signal.Migrations;
|
||||
|
@ -24,13 +24,13 @@
|
|||
};
|
||||
|
||||
function clearStores(db, names) {
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storeNames = names || db.objectStoreNames;
|
||||
console.log('Clearing these indexeddb stores:', storeNames);
|
||||
const transaction = db.transaction(storeNames, 'readwrite');
|
||||
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('clearing all stores done via', via);
|
||||
if (finished) {
|
||||
resolve();
|
||||
|
@ -50,7 +50,7 @@
|
|||
let count = 0;
|
||||
|
||||
// can't use built-in .forEach because db.objectStoreNames is not a plain array
|
||||
_.forEach(storeNames, (storeName) => {
|
||||
_.forEach(storeNames, storeName => {
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.clear();
|
||||
|
||||
|
@ -72,7 +72,7 @@
|
|||
);
|
||||
};
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Whisper.Database.open = () => {
|
||||
|
@ -80,7 +80,7 @@
|
|||
const { version } = migrations[migrations.length - 1];
|
||||
const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version);
|
||||
|
||||
return new Promise(((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// these two event handlers act on the IDBDatabase object,
|
||||
// when the database is opened successfully, or not
|
||||
DBOpenRequest.onerror = reject;
|
||||
|
@ -91,7 +91,7 @@
|
|||
// been created before, or a new version number has been
|
||||
// submitted via the window.indexedDB.open line above
|
||||
DBOpenRequest.onupgradeneeded = reject;
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Whisper.Database.clear = async () => {
|
||||
|
@ -99,7 +99,7 @@
|
|||
return clearStores(db);
|
||||
};
|
||||
|
||||
Whisper.Database.clearStores = async (storeNames) => {
|
||||
Whisper.Database.clearStores = async storeNames => {
|
||||
const db = await Whisper.Database.open();
|
||||
return clearStores(db, storeNames);
|
||||
};
|
||||
|
@ -107,7 +107,7 @@
|
|||
Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall'));
|
||||
|
||||
Whisper.Database.drop = () =>
|
||||
new Promise(((resolve, reject) => {
|
||||
new Promise((resolve, reject) => {
|
||||
const request = window.indexedDB.deleteDatabase(Whisper.Database.id);
|
||||
|
||||
request.onblocked = () => {
|
||||
|
@ -121,7 +121,7 @@
|
|||
};
|
||||
|
||||
request.onsuccess = resolve;
|
||||
}));
|
||||
});
|
||||
|
||||
Whisper.Database.migrations = getPlaceholderMigrations();
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,79 +1,105 @@
|
|||
/*
|
||||
* 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
|
||||
);
|
||||
});
|
||||
},
|
||||
}))();
|
||||
})();
|
||||
|
|
150
js/emoji_util.js
150
js/emoji_util.js
|
@ -2,98 +2,94 @@
|
|||
* 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()));
|
||||
};
|
||||
})();
|
||||
|
|
26
js/expire.js
26
js/expire.js
|
@ -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;
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,115 +1,124 @@
|
|||
|
||||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
;(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
(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(),
|
||||
};
|
||||
}));
|
||||
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var windowFocused = false;
|
||||
|
|
|
@ -2,27 +2,31 @@
|
|||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}());
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
* 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 +16,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 +39,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 +55,244 @@
|
|||
|
||||
getAllRegionCodes: function() {
|
||||
return {
|
||||
"AD":"Andorra",
|
||||
"AE":"United Arab Emirates",
|
||||
"AF":"Afghanistan",
|
||||
"AG":"Antigua and Barbuda",
|
||||
"AI":"Anguilla",
|
||||
"AL":"Albania",
|
||||
"AM":"Armenia",
|
||||
"AO":"Angola",
|
||||
"AR":"Argentina",
|
||||
"AS":"AmericanSamoa",
|
||||
"AT":"Austria",
|
||||
"AU":"Australia",
|
||||
"AW":"Aruba",
|
||||
"AX":"Åland Islands",
|
||||
"AZ":"Azerbaijan",
|
||||
"BA":"Bosnia and Herzegovina",
|
||||
"BB":"Barbados",
|
||||
"BD":"Bangladesh",
|
||||
"BE":"Belgium",
|
||||
"BF":"Burkina Faso",
|
||||
"BG":"Bulgaria",
|
||||
"BH":"Bahrain",
|
||||
"BI":"Burundi",
|
||||
"BJ":"Benin",
|
||||
"BL":"Saint Barthélemy",
|
||||
"BM":"Bermuda",
|
||||
"BN":"Brunei Darussalam",
|
||||
"BO":"Bolivia, Plurinational State of",
|
||||
"BR":"Brazil",
|
||||
"BS":"Bahamas",
|
||||
"BT":"Bhutan",
|
||||
"BW":"Botswana",
|
||||
"BY":"Belarus",
|
||||
"BZ":"Belize",
|
||||
"CA":"Canada",
|
||||
"CC":"Cocos (Keeling) Islands",
|
||||
"CD":"Congo, The Democratic Republic of the",
|
||||
"CF":"Central African Republic",
|
||||
"CG":"Congo",
|
||||
"CH":"Switzerland",
|
||||
"CI":"Cote d'Ivoire",
|
||||
"CK":"Cook Islands",
|
||||
"CL":"Chile",
|
||||
"CM":"Cameroon",
|
||||
"CN":"China",
|
||||
"CO":"Colombia",
|
||||
"CR":"Costa Rica",
|
||||
"CU":"Cuba",
|
||||
"CV":"Cape Verde",
|
||||
"CX":"Christmas Island",
|
||||
"CY":"Cyprus",
|
||||
"CZ":"Czech Republic",
|
||||
"DE":"Germany",
|
||||
"DJ":"Djibouti",
|
||||
"DK":"Denmark",
|
||||
"DM":"Dominica",
|
||||
"DO":"Dominican Republic",
|
||||
"DZ":"Algeria",
|
||||
"EC":"Ecuador",
|
||||
"EE":"Estonia",
|
||||
"EG":"Egypt",
|
||||
"ER":"Eritrea",
|
||||
"ES":"Spain",
|
||||
"ET":"Ethiopia",
|
||||
"FI":"Finland",
|
||||
"FJ":"Fiji",
|
||||
"FK":"Falkland Islands (Malvinas)",
|
||||
"FM":"Micronesia, Federated States of",
|
||||
"FO":"Faroe Islands",
|
||||
"FR":"France",
|
||||
"GA":"Gabon",
|
||||
"GB":"United Kingdom",
|
||||
"GD":"Grenada",
|
||||
"GE":"Georgia",
|
||||
"GF":"French Guiana",
|
||||
"GG":"Guernsey",
|
||||
"GH":"Ghana",
|
||||
"GI":"Gibraltar",
|
||||
"GL":"Greenland",
|
||||
"GM":"Gambia",
|
||||
"GN":"Guinea",
|
||||
"GP":"Guadeloupe",
|
||||
"GQ":"Equatorial Guinea",
|
||||
"GR":"Ελλάδα",
|
||||
"GT":"Guatemala",
|
||||
"GU":"Guam",
|
||||
"GW":"Guinea-Bissau",
|
||||
"GY":"Guyana",
|
||||
"HK":"Hong Kong",
|
||||
"HN":"Honduras",
|
||||
"HR":"Croatia",
|
||||
"HT":"Haiti",
|
||||
"HU":"Magyarország",
|
||||
"ID":"Indonesia",
|
||||
"IE":"Ireland",
|
||||
"IL":"Israel",
|
||||
"IM":"Isle of Man",
|
||||
"IN":"India",
|
||||
"IO":"British Indian Ocean Territory",
|
||||
"IQ":"Iraq",
|
||||
"IR":"Iran, Islamic Republic of",
|
||||
"IS":"Iceland",
|
||||
"IT":"Italy",
|
||||
"JE":"Jersey",
|
||||
"JM":"Jamaica",
|
||||
"JO":"Jordan",
|
||||
"JP":"Japan",
|
||||
"KE":"Kenya",
|
||||
"KG":"Kyrgyzstan",
|
||||
"KH":"Cambodia",
|
||||
"KI":"Kiribati",
|
||||
"KM":"Comoros",
|
||||
"KN":"Saint Kitts and Nevis",
|
||||
"KP":"Korea, Democratic People's Republic of",
|
||||
"KR":"Korea, Republic of",
|
||||
"KW":"Kuwait",
|
||||
"KY":"Cayman Islands",
|
||||
"KZ":"Kazakhstan",
|
||||
"LA":"Lao People's Democratic Republic",
|
||||
"LB":"Lebanon",
|
||||
"LC":"Saint Lucia",
|
||||
"LI":"Liechtenstein",
|
||||
"LK":"Sri Lanka",
|
||||
"LR":"Liberia",
|
||||
"LS":"Lesotho",
|
||||
"LT":"Lithuania",
|
||||
"LU":"Luxembourg",
|
||||
"LV":"Latvia",
|
||||
"LY":"Libyan Arab Jamahiriya",
|
||||
"MA":"Morocco",
|
||||
"MC":"Monaco",
|
||||
"MD":"Moldova, Republic of",
|
||||
"ME":"Црна Гора",
|
||||
"MF":"Saint Martin",
|
||||
"MG":"Madagascar",
|
||||
"MH":"Marshall Islands",
|
||||
"MK":"Macedonia, The Former Yugoslav Republic of",
|
||||
"ML":"Mali",
|
||||
"MM":"Myanmar",
|
||||
"MN":"Mongolia",
|
||||
"MO":"Macao",
|
||||
"MP":"Northern Mariana Islands",
|
||||
"MQ":"Martinique",
|
||||
"MR":"Mauritania",
|
||||
"MS":"Montserrat",
|
||||
"MT":"Malta",
|
||||
"MU":"Mauritius",
|
||||
"MV":"Maldives",
|
||||
"MW":"Malawi",
|
||||
"MX":"Mexico",
|
||||
"MY":"Malaysia",
|
||||
"MZ":"Mozambique",
|
||||
"NA":"Namibia",
|
||||
"NC":"New Caledonia",
|
||||
"NE":"Niger",
|
||||
"NF":"Norfolk Island",
|
||||
"NG":"Nigeria",
|
||||
"NI":"Nicaragua",
|
||||
"NL":"Netherlands",
|
||||
"NO":"Norway",
|
||||
"NP":"Nepal",
|
||||
"NR":"Nauru",
|
||||
"NU":"Niue",
|
||||
"NZ":"New Zealand",
|
||||
"OM":"Oman",
|
||||
"PA":"Panama",
|
||||
"PE":"Peru",
|
||||
"PF":"French Polynesia",
|
||||
"PG":"Papua New Guinea",
|
||||
"PH":"Philippines",
|
||||
"PK":"Pakistan",
|
||||
"PL":"Polska",
|
||||
"PM":"Saint Pierre and Miquelon",
|
||||
"PR":"Puerto Rico",
|
||||
"PS":"Palestinian Territory, Occupied",
|
||||
"PT":"Portugal",
|
||||
"PW":"Palau",
|
||||
"PY":"Paraguay",
|
||||
"QA":"Qatar",
|
||||
"RE":"Réunion",
|
||||
"RO":"Romania",
|
||||
"RS":"Србија",
|
||||
"RU":"Russia",
|
||||
"RW":"Rwanda",
|
||||
"SA":"Saudi Arabia",
|
||||
"SB":"Solomon Islands",
|
||||
"SC":"Seychelles",
|
||||
"SD":"Sudan",
|
||||
"SE":"Sweden",
|
||||
"SG":"Singapore",
|
||||
"SH":"Saint Helena, Ascension and Tristan Da Cunha",
|
||||
"SI":"Slovenia",
|
||||
"SJ":"Svalbard and Jan Mayen",
|
||||
"SK":"Slovakia",
|
||||
"SL":"Sierra Leone",
|
||||
"SM":"San Marino",
|
||||
"SN":"Senegal",
|
||||
"SO":"Somalia",
|
||||
"SR":"Suriname",
|
||||
"ST":"Sao Tome and Principe",
|
||||
"SV":"El Salvador",
|
||||
"SY":"Syrian Arab Republic",
|
||||
"SZ":"Swaziland",
|
||||
"TC":"Turks and Caicos Islands",
|
||||
"TD":"Chad",
|
||||
"TG":"Togo",
|
||||
"TH":"Thailand",
|
||||
"TJ":"Tajikistan",
|
||||
"TK":"Tokelau",
|
||||
"TL":"Timor-Leste",
|
||||
"TM":"Turkmenistan",
|
||||
"TN":"Tunisia",
|
||||
"TO":"Tonga",
|
||||
"TR":"Turkey",
|
||||
"TT":"Trinidad and Tobago",
|
||||
"TV":"Tuvalu",
|
||||
"TW":"Taiwan, Province of China",
|
||||
"TZ":"Tanzania, United Republic of",
|
||||
"UA":"Ukraine",
|
||||
"UG":"Uganda",
|
||||
"US":"United States",
|
||||
"UY":"Uruguay",
|
||||
"UZ":"Uzbekistan",
|
||||
"VA":"Holy See (Vatican City State)",
|
||||
"VC":"Saint Vincent and the Grenadines",
|
||||
"VE":"Venezuela",
|
||||
"VG":"Virgin Islands, British",
|
||||
"VI":"Virgin Islands, U.S.",
|
||||
"VN":"Viet Nam",
|
||||
"VU":"Vanuatu",
|
||||
"WF":"Wallis and Futuna",
|
||||
"WS":"Samoa",
|
||||
"YE":"Yemen",
|
||||
"YT":"Mayotte",
|
||||
"ZA":"South Africa",
|
||||
"ZM":"Zambia",
|
||||
"ZW":"Zimbabwe"
|
||||
AD: 'Andorra',
|
||||
AE: 'United Arab Emirates',
|
||||
AF: 'Afghanistan',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AI: 'Anguilla',
|
||||
AL: 'Albania',
|
||||
AM: 'Armenia',
|
||||
AO: 'Angola',
|
||||
AR: 'Argentina',
|
||||
AS: 'AmericanSamoa',
|
||||
AT: 'Austria',
|
||||
AU: 'Australia',
|
||||
AW: 'Aruba',
|
||||
AX: 'Åland Islands',
|
||||
AZ: 'Azerbaijan',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BB: 'Barbados',
|
||||
BD: 'Bangladesh',
|
||||
BE: 'Belgium',
|
||||
BF: 'Burkina Faso',
|
||||
BG: 'Bulgaria',
|
||||
BH: 'Bahrain',
|
||||
BI: 'Burundi',
|
||||
BJ: 'Benin',
|
||||
BL: 'Saint Barthélemy',
|
||||
BM: 'Bermuda',
|
||||
BN: 'Brunei Darussalam',
|
||||
BO: 'Bolivia, Plurinational State of',
|
||||
BR: 'Brazil',
|
||||
BS: 'Bahamas',
|
||||
BT: 'Bhutan',
|
||||
BW: 'Botswana',
|
||||
BY: 'Belarus',
|
||||
BZ: 'Belize',
|
||||
CA: 'Canada',
|
||||
CC: 'Cocos (Keeling) Islands',
|
||||
CD: 'Congo, The Democratic Republic of the',
|
||||
CF: 'Central African Republic',
|
||||
CG: 'Congo',
|
||||
CH: 'Switzerland',
|
||||
CI: "Cote d'Ivoire",
|
||||
CK: 'Cook Islands',
|
||||
CL: 'Chile',
|
||||
CM: 'Cameroon',
|
||||
CN: 'China',
|
||||
CO: 'Colombia',
|
||||
CR: 'Costa Rica',
|
||||
CU: 'Cuba',
|
||||
CV: 'Cape Verde',
|
||||
CX: 'Christmas Island',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czech Republic',
|
||||
DE: 'Germany',
|
||||
DJ: 'Djibouti',
|
||||
DK: 'Denmark',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic',
|
||||
DZ: 'Algeria',
|
||||
EC: 'Ecuador',
|
||||
EE: 'Estonia',
|
||||
EG: 'Egypt',
|
||||
ER: 'Eritrea',
|
||||
ES: 'Spain',
|
||||
ET: 'Ethiopia',
|
||||
FI: 'Finland',
|
||||
FJ: 'Fiji',
|
||||
FK: 'Falkland Islands (Malvinas)',
|
||||
FM: 'Micronesia, Federated States of',
|
||||
FO: 'Faroe Islands',
|
||||
FR: 'France',
|
||||
GA: 'Gabon',
|
||||
GB: 'United Kingdom',
|
||||
GD: 'Grenada',
|
||||
GE: 'Georgia',
|
||||
GF: 'French Guiana',
|
||||
GG: 'Guernsey',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GL: 'Greenland',
|
||||
GM: 'Gambia',
|
||||
GN: 'Guinea',
|
||||
GP: 'Guadeloupe',
|
||||
GQ: 'Equatorial Guinea',
|
||||
GR: 'Ελλάδα',
|
||||
GT: 'Guatemala',
|
||||
GU: 'Guam',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HK: 'Hong Kong',
|
||||
HN: 'Honduras',
|
||||
HR: 'Croatia',
|
||||
HT: 'Haiti',
|
||||
HU: 'Magyarország',
|
||||
ID: 'Indonesia',
|
||||
IE: 'Ireland',
|
||||
IL: 'Israel',
|
||||
IM: 'Isle of Man',
|
||||
IN: 'India',
|
||||
IO: 'British Indian Ocean Territory',
|
||||
IQ: 'Iraq',
|
||||
IR: 'Iran, Islamic Republic of',
|
||||
IS: 'Iceland',
|
||||
IT: 'Italy',
|
||||
JE: 'Jersey',
|
||||
JM: 'Jamaica',
|
||||
JO: 'Jordan',
|
||||
JP: 'Japan',
|
||||
KE: 'Kenya',
|
||||
KG: 'Kyrgyzstan',
|
||||
KH: 'Cambodia',
|
||||
KI: 'Kiribati',
|
||||
KM: 'Comoros',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
KP: "Korea, Democratic People's Republic of",
|
||||
KR: 'Korea, Republic of',
|
||||
KW: 'Kuwait',
|
||||
KY: 'Cayman Islands',
|
||||
KZ: 'Kazakhstan',
|
||||
LA: "Lao People's Democratic Republic",
|
||||
LB: 'Lebanon',
|
||||
LC: 'Saint Lucia',
|
||||
LI: 'Liechtenstein',
|
||||
LK: 'Sri Lanka',
|
||||
LR: 'Liberia',
|
||||
LS: 'Lesotho',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
LV: 'Latvia',
|
||||
LY: 'Libyan Arab Jamahiriya',
|
||||
MA: 'Morocco',
|
||||
MC: 'Monaco',
|
||||
MD: 'Moldova, Republic of',
|
||||
ME: 'Црна Гора',
|
||||
MF: 'Saint Martin',
|
||||
MG: 'Madagascar',
|
||||
MH: 'Marshall Islands',
|
||||
MK: 'Macedonia, The Former Yugoslav Republic of',
|
||||
ML: 'Mali',
|
||||
MM: 'Myanmar',
|
||||
MN: 'Mongolia',
|
||||
MO: 'Macao',
|
||||
MP: 'Northern Mariana Islands',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MS: 'Montserrat',
|
||||
MT: 'Malta',
|
||||
MU: 'Mauritius',
|
||||
MV: 'Maldives',
|
||||
MW: 'Malawi',
|
||||
MX: 'Mexico',
|
||||
MY: 'Malaysia',
|
||||
MZ: 'Mozambique',
|
||||
NA: 'Namibia',
|
||||
NC: 'New Caledonia',
|
||||
NE: 'Niger',
|
||||
NF: 'Norfolk Island',
|
||||
NG: 'Nigeria',
|
||||
NI: 'Nicaragua',
|
||||
NL: 'Netherlands',
|
||||
NO: 'Norway',
|
||||
NP: 'Nepal',
|
||||
NR: 'Nauru',
|
||||
NU: 'Niue',
|
||||
NZ: 'New Zealand',
|
||||
OM: 'Oman',
|
||||
PA: 'Panama',
|
||||
PE: 'Peru',
|
||||
PF: 'French Polynesia',
|
||||
PG: 'Papua New Guinea',
|
||||
PH: 'Philippines',
|
||||
PK: 'Pakistan',
|
||||
PL: 'Polska',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
PR: 'Puerto Rico',
|
||||
PS: 'Palestinian Territory, Occupied',
|
||||
PT: 'Portugal',
|
||||
PW: 'Palau',
|
||||
PY: 'Paraguay',
|
||||
QA: 'Qatar',
|
||||
RE: 'Réunion',
|
||||
RO: 'Romania',
|
||||
RS: 'Србија',
|
||||
RU: 'Russia',
|
||||
RW: 'Rwanda',
|
||||
SA: 'Saudi Arabia',
|
||||
SB: 'Solomon Islands',
|
||||
SC: 'Seychelles',
|
||||
SD: 'Sudan',
|
||||
SE: 'Sweden',
|
||||
SG: 'Singapore',
|
||||
SH: 'Saint Helena, Ascension and Tristan Da Cunha',
|
||||
SI: 'Slovenia',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SK: 'Slovakia',
|
||||
SL: 'Sierra Leone',
|
||||
SM: 'San Marino',
|
||||
SN: 'Senegal',
|
||||
SO: 'Somalia',
|
||||
SR: 'Suriname',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SV: 'El Salvador',
|
||||
SY: 'Syrian Arab Republic',
|
||||
SZ: 'Swaziland',
|
||||
TC: 'Turks and Caicos Islands',
|
||||
TD: 'Chad',
|
||||
TG: 'Togo',
|
||||
TH: 'Thailand',
|
||||
TJ: 'Tajikistan',
|
||||
TK: 'Tokelau',
|
||||
TL: 'Timor-Leste',
|
||||
TM: 'Turkmenistan',
|
||||
TN: 'Tunisia',
|
||||
TO: 'Tonga',
|
||||
TR: 'Turkey',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TV: 'Tuvalu',
|
||||
TW: 'Taiwan, Province of China',
|
||||
TZ: 'Tanzania, United Republic of',
|
||||
UA: 'Ukraine',
|
||||
UG: 'Uganda',
|
||||
US: 'United States',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VA: 'Holy See (Vatican City State)',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
VE: 'Venezuela',
|
||||
VG: 'Virgin Islands, British',
|
||||
VI: 'Virgin Islands, U.S.',
|
||||
VN: 'Viet Nam',
|
||||
VU: 'Vanuatu',
|
||||
WF: 'Wallis and Futuna',
|
||||
WS: 'Samoa',
|
||||
YE: 'Yemen',
|
||||
YT: 'Mayotte',
|
||||
ZA: 'South Africa',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
};
|
||||
} // getAllRegionCodes
|
||||
}, // getAllRegionCodes
|
||||
}; // libphonenumber.util
|
||||
})();
|
||||
|
|
|
@ -34,7 +34,7 @@ function log(...args) {
|
|||
console._log(...consoleArgs);
|
||||
|
||||
// To avoid [Object object] in our log since console.log handles non-strings smoothly
|
||||
const str = args.map((item) => {
|
||||
const str = args.map(item => {
|
||||
if (typeof item !== 'string') {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
|
@ -55,7 +55,6 @@ if (window.console) {
|
|||
console.log = log;
|
||||
}
|
||||
|
||||
|
||||
// The mechanics of preparing a log for publish
|
||||
|
||||
function getHeader() {
|
||||
|
@ -85,7 +84,7 @@ function format(entries) {
|
|||
}
|
||||
|
||||
function fetch() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
ipc.send('fetch-log');
|
||||
|
||||
ipc.on('fetched-log', (event, text) => {
|
||||
|
@ -103,14 +102,16 @@ const publish = debuglogs.upload;
|
|||
// Anyway, the default process.stdout stream goes to the command-line, not the devtools.
|
||||
const logger = bunyan.createLogger({
|
||||
name: 'log',
|
||||
streams: [{
|
||||
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}`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
/*
|
||||
* 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
|
@ -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,280 @@
|
|||
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 +824,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 +848,6 @@
|
|||
console.log('message', this.get('sent_at'), 'expires at', expiresAt);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Whisper.MessageCollection = Backbone.Collection.extend({
|
||||
|
@ -804,19 +867,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 +914,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 +966,4 @@
|
|||
});
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -19,8 +19,15 @@ async function encryptSymmetric(key, plaintext) {
|
|||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext);
|
||||
const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH);
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
||||
cipherKey,
|
||||
iv,
|
||||
plaintext
|
||||
);
|
||||
const mac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
|
||||
return _concatData([nonce, cipherText, mac]);
|
||||
}
|
||||
|
@ -39,9 +46,14 @@ async function decryptSymmetric(key, data) {
|
|||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
|
||||
const ourMac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH);
|
||||
const ourMac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
if (!constantTimeEqual(theirMac, ourMac)) {
|
||||
throw new Error('decryptSymmetric: Failed to decrypt; MAC verification failed');
|
||||
throw new Error(
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
}
|
||||
|
||||
return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText);
|
||||
|
@ -61,7 +73,6 @@ function constantTimeEqual(left, right) {
|
|||
return result === 0;
|
||||
}
|
||||
|
||||
|
||||
async function _hmac_SHA256(key, data) {
|
||||
const extractable = false;
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
|
@ -72,7 +83,11 @@ async function _hmac_SHA256(key, data) {
|
|||
['sign']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, cryptoKey, data);
|
||||
return window.crypto.subtle.sign(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
cryptoKey,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
||||
|
@ -101,7 +116,6 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
|||
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
||||
}
|
||||
|
||||
|
||||
function _getRandomBytes(n) {
|
||||
const bytes = new Uint8Array(n);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
|
||||
const { isObject, isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
||||
const request = indexedDB.open(name, version);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onblocked = () =>
|
||||
reject(new Error('Database blocked'));
|
||||
request.onblocked = () => reject(new Error('Database blocked'));
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
request.onupgradeneeded = event => {
|
||||
const hasRequestedSpecificVersion = isNumber(version);
|
||||
if (!hasRequestedSpecificVersion) {
|
||||
return;
|
||||
|
@ -26,14 +24,17 @@ exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
|||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`));
|
||||
reject(
|
||||
new Error(
|
||||
'Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const connection = event.target.result;
|
||||
resolve(connection);
|
||||
};
|
||||
|
@ -47,7 +48,7 @@ exports.completeTransaction = transaction =>
|
|||
transaction.addEventListener('complete', () => resolve());
|
||||
});
|
||||
|
||||
exports.getVersion = async (name) => {
|
||||
exports.getVersion = async name => {
|
||||
const connection = await exports.open(name);
|
||||
const { version } = connection;
|
||||
connection.close();
|
||||
|
@ -61,9 +62,7 @@ exports.getCount = async ({ store } = {}) => {
|
|||
|
||||
const request = store.count();
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onsuccess = event =>
|
||||
resolve(event.target.result);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
request.onsuccess = event => resolve(event.target.result);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ const Message = require('./types/message');
|
|||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
const { sleep } = require('./sleep');
|
||||
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan
|
||||
const SENDER_ID = '+12126647665';
|
||||
|
||||
|
@ -27,8 +26,10 @@ exports.createConversation = async ({
|
|||
numMessages,
|
||||
WhisperMessage,
|
||||
} = {}) => {
|
||||
if (!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)) {
|
||||
if (
|
||||
!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)
|
||||
) {
|
||||
throw new TypeError("'ConversationController' is required");
|
||||
}
|
||||
|
||||
|
@ -40,8 +41,10 @@ exports.createConversation = async ({
|
|||
throw new TypeError("'WhisperMessage' is required");
|
||||
}
|
||||
|
||||
const conversation =
|
||||
await ConversationController.getOrCreateAndWait(SENDER_ID, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
SENDER_ID,
|
||||
'private'
|
||||
);
|
||||
conversation.set({
|
||||
active_at: Date.now(),
|
||||
unread: numMessages,
|
||||
|
@ -50,13 +53,15 @@ exports.createConversation = async ({
|
|||
|
||||
const conversationId = conversation.get('id');
|
||||
|
||||
await Promise.all(range(0, numMessages).map(async (index) => {
|
||||
await 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':
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
const FormData = require('form-data');
|
||||
const got = require('got');
|
||||
|
||||
|
||||
const BASE_URL = 'https://debuglogs.org';
|
||||
|
||||
// Workaround: Submitting `FormData` using native `FormData::submit` procedure
|
||||
|
@ -12,7 +11,7 @@ const BASE_URL = 'https://debuglogs.org';
|
|||
// https://github.com/sindresorhus/got/pull/466
|
||||
const submitFormData = (form, url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
form.submit(url, (error) => {
|
||||
form.submit(url, error => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
@ -22,7 +21,7 @@ const submitFormData = (form, url) =>
|
|||
});
|
||||
|
||||
// upload :: String -> Promise URL
|
||||
exports.upload = async (content) => {
|
||||
exports.upload = async content => {
|
||||
const signedForm = await got.get(BASE_URL, { json: true });
|
||||
const { fields, url } = signedForm.body;
|
||||
|
||||
|
|
|
@ -2,11 +2,10 @@ const addUnhandledErrorHandler = require('electron-unhandled');
|
|||
|
||||
const Errors = require('./types/errors');
|
||||
|
||||
|
||||
// addHandler :: Unit -> Unit
|
||||
exports.addHandler = () => {
|
||||
addUnhandledErrorHandler({
|
||||
logger: (error) => {
|
||||
logger: error => {
|
||||
console.error(
|
||||
'Uncaught error or unhandled promise rejection:',
|
||||
Errors.toLogFormat(error)
|
||||
|
|
|
@ -11,7 +11,9 @@ exports.setup = (locale, messages) => {
|
|||
function getMessage(key, substitutions) {
|
||||
const entry = messages[key];
|
||||
if (!entry) {
|
||||
console.error(`i18n: Attempted to get translation for nonexistent key '${key}'`);
|
||||
console.error(
|
||||
`i18n: Attempted to get translation for nonexistent key '${key}'`
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
|
||||
const POLL_INTERVAL_MS = 5 * 1000;
|
||||
const IDLE_THRESHOLD_MS = 20;
|
||||
|
||||
|
@ -35,14 +34,17 @@ class IdleDetector extends EventEmitter {
|
|||
|
||||
_scheduleNextCallback() {
|
||||
this._clearScheduledCallbacks();
|
||||
this.handle = window.requestIdleCallback((deadline) => {
|
||||
this.handle = window.requestIdleCallback(deadline => {
|
||||
const { didTimeout } = deadline;
|
||||
const timeRemaining = deadline.timeRemaining();
|
||||
const isIdle = timeRemaining >= IDLE_THRESHOLD_MS;
|
||||
if (isIdle || didTimeout) {
|
||||
this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining });
|
||||
}
|
||||
this.timeoutId = setTimeout(() => this._scheduleNextCallback(), POLL_INTERVAL_MS);
|
||||
this.timeoutId = setTimeout(
|
||||
() => this._scheduleNextCallback(),
|
||||
POLL_INTERVAL_MS
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ function createLink(url, text, attrs = {}) {
|
|||
const html = [];
|
||||
html.push('<a ');
|
||||
html.push(`href="${url}"`);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
Object.keys(attrs).forEach(key => {
|
||||
html.push(` ${key}="${attrs[key]}"`);
|
||||
});
|
||||
html.push('>');
|
||||
|
@ -23,7 +23,7 @@ module.exports = (text, attrs = {}) => {
|
|||
const result = [];
|
||||
let last = 0;
|
||||
|
||||
matchData.forEach((match) => {
|
||||
matchData.forEach(match => {
|
||||
if (last < match.index) {
|
||||
result.push(text.slice(last, match.index));
|
||||
}
|
||||
|
|
|
@ -6,20 +6,13 @@
|
|||
|
||||
/* global IDBKeyRange */
|
||||
|
||||
const {
|
||||
isFunction,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
const { isFunction, isNumber, isObject, isString, last } = require('lodash');
|
||||
|
||||
const database = require('./database');
|
||||
const Message = require('./types/message');
|
||||
const settings = require('./settings');
|
||||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
|
||||
|
||||
const MESSAGES_STORE_NAME = 'messages';
|
||||
|
||||
exports.processNext = async ({
|
||||
|
@ -29,12 +22,16 @@ exports.processNext = async ({
|
|||
upgradeMessageSchema,
|
||||
} = {}) => {
|
||||
if (!isFunction(BackboneMessage)) {
|
||||
throw new TypeError("'BackboneMessage' (Whisper.Message) constructor is required");
|
||||
throw new TypeError(
|
||||
"'BackboneMessage' (Whisper.Message) constructor is required"
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required');
|
||||
throw new TypeError(
|
||||
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNumber(numMessagesPerBatch)) {
|
||||
|
@ -48,16 +45,18 @@ exports.processNext = async ({
|
|||
const startTime = Date.now();
|
||||
|
||||
const fetchStartTime = Date.now();
|
||||
const messagesRequiringSchemaUpgrade =
|
||||
await _fetchMessagesRequiringSchemaUpgrade({
|
||||
const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade(
|
||||
{
|
||||
BackboneMessageCollection,
|
||||
count: numMessagesPerBatch,
|
||||
});
|
||||
}
|
||||
);
|
||||
const fetchDuration = Date.now() - fetchStartTime;
|
||||
|
||||
const upgradeStartTime = Date.now();
|
||||
const upgradedMessages =
|
||||
await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema));
|
||||
const upgradedMessages = await Promise.all(
|
||||
messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)
|
||||
);
|
||||
const upgradeDuration = Date.now() - upgradeStartTime;
|
||||
|
||||
const saveStartTime = Date.now();
|
||||
|
@ -109,8 +108,10 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
|||
minDatabaseVersion,
|
||||
});
|
||||
if (!isValidDatabaseVersion) {
|
||||
throw new Error(`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`);
|
||||
throw new Error(
|
||||
`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Even if we make this async using `then`, requesting `count` on an
|
||||
|
@ -132,10 +133,13 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
|||
break;
|
||||
}
|
||||
numCumulativeMessagesProcessed += status.numMessagesProcessed;
|
||||
console.log('Upgrade message schema:', Object.assign({}, status, {
|
||||
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)) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
exports.run = (transaction) => {
|
||||
exports.run = transaction => {
|
||||
const messagesStore = transaction.objectStore('messages');
|
||||
|
||||
console.log("Create message attachment metadata index: 'hasAttachments'");
|
||||
|
@ -8,12 +8,10 @@ exports.run = (transaction) => {
|
|||
{ unique: false }
|
||||
);
|
||||
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach((name) => {
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => {
|
||||
console.log(`Create message attachment metadata index: '${name}'`);
|
||||
messagesStore.createIndex(
|
||||
name,
|
||||
['conversationId', 'received_at', name],
|
||||
{ unique: false }
|
||||
);
|
||||
messagesStore.createIndex(name, ['conversationId', 'received_at', name], {
|
||||
unique: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
const Migrations0DatabaseWithAttachmentData =
|
||||
require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData =
|
||||
require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData = require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
exports.getPlaceholderMigrations = () => {
|
||||
const last0MigrationVersion =
|
||||
Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion =
|
||||
Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion = Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
|
||||
const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion;
|
||||
|
||||
return [{
|
||||
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.'
|
||||
);
|
||||
},
|
||||
},
|
||||
}];
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ const db = require('../database');
|
|||
const settings = require('../settings');
|
||||
const { runMigrations } = require('./run_migrations');
|
||||
|
||||
|
||||
// IMPORTANT: Add new migrations that need to traverse entire database, e.g.
|
||||
// messages store, below. Whenever we need this, we need to force attachment
|
||||
// migration on startup:
|
||||
|
@ -20,7 +19,9 @@ const migrations = [
|
|||
exports.run = async ({ Backbone, database } = {}) => {
|
||||
const { canRun } = await exports.getStatus({ database });
|
||||
if (!canRun) {
|
||||
throw new Error('Cannot run migrations on database without attachment data');
|
||||
throw new Error(
|
||||
'Cannot run migrations on database without attachment data'
|
||||
);
|
||||
}
|
||||
|
||||
await runMigrations({ Backbone, database });
|
||||
|
@ -28,8 +29,9 @@ exports.run = async ({ Backbone, database } = {}) => {
|
|||
|
||||
exports.getStatus = async ({ database } = {}) => {
|
||||
const connection = await db.open(database.id, database.version);
|
||||
const isAttachmentMigrationComplete =
|
||||
await settings.isAttachmentMigrationComplete(connection);
|
||||
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
|
||||
connection
|
||||
);
|
||||
const hasMigrations = migrations.length > 0;
|
||||
|
||||
const canRun = isAttachmentMigrationComplete && hasMigrations;
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
/* eslint-env browser */
|
||||
|
||||
const {
|
||||
head,
|
||||
isFunction,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
|
||||
const { head, isFunction, isObject, isString, last } = require('lodash');
|
||||
|
||||
const db = require('../database');
|
||||
const { deferredToPromise } = require('../deferred_to_promise');
|
||||
|
||||
|
||||
const closeDatabaseConnection = ({ Backbone } = {}) =>
|
||||
deferredToPromise(Backbone.sync('closeall'));
|
||||
|
||||
exports.runMigrations = async ({ Backbone, database } = {}) => {
|
||||
if (!isObject(Backbone) || !isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)) {
|
||||
if (
|
||||
!isObject(Backbone) ||
|
||||
!isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)
|
||||
) {
|
||||
throw new TypeError("'Backbone' is required");
|
||||
}
|
||||
|
||||
if (!isObject(database) || !isString(database.id) ||
|
||||
!Array.isArray(database.migrations)) {
|
||||
if (
|
||||
!isObject(database) ||
|
||||
!isString(database.id) ||
|
||||
!Array.isArray(database.migrations)
|
||||
) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
|
||||
|
@ -56,7 +54,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => {
|
|||
await closeDatabaseConnection({ Backbone });
|
||||
};
|
||||
|
||||
const getMigrationVersions = (database) => {
|
||||
const getMigrationVersions = database => {
|
||||
if (!isObject(database) || !Array.isArray(database.migrations)) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
|
@ -64,8 +62,12 @@ const getMigrationVersions = (database) => {
|
|||
const firstMigration = head(database.migrations);
|
||||
const lastMigration = last(database.migrations);
|
||||
|
||||
const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null;
|
||||
const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null;
|
||||
const firstVersion = firstMigration
|
||||
? parseInt(firstMigration.version, 10)
|
||||
: null;
|
||||
const lastVersion = lastMigration
|
||||
? parseInt(lastMigration.version, 10)
|
||||
: null;
|
||||
|
||||
return { firstVersion, lastVersion };
|
||||
};
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
/* eslint-env node */
|
||||
|
||||
exports.isMacOS = () =>
|
||||
process.platform === 'darwin';
|
||||
exports.isMacOS = () => process.platform === 'darwin';
|
||||
|
||||
exports.isLinux = () =>
|
||||
process.platform === 'linux';
|
||||
exports.isLinux = () => process.platform === 'linux';
|
||||
|
||||
exports.isWindows = () =>
|
||||
process.platform === 'win32';
|
||||
exports.isWindows = () => process.platform === 'win32';
|
||||
|
|
|
@ -6,22 +6,20 @@ const path = require('path');
|
|||
const { compose } = require('lodash/fp');
|
||||
const { escapeRegExp } = require('lodash');
|
||||
|
||||
|
||||
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
|
||||
// _redactPath :: Path -> String -> String
|
||||
exports._redactPath = (filePath) => {
|
||||
exports._redactPath = filePath => {
|
||||
if (!is.string(filePath)) {
|
||||
throw new TypeError("'filePath' must be a string");
|
||||
}
|
||||
|
||||
const filePathPattern = exports._pathToRegExp(filePath);
|
||||
|
||||
return (text) => {
|
||||
return text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
@ -35,7 +33,7 @@ exports._redactPath = (filePath) => {
|
|||
};
|
||||
|
||||
// _pathToRegExp :: Path -> Maybe RegExp
|
||||
exports._pathToRegExp = (filePath) => {
|
||||
exports._pathToRegExp = filePath => {
|
||||
try {
|
||||
const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\');
|
||||
const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\');
|
||||
|
@ -47,7 +45,9 @@ exports._pathToRegExp = (filePath) => {
|
|||
pathWithNormalizedSlashes,
|
||||
pathWithEscapedSlashes,
|
||||
urlEncodedPath,
|
||||
].map(escapeRegExp).join('|');
|
||||
]
|
||||
.map(escapeRegExp)
|
||||
.join('|');
|
||||
return new RegExp(patternString, 'g');
|
||||
} catch (error) {
|
||||
return null;
|
||||
|
@ -56,7 +56,7 @@ exports._pathToRegExp = (filePath) => {
|
|||
|
||||
// Public API
|
||||
// redactPhoneNumbers :: String -> String
|
||||
exports.redactPhoneNumbers = (text) => {
|
||||
exports.redactPhoneNumbers = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ exports.redactPhoneNumbers = (text) => {
|
|||
};
|
||||
|
||||
// redactGroupIds :: String -> String
|
||||
exports.redactGroupIds = (text) => {
|
||||
exports.redactGroupIds = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const { isObject, isString } = require('lodash');
|
||||
|
||||
|
||||
const ITEMS_STORE_NAME = 'items';
|
||||
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
||||
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
||||
|
@ -37,8 +36,7 @@ exports._getItem = (connection, key) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.get(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = event =>
|
||||
resolve(event.target.result ? event.target.result.value : null);
|
||||
|
@ -58,11 +56,9 @@ exports._setItem = (connection, key, value) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.put({ id: key, value }, key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -79,10 +75,8 @@ exports._deleteItem = (connection, key) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.delete(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* global setTimeout */
|
||||
|
||||
exports.sleep = ms =>
|
||||
new Promise(resolve => setTimeout(resolve, ms));
|
||||
exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
|
|
@ -3,7 +3,6 @@ const is = require('@sindresorhus/is');
|
|||
const Errors = require('./types/errors');
|
||||
const Settings = require('./settings');
|
||||
|
||||
|
||||
exports.syncReadReceiptConfiguration = async ({
|
||||
deviceId,
|
||||
sendRequestConfigurationSyncMessage,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
exports.stringToArrayBuffer = (string) => {
|
||||
exports.stringToArrayBuffer = string => {
|
||||
if (typeof string !== 'string') {
|
||||
throw new TypeError("'string' must be a string");
|
||||
}
|
||||
|
|
|
@ -2,9 +2,15 @@ const is = require('@sindresorhus/is');
|
|||
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
const MIME = require('../../../ts/types/MIME');
|
||||
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
|
||||
const {
|
||||
arrayBufferToBlob,
|
||||
blobToArrayBuffer,
|
||||
dataURLToBlob,
|
||||
} = require('blob-util');
|
||||
const { autoOrientImage } = require('../auto_orient_image');
|
||||
const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system');
|
||||
const {
|
||||
migrateDataToFileSystem,
|
||||
} = require('./attachment/migrate_data_to_file_system');
|
||||
|
||||
// // Incoming message attachment fields
|
||||
// {
|
||||
|
@ -30,7 +36,7 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s
|
|||
// Returns true if `rawAttachment` is a valid attachment based on our current schema.
|
||||
// Over time, we can expand this definition to become more narrow, e.g. require certain
|
||||
// fields, etc.
|
||||
exports.isValid = (rawAttachment) => {
|
||||
exports.isValid = rawAttachment => {
|
||||
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
||||
// deserialized by protobuf:
|
||||
if (!rawAttachment) {
|
||||
|
@ -41,12 +47,15 @@ exports.isValid = (rawAttachment) => {
|
|||
};
|
||||
|
||||
// Upgrade steps
|
||||
exports.autoOrientJPEG = async (attachment) => {
|
||||
exports.autoOrientJPEG = async attachment => {
|
||||
if (!MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType);
|
||||
const dataBlob = await arrayBufferToBlob(
|
||||
attachment.data,
|
||||
attachment.contentType
|
||||
);
|
||||
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
|
||||
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
|
||||
|
||||
|
@ -76,7 +85,7 @@ const INVALID_CHARACTERS_PATTERN = new RegExp(
|
|||
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
|
||||
// which currently doesn’t support async testing:
|
||||
// https://github.com/leebyron/testcheck-js/issues/45
|
||||
exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
||||
exports._replaceUnicodeOrderOverridesSync = attachment => {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
|
@ -95,9 +104,12 @@ exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
|||
exports.replaceUnicodeOrderOverrides = async attachment =>
|
||||
exports._replaceUnicodeOrderOverridesSync(attachment);
|
||||
|
||||
exports.removeSchemaVersion = (attachment) => {
|
||||
exports.removeSchemaVersion = attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
console.log('Attachment.removeSchemaVersion: Invalid input attachment:', attachment);
|
||||
console.log(
|
||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||
attachment
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
|
@ -115,12 +127,12 @@ exports.hasData = attachment =>
|
|||
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
|
||||
// Attachment ->
|
||||
// IO (Promise Attachment)
|
||||
exports.loadData = (readAttachmentData) => {
|
||||
exports.loadData = readAttachmentData => {
|
||||
if (!is.function(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
@ -142,12 +154,12 @@ exports.loadData = (readAttachmentData) => {
|
|||
// deleteData :: (RelativePath -> IO Unit)
|
||||
// Attachment ->
|
||||
// IO Unit
|
||||
exports.deleteData = (deleteAttachmentData) => {
|
||||
exports.deleteData = deleteAttachmentData => {
|
||||
if (!is.function(deleteAttachmentData)) {
|
||||
throw new TypeError("'deleteAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
const {
|
||||
isArrayBuffer,
|
||||
isFunction,
|
||||
isUndefined,
|
||||
omit,
|
||||
} = require('lodash');
|
||||
|
||||
const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
|
||||
|
||||
// type Context :: {
|
||||
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
|
||||
|
@ -13,7 +7,10 @@ const {
|
|||
// migrateDataToFileSystem :: Attachment ->
|
||||
// Context ->
|
||||
// Promise Attachment
|
||||
exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } = {}) => {
|
||||
exports.migrateDataToFileSystem = async (
|
||||
attachment,
|
||||
{ writeNewAttachmentData } = {}
|
||||
) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||
}
|
||||
|
@ -28,15 +25,16 @@ exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData }
|
|||
|
||||
const isValidData = isArrayBuffer(data);
|
||||
if (!isValidData) {
|
||||
throw new TypeError('Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`);
|
||||
throw new TypeError(
|
||||
'Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`
|
||||
);
|
||||
}
|
||||
|
||||
const path = await writeNewAttachmentData(data);
|
||||
|
||||
const attachmentWithoutData = omit(
|
||||
Object.assign({}, attachment, { path }),
|
||||
['data']
|
||||
);
|
||||
const attachmentWithoutData = omit(Object.assign({}, attachment, { path }), [
|
||||
'data',
|
||||
]);
|
||||
return attachmentWithoutData;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// toLogFormat :: Error -> String
|
||||
exports.toLogFormat = (error) => {
|
||||
exports.toLogFormat = error => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ const { isFunction, isString, omit } = require('lodash');
|
|||
const Attachment = require('./attachment');
|
||||
const Errors = require('./errors');
|
||||
const SchemaVersion = require('./schema_version');
|
||||
const { initializeAttachmentMetadata } =
|
||||
require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const {
|
||||
initializeAttachmentMetadata,
|
||||
} = require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const GROUP = 'group';
|
||||
const PRIVATE = 'private';
|
||||
|
@ -37,19 +37,17 @@ const INITIAL_SCHEMA_VERSION = 0;
|
|||
// how we do database migrations:
|
||||
exports.CURRENT_SCHEMA_VERSION = 5;
|
||||
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
exports.PRIVATE = PRIVATE;
|
||||
|
||||
// Placeholder until we have stronger preconditions:
|
||||
exports.isValid = () =>
|
||||
true;
|
||||
exports.isValid = () => true;
|
||||
|
||||
// Schema
|
||||
exports.initializeSchemaVersion = (message) => {
|
||||
const isInitialized = SchemaVersion.isValid(message.schemaVersion) &&
|
||||
message.schemaVersion >= 1;
|
||||
exports.initializeSchemaVersion = message => {
|
||||
const isInitialized =
|
||||
SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
|
||||
if (isInitialized) {
|
||||
return message;
|
||||
}
|
||||
|
@ -59,27 +57,23 @@ exports.initializeSchemaVersion = (message) => {
|
|||
: 0;
|
||||
const hasAttachments = numAttachments > 0;
|
||||
if (!hasAttachments) {
|
||||
return Object.assign(
|
||||
{},
|
||||
message,
|
||||
{ schemaVersion: INITIAL_SCHEMA_VERSION }
|
||||
);
|
||||
return Object.assign({}, message, {
|
||||
schemaVersion: INITIAL_SCHEMA_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
// All attachments should have the same schema version, so we just pick
|
||||
// the first one:
|
||||
const firstAttachment = message.attachments[0];
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(firstAttachment.schemaVersion)
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(
|
||||
firstAttachment.schemaVersion
|
||||
)
|
||||
? firstAttachment.schemaVersion
|
||||
: INITIAL_SCHEMA_VERSION;
|
||||
const messageWithInitialSchema = Object.assign(
|
||||
{},
|
||||
message,
|
||||
{
|
||||
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']);
|
||||
})
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
const { isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.isValid = value =>
|
||||
isNumber(value) && value >= 0;
|
||||
exports.isValid = value => isNumber(value) && value >= 0;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const OS = require('../os');
|
||||
|
||||
exports.isAudioNotificationSupported = () =>
|
||||
!OS.isLinux();
|
||||
exports.isAudioNotificationSupported = () => !OS.isLinux();
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
/* global i18n: false */
|
||||
|
||||
|
||||
const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds
|
||||
|
||||
const setMessage = () => {
|
||||
|
|
|
@ -2,140 +2,146 @@
|
|||
* 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;
|
||||
},
|
||||
}))();
|
||||
})();
|
||||
|
|
|
@ -1,79 +1,101 @@
|
|||
/*
|
||||
* 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
|
||||
);
|
||||
});
|
||||
},
|
||||
}))();
|
||||
})();
|
||||
|
|
|
@ -1,49 +1,57 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
},
|
||||
}))();
|
||||
})();
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
/*
|
||||
* 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');
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
})();
|
||||
|
||||
|
|
|
@ -2,83 +2,89 @@
|
|||
* 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
|
@ -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,
|
||||
|
|
149
js/storage.js
149
js/storage.js
|
@ -1,80 +1,89 @@
|
|||
/*
|
||||
* 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;
|
||||
})();
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
/*
|
||||
* 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 };
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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',
|
||||
|
@ -123,7 +120,10 @@
|
|||
},
|
||||
isVoiceMessage() {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
||||
if (
|
||||
this.model.flags &
|
||||
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -241,4 +241,4 @@
|
|||
this.trigger('update');
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
/*
|
||||
* 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
/*
|
||||
* 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
/*
|
||||
* 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);
|
||||
},
|
||||
}),
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,73 +1,92 @@
|
|||
/*
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const isSearchable = conversation =>
|
||||
conversation.isSearchable();
|
||||
const isSearchable = conversation => conversation.isSearchable();
|
||||
|
||||
Whisper.NewContactView = Whisper.View.extend({
|
||||
templateName: 'new-contact',
|
||||
|
@ -46,7 +45,9 @@
|
|||
// View to display the matched contacts from typeahead
|
||||
this.typeahead_view = new Whisper.ConversationListView({
|
||||
collection: new Whisper.ConversationCollection([], {
|
||||
comparator(m) { return m.getTitle().toLowerCase(); },
|
||||
comparator(m) {
|
||||
return m.getTitle().toLowerCase();
|
||||
},
|
||||
}),
|
||||
});
|
||||
this.$el.append(this.typeahead_view.el);
|
||||
|
@ -75,8 +76,11 @@
|
|||
/* eslint-disable more/no-then */
|
||||
this.pending = this.pending.then(() =>
|
||||
this.typeahead.search(query).then(() => {
|
||||
this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable));
|
||||
}));
|
||||
this.typeahead_view.collection.reset(
|
||||
this.typeahead.filter(isSearchable)
|
||||
);
|
||||
})
|
||||
);
|
||||
/* eslint-enable more/no-then */
|
||||
this.trigger('show');
|
||||
} else {
|
||||
|
@ -105,8 +109,10 @@
|
|||
}
|
||||
|
||||
const newConversationId = this.new_contact_view.model.id;
|
||||
const conversation =
|
||||
await ConversationController.getOrCreateAndWait(newConversationId, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
newConversationId,
|
||||
'private'
|
||||
);
|
||||
this.trigger('open', conversation);
|
||||
this.initNewContact();
|
||||
this.resetTypeahead();
|
||||
|
@ -129,7 +135,9 @@
|
|||
// eslint-disable-next-line more/no-then
|
||||
this.typeahead.fetchAlphabetical().then(() => {
|
||||
if (this.typeahead.length > 0) {
|
||||
this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable));
|
||||
this.typeahead_view.collection.reset(
|
||||
this.typeahead.filter(isSearchable)
|
||||
);
|
||||
} else {
|
||||
this.showHints();
|
||||
}
|
||||
|
@ -163,4 +171,4 @@
|
|||
return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -120,20 +120,32 @@
|
|||
this.listenTo(this.model, 'destroy', this.stopListening);
|
||||
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
|
||||
this.listenTo(this.model, 'change:color', this.updateColor);
|
||||
this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar);
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'change:avatar change:profileAvatar',
|
||||
this.updateAvatar
|
||||
);
|
||||
this.listenTo(this.model, 'newmessage', this.addMessage);
|
||||
this.listenTo(this.model, 'delivered', this.updateMessage);
|
||||
this.listenTo(this.model, 'read', this.updateMessage);
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
this.listenTo(this.model, 'expired', this.onExpired);
|
||||
this.listenTo(this.model, 'prune', this.onPrune);
|
||||
this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'expired',
|
||||
this.onExpiredCollection
|
||||
);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'scroll-to-message',
|
||||
this.scrollToMessage
|
||||
);
|
||||
this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'reply',
|
||||
this.setQuoteMessage
|
||||
);
|
||||
|
||||
this.lazyUpdateVerified = _.debounce(
|
||||
this.model.updateVerified.bind(this.model),
|
||||
|
@ -247,7 +259,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
||||
if (this.isHidden() && this.lastActivity < oneHourAgo) {
|
||||
this.unload('inactivity');
|
||||
} else if (this.view.atBottom()) {
|
||||
|
@ -301,7 +313,7 @@
|
|||
|
||||
this.remove();
|
||||
|
||||
this.model.messageCollection.forEach((model) => {
|
||||
this.model.messageCollection.forEach(model => {
|
||||
model.trigger('unload');
|
||||
});
|
||||
this.model.messageCollection.reset([]);
|
||||
|
@ -333,19 +345,21 @@
|
|||
);
|
||||
|
||||
this.model.messageCollection.remove(models);
|
||||
_.forEach(models, (model) => {
|
||||
_.forEach(models, model => {
|
||||
model.trigger('unload');
|
||||
});
|
||||
},
|
||||
|
||||
markAllAsVerifiedDefault(unverified) {
|
||||
return Promise.all(unverified.map((contact) => {
|
||||
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,13 @@
|
|||
|
||||
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(() =>
|
||||
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 +606,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 +648,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 +710,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 +770,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 +785,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 +848,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 +1245,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 +1304,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 +1339,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 +1363,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 +1385,10 @@
|
|||
},
|
||||
|
||||
isHidden() {
|
||||
return this.$el.css('display') === 'none' ||
|
||||
this.$('.panel').css('display') === 'none';
|
||||
return (
|
||||
this.$el.css('display') === 'none' ||
|
||||
this.$('.panel').css('display') === 'none'
|
||||
);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -27,7 +27,7 @@
|
|||
this.$('textarea').val(i18n('loading'));
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
window.log.fetch().then((text) => {
|
||||
window.log.fetch().then(text => {
|
||||
this.$('textarea').val(text);
|
||||
});
|
||||
},
|
||||
|
@ -63,7 +63,9 @@
|
|||
});
|
||||
this.$('.loading').removeClass('loading');
|
||||
view.render();
|
||||
this.$('.link').focus().select();
|
||||
this.$('.link')
|
||||
.focus()
|
||||
.select();
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
/*
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
/*
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/*
|
||||
* 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 };
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,59 +1,60 @@
|
|||
/*
|
||||
* 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',
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,51 +1,54 @@
|
|||
/*
|
||||
* 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'),
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
reset: function() {
|
||||
return Whisper.Database.clear();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Whisper.ImportView = Whisper.View.extend({
|
||||
|
@ -102,16 +102,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 +130,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 +202,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)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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 @@
|
|||
};
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,196 +1,204 @@
|
|||
/*
|
||||
* 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)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,121 +1,138 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
/*
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,168 +1,193 @@
|
|||
/*
|
||||
* 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)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,119 +1,123 @@
|
|||
/*
|
||||
* 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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 isn’t ready');
|
||||
throw new Error(
|
||||
'Invariant violation:' +
|
||||
' Cannot render an attachment view that isn’t ready'
|
||||
);
|
||||
}
|
||||
|
||||
const parent = this.$('.attachments')[0];
|
||||
|
@ -570,4 +602,4 @@
|
|||
this.trigger('afterChangeHeight');
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,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();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,82 +1,89 @@
|
|||
/*
|
||||
* 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)
|
||||
);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
/*
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
/* global ReactDOM: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -44,4 +44,4 @@
|
|||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
});
|
||||
}());
|
||||
})();
|
||||
|
|
|
@ -1,185 +1,182 @@
|
|||
/*
|
||||
* 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]*$/);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,80 +1,84 @@
|
|||
/*
|
||||
* 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,88 +1,111 @@
|
|||
/*
|
||||
* 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');
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,88 +1,102 @@
|
|||
/*
|
||||
* 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',
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
/*
|
||||
* 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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -19,62 +19,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;
|
||||
})(),
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -2,26 +2,26 @@
|
|||
* 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);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
181
main.js
181
main.js
|
@ -6,13 +6,7 @@ const _ = require('lodash');
|
|||
const electron = require('electron');
|
||||
const semver = require('semver');
|
||||
|
||||
const {
|
||||
BrowserWindow,
|
||||
app,
|
||||
Menu,
|
||||
shell,
|
||||
ipcMain: ipc,
|
||||
} = electron;
|
||||
const { BrowserWindow, app, Menu, shell, ipcMain: ipc } = electron;
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
|
@ -27,7 +21,9 @@ const { createTemplate } = require('./app/menu');
|
|||
GlobalErrors.addHandler();
|
||||
|
||||
const appUserModelId = `org.whispersystems.${packageJson.name}`;
|
||||
console.log('Set Windows Application User Model ID (AUMID)', { appUserModelId });
|
||||
console.log('Set Windows Application User Model ID (AUMID)', {
|
||||
appUserModelId,
|
||||
});
|
||||
app.setAppUserModelId(appUserModelId);
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
|
@ -41,13 +37,13 @@ function getMainWindow() {
|
|||
// Tray icon and related objects
|
||||
let tray = null;
|
||||
const startInTray = process.argv.some(arg => arg === '--start-in-tray');
|
||||
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||
|
||||
const usingTrayIcon =
|
||||
startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||
|
||||
const config = require('./app/config');
|
||||
|
||||
const importMode = process.argv.some(arg => arg === '--import') || config.get('import');
|
||||
|
||||
const importMode =
|
||||
process.argv.some(arg => arg === '--import') || config.get('import');
|
||||
|
||||
const development = config.environment === 'development';
|
||||
|
||||
|
@ -107,7 +103,12 @@ const WINDOWS_8 = '8.0.0';
|
|||
const osRelease = os.release();
|
||||
const polyfillNotifications =
|
||||
os.platform() === 'win32' && semver.lt(osRelease, WINDOWS_8);
|
||||
console.log('OS Release:', osRelease, '- notifications polyfill?', polyfillNotifications);
|
||||
console.log(
|
||||
'OS Release:',
|
||||
osRelease,
|
||||
'- notifications polyfill?',
|
||||
polyfillNotifications
|
||||
);
|
||||
|
||||
function prepareURL(pathSegments) {
|
||||
return url.format({
|
||||
|
@ -146,7 +147,6 @@ function captureClicks(window) {
|
|||
window.webContents.on('new-window', handleUrl);
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_WIDTH = 800;
|
||||
const DEFAULT_HEIGHT = 610;
|
||||
const MIN_WIDTH = 640;
|
||||
|
@ -160,35 +160,50 @@ function isVisible(window, bounds) {
|
|||
const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT;
|
||||
|
||||
// requiring BOUNDS_BUFFER pixels on the left or right side
|
||||
const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER);
|
||||
const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER);
|
||||
const rightSideClearOfLeftBound =
|
||||
window.x + window.width >= boundsX + BOUNDS_BUFFER;
|
||||
const leftSideClearOfRightBound =
|
||||
window.x <= boundsX + boundsWidth - BOUNDS_BUFFER;
|
||||
|
||||
// top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
|
||||
const topClearOfUpperBound = window.y >= boundsY;
|
||||
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
||||
const topClearOfLowerBound =
|
||||
window.y <= boundsY + boundsHeight - BOUNDS_BUFFER;
|
||||
|
||||
return rightSideClearOfLeftBound &&
|
||||
return (
|
||||
rightSideClearOfLeftBound &&
|
||||
leftSideClearOfRightBound &&
|
||||
topClearOfUpperBound &&
|
||||
topClearOfLowerBound;
|
||||
topClearOfLowerBound
|
||||
);
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const { screen } = electron;
|
||||
const windowOptions = Object.assign({
|
||||
show: !startInTray, // allow to start minimised in tray
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
autoHideMenuBar: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
// sandbox: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
const windowOptions = Object.assign(
|
||||
{
|
||||
show: !startInTray, // allow to start minimised in tray
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
autoHideMenuBar: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
// sandbox: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
icon: path.join(__dirname, 'images', 'icon_256.png'),
|
||||
},
|
||||
icon: path.join(__dirname, 'images', 'icon_256.png'),
|
||||
}, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y']));
|
||||
_.pick(windowConfig, [
|
||||
'maximized',
|
||||
'autoHideMenuBar',
|
||||
'width',
|
||||
'height',
|
||||
'x',
|
||||
'y',
|
||||
])
|
||||
);
|
||||
|
||||
if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
|
||||
windowOptions.width = DEFAULT_WIDTH;
|
||||
|
@ -203,7 +218,7 @@ function createWindow() {
|
|||
delete windowOptions.autoHideMenuBar;
|
||||
}
|
||||
|
||||
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => {
|
||||
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), display => {
|
||||
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -220,7 +235,10 @@ function createWindow() {
|
|||
delete windowOptions.fullscreen;
|
||||
}
|
||||
|
||||
logger.info('Initializing BrowserWindow config: %s', JSON.stringify(windowOptions));
|
||||
logger.info(
|
||||
'Initializing BrowserWindow config: %s',
|
||||
JSON.stringify(windowOptions)
|
||||
);
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(windowOptions);
|
||||
|
@ -249,7 +267,10 @@ function createWindow() {
|
|||
windowConfig.fullscreen = true;
|
||||
}
|
||||
|
||||
logger.info('Updating BrowserWindow config: %s', JSON.stringify(windowConfig));
|
||||
logger.info(
|
||||
'Updating BrowserWindow config: %s',
|
||||
JSON.stringify(windowConfig)
|
||||
);
|
||||
userConfig.set('window', windowConfig);
|
||||
}
|
||||
|
||||
|
@ -263,7 +284,7 @@ function createWindow() {
|
|||
});
|
||||
|
||||
// Ingested in preload.js via a sendSync call
|
||||
ipc.on('locale-data', (event) => {
|
||||
ipc.on('locale-data', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = locale.messages;
|
||||
});
|
||||
|
@ -271,7 +292,9 @@ function createWindow() {
|
|||
if (config.environment === 'test') {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html']));
|
||||
} else if (config.environment === 'test-lib') {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']));
|
||||
mainWindow.loadURL(
|
||||
prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'])
|
||||
);
|
||||
} else {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'background.html']));
|
||||
}
|
||||
|
@ -283,16 +306,19 @@ function createWindow() {
|
|||
|
||||
captureClicks(mainWindow);
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (e) => {
|
||||
mainWindow.webContents.on('will-navigate', e => {
|
||||
logger.info('will-navigate');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Emitted when the window is about to be closed.
|
||||
mainWindow.on('close', (e) => {
|
||||
mainWindow.on('close', e => {
|
||||
// If the application is terminating, just do the default
|
||||
if (windowState.shouldQuit() ||
|
||||
config.environment === 'test' || config.environment === 'test-lib') {
|
||||
if (
|
||||
windowState.shouldQuit() ||
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -337,7 +363,9 @@ function showSettings() {
|
|||
}
|
||||
|
||||
function openReleaseNotes() {
|
||||
shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`);
|
||||
shell.openExternal(
|
||||
`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`
|
||||
);
|
||||
}
|
||||
|
||||
function openNewBugForm() {
|
||||
|
@ -345,7 +373,9 @@ function openNewBugForm() {
|
|||
}
|
||||
|
||||
function openSupportPage() {
|
||||
shell.openExternal('https://support.signal.org/hc/en-us/categories/202319038-Desktop');
|
||||
shell.openExternal(
|
||||
'https://support.signal.org/hc/en-us/categories/202319038-Desktop'
|
||||
);
|
||||
}
|
||||
|
||||
function openForums() {
|
||||
|
@ -370,7 +400,6 @@ function setupAsStandalone() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
let aboutWindow;
|
||||
function showAbout() {
|
||||
if (aboutWindow) {
|
||||
|
@ -416,38 +445,42 @@ app.on('ready', () => {
|
|||
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
let loggingSetupError;
|
||||
logging.initialize().catch((error) => {
|
||||
loggingSetupError = error;
|
||||
}).then(async () => {
|
||||
/* eslint-enable more/no-then */
|
||||
logger = logging.getLogger();
|
||||
logger.info('app ready');
|
||||
logging
|
||||
.initialize()
|
||||
.catch(error => {
|
||||
loggingSetupError = error;
|
||||
})
|
||||
.then(async () => {
|
||||
/* eslint-enable more/no-then */
|
||||
logger = logging.getLogger();
|
||||
logger.info('app ready');
|
||||
|
||||
if (loggingSetupError) {
|
||||
logger.error('Problem setting up logging', loggingSetupError.stack);
|
||||
}
|
||||
if (loggingSetupError) {
|
||||
logger.error('Problem setting up logging', loggingSetupError.stack);
|
||||
}
|
||||
|
||||
if (!locale) {
|
||||
const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
|
||||
locale = loadLocale({ appLocale, logger });
|
||||
}
|
||||
if (!locale) {
|
||||
const appLocale =
|
||||
process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
|
||||
locale = loadLocale({ appLocale, logger });
|
||||
}
|
||||
|
||||
console.log('Ensure attachments directory exists');
|
||||
const userDataPath = app.getPath('userData');
|
||||
await Attachments.ensureDirectory(userDataPath);
|
||||
console.log('Ensure attachments directory exists');
|
||||
const userDataPath = app.getPath('userData');
|
||||
await Attachments.ensureDirectory(userDataPath);
|
||||
|
||||
ready = true;
|
||||
ready = true;
|
||||
|
||||
autoUpdate.initialize(getMainWindow, locale.messages);
|
||||
autoUpdate.initialize(getMainWindow, locale.messages);
|
||||
|
||||
createWindow();
|
||||
createWindow();
|
||||
|
||||
if (usingTrayIcon) {
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
if (usingTrayIcon) {
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
|
||||
setupMenu();
|
||||
});
|
||||
setupMenu();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMenu(options) {
|
||||
|
@ -472,7 +505,6 @@ function setupMenu(options) {
|
|||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
app.on('before-quit', () => {
|
||||
windowState.markShouldQuit();
|
||||
});
|
||||
|
@ -481,9 +513,11 @@ app.on('before-quit', () => {
|
|||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin' ||
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib') {
|
||||
if (
|
||||
process.platform !== 'darwin' ||
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib'
|
||||
) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
@ -504,7 +538,7 @@ app.on('activate', () => {
|
|||
|
||||
// Defense in depth. We never intend to open webviews, so this prevents it completely.
|
||||
app.on('web-contents-created', (createEvent, win) => {
|
||||
win.on('will-attach-webview', (attachEvent) => {
|
||||
win.on('will-attach-webview', attachEvent => {
|
||||
attachEvent.preventDefault();
|
||||
});
|
||||
});
|
||||
|
@ -523,7 +557,6 @@ ipc.on('add-setup-menu-items', () => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
ipc.on('draw-attention', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.bounce();
|
||||
|
|
60
preload.js
60
preload.js
|
@ -12,7 +12,6 @@ const { deferredToPromise } = require('./js/modules/deferred_to_promise');
|
|||
|
||||
const { app } = electron.remote;
|
||||
|
||||
|
||||
window.PROTO_ROOT = 'protos';
|
||||
window.config = require('url').parse(window.location.toString(), true).query;
|
||||
|
||||
|
@ -21,8 +20,7 @@ window.wrapDeferred = deferredToPromise;
|
|||
const ipc = electron.ipcRenderer;
|
||||
window.config.localeMessages = ipc.sendSync('locale-data');
|
||||
|
||||
window.setBadgeCount = count =>
|
||||
ipc.send('set-badge-count', count);
|
||||
window.setBadgeCount = count => ipc.send('set-badge-count', count);
|
||||
|
||||
window.drawAttention = () => {
|
||||
console.log('draw attention');
|
||||
|
@ -44,8 +42,7 @@ window.restart = () => {
|
|||
ipc.send('restart');
|
||||
};
|
||||
|
||||
window.closeAbout = () =>
|
||||
ipc.send('close-about');
|
||||
window.closeAbout = () => ipc.send('close-about');
|
||||
|
||||
window.updateTrayIcon = unreadCount =>
|
||||
ipc.send('update-tray-icon', unreadCount);
|
||||
|
@ -70,11 +67,9 @@ ipc.on('show-settings', () => {
|
|||
Whisper.events.trigger('showSettings');
|
||||
});
|
||||
|
||||
window.addSetupMenuItems = () =>
|
||||
ipc.send('add-setup-menu-items');
|
||||
window.addSetupMenuItems = () => ipc.send('add-setup-menu-items');
|
||||
|
||||
window.removeSetupMenuItems = () =>
|
||||
ipc.send('remove-setup-menu-items');
|
||||
window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items');
|
||||
|
||||
// We pull these dependencies in now, from here, because they have Node.js dependencies
|
||||
|
||||
|
@ -101,8 +96,7 @@ window.emojiData = require('emoji-datasource');
|
|||
window.EmojiPanel = require('emoji-panel');
|
||||
window.filesize = require('filesize');
|
||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||
window.libphonenumber.PhoneNumberFormat =
|
||||
require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.loadImage = require('blueimp-load-image');
|
||||
|
||||
window.nodeBuffer = Buffer;
|
||||
|
@ -136,11 +130,15 @@ window.moment.locale(locale);
|
|||
|
||||
// ES2015+ modules
|
||||
const attachmentsPath = Attachments.getPath(app.getPath('userData'));
|
||||
const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(attachmentsPath);
|
||||
const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(
|
||||
attachmentsPath
|
||||
);
|
||||
const deleteAttachmentData = Attachments.createDeleter(attachmentsPath);
|
||||
const readAttachmentData = Attachments.createReader(attachmentsPath);
|
||||
const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath);
|
||||
const writeExistingAttachmentData = Attachments.createWriterForExisting(attachmentsPath);
|
||||
const writeExistingAttachmentData = Attachments.createWriterForExisting(
|
||||
attachmentsPath
|
||||
);
|
||||
|
||||
const loadAttachmentData = Attachment.loadData(readAttachmentData);
|
||||
|
||||
|
@ -151,8 +149,9 @@ const upgradeSchemaContext = {
|
|||
const upgradeMessageSchema = message =>
|
||||
Message.upgradeSchema(message, upgradeSchemaContext);
|
||||
|
||||
const { getPlaceholderMigrations } =
|
||||
require('./js/modules/migrations/get_placeholder_migrations');
|
||||
const {
|
||||
getPlaceholderMigrations,
|
||||
} = require('./js/modules/migrations/get_placeholder_migrations');
|
||||
const { IdleDetector } = require('./js/modules/idle_detector');
|
||||
|
||||
window.Signal = {};
|
||||
|
@ -167,12 +166,12 @@ window.Signal.Logs = require('./js/modules/logs');
|
|||
// React components
|
||||
const { Lightbox } = require('./ts/components/Lightbox');
|
||||
const { LightboxGallery } = require('./ts/components/LightboxGallery');
|
||||
const { MediaGallery } =
|
||||
require('./ts/components/conversation/media-gallery/MediaGallery');
|
||||
const {
|
||||
MediaGallery,
|
||||
} = require('./ts/components/conversation/media-gallery/MediaGallery');
|
||||
const { Quote } = require('./ts/components/conversation/Quote');
|
||||
|
||||
const MediaGalleryMessage =
|
||||
require('./ts/components/conversation/media-gallery/types/Message');
|
||||
const MediaGalleryMessage = require('./ts/components/conversation/media-gallery/types/Message');
|
||||
|
||||
window.Signal.Components = {
|
||||
Lightbox,
|
||||
|
@ -185,18 +184,20 @@ window.Signal.Components = {
|
|||
};
|
||||
|
||||
window.Signal.Migrations = {};
|
||||
window.Signal.Migrations.deleteAttachmentData =
|
||||
Attachment.deleteData(deleteAttachmentData);
|
||||
window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData(
|
||||
deleteAttachmentData
|
||||
);
|
||||
window.Signal.Migrations.getPlaceholderMigrations = getPlaceholderMigrations;
|
||||
window.Signal.Migrations.writeMessageAttachments =
|
||||
Message.createAttachmentDataWriter(writeExistingAttachmentData);
|
||||
window.Signal.Migrations.writeMessageAttachments = Message.createAttachmentDataWriter(
|
||||
writeExistingAttachmentData
|
||||
);
|
||||
window.Signal.Migrations.getAbsoluteAttachmentPath = getAbsoluteAttachmentPath;
|
||||
window.Signal.Migrations.loadAttachmentData = loadAttachmentData;
|
||||
window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(loadAttachmentData);
|
||||
window.Signal.Migrations.Migrations0DatabaseWithAttachmentData =
|
||||
require('./js/modules/migrations/migrations_0_database_with_attachment_data');
|
||||
window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData =
|
||||
require('./js/modules/migrations/migrations_1_database_without_attachment_data');
|
||||
window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(
|
||||
loadAttachmentData
|
||||
);
|
||||
window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = require('./js/modules/migrations/migrations_0_database_with_attachment_data');
|
||||
window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = require('./js/modules/migrations/migrations_1_database_without_attachment_data');
|
||||
|
||||
window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema;
|
||||
window.Signal.OS = require('./js/modules/os');
|
||||
|
@ -218,8 +219,7 @@ window.Signal.Views.Initialization = require('./js/modules/views/initialization'
|
|||
|
||||
window.Signal.Workflow = {};
|
||||
window.Signal.Workflow.IdleDetector = IdleDetector;
|
||||
window.Signal.Workflow.MessageDataMigrator =
|
||||
require('./js/modules/messages_data_migrator');
|
||||
window.Signal.Workflow.MessageDataMigrator = require('./js/modules/messages_data_migrator');
|
||||
|
||||
// We pull this in last, because the native module involved appears to be sensitive to
|
||||
// /tmp mounted as noexec on Linux.
|
||||
|
|
|
@ -3,7 +3,6 @@ const _ = require('lodash');
|
|||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
|
||||
const { version } = packageJson;
|
||||
const beta = /beta/;
|
||||
|
||||
|
@ -37,7 +36,6 @@ const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass';
|
|||
const PRODUCTION_STARTUP_WM_CLASS = 'Signal';
|
||||
const BETA_STARTUP_WM_CLASS = 'Signal Beta';
|
||||
|
||||
|
||||
// -------
|
||||
|
||||
function checkValue(object, objectPath, expected) {
|
||||
|
|
|
@ -56,5 +56,8 @@ _.set(packageJson, WIN_ASSET_PATH, WIN_ASSET_END_VALUE);
|
|||
|
||||
// ---
|
||||
|
||||
fs.writeFileSync('./config/default.json', JSON.stringify(defaultConfig, null, ' '));
|
||||
fs.writeFileSync(
|
||||
'./config/default.json',
|
||||
JSON.stringify(defaultConfig, null, ' ')
|
||||
);
|
||||
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));
|
||||
|
|
|
@ -2,7 +2,6 @@ const webpack = require('webpack');
|
|||
const path = require('path');
|
||||
const typescriptSupport = require('react-docgen-typescript');
|
||||
|
||||
|
||||
const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse;
|
||||
|
||||
module.exports = {
|
||||
|
@ -37,9 +36,7 @@ module.exports = {
|
|||
// Exposes necessary utilities in the global scope for all readme code snippets
|
||||
util: 'ts/styleguide/StyleGuideUtil',
|
||||
},
|
||||
contextDependencies: [
|
||||
path.join(__dirname, 'ts/styleguide'),
|
||||
],
|
||||
contextDependencies: [path.join(__dirname, 'ts/styleguide')],
|
||||
// We don't want one long, single page
|
||||
pagePerSection: true,
|
||||
// Expose entire repository to the styleguidist server, primarily for stylesheets
|
||||
|
@ -49,11 +46,13 @@ module.exports = {
|
|||
// https://react-styleguidist.js.org/docs/configuration.html#template
|
||||
template: {
|
||||
head: {
|
||||
links: [{
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: '/stylesheets/manifest.css',
|
||||
}],
|
||||
links: [
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: '/stylesheets/manifest.css',
|
||||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
// Brings in all the necessary components to boostrap Backbone views
|
||||
|
@ -157,10 +156,7 @@ module.exports = {
|
|||
|
||||
resolve: {
|
||||
// Necessary to enable the absolute path used in the context option above
|
||||
modules: [
|
||||
__dirname,
|
||||
path.join(__dirname, 'node_modules'),
|
||||
],
|
||||
modules: [__dirname, path.join(__dirname, 'node_modules')],
|
||||
extensions: ['.tsx'],
|
||||
},
|
||||
|
||||
|
@ -168,7 +164,7 @@ module.exports = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader'
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
{
|
||||
// To test handling of attachments, we need arraybuffers in memory
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue