Format all source code using Prettier

This commit is contained in:
Daniel Gasienica 2018-04-27 17:25:04 -04:00
parent b4dee3f30b
commit 1dd87ad197
149 changed files with 17847 additions and 15439 deletions

View File

@ -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',

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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);
},
};
})();

View File

@ -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;
},
};
})();

View File

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

View File

@ -1,79 +1,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
);
});
},
}))();
})();

View File

@ -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()));
};
})();

View File

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

View File

@ -1,115 +1,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(),
};
}));
})
);
})();

View File

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

View File

@ -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);
});
});
});
}
};
}());
});
},
};
})();

View File

@ -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
})();

View File

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

View File

@ -1,29 +1,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

View File

@ -9,7 +9,7 @@
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@ -32,10 +32,13 @@
this.on('unload', this.unload);
this.setToExpire();
this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
this.VOICE_FLAG =
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
},
idForLogging() {
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`;
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
'sent_at'
)}`;
},
defaults() {
return {
@ -56,12 +59,13 @@
return !!(this.get('flags') & flag);
},
isExpirationTimerUpdate() {
const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag);
},
isGroupUpdate() {
return !!(this.get('group_update'));
return !!this.get('group_update');
},
isIncoming() {
return this.get('type') === 'incoming';
@ -79,14 +83,14 @@
if (options.parse === void 0) options.parse = true;
const model = this;
const success = options.success;
options.success = function (resp) {
options.success = function(resp) {
model.attributes = {}; // this is the only changed line
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
const error = options.error;
options.error = function (resp) {
options.error = function(resp) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
@ -116,7 +120,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name));
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this));
const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
);
if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else {
@ -186,7 +193,7 @@
}
const quote = this.get('quote');
const attachments = (quote && quote.attachments) || [];
attachments.forEach((attachment) => {
attachments.forEach(attachment => {
if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
URL.revokeObjectURL(attachment.thumbnail.objectUrl);
// eslint-disable-next-line no-param-reassign
@ -235,8 +242,8 @@
const thumbnailWithObjectUrl = !objectUrl
? null
: Object.assign({}, attachment.thumbnail || {}, {
objectUrl,
});
objectUrl,
});
return Object.assign({}, attachment, {
// eslint-disable-next-line no-bitwise
@ -269,7 +276,8 @@
return {
attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)),
this.processAttachment(attachment, objectUrl)
),
authorColor,
authorProfileName,
authorTitle,
@ -342,59 +350,63 @@
send(promise) {
this.trigger('pending');
return promise.then((result) => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
const sentTo = this.get('sent_to') || [];
this.save({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
this.sendSyncMessage();
}).catch((result) => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
let promises = [];
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles());
return promise
.then(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
} else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
promises.push(this.sendSyncMessage());
const sentTo = this.get('sent_to') || [];
this.save({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
this.sendSyncMessage();
})
.catch(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
promises = promises.concat(_.map(result.errors, (error) => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
let promises = [];
if (result instanceof Error) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number);
promises.push(c.getProfiles());
}
}));
}
} else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp: now,
});
promises.push(this.sendSyncMessage());
}
promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
promises.push(c.getProfiles());
}
})
);
}
return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors'));
return Promise.all(promises).then(() => {
this.trigger('send-error', this.get('errors'));
});
});
});
},
someRecipientsFailed() {
@ -423,14 +435,16 @@
if (this.get('synced') || !dataMessage) {
return Promise.resolve();
}
return textsecure.messaging.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
this.get('expirationStartTimestamp')
).then(() => {
this.save({ synced: true, dataMessage: null });
});
return textsecure.messaging
.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
this.get('expirationStartTimestamp')
)
.then(() => {
this.save({ synced: true, dataMessage: null });
});
});
},
@ -440,17 +454,19 @@
if (!(errors instanceof Array)) {
errors = [errors];
}
errors.forEach((e) => {
errors.forEach(e => {
console.log(
'Message.saveErrors:',
e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e
);
});
errors = errors.map((e) => {
if (e.constructor === Error ||
e.constructor === TypeError ||
e.constructor === ReferenceError) {
errors = errors.map(e => {
if (
e.constructor === Error ||
e.constructor === TypeError ||
e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
}
return e;
@ -463,32 +479,36 @@
hasNetworkError() {
const error = _.find(
this.get('errors'),
e => (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError')
e =>
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError'
);
return !!error;
},
removeOutgoingErrors(number) {
const errors = _.partition(
this.get('errors'),
e => e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
e =>
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
this.set({ errors: errors[1] });
return errors[0][0];
},
isReplayableError(e) {
return (e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError');
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
},
resend(number) {
const error = this.removeOutgoingErrors(number);
@ -513,236 +533,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 @@
});
},
});
}());
})();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
},
}))();
})();

View File

@ -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
);
});
},
}))();
})();

View File

@ -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);
}
},
}))();
})();

View File

@ -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');
},
};
})();

View File

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

View File

@ -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

View File

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

View File

@ -1,80 +1,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;
})();

View File

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

View File

@ -1,15 +1,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 };
},
});
})();

View File

@ -9,7 +9,7 @@
/* global Whisper: false */
// eslint-disable-next-line func-names
(function () {
(function() {
'use strict';
const FileView = Whisper.View.extend({
@ -62,10 +62,7 @@
const VideoView = MediaView.extend({ tagName: 'video' });
// Blacklist common file types known to be unsupported in Chrome
const unsupportedFileTypes = [
'audio/aiff',
'video/quicktime',
];
const unsupportedFileTypes = ['audio/aiff', 'video/quicktime'];
Whisper.AttachmentView = Backbone.View.extend({
tagName: 'div',
@ -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');
},
});
}());
})();

View File

@ -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();
},
});
})();

View File

@ -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();
},
});
})();

View File

@ -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);
},
}),
});
})();

View File

@ -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;
},
});
})();

View File

@ -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();
}
},
});
})();

View File

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

View File

@ -13,7 +13,7 @@
/* global Whisper: false */
// eslint-disable-next-line func-names
(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@ -120,20 +120,32 @@
this.listenTo(this.model, 'destroy', this.stopListening);
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(this.model, 'change:color', this.updateColor);
this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar);
this.listenTo(
this.model,
'change:avatar change:profileAvatar',
this.updateAvatar
);
this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'delivered', this.updateMessage);
this.listenTo(this.model, 'read', this.updateMessage);
this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'expired', this.onExpired);
this.listenTo(this.model, 'prune', this.onPrune);
this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection);
this.listenTo(
this.model.messageCollection,
'expired',
this.onExpiredCollection
);
this.listenTo(
this.model.messageCollection,
'scroll-to-message',
this.scrollToMessage
);
this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage);
this.listenTo(
this.model.messageCollection,
'reply',
this.setQuoteMessage
);
this.lazyUpdateVerified = _.debounce(
this.model.updateVerified.bind(this.model),
@ -247,7 +259,7 @@
return;
}
const oneHourAgo = Date.now() - (60 * 60 * 1000);
const oneHourAgo = Date.now() - 60 * 60 * 1000;
if (this.isHidden() && this.lastActivity < oneHourAgo) {
this.unload('inactivity');
} else if (this.view.atBottom()) {
@ -301,7 +313,7 @@
this.remove();
this.model.messageCollection.forEach((model) => {
this.model.messageCollection.forEach(model => {
model.trigger('unload');
});
this.model.messageCollection.reset([]);
@ -333,19 +345,21 @@
);
this.model.messageCollection.remove(models);
_.forEach(models, (model) => {
_.forEach(models, model => {
model.trigger('unload');
});
},
markAllAsVerifiedDefault(unverified) {
return Promise.all(unverified.map((contact) => {
if (contact.isUnverified()) {
return contact.setVerifiedDefault();
}
return Promise.all(
unverified.map(contact => {
if (contact.isUnverified()) {
return contact.setVerifiedDefault();
}
return null;
}));
return null;
})
);
},
markAllAsApproved(untrusted) {
@ -404,7 +418,10 @@
}
},
toggleMicrophone() {
if (this.$('.send-message').val().length > 0 || this.fileInput.hasFiles()) {
if (
this.$('.send-message').val().length > 0 ||
this.fileInput.hasFiles()
) {
this.$('.capture-audio').hide();
} else {
this.$('.capture-audio').show();
@ -495,11 +512,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'
);
},
});
}());
})();

View File

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

View File

@ -1,16 +1,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;
},
});
})();

View File

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

View File

@ -1,40 +1,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,
};
},
});
})();

View File

@ -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;
},
});
})();

View File

@ -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 };
},
});
})();

View File

@ -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',
};
})();

View File

@ -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'),
};
},
});
})();

View File

@ -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)
);
},
});
})();

View File

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

View File

@ -1,196 +1,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)
);
},
});
})();

View File

@ -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,
};
},
});
})();

View File

@ -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,
};
},
});
})();

View File

@ -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;
},
});
})();

View File

@ -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)
);
},
});
})();

View File

@ -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();
},
});
})();

View File

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

View File

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

View File

@ -1,82 +1,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)
);
},
});
})();

View File

@ -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;
},
});
})();

View File

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

View File

@ -1,185 +1,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]*$/);
},
});
})();

View File

@ -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();
},
});
})();

View File

@ -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,
};
},
});
})();

View File

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

View File

@ -1,88 +1,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');
}
},
});
})();

View File

@ -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',
},
});
})();

View File

@ -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);
},
});
})();

View File

@ -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;
})(),
}
);
})();

View File

@ -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
View File

@ -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();

View File

@ -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.

View File

@ -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) {

View File

@ -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, ' '));

View File

@ -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