Prettier (All The Things) (#2303)

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

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

## Sublime Text Plugin

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

## Changes

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

View File

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

View File

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

4
.gitignore vendored
View File

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

18
.prettierignore Normal file
View File

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

4
.prettierrc.js Normal file
View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
/* global Whisper: false */
/* global wrapDeferred: false */
;(async function() {
(async function() {
'use strict';
const { IdleDetector, MessageDataMigrator } = Signal.Workflow;
@@ -65,7 +65,9 @@
var USERNAME = storage.get('number_id');
var PASSWORD = storage.get('password');
accountManager = new textsecure.AccountManager(
SERVER_URL, USERNAME, PASSWORD
SERVER_URL,
USERNAME,
PASSWORD
);
accountManager.addEventListener('registration', function() {
Whisper.Registration.markDone();
@@ -105,18 +107,20 @@
if (!isMigrationWithoutIndexComplete) {
const database = Migrations0DatabaseWithAttachmentData.getDatabase();
const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex({
const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex(
{
databaseName: database.name,
minDatabaseVersion: database.version,
numMessagesPerBatch: NUM_MESSAGES_PER_BATCH,
upgradeMessageSchema,
});
}
);
console.log('Upgrade message schema (without index):', batchWithoutIndex);
isMigrationWithoutIndexComplete = batchWithoutIndex.done;
}
const areAllMigrationsComplete = isMigrationWithIndexComplete &&
isMigrationWithoutIndexComplete;
const areAllMigrationsComplete =
isMigrationWithIndexComplete && isMigrationWithoutIndexComplete;
if (areAllMigrationsComplete) {
idleDetector.stop();
}
@@ -188,7 +192,9 @@
});
cancelInitializationMessage();
var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')});
var appView = (window.owsDesktopApp.appView = new Whisper.AppView({
el: $('body'),
}));
Whisper.WallClockListener.init(Whisper.events);
Whisper.ExpiringMessagesListener.init(Whisper.events);
@@ -200,7 +206,7 @@
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
connect();
appView.openInbox({
initialLoadComplete: initialLoadComplete
initialLoadComplete: initialLoadComplete,
});
} else if (window.config.importMode) {
appView.openImporter();
@@ -214,7 +220,7 @@
Whisper.events.on('showSettings', () => {
if (!appView || !appView.inboxView) {
console.log(
'background: Event: \'showSettings\':' +
"background: Event: 'showSettings':" +
' Expected `appView.inboxView` to exist.'
);
return;
@@ -238,7 +244,7 @@
appView.openConversation(conversation);
} else {
appView.openInbox({
initialLoadComplete: initialLoadComplete
initialLoadComplete: initialLoadComplete,
});
}
});
@@ -258,7 +264,6 @@
}
});
var disconnectTimer = null;
function onOffline() {
console.log('offline');
@@ -294,7 +299,9 @@
function isSocketOnline() {
var socketStatus = window.getSocketStatus();
return socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN;
return (
socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN
);
}
function disconnect() {
@@ -317,14 +324,20 @@
window.addEventListener('offline', onOffline);
}
if (connectCount === 0 && !navigator.onLine) {
console.log('Starting up offline; will connect when we have network access');
console.log(
'Starting up offline; will connect when we have network access'
);
window.addEventListener('online', onOnline);
onEmpty(); // this ensures that the loading screen is dismissed
return;
}
if (!Whisper.Registration.everDone()) { return; }
if (Whisper.Import.isIncomplete()) { return; }
if (!Whisper.Registration.everDone()) {
return;
}
if (Whisper.Import.isIncomplete()) {
return;
}
if (messageReceiver) {
messageReceiver.close();
@@ -343,7 +356,11 @@
// initialize the socket and start listening for messages
messageReceiver = new textsecure.MessageReceiver(
SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options
SERVER_URL,
USERNAME,
PASSWORD,
mySignalingKey,
options
);
messageReceiver.addEventListener('message', onMessageReceived);
messageReceiver.addEventListener('delivery', onDeliveryReceipt);
@@ -359,7 +376,10 @@
messageReceiver.addEventListener('configuration', onConfiguration);
window.textsecure.messaging = new textsecure.MessageSender(
SERVER_URL, USERNAME, PASSWORD, CDN_URL
SERVER_URL,
USERNAME,
PASSWORD,
CDN_URL
);
// Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces
@@ -406,7 +426,7 @@
});
if (Whisper.Import.isComplete()) {
textsecure.messaging.sendRequestConfigurationSyncMessage().catch((e) => {
textsecure.messaging.sendRequestConfigurationSyncMessage().catch(e => {
console.log(e);
});
}
@@ -416,8 +436,10 @@
const shouldSkipAttachmentMigrationForNewUsers = firstRun === true;
if (shouldSkipAttachmentMigrationForNewUsers) {
const database = Migrations0DatabaseWithAttachmentData.getDatabase();
const connection =
await Signal.Database.open(database.name, database.version);
const connection = await Signal.Database.open(
database.name,
database.version
);
await Signal.Settings.markAttachmentMigrationComplete(connection);
}
idleDetector.start();
@@ -471,7 +493,7 @@
}
var c = new Whisper.Conversation({
id: id
id: id,
});
var error = c.validateNumber();
if (error) {
@@ -501,18 +523,21 @@
}
}
return wrapDeferred(conversation.save({
return wrapDeferred(
conversation.save({
name: details.name,
avatar: details.avatar,
color: details.color,
active_at: activeAt,
})).then(function() {
})
).then(function() {
const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) {
console.log(
'Ignore invalid expire timer.',
'Expected numeric `expireTimer`, got:', expireTimer
'Expected numeric `expireTimer`, got:',
expireTimer
);
return;
}
@@ -542,10 +567,7 @@
})
.then(ev.confirm)
.catch(function(error) {
console.log(
'onContactReceived error:',
Errors.toLogFormat(error)
);
console.log('onContactReceived error:', Errors.toLogFormat(error));
});
}
@@ -553,7 +575,9 @@
var details = ev.groupDetails;
var id = details.id;
return ConversationController.getOrCreateAndWait(id, 'group').then(function(conversation) {
return ConversationController.getOrCreateAndWait(id, 'group').then(function(
conversation
) {
var updates = {
name: details.name,
members: details.members,
@@ -573,13 +597,15 @@
updates.left = true;
}
return wrapDeferred(conversation.save(updates)).then(function() {
return wrapDeferred(conversation.save(updates))
.then(function() {
const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) {
console.log(
'Ignore invalid expire timer.',
'Expected numeric `expireTimer`, got:', expireTimer
'Expected numeric `expireTimer`, got:',
expireTimer
);
return;
}
@@ -592,7 +618,8 @@
receivedAt,
{ fromSync: true }
);
}).then(ev.confirm);
})
.then(ev.confirm);
});
}
@@ -605,25 +632,23 @@
});
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
const getDescriptorForSent = ({ message, destination }) => (
const getDescriptorForSent = ({ message, destination }) =>
message.group
? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: destination }
);
: { type: Message.PRIVATE, id: destination };
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
const getDescriptorForReceived = ({ message, source }) => (
const getDescriptorForReceived = ({ message, source }) =>
message.group
? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: source }
);
: { type: Message.PRIVATE, id: source };
function createMessageHandler({
createMessage,
getMessageDescriptor,
handleProfileUpdate,
}) {
return async (event) => {
return async event => {
const { data, confirm } = event;
const messageDescriptor = getMessageDescriptor(data);
@@ -647,11 +672,9 @@
messageDescriptor.id,
messageDescriptor.type
);
return message.handleDataMessage(
upgradedMessage,
event.confirm,
{ initialLoadComplete }
);
return message.handleDataMessage(upgradedMessage, event.confirm, {
initialLoadComplete,
});
};
}
@@ -677,7 +700,10 @@
});
// Sent:
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) {
async function handleMessageSentProfileUpdate({
confirm,
messageDescriptor,
}) {
const conversation = await ConversationController.getOrCreateAndWait(
messageDescriptor.id,
messageDescriptor.type
@@ -716,9 +742,9 @@
value: [
message.get('source'),
message.get('sourceDevice'),
message.get('sent_at')
]
}
message.get('sent_at'),
],
},
};
fetcher.fetch(options).always(function() {
@@ -742,7 +768,7 @@
received_at: data.receivedAt || Date.now(),
conversationId: data.source,
type: 'incoming',
unread : 1
unread: 1,
});
return message;
@@ -752,25 +778,33 @@
var error = ev.error;
console.log('background onError:', Errors.toLogFormat(error));
if (error.name === 'HTTPError' && (error.code == 401 || error.code == 403)) {
if (
error.name === 'HTTPError' &&
(error.code == 401 || error.code == 403)
) {
Whisper.events.trigger('unauthorized');
console.log('Client is no longer authorized; deleting local configuration');
console.log(
'Client is no longer authorized; deleting local configuration'
);
Whisper.Registration.remove();
var previousNumberId = textsecure.storage.get('number_id');
textsecure.storage.protocol.removeAllConfiguration().then(function() {
textsecure.storage.protocol.removeAllConfiguration().then(
function() {
// These two bits of data are important to ensure that the app loads up
// the conversation list, instead of showing just the QR code screen.
Whisper.Registration.markEverDone();
textsecure.storage.put('number_id', previousNumberId);
console.log('Successfully cleared local configuration');
}, function(error) {
},
function(error) {
console.log(
'Something went wrong clearing local configuration',
error && error.stack ? error.stack : error
);
});
}
);
return;
}
@@ -800,15 +834,19 @@
return message.saveErrors(error).then(function() {
var id = message.get('conversationId');
return ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) {
return ConversationController.getOrCreateAndWait(id, 'private').then(
function(conversation) {
conversation.set({
active_at: Date.now(),
unreadCount: conversation.get('unreadCount') + 1
unreadCount: conversation.get('unreadCount') + 1,
});
var conversation_timestamp = conversation.get('timestamp');
var message_timestamp = message.get('timestamp');
if (!conversation_timestamp || message_timestamp > conversation_timestamp) {
if (
!conversation_timestamp ||
message_timestamp > conversation_timestamp
) {
conversation.set({ timestamp: message.get('sent_at') });
}
@@ -822,7 +860,8 @@
return new Promise(function(resolve, reject) {
conversation.save().then(resolve, reject);
});
});
}
);
});
}
@@ -860,7 +899,7 @@
var receipt = Whisper.ReadSyncs.add({
sender: sender,
timestamp: timestamp,
read_at : read_at
read_at: read_at,
});
receipt.on('remove', ev.confirm);
@@ -875,14 +914,11 @@
var state;
var c = new Whisper.Conversation({
id: number
id: number,
});
var error = c.validateNumber();
if (error) {
console.log(
'Invalid verified sync received:',
Errors.toLogFormat(error)
);
console.log('Invalid verified sync received:', Errors.toLogFormat(error));
return;
}
@@ -898,14 +934,19 @@
break;
}
console.log('got verified sync for', number, state,
ev.viaContactSync ? 'via contact sync' : '');
console.log(
'got verified sync for',
number,
state,
ev.viaContactSync ? 'via contact sync' : ''
);
return ConversationController.getOrCreateAndWait(number, 'private').then(function(contact) {
return ConversationController.getOrCreateAndWait(number, 'private').then(
function(contact) {
var options = {
viaSyncMessage: true,
viaContactSync: ev.viaContactSync,
key: key
key: key,
};
if (state === 'VERIFIED') {
@@ -915,7 +956,8 @@
} else {
return contact.setUnverified(options).then(ev.confirm);
}
});
}
);
}
function onDeliveryReceipt(ev) {
@@ -928,7 +970,7 @@
var receipt = Whisper.DeliveryReceipts.add({
timestamp: deliveryReceipt.timestamp,
source: deliveryReceipt.source
source: deliveryReceipt.source,
});
ev.confirm();

View File

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

View File

@@ -1,7 +1,4 @@
/*global $, Whisper, Backbone, textsecure, extension*/
/*
* vim: ts=4:sw=4:expandtab
*/
// This script should only be included in background.html
(function() {
@@ -19,7 +16,8 @@
this.reset([]);
});
this.on('add remove change:unreadCount',
this.on(
'add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000)
);
this.startPruning();
@@ -52,17 +50,20 @@
},
updateUnreadCount: function() {
var newUnreadCount = _.reduce(
this.map(function(m) { return m.get('unreadCount'); }),
this.map(function(m) {
return m.get('unreadCount');
}),
function(item, memo) {
return item + memo;
},
0
);
storage.put("unreadCount", newUnreadCount);
storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = window.config.title + " (" + newUnreadCount + ")";
window.document.title =
window.config.title + ' (' + newUnreadCount + ')';
} else {
window.setBadgeCount(0);
window.document.title = window.config.title;
@@ -71,12 +72,15 @@
},
startPruning: function() {
var halfHour = 30 * 60 * 1000;
this.interval = setInterval(function() {
this.interval = setInterval(
function() {
this.forEach(function(conversation) {
conversation.trigger('prune');
});
}.bind(this), halfHour);
}
}.bind(this),
halfHour
);
},
}))();
window.getInboxCollection = function() {
@@ -86,7 +90,9 @@
window.ConversationController = {
get: function(id) {
if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch');
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
return conversations.get(id);
@@ -104,11 +110,15 @@
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(`'type' must be 'private' or 'group'; got: '${type}'`);
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch');
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
var conversation = conversations.get(id);
@@ -118,7 +128,7 @@
conversation = conversations.add({
id: id,
type: type
type: type,
});
conversation.initialPromise = new Promise(function(resolve, reject) {
if (!conversation.isValid()) {
@@ -146,7 +156,8 @@
return conversation;
},
getOrCreateAndWait: function(id, type) {
return this._initialPromise.then(function() {
return this._initialPromise.then(
function() {
var conversation = this.getOrCreate(id, type);
if (conversation) {
@@ -158,7 +169,8 @@
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
}.bind(this));
}.bind(this)
);
},
getAllGroupsInvolvingId: function(id) {
var groups = new Whisper.GroupCollection();
@@ -178,21 +190,26 @@
load: function() {
console.log('ConversationController: starting initial fetch');
this._initialPromise = new Promise(function(resolve, reject) {
conversations.fetch().then(function() {
this._initialPromise = new Promise(
function(resolve, reject) {
conversations.fetch().then(
function() {
console.log('ConversationController: done with initial fetch');
this._initialFetchComplete = true;
resolve();
}.bind(this), function(error) {
}.bind(this),
function(error) {
console.log(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
reject(error);
});
}.bind(this));
}
);
}.bind(this)
);
return this._initialPromise;
}
},
};
})();

View File

@@ -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,7 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -14,39 +11,60 @@
recipients = conversation.get('members') || [];
}
var receipts = this.filter(function(receipt) {
return (receipt.get('timestamp') === message.get('sent_at')) &&
(recipients.indexOf(receipt.get('source')) > -1);
return (
receipt.get('timestamp') === message.get('sent_at') &&
recipients.indexOf(receipt.get('source')) > -1
);
});
this.remove(receipts);
return receipts;
},
onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
if (messages.length === 0) { return; }
return messages
.fetchSentAt(receipt.get('timestamp'))
.then(function() {
if (messages.length === 0) {
return;
}
var message = messages.find(function(message) {
return (!message.isIncoming() && receipt.get('source') === message.get('conversationId'));
return (
!message.isIncoming() &&
receipt.get('source') === message.get('conversationId')
);
});
if (message) { return message; }
if (message) {
return message;
}
var groups = new Whisper.GroupCollection();
return groups.fetchGroups(receipt.get('source')).then(function() {
var ids = groups.pluck('id');
ids.push(receipt.get('source'));
return messages.find(function(message) {
return (!message.isIncoming() &&
_.contains(ids, message.get('conversationId')));
return (
!message.isIncoming() &&
_.contains(ids, message.get('conversationId'))
);
});
});
}).then(function(message) {
})
.then(
function(message) {
if (message) {
var deliveries = message.get('delivered') || 0;
var delivered_to = message.get('delivered_to') || [];
return new Promise(function(resolve, reject) {
message.save({
delivered_to: _.union(delivered_to, [receipt.get('source')]),
delivered: deliveries + 1
}).then(function() {
return new Promise(
function(resolve, reject) {
message
.save({
delivered_to: _.union(delivered_to, [
receipt.get('source'),
]),
delivered: deliveries + 1,
})
.then(
function() {
// notify frontend listeners
var conversation = ConversationController.get(
message.get('conversationId')
@@ -57,8 +75,11 @@
this.remove(receipt);
resolve();
}.bind(this), reject);
}.bind(this));
}.bind(this),
reject
);
}.bind(this)
);
// TODO: consider keeping a list of numbers we've
// successfully delivered to?
} else {
@@ -68,12 +89,14 @@
receipt.get('timestamp')
);
}
}.bind(this)).catch(function(error) {
}.bind(this)
)
.catch(function(error) {
console.log(
'DeliveryReceipts.onReceipt error:',
error && error.stack ? error.stack : error
);
});
}
},
}))();
})();

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
(function() {
'use strict';
window.emoji_util = window.emoji_util || {};
@@ -39,17 +35,13 @@
var emojiCount = self.getCountOfAllMatches(str, self.rx_unified);
if (emojiCount > 8) {
return '';
}
else if (emojiCount > 6) {
} else if (emojiCount > 6) {
return 'small';
}
else if (emojiCount > 4) {
} else if (emojiCount > 4) {
return 'medium';
}
else if (emojiCount > 2) {
} else if (emojiCount > 2) {
return 'large';
}
else {
} else {
return 'jumbo';
}
};
@@ -83,7 +75,8 @@
window.emoji = new EmojiConvertor();
emoji.init_colons();
emoji.img_sets.apple.path = 'node_modules/emoji-datasource-apple/img/apple/64/';
emoji.img_sets.apple.path =
'node_modules/emoji-datasource-apple/img/apple/64/';
emoji.include_title = true;
emoji.replace_mode = 'img';
emoji.supports_css = false; // needed to avoid spans with background-image
@@ -95,5 +88,4 @@
$el.html(emoji.signalReplace($el.html()));
};
})();

View File

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

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -36,10 +32,14 @@
var wait = expires_at - Date.now();
// In the past
if (wait < 0) { wait = 0; }
if (wait < 0) {
wait = 0;
}
// Too far in the future, since it's limited to a 32-bit value
if (wait > 2147483647) { wait = 2147483647; }
if (wait > 2147483647) {
wait = 2147483647;
}
clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait);
@@ -53,20 +53,23 @@
checkExpiringMessages();
events.on('timetravel', throttledCheckExpiringMessages);
},
update: throttledCheckExpiringMessages
update: throttledCheckExpiringMessages,
};
var TimerOption = Backbone.Model.extend({
getName: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'),
].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize();
return (
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
moment.duration(this.get('time'), this.get('unit')).humanize()
);
},
getAbbreviated: function() {
return i18n([
'timerOption', this.get('time'), this.get('unit'), 'abbreviated'
].join('_'));
}
return i18n(
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
'_'
)
);
},
});
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption,
@@ -75,8 +78,9 @@
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) { return o.getName(); }
else {
if (o) {
return o.getName();
} else {
return [seconds, 'seconds'].join(' ');
}
},
@@ -85,12 +89,14 @@
seconds = 0;
}
var o = this.findWhere({ seconds: seconds });
if (o) { return o.getAbbreviated(); }
else {
if (o) {
return o.getAbbreviated();
} else {
return [seconds, 's'].join('');
}
}
}))([
},
}))(
[
[0, 'seconds'],
[5, 'seconds'],
[10, 'seconds'],
@@ -108,8 +114,8 @@
return {
time: o[0],
unit: o[1],
seconds: duration.asSeconds()
seconds: duration.asSeconds(),
};
}));
})
);
})();

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -13,16 +9,20 @@
}
signalProtocolStore.on('keychange', function(id) {
ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) {
ConversationController.getOrCreateAndWait(id, 'private').then(function(
conversation
) {
conversation.addKeyChange(id);
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) {
ConversationController.getAllGroupsInvolvingId(id).then(function(
groups
) {
_.forEach(groups, function(group) {
group.addKeyChange(id);
});
});
});
});
}
},
};
}());
})();

View File

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

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: [{
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,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
storage.isBlocked = function(number) {

View File

@@ -124,27 +124,34 @@
},
safeGetVerified() {
const promise = textsecure.storage.protocol.getVerified(this.id);
return promise.catch(() => textsecure.storage.protocol.VerifiedStatus.DEFAULT);
return promise.catch(
() => textsecure.storage.protocol.VerifiedStatus.DEFAULT
);
},
updateVerified() {
if (this.isPrivate()) {
return Promise.all([
this.safeGetVerified(),
this.initialPromise,
]).then((results) => {
return Promise.all([this.safeGetVerified(), this.initialPromise]).then(
results => {
const trust = results[0];
// we don't return here because we don't need to wait for this to finish
this.save({ verified: trust });
});
}
);
}
const promise = this.fetchContacts();
return promise.then(() => Promise.all(this.contactCollection.map((contact) => {
return promise
.then(() =>
Promise.all(
this.contactCollection.map(contact => {
if (!contact.isMe()) {
return contact.updateVerified();
}
return Promise.resolve();
}))).then(this.onMemberVerifiedChange.bind(this));
})
)
)
.then(this.onMemberVerifiedChange.bind(this));
},
setVerifiedDefault(options) {
const { DEFAULT } = this.verifiedEnum;
@@ -160,16 +167,19 @@
},
_setVerified(verified, providedOptions) {
const options = providedOptions || {};
_.defaults(options, { viaSyncMessage: false, viaContactSync: false, key: null });
_.defaults(options, {
viaSyncMessage: false,
viaContactSync: false,
key: null,
});
const {
VERIFIED,
UNVERIFIED,
} = this.verifiedEnum;
const { VERIFIED, UNVERIFIED } = this.verifiedEnum;
if (!this.isPrivate()) {
throw new Error('You cannot verify a group conversation. ' +
'You must verify individual contacts.');
throw new Error(
'You cannot verify a group conversation. ' +
'You must verify individual contacts.'
);
}
const beginningVerified = this.get('verified');
@@ -187,10 +197,14 @@
}
let keychange;
return promise.then((updatedKey) => {
return promise
.then(updatedKey => {
keychange = updatedKey;
return new Promise((resolve => this.save({ verified }).always(resolve)));
}).then(() => {
return new Promise(resolve =>
this.save({ verified }).always(resolve)
);
})
.then(() => {
// Three situations result in a verification notice in the conversation:
// 1) The message came from an explicit verification in another client (not
// a contact sync)
@@ -199,14 +213,14 @@
// 3) Our local verification status is VERIFIED and it hasn't changed,
// but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
if (!options.viaContactSync ||
if (
!options.viaContactSync ||
(beginningVerified !== verified && verified !== UNVERIFIED) ||
(keychange && verified === VERIFIED)) {
return this.addVerifiedChange(
this.id,
verified === VERIFIED,
{ local: !options.viaSyncMessage }
);
(keychange && verified === VERIFIED)
) {
return this.addVerifiedChange(this.id, verified === VERIFIED, {
local: !options.viaSyncMessage,
});
}
if (!options.viaSyncMessage) {
return this.sendVerifySyncMessage(this.id, verified);
@@ -216,20 +230,21 @@
},
sendVerifySyncMessage(number, state) {
const promise = textsecure.storage.protocol.loadIdentityKey(number);
return promise.then(key => textsecure.messaging.syncVerification(
number,
state,
key
));
return promise.then(key =>
textsecure.messaging.syncVerification(number, state, key)
);
},
getIdentityKeys() {
const lookup = {};
if (this.isPrivate()) {
return textsecure.storage.protocol.loadIdentityKey(this.id).then((key) => {
return textsecure.storage.protocol
.loadIdentityKey(this.id)
.then(key => {
lookup[this.id] = key;
return lookup;
}).catch((error) => {
})
.catch(error => {
console.log(
'getIdentityKeys error for conversation',
this.idForLogging(),
@@ -240,27 +255,25 @@
}
const promises = this.contactCollection.map(contact =>
textsecure.storage.protocol.loadIdentityKey(contact.id).then(
(key) => {
key => {
lookup[contact.id] = key;
},
(error) => {
error => {
console.log(
'getIdentityKeys error for group member',
contact.idForLogging(),
error && error.stack ? error.stack : error
);
}
));
)
);
return Promise.all(promises).then(() => lookup);
},
replay(error, message) {
const replayable = new textsecure.ReplayableError(error);
return replayable.replay(message.attributes).catch((e) => {
console.log(
'replay error:',
e && e.stack ? e.stack : e
);
return replayable.replay(message.attributes).catch(e => {
console.log('replay error:', e && e.stack ? e.stack : e);
});
},
decryptOldIncomingKeyErrors() {
@@ -270,19 +283,25 @@
}
console.log('decryptOldIncomingKeyErrors start for', this.idForLogging());
const messages = this.messageCollection.filter((message) => {
const messages = this.messageCollection.filter(message => {
const errors = message.get('errors');
if (!errors || !errors[0]) {
return false;
}
const error = _.find(errors, e => e.name === 'IncomingIdentityKeyError');
const error = _.find(
errors,
e => e.name === 'IncomingIdentityKeyError'
);
return Boolean(error);
});
const markComplete = () => {
console.log('decryptOldIncomingKeyErrors complete for', this.idForLogging());
return new Promise((resolve) => {
console.log(
'decryptOldIncomingKeyErrors complete for',
this.idForLogging()
);
return new Promise(resolve => {
this.save({ decryptedOldIncomingKeyErrors: true }).always(resolve);
});
};
@@ -296,12 +315,16 @@
messages.length,
'messages to process'
);
const safeDelete = message => new Promise((resolve) => {
const safeDelete = message =>
new Promise(resolve => {
message.destroy().always(resolve);
});
const promise = this.getIdentityKeys();
return promise.then(lookup => Promise.all(_.map(messages, (message) => {
return promise
.then(lookup =>
Promise.all(
_.map(messages, message => {
const source = message.get('source');
const error = _.find(
message.get('errors'),
@@ -314,16 +337,22 @@
}
if (constantTimeEqualArrayBuffers(key, error.identityKey)) {
return this.replay(error, message).then(() => safeDelete(message));
return this.replay(error, message).then(() =>
safeDelete(message)
);
}
return Promise.resolve();
}))).catch((error) => {
})
)
)
.catch(error => {
console.log(
'decryptOldIncomingKeyErrors error:',
error && error.stack ? error.stack : error
);
}).then(markComplete);
})
.then(markComplete);
},
isVerified() {
if (this.isPrivate()) {
@@ -333,7 +362,7 @@
return false;
}
return this.contactCollection.every((contact) => {
return this.contactCollection.every(contact => {
if (contact.isMe()) {
return true;
}
@@ -343,14 +372,16 @@
isUnverified() {
if (this.isPrivate()) {
const verified = this.get('verified');
return verified !== this.verifiedEnum.VERIFIED &&
verified !== this.verifiedEnum.DEFAULT;
return (
verified !== this.verifiedEnum.VERIFIED &&
verified !== this.verifiedEnum.DEFAULT
);
}
if (!this.contactCollection.length) {
return true;
}
return this.contactCollection.any((contact) => {
return this.contactCollection.any(contact => {
if (contact.isMe()) {
return false;
}
@@ -363,23 +394,29 @@
? new Backbone.Collection([this])
: new Backbone.Collection();
}
return new Backbone.Collection(this.contactCollection.filter((contact) => {
return new Backbone.Collection(
this.contactCollection.filter(contact => {
if (contact.isMe()) {
return false;
}
return contact.isUnverified();
}));
})
);
},
setApproved() {
if (!this.isPrivate()) {
throw new Error('You cannot set a group conversation as trusted. ' +
'You must set individual contacts as trusted.');
throw new Error(
'You cannot set a group conversation as trusted. ' +
'You must set individual contacts as trusted.'
);
}
return textsecure.storage.protocol.setApproval(this.id, true);
},
safeIsUntrusted() {
return textsecure.storage.protocol.isUntrusted(this.id).catch(() => false);
return textsecure.storage.protocol
.isUntrusted(this.id)
.catch(() => false);
},
isUntrusted() {
if (this.isPrivate()) {
@@ -389,18 +426,20 @@
return Promise.resolve(false);
}
return Promise.all(this.contactCollection.map((contact) => {
return Promise.all(
this.contactCollection.map(contact => {
if (contact.isMe()) {
return false;
}
return contact.safeIsUntrusted();
})).then(results => _.any(results, result => result));
})
).then(results => _.any(results, result => result));
},
getUntrusted() {
// This is a bit ugly because isUntrusted() is async. Could do the work to cache
// it locally, but we really only need it for this call.
if (this.isPrivate()) {
return this.isUntrusted().then((untrusted) => {
return this.isUntrusted().then(untrusted => {
if (untrusted) {
return new Backbone.Collection([this]);
}
@@ -408,20 +447,24 @@
return new Backbone.Collection();
});
}
return Promise.all(this.contactCollection.map((contact) => {
return Promise.all(
this.contactCollection.map(contact => {
if (contact.isMe()) {
return [false, contact];
}
return Promise.all([contact.isUntrusted(), contact]);
})).then((results) => {
const filtered = _.filter(results, (result) => {
})
).then(results => {
const filtered = _.filter(results, result => {
const untrusted = result[0];
return untrusted;
});
return new Backbone.Collection(_.map(filtered, (result) => {
return new Backbone.Collection(
_.map(filtered, result => {
const contact = result[1];
return contact;
}));
})
);
});
},
onMemberVerifiedChange() {
@@ -461,7 +504,9 @@
_.defaults(options, { local: true });
if (this.isMe()) {
console.log('refusing to add verified change advisory for our own number');
console.log(
'refusing to add verified change advisory for our own number'
);
return;
}
@@ -488,8 +533,8 @@
message.save().then(this.trigger.bind(this, 'newmessage', message));
if (this.isPrivate()) {
ConversationController.getAllGroupsInvolvingId(id).then((groups) => {
_.forEach(groups, (group) => {
ConversationController.getAllGroupsInvolvingId(id).then(groups => {
_.forEach(groups, group => {
group.addVerifiedChange(id, verified, options);
});
});
@@ -512,31 +557,36 @@
// Lastly, we don't send read syncs for any message marked read due to a read
// sync. That's a notification explosion we don't need.
return this.queueJob(() => this.markRead(
message.get('received_at'),
{ sendReadReceipts: false }
));
return this.queueJob(() =>
this.markRead(message.get('received_at'), { sendReadReceipts: false })
);
},
getUnread() {
const conversationId = this.id;
const unreadMessages = new Whisper.MessageCollection();
return new Promise((resolve => unreadMessages.fetch({
return new Promise(resolve =>
unreadMessages
.fetch({
index: {
// 'unread' index
name: 'unread',
lower: [conversationId],
upper: [conversationId, Number.MAX_VALUE],
},
}).always(() => {
})
.always(() => {
resolve(unreadMessages);
})));
})
);
},
validate(attributes) {
const required = ['id', 'type'];
const missing = _.filter(required, attr => !attributes[attr]);
if (missing.length) { return `Conversation must have ${missing}`; }
if (missing.length) {
return `Conversation must have ${missing}`;
}
if (attributes.type !== 'private' && attributes.type !== 'group') {
return `Invalid conversation type: ${attributes.type}`;
@@ -572,7 +622,12 @@
const name = this.get('name');
if (typeof name === 'string') {
tokens.push(name.toLowerCase());
tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_()+]+/));
tokens = tokens.concat(
name
.trim()
.toLowerCase()
.split(/[\s\-_()+]+/)
);
}
if (this.isPrivate()) {
const regionCode = storage.get('regionCode');
@@ -633,7 +688,9 @@
type: contentType,
});
const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported(contentType)
const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported(
contentType
)
? await Whisper.FileInputView.makeImageThumbnail(128, objectUrl)
: await Whisper.FileInputView.makeVideoThumbnail(128, objectUrl);
@@ -661,7 +718,8 @@
author: contact.id,
id: quotedMessage.get('sent_at'),
text: quotedMessage.get('body'),
attachments: await Promise.all((attachments || []).map(async (attachment) => {
attachments: await Promise.all(
(attachments || []).map(async attachment => {
const { contentType } = attachment;
const willMakeThumbnail =
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
@@ -674,7 +732,8 @@
? await this.makeThumbnailAttachment(attachment)
: null,
};
})),
})
),
};
},
@@ -721,7 +780,9 @@
case Message.GROUP:
return textsecure.messaging.sendMessageToGroup;
default:
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
throw new TypeError(
`Invalid conversation type: '${conversationType}'`
);
}
})();
@@ -730,9 +791,11 @@
profileKey = storage.get('profileKey');
}
const attachmentsWithData =
await Promise.all(messageWithSchema.attachments.map(loadAttachmentData));
message.send(sendFunction(
const attachmentsWithData = await Promise.all(
messageWithSchema.attachments.map(loadAttachmentData)
);
message.send(
sendFunction(
this.get('id'),
body,
attachmentsWithData,
@@ -740,7 +803,8 @@
now,
this.get('expireTimer'),
profileKey
));
)
);
});
},
@@ -749,13 +813,16 @@
await collection.fetchConversation(this.id, 1);
const lastMessage = collection.at(0);
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({
const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate(
{
currentLastMessageText: this.get('lastMessage') || null,
currentTimestamp: this.get('timestamp') || null,
lastMessage: lastMessage ? lastMessage.toJSON() : null,
lastMessageNotificationText: lastMessage
? lastMessage.getNotificationText() : null,
});
? lastMessage.getNotificationText()
: null,
}
);
this.set(lastMessageUpdate);
@@ -779,8 +846,10 @@
if (!expireTimer) {
expireTimer = null;
}
if (this.get('expireTimer') === expireTimer ||
(!expireTimer && !this.get('expireTimer'))) {
if (
this.get('expireTimer') === expireTimer ||
(!expireTimer && !this.get('expireTimer'))
) {
return Promise.resolve();
}
@@ -881,12 +950,14 @@
received_at: now,
group_update: groupUpdate,
});
message.send(textsecure.messaging.updateGroup(
message.send(
textsecure.messaging.updateGroup(
this.id,
this.get('name'),
this.get('avatar'),
this.get('members')
));
)
);
},
leaveGroup() {
@@ -909,25 +980,30 @@
_.defaults(options, { sendReadReceipts: true });
const conversationId = this.id;
Whisper.Notifications.remove(Whisper.Notifications.where({
Whisper.Notifications.remove(
Whisper.Notifications.where({
conversationId,
}));
})
);
return this.getUnread().then((providedUnreadMessages) => {
return this.getUnread().then(providedUnreadMessages => {
let unreadMessages = providedUnreadMessages;
const promises = [];
const oldUnread = unreadMessages.filter(message =>
message.get('received_at') <= newestUnreadDate);
const oldUnread = unreadMessages.filter(
message => message.get('received_at') <= newestUnreadDate
);
let read = _.map(oldUnread, (providedM) => {
let read = _.map(oldUnread, providedM => {
let m = providedM;
if (this.messageCollection.get(m.id)) {
m = this.messageCollection.get(m.id);
} else {
console.log('Marked a message as read in the database, but ' +
'it was not in messageCollection.');
console.log(
'Marked a message as read in the database, but ' +
'it was not in messageCollection.'
);
}
promises.push(m.markRead());
const errors = m.get('errors');
@@ -962,7 +1038,9 @@
if (storage.get('read-receipt-setting')) {
_.each(_.groupBy(read, 'sender'), (receipts, sender) => {
const timestamps = _.map(receipts, 'timestamp');
promises.push(textsecure.messaging.sendReadReceipts(sender, timestamps));
promises.push(
textsecure.messaging.sendReadReceipts(sender, timestamps)
);
});
}
}
@@ -990,21 +1068,22 @@
getProfile(id) {
if (!textsecure.messaging) {
const message = 'Conversation.getProfile: textsecure.messaging not available';
const message =
'Conversation.getProfile: textsecure.messaging not available';
return Promise.reject(new Error(message));
}
return textsecure.messaging.getProfile(id).then((profile) => {
return textsecure.messaging
.getProfile(id)
.then(profile => {
const identityKey = dcodeIO.ByteBuffer.wrap(
profile.identityKey,
'base64'
).toArrayBuffer();
return textsecure.storage.protocol.saveIdentity(
`${id}.1`,
identityKey,
false
).then((changed) => {
return textsecure.storage.protocol
.saveIdentity(`${id}.1`, identityKey, false)
.then(changed => {
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
@@ -1017,18 +1096,20 @@
return sessionCipher.closeOpenSessionForDevice();
}
return Promise.resolve();
}).then(() => {
})
.then(() => {
const c = ConversationController.get(id);
return Promise.all([
c.setProfileName(profile.name),
c.setProfileAvatar(profile.avatar),
]).then(
// success
() => new Promise((resolve, reject) => {
() =>
new Promise((resolve, reject) => {
c.save().then(resolve, reject);
}),
// fail
(e) => {
e => {
if (e.name === 'ProfileDecryptError') {
// probably the profile key has changed.
console.log(
@@ -1041,7 +1122,8 @@
}
);
});
}).catch((error) => {
})
.catch(error => {
console.log(
'getProfile error:',
error && error.stack ? error.stack : error
@@ -1056,10 +1138,15 @@
try {
// decode
const data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer();
const data = dcodeIO.ByteBuffer.wrap(
encryptedName,
'base64'
).toArrayBuffer();
// decrypt
return textsecure.crypto.decryptProfileName(data, key).then((decrypted) => {
return textsecure.crypto
.decryptProfileName(data, key)
.then(decrypted => {
// encode
const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
@@ -1075,13 +1162,13 @@
return Promise.resolve();
}
return textsecure.messaging.getAvatar(avatarPath).then((avatar) => {
return textsecure.messaging.getAvatar(avatarPath).then(avatar => {
const key = this.get('profileKey');
if (!key) {
return Promise.resolve();
}
// decrypt
return textsecure.crypto.decryptProfile(avatar, key).then((decrypted) => {
return textsecure.crypto.decryptProfile(avatar, key).then(decrypted => {
// set
this.set({
profileAvatar: {
@@ -1125,9 +1212,11 @@
const first = attachments[0];
const { thumbnail, contentType } = first;
return thumbnail ||
return (
thumbnail ||
Signal.Util.GoogleChrome.isImageTypeSupported(contentType) ||
Signal.Util.GoogleChrome.isVideoTypeSupported(contentType);
Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)
);
},
forceRender(message) {
message.trigger('change', message);
@@ -1163,14 +1252,18 @@
return false;
}
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) {
if (
!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
) {
return false;
}
const collection = new Whisper.MessageCollection();
await collection.fetchSentAt(id);
const queryMessage = collection.find(m => this.doesMessageMatch(id, author, m));
const queryMessage = collection.find(m =>
this.doesMessageMatch(id, author, m)
);
if (!queryMessage) {
return false;
@@ -1206,8 +1299,10 @@
return;
}
if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) {
if (
!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) &&
!Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)
) {
return;
}
@@ -1267,7 +1362,7 @@
async processQuotes(messages) {
const lookup = this.makeMessagesLookup(messages);
const promises = messages.map(async (message) => {
const promises = messages.map(async message => {
const { quote } = message.attributes;
if (!quote) {
return;
@@ -1350,11 +1445,16 @@
}
const members = this.get('members') || [];
const promises = members.map(number =>
ConversationController.getOrCreateAndWait(number, 'private'));
ConversationController.getOrCreateAndWait(number, 'private')
);
return Promise.all(promises).then((contacts) => {
_.forEach(contacts, (contact) => {
this.listenTo(contact, 'change:verified', this.onMemberVerifiedChange);
return Promise.all(promises).then(contacts => {
_.forEach(contacts, contact => {
this.listenTo(
contact,
'change:verified',
this.onMemberVerifiedChange
);
});
this.contactCollection.reset(contacts);
@@ -1362,17 +1462,19 @@
},
destroyMessages() {
this.messageCollection.fetch({
this.messageCollection
.fetch({
index: {
// 'conversation' index on [conversationId, received_at]
name: 'conversation',
lower: [this.id],
upper: [this.id, Number.MAX_VALUE],
},
}).then(() => {
})
.then(() => {
const { models } = this.messageCollection;
this.messageCollection.reset([]);
_.each(models, (message) => {
_.each(models, message => {
message.destroy();
});
this.save({
@@ -1460,10 +1562,9 @@
this.revokeAvatarUrl();
const avatar = this.get('avatar') || this.get('profileAvatar');
if (avatar) {
this.avatarUrl = URL.createObjectURL(new Blob(
[avatar.data],
{ type: avatar.contentType }
));
this.avatarUrl = URL.createObjectURL(
new Blob([avatar.data], { type: avatar.contentType })
);
} else {
this.avatarUrl = null;
}
@@ -1507,7 +1608,7 @@
},
getNotificationIcon() {
return new Promise((resolve) => {
return new Promise(resolve => {
const avatar = this.getAvatar();
if (avatar.url) {
resolve(avatar.url);
@@ -1523,8 +1624,11 @@
}
const conversationId = this.id;
return ConversationController.getOrCreateAndWait(message.get('source'), 'private')
.then(sender => sender.getNotificationIcon().then((iconUrl) => {
return ConversationController.getOrCreateAndWait(
message.get('source'),
'private'
).then(sender =>
sender.getNotificationIcon().then(iconUrl => {
console.log('adding notification');
Whisper.Notifications.add({
title: sender.getTitle(),
@@ -1534,7 +1638,8 @@
conversationId,
messageId: message.id,
});
}));
})
);
},
hashCode() {
if (this.hash === undefined) {
@@ -1545,7 +1650,7 @@
let hash = 0;
for (let i = 0; i < string.length; i += 1) {
// eslint-disable-next-line no-bitwise
hash = ((hash << 5) - hash) + string.charCodeAt(i);
hash = (hash << 5) - hash + string.charCodeAt(i);
// eslint-disable-next-line no-bitwise
hash &= hash; // Convert to 32bit integer
}
@@ -1566,9 +1671,17 @@
},
destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => {
m.destroy().then(resolve).fail(reject);
})));
return Promise.all(
this.models.map(
m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
},
search(providedQuery) {
@@ -1578,7 +1691,7 @@
const lastCharCode = query.charCodeAt(query.length - 1);
const nextChar = String.fromCharCode(lastCharCode + 1);
const upper = query.slice(0, -1) + nextChar;
return new Promise((resolve) => {
return new Promise(resolve => {
this.fetch({
index: {
name: 'search', // 'search' index on tokens array
@@ -1593,7 +1706,7 @@
},
fetchAlphabetical() {
return new Promise((resolve) => {
return new Promise(resolve => {
this.fetch({
index: {
name: 'search', // 'search' index on tokens array
@@ -1604,7 +1717,7 @@
},
fetchGroups(number) {
return new Promise((resolve) => {
return new Promise(resolve => {
this.fetch({
index: {
name: 'group',
@@ -1623,7 +1736,7 @@
storeName: 'conversations',
model: Whisper.Conversation,
fetchGroups(number) {
return new Promise((resolve) => {
return new Promise(resolve => {
this.fetch({
index: {
name: 'group',
@@ -1633,4 +1746,4 @@
});
},
});
}());
})();

View File

@@ -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';
@@ -116,7 +120,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name));
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this));
const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
);
if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else {
@@ -186,7 +193,7 @@
}
const quote = this.get('quote');
const attachments = (quote && quote.attachments) || [];
attachments.forEach((attachment) => {
attachments.forEach(attachment => {
if (attachment.thumbnail && attachment.thumbnail.objectUrl) {
URL.revokeObjectURL(attachment.thumbnail.objectUrl);
// eslint-disable-next-line no-param-reassign
@@ -269,7 +276,8 @@
return {
attachments: (quote.attachments || []).map(attachment =>
this.processAttachment(attachment, objectUrl)),
this.processAttachment(attachment, objectUrl)
),
authorColor,
authorProfileName,
authorTitle,
@@ -342,7 +350,8 @@
send(promise) {
this.trigger('pending');
return promise.then((result) => {
return promise
.then(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
@@ -355,7 +364,8 @@
expirationStartTimestamp: now,
});
this.sendSyncMessage();
}).catch((result) => {
})
.catch(result => {
const now = Date.now();
this.trigger('done');
if (result.dataMessage) {
@@ -383,12 +393,14 @@
});
promises.push(this.sendSyncMessage());
}
promises = promises.concat(_.map(result.errors, (error) => {
promises = promises.concat(
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(error.number);
promises.push(c.getProfiles());
}
}));
})
);
}
return Promise.all(promises).then(() => {
@@ -423,12 +435,14 @@
if (this.get('synced') || !dataMessage) {
return Promise.resolve();
}
return textsecure.messaging.sendSyncMessage(
return textsecure.messaging
.sendSyncMessage(
dataMessage,
this.get('sent_at'),
this.get('destination'),
this.get('expirationStartTimestamp')
).then(() => {
)
.then(() => {
this.save({ synced: true, dataMessage: null });
});
});
@@ -440,17 +454,19 @@
if (!(errors instanceof Array)) {
errors = [errors];
}
errors.forEach((e) => {
errors.forEach(e => {
console.log(
'Message.saveErrors:',
e && e.reason ? e.reason : null,
e && e.stack ? e.stack : e
);
});
errors = errors.map((e) => {
if (e.constructor === Error ||
errors = errors.map(e => {
if (
e.constructor === Error ||
e.constructor === TypeError ||
e.constructor === ReferenceError) {
e.constructor === ReferenceError
) {
return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
}
return e;
@@ -463,17 +479,19 @@
hasNetworkError() {
const error = _.find(
this.get('errors'),
e => (e.name === 'MessageError' ||
e =>
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError')
e.name === 'SignedPreKeyRotationError'
);
return !!error;
},
removeOutgoingErrors(number) {
const errors = _.partition(
this.get('errors'),
e => e.number === number &&
e =>
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
@@ -484,11 +502,13 @@
return errors[0][0];
},
isReplayableError(e) {
return (e.name === 'MessageError' ||
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError');
e.name === 'OutgoingIdentityKeyError'
);
},
resend(number) {
const error = this.removeOutgoingErrors(number);
@@ -513,7 +533,9 @@
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
const conversation = ConversationController.get(conversationId);
return conversation.queueJob(() => new Promise((resolve) => {
return conversation.queueJob(
() =>
new Promise(resolve => {
const now = new Date().getTime();
let attributes = { type: 'private' };
if (dataMessage.group) {
@@ -528,13 +550,15 @@
groupId: dataMessage.group.id,
name: dataMessage.group.name,
avatar: dataMessage.group.avatar,
members: _.union(dataMessage.group.members, conversation.get('members')),
members: _.union(
dataMessage.group.members,
conversation.get('members')
),
};
groupUpdate = conversation.changedAttributes(_.pick(
dataMessage.group,
'name',
'avatar'
)) || {};
groupUpdate =
conversation.changedAttributes(
_.pick(dataMessage.group, 'name', 'avatar')
) || {};
const difference = _.difference(
attributes.members,
conversation.get('members')
@@ -553,7 +577,10 @@
} else {
groupUpdate = { left: source };
}
attributes.members = _.without(conversation.get('members'), source);
attributes.members = _.without(
conversation.get('members'),
source
);
}
if (groupUpdate !== null) {
@@ -574,10 +601,15 @@
schemaVersion: dataMessage.schemaVersion,
});
if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message);
receipts.forEach(() => message.set({
const receipts = Whisper.DeliveryReceipts.forMessage(
conversation,
message
);
receipts.forEach(() =>
message.set({
delivered: (message.get('delivered') || 0) + 1,
}));
})
);
}
attributes.active_at = now;
conversation.set(attributes);
@@ -611,15 +643,19 @@
if (!message.isEndSession() && !message.isGroupUpdate()) {
if (dataMessage.expireTimer) {
if (dataMessage.expireTimer !== conversation.get('expireTimer')) {
if (
dataMessage.expireTimer !== conversation.get('expireTimer')
) {
conversation.updateExpirationTimer(
dataMessage.expireTimer, source,
dataMessage.expireTimer,
source,
message.get('received_at')
);
}
} else if (conversation.get('expireTimer')) {
conversation.updateExpirationTimer(
null, source,
null,
source,
message.get('received_at')
);
}
@@ -627,23 +663,35 @@
if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) {
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) {
message.set('expirationStartTimestamp', readSync.get('read_at'));
if (
message.get('expireTimer') &&
!message.get('expirationStartTimestamp')
) {
message.set(
'expirationStartTimestamp',
readSync.get('read_at')
);
}
}
if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as
// read, as is done when we receive a read sync for a message we already
// know about.
// This is primarily to allow the conversation to mark all older
// messages as read, as is done when we receive a read sync for
// a message we already know about.
Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1);
conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
}
}
if (type === 'outgoing') {
const reads = Whisper.ReadReceipts.forMessage(conversation, message);
const reads = Whisper.ReadReceipts.forMessage(
conversation,
message
);
if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader'));
message.set({
@@ -655,7 +703,10 @@
}
const conversationTimestamp = conversation.get('timestamp');
if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) {
if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
@@ -672,15 +723,20 @@
ConversationController.getOrCreateAndWait(
source,
'private'
).then((sender) => {
).then(sender => {
sender.setProfileKey(profileKey);
});
}
}
const handleError = (error) => {
const handleError = error => {
const errorForLog = error && error.stack ? error.stack : error;
console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog);
console.log(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
return resolve();
};
@@ -691,17 +747,22 @@
} catch (e) {
return handleError(e);
}
// We fetch() here because, between the message.save() above and the previous
// line's trigger() call, we might have marked all messages unread in the
// database. This message might already be read!
// We fetch() here because, between the message.save() above and
// the previous line's trigger() call, we might have marked all
// messages unread in the database. This message might already
// be read!
const previousUnread = message.get('unread');
return message.fetch().then(() => {
return message.fetch().then(
() => {
try {
if (previousUnread !== message.get('unread')) {
console.log('Caught race condition on new message read state! ' +
'Manually starting timers.');
// We call markRead() even though the message is already marked read
// because we need to start expiration timers, etc.
console.log(
'Caught race condition on new message read state! ' +
'Manually starting timers.'
);
// We call markRead() even though the message is already
// marked read because we need to start expiration
// timers, etc.
message.markRead();
}
@@ -717,7 +778,8 @@
} catch (e) {
return handleError(e);
}
}, () => {
},
() => {
try {
console.log(
'handleDataMessage: Message',
@@ -730,19 +792,23 @@
} catch (e) {
return handleError(e);
}
});
}
);
}, handleError);
}, handleError);
}));
})
);
},
markRead(readAt) {
this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
this.set('expirationStartTimestamp', readAt || Date.now());
}
Whisper.Notifications.remove(Whisper.Notifications.where({
Whisper.Notifications.remove(
Whisper.Notifications.where({
messageId: this.id,
}));
})
);
return new Promise((resolve, reject) => {
this.save().then(resolve, reject);
});
@@ -760,7 +826,7 @@
const now = Date.now();
const start = this.get('expirationStartTimestamp');
const delta = this.get('expireTimer') * 1000;
let msFromNow = (start + delta) - now;
let msFromNow = start + delta - now;
if (msFromNow < 0) {
msFromNow = 0;
}
@@ -784,7 +850,6 @@
console.log('message', this.get('sent_at'), 'expires at', expiresAt);
}
},
});
Whisper.MessageCollection = Backbone.Collection.extend({
@@ -804,19 +869,29 @@
}
},
destroyAll() {
return Promise.all(this.models.map(m => new Promise((resolve, reject) => {
m.destroy().then(resolve).fail(reject);
})));
return Promise.all(
this.models.map(
m =>
new Promise((resolve, reject) => {
m
.destroy()
.then(resolve)
.fail(reject);
})
)
);
},
fetchSentAt(timestamp) {
return new Promise((resolve => this.fetch({
return new Promise(resolve =>
this.fetch({
index: {
// 'receipt' index on sent_at
name: 'receipt',
only: timestamp,
},
}).always(resolve)));
}).always(resolve)
);
},
getLoadedUnreadCount() {
@@ -841,7 +916,7 @@
if (unreadCount > 0) {
startingLoadedUnread = this.getLoadedUnreadCount();
}
return new Promise((resolve) => {
return new Promise(resolve => {
let upper;
if (this.length === 0) {
// fetch the most recent messages first
@@ -893,4 +968,4 @@
});
},
});
}());
})();

View File

@@ -20,7 +20,9 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
);
return new Promise((resolve, reject) => {
loadImage(fileOrBlobOrURL, (canvasOrError) => {
loadImage(
fileOrBlobOrURL,
canvasOrError => {
if (canvasOrError.type === 'error') {
const error = new Error('autoOrientImage: Failed to process image');
error.cause = canvasOrError;
@@ -35,6 +37,8 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
);
resolve(dataURL);
}, optionsWithDefaults);
},
optionsWithDefaults
);
});
};

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

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 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, {
console.log(
'Upgrade message schema:',
Object.assign({}, status, {
numTotalMessages,
numCumulativeMessagesProcessed,
}));
})
);
}
console.log('Close database connection');
@@ -181,8 +185,10 @@ const _getConnection = async ({ databaseName, minDatabaseVersion }) => {
const databaseVersion = connection.version;
const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion;
if (!isValidDatabaseVersion) {
throw new Error(`Expected database version (${databaseVersion})` +
` to be at least ${minDatabaseVersion}`);
throw new Error(
`Expected database version (${databaseVersion})` +
` to be at least ${minDatabaseVersion}`
);
}
return connection;
@@ -205,29 +211,33 @@ const _processBatch = async ({
throw new TypeError("'numMessagesPerBatch' is required");
}
const isAttachmentMigrationComplete =
await settings.isAttachmentMigrationComplete(connection);
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
connection
);
if (isAttachmentMigrationComplete) {
return {
done: true,
};
}
const lastProcessedIndex =
await settings.getAttachmentMigrationLastProcessedIndex(connection);
const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex(
connection
);
const fetchUnprocessedMessagesStartTime = Date.now();
const unprocessedMessages =
await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({
const unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex(
{
connection,
count: numMessagesPerBatch,
lastIndex: lastProcessedIndex,
});
}
);
const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
const upgradeStartTime = Date.now();
const upgradedMessages =
await Promise.all(unprocessedMessages.map(upgradeMessageSchema));
const upgradedMessages = await Promise.all(
unprocessedMessages.map(upgradeMessageSchema)
);
const upgradeDuration = Date.now() - upgradeStartTime;
const saveMessagesStartTime = Date.now();
@@ -266,12 +276,12 @@ const _processBatch = async ({
};
};
const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => {
const _saveMessageBackbone = ({ BackboneMessage } = {}) => message => {
const backboneMessage = new BackboneMessage(message);
return deferredToPromise(backboneMessage.save());
};
const _saveMessage = ({ transaction } = {}) => (message) => {
const _saveMessage = ({ transaction } = {}) => message => {
if (!isObject(transaction)) {
throw new TypeError("'transaction' is required");
}
@@ -279,18 +289,20 @@ const _saveMessage = ({ transaction } = {}) => (message) => {
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
const request = messagesStore.put(message, message.id);
return new Promise((resolve, reject) => {
request.onsuccess = () =>
resolve();
request.onerror = event =>
reject(event.target.error);
request.onsuccess = () => resolve();
request.onerror = event => reject(event.target.error);
});
};
const _fetchMessagesRequiringSchemaUpgrade =
async ({ BackboneMessageCollection, count } = {}) => {
const _fetchMessagesRequiringSchemaUpgrade = async ({
BackboneMessageCollection,
count,
} = {}) => {
if (!isFunction(BackboneMessageCollection)) {
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
' constructor is required');
throw new TypeError(
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
' constructor is required'
);
}
if (!isNumber(count)) {
@@ -298,7 +310,9 @@ const _fetchMessagesRequiringSchemaUpgrade =
}
const collection = new BackboneMessageCollection();
return new Promise(resolve => collection.fetch({
return new Promise(resolve =>
collection
.fetch({
limit: count,
index: {
name: 'schemaVersion',
@@ -306,17 +320,22 @@ const _fetchMessagesRequiringSchemaUpgrade =
excludeUpper: true,
order: 'desc',
},
}).always(() => {
})
.always(() => {
const models = collection.models || [];
const messages = models.map(model => model.toJSON());
resolve(messages);
}));
})
);
};
// NOTE: Named dangerous because it is not as efficient as using our
// `messages` `schemaVersion` index:
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
({ connection, count, lastIndex } = {}) => {
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({
connection,
count,
lastIndex,
} = {}) => {
if (!isObject(connection)) {
throw new TypeError("'connection' is required");
}
@@ -341,7 +360,7 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
return new Promise((resolve, reject) => {
const items = [];
const request = messagesStore.openCursor(range);
request.onsuccess = (event) => {
request.onsuccess = event => {
const cursor = event.target.result;
const hasMoreData = Boolean(cursor);
if (!hasMoreData || items.length === count) {
@@ -352,8 +371,7 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
items.push(item);
cursor.continue();
};
request.onerror = event =>
reject(event.target.error);
request.onerror = event => reject(event.target.error);
});
};

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 [{
return [
{
version: lastMigrationVersion,
migrate() {
throw new Error('Unexpected invocation of placeholder migration!' +
throw new Error(
'Unexpected invocation of placeholder migration!' +
'\n\nMigrations must explicitly be run upon application startup instead' +
' of implicitly via Backbone IndexedDB adapter at any time.');
' of implicitly via Backbone IndexedDB adapter at any time.'
);
},
}];
},
];
};

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) => {
promises.push(
new Promise((resolve, reject) => {
const putRequest = identityKeys.put(attributes, attributes.id);
putRequest.onsuccess = resolve;
putRequest.onerror = (e) => {
putRequest.onerror = e => {
console.log(e);
reject(e);
};
})));
})
);
cursor.continue();
} else {
// no more results
@@ -84,7 +87,7 @@ const migrations = [
});
}
};
request.onerror = (event) => {
request.onerror = event => {
console.log(event);
};
},
@@ -129,7 +132,9 @@ const migrations = [
const messagesStore = transaction.objectStore('messages');
console.log('Create index from attachment schema version to attachment');
messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false });
messagesStore.createIndex('schemaVersion', 'schemaVersion', {
unique: false,
});
const duration = Date.now() - start;

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,
{
const messageWithInitialSchema = Object.assign({}, message, {
schemaVersion: inheritedSchemaVersion,
attachments: message.attachments.map(Attachment.removeSchemaVersion),
}
);
});
return messageWithInitialSchema;
};
@@ -98,7 +92,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
return async (message, context) => {
if (!exports.isValid(message)) {
console.log('Message._withSchemaVersion: Invalid input message:', message);
console.log(
'Message._withSchemaVersion: Invalid input message:',
message
);
return message;
}
@@ -138,15 +135,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
return message;
}
return Object.assign(
{},
upgradedMessage,
{ schemaVersion }
);
return Object.assign({}, upgradedMessage, { schemaVersion });
};
};
// Public API
// _mapAttachments :: (Attachment -> Promise Attachment) ->
// (Message, Context) ->
@@ -154,19 +146,24 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
exports._mapAttachments = upgradeAttachment => async (message, context) => {
const upgradeWithContext = attachment =>
upgradeAttachment(attachment, context);
const attachments = await Promise.all(message.attachments.map(upgradeWithContext));
const attachments = await Promise.all(
message.attachments.map(upgradeWithContext)
);
return Object.assign({}, message, { attachments });
};
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
// (Message, Context) ->
// Promise Message
exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => {
exports._mapQuotedAttachments = upgradeAttachment => async (
message,
context
) => {
if (!message.quote) {
return message;
}
const upgradeWithContext = async (attachment) => {
const upgradeWithContext = async attachment => {
const { thumbnail } = attachment;
if (!thumbnail) {
return attachment;
@@ -185,7 +182,9 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
const quotedAttachments = (message.quote && message.quote.attachments) || [];
const attachments = await Promise.all(quotedAttachments.map(upgradeWithContext));
const attachments = await Promise.all(
quotedAttachments.map(upgradeWithContext)
);
return Object.assign({}, message, {
quote: Object.assign({}, message.quote, {
attachments,
@@ -193,8 +192,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
});
};
const toVersion0 = async message =>
exports.initializeSchemaVersion(message);
const toVersion0 = async message => exports.initializeSchemaVersion(message);
const toVersion1 = exports._withSchemaVersion(
1,
@@ -241,25 +239,28 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
return message;
};
exports.createAttachmentLoader = (loadAttachmentData) => {
exports.createAttachmentLoader = loadAttachmentData => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('`loadAttachmentData` is required');
}
return async message => (Object.assign({}, message, {
attachments: await Promise.all(message.attachments.map(loadAttachmentData)),
}));
return async message =>
Object.assign({}, message, {
attachments: await Promise.all(
message.attachments.map(loadAttachmentData)
),
});
};
// createAttachmentDataWriter :: (RelativePath -> IO Unit)
// Message ->
// IO (Promise Message)
exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
exports.createAttachmentDataWriter = writeExistingAttachmentData => {
if (!isFunction(writeExistingAttachmentData)) {
throw new TypeError("'writeExistingAttachmentData' must be a function");
}
return async (rawMessage) => {
return async rawMessage => {
if (!exports.isValid(rawMessage)) {
throw new TypeError("'rawMessage' is not valid");
}
@@ -282,17 +283,21 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
return message;
}
(attachments || []).forEach((attachment) => {
(attachments || []).forEach(attachment => {
if (!Attachment.hasData(attachment)) {
throw new TypeError("'attachment.data' is required during message import");
throw new TypeError(
"'attachment.data' is required during message import"
);
}
if (!isString(attachment.path)) {
throw new TypeError("'attachment.path' is required during message import");
throw new TypeError(
"'attachment.path' is required during message import"
);
}
});
const writeThumbnails = exports._mapQuotedAttachments(async (thumbnail) => {
const writeThumbnails = exports._mapQuotedAttachments(async thumbnail => {
const { data, path } = thumbnail;
// we want to be bulletproof to thumbnails without data
@@ -315,10 +320,12 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
{},
await writeThumbnails(message),
{
attachments: await Promise.all((attachments || []).map(async (attachment) => {
attachments: await Promise.all(
(attachments || []).map(async attachment => {
await writeExistingAttachmentData(attachment);
return omit(attachment, ['data']);
})),
})
),
}
);

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
const { Settings } = window.Signal.Types;
@@ -11,7 +7,7 @@
OFF: 'off',
COUNT: 'count',
NAME: 'name',
MESSAGE : 'message'
MESSAGE: 'message',
};
Whisper.Notifications = new (Backbone.Collection.extend({
@@ -27,15 +23,18 @@
update: function() {
const { isEnabled } = this;
const isFocused = window.isFocused();
const isAudioNotificationEnabled = storage.get('audio-notification') || false;
const isAudioNotificationEnabled =
storage.get('audio-notification') || false;
const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
const shouldPlayNotificationSound = isAudioNotificationSupported &&
isAudioNotificationEnabled;
const shouldPlayNotificationSound =
isAudioNotificationSupported && isAudioNotificationEnabled;
const numNotifications = this.length;
console.log(
'Update notifications:',
{isFocused, isEnabled, numNotifications, shouldPlayNotificationSound}
);
console.log('Update notifications:', {
isFocused,
isEnabled,
numNotifications,
shouldPlayNotificationSound,
});
if (!isEnabled) {
return;
@@ -69,7 +68,7 @@
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
var newMessageCount = [
numNotifications,
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages')
numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'),
].join(' ');
var last = this.last();
@@ -111,7 +110,10 @@
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

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
Whisper.Registration = {
@@ -15,11 +12,13 @@
return storage.get('chromiumRegistrationDone') === '';
},
everDone: function() {
return storage.get('chromiumRegistrationDoneEver') === '' ||
storage.get('chromiumRegistrationDone') === '';
return (
storage.get('chromiumRegistrationDoneEver') === '' ||
storage.get('chromiumRegistrationDone') === ''
);
},
remove: function() {
storage.remove('chromiumRegistrationDone');
}
},
};
}());
})();

View File

@@ -49,17 +49,26 @@
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, name, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
var ev,
i = -1,
l = events.length,
a1 = args[0],
a2 = args[1],
a3 = args[2];
var logError = function(error) {
console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error);
console.log(
'Model caught error triggering',
name,
'event:',
error && error.stack ? error.stack : error
);
};
switch (args.length) {
case 0:
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx);
}
catch (error) {
} catch (error) {
logError(error);
}
}
@@ -68,8 +77,7 @@
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1);
}
catch (error) {
} catch (error) {
logError(error);
}
}
@@ -78,8 +86,7 @@
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1, a2);
}
catch (error) {
} catch (error) {
logError(error);
}
}
@@ -88,8 +95,7 @@
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
}
catch (error) {
} catch (error) {
logError(error);
}
}
@@ -98,8 +104,7 @@
while (++i < l) {
try {
(ev = events[i]).callback.apply(ev.ctx, args);
}
catch (error) {
} catch (error) {
logError(error);
}
}
@@ -122,10 +127,5 @@
return this;
}
Backbone.Model.prototype.trigger
= Backbone.View.prototype.trigger
= Backbone.Collection.prototype.trigger
= Backbone.Events.trigger
= trigger;
Backbone.Model.prototype.trigger = Backbone.View.prototype.trigger = Backbone.Collection.prototype.trigger = Backbone.Events.trigger = trigger;
})();

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
@@ -17,8 +13,12 @@
function run() {
console.log('Rotating signed prekey...');
getAccountManager().rotateSignedPreKey().catch(function() {
console.log('rotateSignedPrekey() failed. Trying again in five seconds');
getAccountManager()
.rotateSignedPreKey()
.catch(function() {
console.log(
'rotateSignedPrekey() failed. Trying again in five seconds'
);
setTimeout(runWhenOnline, 5000);
});
scheduleNextRotation();
@@ -29,7 +29,9 @@
if (navigator.onLine) {
run();
} else {
console.log('We are offline; keys will be rotated when we are next online');
console.log(
'We are offline; keys will be rotated when we are next online'
);
var listener = function() {
window.removeEventListener('online', listener);
run();
@@ -79,6 +81,6 @@
setTimeoutForNextRun();
}
});
}
},
};
}());
})();

File diff suppressed because it is too large Load Diff

View File

@@ -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,12 +1,9 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
var Item = Backbone.Model.extend({
database: Whisper.Database,
storeName: 'items'
storeName: 'items',
});
var ItemCollection = Backbone.Collection.extend({
model: Item,
@@ -16,14 +13,16 @@
var ready = false;
var items = new ItemCollection();
items.on('reset', function() { ready = true; });
items.on('reset', function() {
ready = true;
});
window.storage = {
/*****************************
*** Base Storage Routines ***
*****************************/
put: function(key, value) {
if (value === undefined) {
throw new Error("Tried to store undefined");
throw new Error('Tried to store undefined');
}
if (!ready) {
console.log('Called storage.put before storage is ready. key:', key);
@@ -35,7 +34,7 @@
},
get: function(key, defaultValue) {
var item = items.get("" + key);
var item = items.get('' + key);
if (!item) {
return defaultValue;
}
@@ -43,7 +42,7 @@
},
remove: function(key) {
var item = items.get("" + key);
var item = items.get('' + key);
if (item) {
items.remove(item);
return new Promise(function(resolve, reject) {
@@ -63,16 +62,23 @@
fetch: function() {
return new Promise((resolve, reject) => {
items.fetch({reset: true})
.fail(() => reject(new Error('Failed to fetch from storage.' +
' This may be due to an unexpected database version.')))
items
.fetch({ reset: true })
.fail(() =>
reject(
new Error(
'Failed to fetch from storage.' +
' This may be due to an unexpected database version.'
)
)
)
.always(resolve);
});
},
reset: function() {
items.reset();
}
},
};
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};

View File

@@ -13,13 +13,14 @@
},
events: {
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
'openInbox': 'openInbox',
openInbox: 'openInbox',
'change-theme': 'applyTheme',
'change-hide-menu': 'applyHideMenu',
},
applyTheme: function() {
var theme = storage.get('theme-setting') || 'android';
this.$el.removeClass('ios')
this.$el
.removeClass('ios')
.removeClass('android-dark')
.removeClass('android')
.addClass(theme);
@@ -30,7 +31,7 @@
window.setMenuBarVisibility(!hideMenuBar);
},
openView: function(view) {
this.el.innerHTML = "";
this.el.innerHTML = '';
this.el.append(view.el);
this.delegateEvents();
},
@@ -48,13 +49,17 @@
openImporter: function() {
window.addSetupMenuItems();
this.resetViews();
var importView = this.importView = new Whisper.ImportView();
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this));
var importView = (this.importView = new Whisper.ImportView());
this.listenTo(
importView,
'light-import',
this.finishLightImport.bind(this)
);
this.openView(this.importView);
},
finishLightImport: function() {
var options = {
hasExistingData: true
hasExistingData: true,
};
this.openInstaller(options);
},
@@ -76,7 +81,7 @@
}
this.resetViews();
var installView = this.installView = new Whisper.InstallView(options);
var installView = (this.installView = new Whisper.InstallView(options));
this.openView(this.installView);
},
closeInstaller: function() {
@@ -130,11 +135,13 @@
this.inboxView = new Whisper.InboxView({
model: self,
window: window,
initialLoadComplete: options.initialLoadComplete
initialLoadComplete: options.initialLoadComplete,
});
return ConversationController.loadPromise().then(function() {
return ConversationController.loadPromise().then(
function() {
this.openView(this.inboxView);
}.bind(this));
}.bind(this)
);
} else {
if (!$.contains(this.el, this.inboxView.el)) {
this.openView(this.inboxView);
@@ -159,9 +166,11 @@
},
openConversation: function(conversation) {
if (conversation) {
this.openInbox().then(function() {
this.openInbox().then(
function() {
this.inboxView.openConversation(null, conversation);
}.bind(this));
}.bind(this)
);
}
},
});

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -10,6 +7,6 @@
templateName: 'attachment-preview',
render_attributes: function() {
return { source: this.src };
}
},
});
})();

View File

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

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -16,13 +13,13 @@
this.message = options.message;
this.callbacks = {
onDismiss: options.onDismiss,
onClick: options.onClick
onClick: options.onClick,
};
this.render();
},
render_attributes: function() {
return {
message: this.message
message: this.message,
};
},
onDismiss: function(e) {
@@ -31,6 +28,6 @@
},
onClick: function() {
this.callbacks.onClick();
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -21,7 +18,7 @@
this.render();
},
events: {
'keyup': 'onKeyup',
keyup: 'onKeyup',
'click .ok': 'ok',
'click .cancel': 'cancel',
},
@@ -30,7 +27,7 @@
message: this.message,
showCancel: !this.hideCancel,
cancel: this.cancelText,
ok: this.okText
ok: this.okText,
};
},
ok: function() {
@@ -52,6 +49,6 @@
},
focusCancel: function() {
this.$('.cancel').focus();
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -12,7 +9,7 @@
className: 'contact',
templateName: 'contact',
events: {
'click': 'showIdentity'
click: 'showIdentity',
},
initialize: function(options) {
this.ourNumber = textsecure.storage.user.getNumber();
@@ -25,7 +22,7 @@
return {
title: i18n('me'),
number: this.model.getNumber(),
avatar: this.model.getAvatar()
avatar: this.model.getAvatar(),
};
}
@@ -36,7 +33,7 @@
avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(),
isVerified: this.model.isVerified(),
verified: i18n('verified')
verified: i18n('verified'),
};
},
showIdentity: function() {
@@ -44,10 +41,10 @@
return;
}
var view = new Whisper.KeyVerificationPanelView({
model: this.model
model: this.model,
});
this.listenBack(view);
}
})
},
}),
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -13,27 +10,43 @@
},
templateName: 'conversation-preview',
events: {
'click': 'select'
click: 'select',
},
initialize: function() {
// auto update
this.listenTo(this.model, 'change', _.debounce(this.render.bind(this), 1000));
this.listenTo(
this.model,
'change',
_.debounce(this.render.bind(this), 1000)
);
this.listenTo(this.model, 'destroy', this.remove); // auto update
this.listenTo(this.model, 'opened', this.markSelected); // auto update
var updateLastMessage = _.debounce(this.model.updateLastMessage.bind(this.model), 1000);
this.listenTo(this.model.messageCollection, 'add remove', updateLastMessage);
var updateLastMessage = _.debounce(
this.model.updateLastMessage.bind(this.model),
1000
);
this.listenTo(
this.model.messageCollection,
'add remove',
updateLastMessage
);
this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed(function() {
extension.windows.onClosed(
function() {
this.stopListening();
}.bind(this));
}.bind(this)
);
this.timeStampView = new Whisper.TimestampView({ brief: true });
this.model.updateLastMessage();
},
markSelected: function() {
this.$el.addClass('selected').siblings('.selected').removeClass('selected');
this.$el
.addClass('selected')
.siblings('.selected')
.removeClass('selected');
},
select: function(e) {
@@ -43,15 +56,19 @@
render: function() {
this.$el.html(
Mustache.render(_.result(this,'template', ''), {
Mustache.render(
_.result(this, 'template', ''),
{
title: this.model.getTitle(),
last_message: this.model.get('lastMessage'),
last_message_timestamp: this.model.get('timestamp'),
number: this.model.getNumber(),
avatar: this.model.getAvatar(),
profileName: this.model.getProfileName(),
unreadCount: this.model.get('unreadCount')
}, this.render_partials())
unreadCount: this.model.get('unreadCount'),
},
this.render_partials()
)
);
this.timeStampView.setElement(this.$('.last-timestamp'));
this.timeStampView.update();
@@ -67,7 +84,6 @@
}
return this;
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -56,6 +53,6 @@
if ($el && $el.length > 0) {
$el.remove();
}
}
},
});
})();

View File

@@ -8,8 +8,7 @@
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

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

View File

@@ -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,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
@@ -11,6 +8,6 @@
templateName: 'generic-error',
render_attributes: function() {
return this.model;
}
},
});
})();

View File

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

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -18,8 +15,8 @@
collection: this.model,
className: 'members',
toInclude: {
listenBack: options.listenBack
}
listenBack: options.listenBack,
},
});
this.member_list_view.render();
@@ -33,8 +30,8 @@
return {
members: i18n('groupMembers'),
summary: summary
summary: summary,
};
}
},
});
})();

View File

@@ -1,14 +1,11 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.GroupUpdateView = Backbone.View.extend({
tagName: "div",
className: "group-update",
tagName: 'div',
className: 'group-update',
render: function() {
//TODO l10n
if (this.model.left) {
@@ -27,7 +24,6 @@
this.$el.text(messages.join(' '));
return this;
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -12,6 +9,6 @@
},
render_attributes: function() {
return { content: this.content };
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -25,7 +22,9 @@
var img = document.createElement('img');
img.onload = function() {
var canvas = loadImage.scale(img, {
canvas: true, maxWidth: 100, maxHeight: 100
canvas: true,
maxWidth: 100,
maxHeight: 100,
});
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
@@ -35,7 +34,7 @@
img.src = svgurl;
});
}
},
});
var COLORS = {
@@ -53,7 +52,6 @@
orange: '#FF9800',
deep_orange: '#FF5722',
amber: '#FFB300',
blue_grey : '#607D8B'
blue_grey: '#607D8B',
};
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -18,11 +15,11 @@
events: {
'click .show-safety-number': 'showSafetyNumber',
'click .send-anyway': 'sendAnyway',
'click .cancel': 'cancel'
'click .cancel': 'cancel',
},
showSafetyNumber: function() {
var view = new Whisper.KeyVerificationPanelView({
model: this.model
model: this.model,
});
this.listenBack(view);
},
@@ -39,13 +36,16 @@
send = i18n('resend');
}
var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]);
var errorExplanation = i18n('identityKeyErrorOnSend', [
this.model.getTitle(),
this.model.getTitle(),
]);
return {
errorExplanation: errorExplanation,
showSafetyNumber: i18n('showSafetyNumber'),
sendAnyway: send,
cancel : i18n('cancel')
cancel: i18n('cancel'),
};
}
},
});
})();

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -34,23 +31,24 @@
this.on('disconnected', this.reconnect);
// Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
this.shouldRetainData =
Whisper.Registration.everDone() || options.hasExistingData;
},
render_attributes: function() {
var errorMessage;
if (this.error) {
if (this.error.name === 'HTTPError'
&& this.error.code == TOO_MANY_DEVICES) {
if (
this.error.name === 'HTTPError' &&
this.error.code == TOO_MANY_DEVICES
) {
errorMessage = i18n('installTooManyDevices');
}
else if (this.error.name === 'HTTPError'
&& this.error.code == CONNECTION_ERROR) {
} else if (
this.error.name === 'HTTPError' &&
this.error.code == CONNECTION_ERROR
) {
errorMessage = i18n('installConnectionFailed');
}
else if (this.error.message === 'websocket closed') {
} else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message
errorMessage = i18n('installConnectionFailed');
@@ -95,10 +93,12 @@
var accountManager = getAccountManager();
accountManager.registerSecondDevice(
accountManager
.registerSecondDevice(
this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this)
).catch(this.handleDisconnect.bind(this));
)
.catch(this.handleDisconnect.bind(this));
},
handleDisconnect: function(e) {
console.log('provisioning failed', e.stack);
@@ -108,9 +108,10 @@
if (e.message === 'websocket closed') {
this.trigger('disconnected');
} else if (e.name !== 'HTTPError'
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
} else if (
e.name !== 'HTTPError' ||
(e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)
) {
throw e;
}
},
@@ -155,8 +156,10 @@
this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault();
return new Promise(function(resolve, reject) {
this.$('#link-phone').submit(function(e) {
return new Promise(
function(resolve, reject) {
this.$('#link-phone').submit(
function(e) {
e.stopPropagation();
e.preventDefault();
@@ -189,8 +192,10 @@
);
finish();
});
}.bind(this));
}.bind(this));
}.bind(this)
);
}.bind(this)
);
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -17,15 +14,15 @@
this.theirKey = options.newKey;
}
this.loadKeys().then(function() {
this.loadKeys().then(
function() {
this.listenTo(this.model, 'change', this.render);
}.bind(this));
}.bind(this)
);
},
loadKeys: function() {
return Promise.all([
this.loadTheirKey(),
this.loadOurKey(),
]).then(this.generateSecurityNumber.bind(this))
return Promise.all([this.loadTheirKey(), this.loadOurKey()])
.then(this.generateSecurityNumber.bind(this))
.then(this.render.bind(this));
//.then(this.makeQRCode.bind(this));
},
@@ -37,32 +34,37 @@
);
},
loadTheirKey: function() {
return textsecure.storage.protocol.loadIdentityKey(
this.model.id
).then(function(theirKey) {
return textsecure.storage.protocol.loadIdentityKey(this.model.id).then(
function(theirKey) {
this.theirKey = theirKey;
}.bind(this));
}.bind(this)
);
},
loadOurKey: function() {
return textsecure.storage.protocol.loadIdentityKey(
this.ourNumber
).then(function(ourKey) {
return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then(
function(ourKey) {
this.ourKey = ourKey;
}.bind(this));
}.bind(this)
);
},
generateSecurityNumber: function() {
return new libsignal.FingerprintGenerator(5200).createFor(
this.ourNumber, this.ourKey, this.model.id, this.theirKey
).then(function(securityNumber) {
return new libsignal.FingerprintGenerator(5200)
.createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey)
.then(
function(securityNumber) {
this.securityNumber = securityNumber;
}.bind(this));
}.bind(this)
);
},
onSafetyNumberChanged: function() {
this.model.getProfiles().then(this.loadKeys.bind(this));
var dialog = new Whisper.ConfirmationDialogView({
message: i18n('changedRightAfterVerify', [this.model.getTitle(), this.model.getTitle()]),
hideCancel: true
message: i18n('changedRightAfterVerify', [
this.model.getTitle(),
this.model.getTitle(),
]),
hideCancel: true,
});
dialog.$el.insertBefore(this.el);
@@ -70,7 +72,10 @@
},
toggleVerified: function() {
this.$('button.verify').attr('disabled', true);
this.model.toggleVerified().catch(function(result) {
this.model
.toggleVerified()
.catch(
function(result) {
if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') {
this.onSafetyNumberChanged();
@@ -89,9 +94,13 @@
});
}
}
}.bind(this)).then(function() {
}.bind(this)
)
.then(
function() {
this.$('button.verify').removeAttr('disabled');
}.bind(this));
}.bind(this)
);
},
render_attributes: function() {
var s = this.securityNumber;
@@ -103,19 +112,24 @@
var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name);
var isVerified = this.model.isVerified();
var verifyButton = isVerified ? i18n('unverify') : i18n('verify');
var verifiedStatus = isVerified ? i18n('isVerified', name) : i18n('isNotVerified', name);
var verifiedStatus = isVerified
? i18n('isVerified', name)
: i18n('isNotVerified', name);
return {
learnMore: i18n('learnMore'),
theirKeyUnknown: i18n('theirIdentityUnknown'),
yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()),
yourSafetyNumberWith: i18n(
'yourSafetyNumberWith',
this.model.getTitle()
),
verifyHelp: i18n('verifyHelp', this.model.getTitle()),
verifyButton: verifyButton,
hasTheirKey: this.theirKey !== undefined,
chunks: chunks,
isVerified: isVerified,
verifiedStatus : verifiedStatus
verifiedStatus: verifiedStatus,
};
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -25,12 +22,14 @@
},
render_attributes: function() {
var unreadMessages = this.count === 1 ? i18n('unreadMessage')
var unreadMessages =
this.count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [this.count]);
return {
unreadMessages: unreadMessages
unreadMessages: unreadMessages,
};
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -35,6 +32,6 @@
render: function() {
this.addAll();
return this;
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -25,14 +22,14 @@
});
},
events: {
'click': 'onClick'
click: 'onClick',
},
onClick: function() {
if (this.outgoingKeyError) {
var view = new Whisper.IdentityKeySendErrorPanelView({
model: this.model,
listenBack: this.listenBack,
resetPanel: this.resetPanel
resetPanel: this.resetPanel,
});
this.listenTo(view, 'send-anyway', this.onSendAnyway);
@@ -44,19 +41,32 @@
}
},
forceSend: function() {
this.model.updateVerified().then(function() {
this.model
.updateVerified()
.then(
function() {
if (this.model.isUnverified()) {
return this.model.setVerifiedDefault();
}
}.bind(this)).then(function() {
}.bind(this)
)
.then(
function() {
return this.model.isUntrusted();
}.bind(this)).then(function(untrusted) {
}.bind(this)
)
.then(
function(untrusted) {
if (untrusted) {
return this.model.setApproved();
}
}.bind(this)).then(function() {
}.bind(this)
)
.then(
function() {
this.message.resend(this.outgoingKeyError.number);
}.bind(this));
}.bind(this)
);
},
onSendAnyway: function() {
if (this.outgoingKeyError) {
@@ -72,9 +82,9 @@
avatar: this.model.getAvatar(),
errors: this.errors,
showErrorButton: showButton,
errorButtonLabel : i18n('view')
errorButtonLabel: i18n('view'),
};
}
},
});
Whisper.MessageDetailView = Whisper.View.extend({
@@ -91,7 +101,7 @@
this.listenTo(this.model, 'change', this.render);
},
events: {
'click button.delete': 'onDelete'
'click button.delete': 'onDelete',
},
onDelete: function() {
var dialog = new Whisper.ConfirmationDialogView({
@@ -100,7 +110,7 @@
resolve: function() {
this.model.destroy();
this.resetPanel();
}.bind(this)
}.bind(this),
});
this.$el.prepend(dialog.el);
@@ -119,9 +129,11 @@
ids = this.conversation.getRecipients();
}
}
return Promise.all(ids.map(function(number) {
return Promise.all(
ids.map(function(number) {
return ConversationController.getOrCreateAndWait(number, 'private');
}));
})
);
},
renderContact: function(contact) {
var view = new ContactView({
@@ -129,18 +141,23 @@
errors: this.grouped[contact.id],
listenBack: this.listenBack,
resetPanel: this.resetPanel,
message: this.model
message: this.model,
}).render();
this.$('.contacts').append(view.el);
},
render: function() {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) {
var errorsWithoutNumber = _.reject(this.model.get('errors'), function(
error
) {
return Boolean(error.number);
});
this.$el.html(Mustache.render(_.result(this, 'template', ''), {
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,
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'),
@@ -148,21 +165,26 @@
received: i18n('received'),
errorLabel: i18n('error'),
deleteLabel: i18n('deleteMessage'),
retryDescription: i18n('retryDescription')
}));
retryDescription: i18n('retryDescription'),
})
);
this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number');
this.getContacts().then(function(contacts) {
_.sortBy(contacts, function(c) {
this.getContacts().then(
function(contacts) {
_.sortBy(
contacts,
function(c) {
var prefix = this.grouped[c.id] ? '0' : '1';
// this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical
return prefix + c.getTitle();
}.bind(this)).forEach(this.renderContact.bind(this));
}.bind(this));
}
}.bind(this)
).forEach(this.renderContact.bind(this));
}.bind(this)
);
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -10,14 +7,17 @@
className: 'message-list',
itemView: Whisper.MessageView,
events: {
'scroll': 'onScroll',
scroll: 'onScroll',
},
initialize: function() {
Whisper.ListView.prototype.initialize.call(this);
this.triggerLazyScroll = _.debounce(function() {
this.triggerLazyScroll = _.debounce(
function() {
this.$el.trigger('lazyScroll');
}.bind(this), 500);
}.bind(this),
500
);
},
onScroll: function() {
this.measureScrollPosition();
@@ -36,7 +36,8 @@
return this.bottomOffset < 30;
},
measureScrollPosition: function() {
if (this.el.scrollHeight === 0) { // hidden
if (this.el.scrollHeight === 0) {
// hidden
return;
}
this.outerHeight = this.$el.outerHeight();

View File

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

@@ -10,9 +10,11 @@
this.$el.hide();
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
extension.windows.onClosed(function () {
extension.windows.onClosed(
function() {
clearInterval(this.renderIntervalHandle);
}.bind(this));
}.bind(this)
);
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
@@ -34,10 +36,13 @@
setSocketReconnectInterval: function(millis) {
this.socketReconnectWaitDuration = moment.duration(millis);
},
navigatorOnLine: function() { return navigator.onLine; },
getSocketStatus: function() { return window.getSocketStatus(); },
navigatorOnLine: function() {
return navigator.onLine;
},
getSocketStatus: function() {
return window.getSocketStatus();
},
getNetworkStatus: function() {
var message = '';
var instructions = '';
var hasInterruption = false;
@@ -65,11 +70,16 @@
break;
}
if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) {
if (
socketStatus == WebSocket.CONNECTING &&
!this.withinConnectingGracePeriod
) {
hasInterruption = true;
}
if (this.socketReconnectWaitDuration.asSeconds() > 0) {
instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]);
instructions = i18n('attemptingReconnection', [
this.socketReconnectWaitDuration.asSeconds(),
]);
}
if (!this.navigatorOnLine()) {
hasInterruption = true;
@@ -88,7 +98,7 @@
instructions: instructions,
hasInterruption: hasInterruption,
action: action,
buttonClass: buttonClass
buttonClass: buttonClass,
};
},
update: function() {
@@ -102,13 +112,9 @@
this.render();
if (this.model.attributes.hasInterruption) {
this.$el.slideDown();
}
else {
} else {
this.$el.hide();
}
}
},
});
})();

View File

@@ -1,34 +1,33 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.NewGroupUpdateView = Whisper.View.extend({
tagName: "div",
tagName: 'div',
className: 'new-group-update',
templateName: 'new-group-update',
initialize: function(options) {
this.render();
this.avatarInput = new Whisper.FileInputView({
el: this.$('.group-avatar'),
window: options.window
window: options.window,
});
this.recipients_view = new Whisper.RecipientsInputView();
this.listenTo(this.recipients_view.typeahead, 'sync', function() {
this.model.contactCollection.models.forEach(function(model) {
this.model.contactCollection.models.forEach(
function(model) {
if (this.recipients_view.typeahead.get(model)) {
this.recipients_view.typeahead.remove(model);
}
}.bind(this));
}.bind(this)
);
});
this.recipients_view.$el.insertBefore(this.$('.container'));
this.member_list_view = new Whisper.ContactListView({
collection: this.model.contactCollection,
className: 'members'
className: 'members',
});
this.member_list_view.render();
this.$('.scrollable').append(this.member_list_view.el);
@@ -51,17 +50,21 @@
render_attributes: function() {
return {
name: this.model.getTitle(),
avatar: this.model.getAvatar()
avatar: this.model.getAvatar(),
};
},
send: function() {
return this.avatarInput.getThumbnail().then(function(avatarFile) {
return this.avatarInput.getThumbnail().then(
function(avatarFile) {
var now = Date.now();
var attrs = {
timestamp: now,
active_at: now,
name: this.$('.name').val(),
members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id'))
members: _.union(
this.model.get('members'),
this.recipients_view.recipients.pluck('id')
),
};
if (avatarFile) {
attrs.avatar = avatarFile;
@@ -76,7 +79,8 @@
this.model.updateGroup(group_update);
this.goBack();
}.bind(this));
}
}.bind(this)
);
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -13,12 +10,14 @@
this.$('input.number').intlTelInput();
},
events: {
'change': 'validateNumber',
'keyup': 'validateNumber'
change: 'validateNumber',
keyup: 'validateNumber',
},
validateNumber: function() {
var input = this.$('input.number');
var regionCode = this.$('li.active').attr('data-country-code').toUpperCase();
var regionCode = this.$('li.active')
.attr('data-country-code')
.toUpperCase();
var number = input.val();
var parsedNumber = libphonenumber.util.parseNumber(number, regionCode);
@@ -31,6 +30,6 @@
input.trigger('validation');
return parsedNumber.e164;
}
},
});
})();

View File

@@ -44,4 +44,4 @@
Backbone.View.prototype.remove.call(this);
},
});
}());
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -10,21 +7,21 @@
'name',
'e164_number',
'national_number',
'international_number'
'international_number',
],
database: Whisper.Database,
storeName: 'conversations',
model: Whisper.Conversation,
fetchContacts: function() {
return this.fetch({ reset: true, conditions: { type: 'private' } });
}
},
});
Whisper.ContactPillView = Whisper.View.extend({
tagName: 'span',
className: 'recipient',
events: {
'click .remove': 'removeModel'
'click .remove': 'removeModel',
},
templateName: 'contact_pill',
initialize: function() {
@@ -39,11 +36,11 @@
},
render_attributes: function() {
return { name: this.model.getTitle() };
}
},
});
Whisper.RecipientListView = Whisper.ListView.extend({
itemView: Whisper.ContactPillView
itemView: Whisper.ContactPillView,
});
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
@@ -52,7 +49,7 @@
});
Whisper.SuggestionListView = Whisper.ConversationListView.extend({
itemView: Whisper.SuggestionView
itemView: Whisper.SuggestionView,
});
Whisper.RecipientsInputView = Whisper.View.extend({
@@ -68,13 +65,13 @@
// Collection of recipients selected for the new message
this.recipients = new Whisper.ConversationCollection([], {
comparator: false
comparator: false,
});
// View to display the selected recipients
this.recipients_view = new Whisper.RecipientListView({
collection: this.recipients,
el: this.$('.recipients')
el: this.$('.recipients'),
});
// Collection of contacts to match user input against
@@ -84,17 +81,18 @@
// 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(); }
})
comparator: function(m) {
return m.getTitle().toLowerCase();
},
}),
});
this.$('.contacts').append(this.typeahead_view.el);
this.initNewContact();
this.listenTo(this.typeahead, 'reset', this.filterContacts);
},
render_attributes: function() {
return { placeholder: this.placeholder || "name or phone number" };
return { placeholder: this.placeholder || 'name or phone number' };
},
events: {
@@ -113,9 +111,7 @@
} else {
this.new_contact_view.$el.hide();
}
this.typeahead_view.collection.reset(
this.typeahead.typeahead(query)
);
this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
} else {
this.resetTypeahead();
}
@@ -131,8 +127,8 @@
el: this.$new_contact,
model: ConversationController.create({
type: 'private',
newContact: true
})
newContact: true,
}),
}).render();
},
@@ -176,10 +172,8 @@
this.typeahead_view.collection.reset([]);
},
maybeNumber: function(number) {
return number.match(/^\+?[0-9]*$/);
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -16,7 +13,7 @@
events: {
'click .close': 'close',
'click .finish': 'finish',
'close': 'close'
close: 'close',
},
updateTime: function() {
var duration = moment.duration(Date.now() - this.startTime, 'ms');
@@ -62,19 +59,23 @@
this.input = this.context.createGain();
this.recorder = new WebAudioRecorder(this.input, {
encoding: 'mp3',
workerDir: 'js/' // must end with slash
workerDir: 'js/', // must end with slash
});
this.recorder.onComplete = this.handleBlob.bind(this);
this.recorder.onError = this.onError;
navigator.webkitGetUserMedia({ audio: true }, function(stream) {
navigator.webkitGetUserMedia(
{ audio: true },
function(stream) {
this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.input);
}.bind(this), this.onError.bind(this));
}.bind(this),
this.onError.bind(this)
);
this.recorder.startRecording();
},
onError: function(error) {
console.log(error.stack);
this.close();
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -32,8 +29,8 @@
return {
cssClass: cssClass,
moreBelow: moreBelow
moreBelow: moreBelow,
};
}
},
});
})();

View File

@@ -20,7 +20,7 @@
this.populate();
},
events: {
'change': 'change'
change: 'change',
},
change: function(e) {
var value = e.target.checked;
@@ -43,7 +43,7 @@
this.populate();
},
events: {
'change': 'change'
change: 'change',
},
change: function(e) {
var value = this.$(e.target).val();
@@ -67,26 +67,26 @@
new RadioButtonGroupView({
el: this.$('.notification-settings'),
defaultValue: 'message',
name: 'notification-setting'
name: 'notification-setting',
});
new RadioButtonGroupView({
el: this.$('.theme-settings'),
defaultValue: 'android',
name: 'theme-setting',
event: 'change-theme'
event: 'change-theme',
});
if (Settings.isAudioNotificationSupported()) {
new CheckboxView({
el: this.$('.audio-notification-setting'),
defaultValue: false,
name: 'audio-notification'
name: 'audio-notification',
});
}
new CheckboxView({
el: this.$('.menu-bar-setting'),
defaultValue: false,
name: 'hide-menu-bar',
event: 'change-hide-menu'
event: 'change-hide-menu',
});
if (textsecure.storage.user.getDeviceId() != '1') {
var syncView = new SyncView().render();
@@ -160,10 +160,7 @@
},
async clearAllData() {
try {
await Promise.all([
Logs.deleteAll(),
Database.drop(),
]);
await Promise.all([Logs.deleteAll(), Database.drop()]);
} catch (error) {
console.log(
'Something went wrong deleting all data:',
@@ -193,7 +190,7 @@
templateName: 'syncSettings',
className: 'syncSettings',
events: {
'click .sync': 'sync'
'click .sync': 'sync',
},
enable: function() {
this.$('.sync').text(i18n('syncNow'));
@@ -223,7 +220,7 @@
syncRequest.addEventListener('success', this.onsuccess.bind(this));
syncRequest.addEventListener('timeout', this.ontimeout.bind(this));
} else {
console.log("Tried to sync from device 1");
console.log('Tried to sync from device 1');
}
},
render_attributes: function() {
@@ -231,7 +228,7 @@
sync: i18n('sync'),
syncNow: i18n('syncNow'),
syncExplanation: i18n('syncExplanation'),
syncFailed: i18n('syncFailed')
syncFailed: i18n('syncFailed'),
};
var date = storage.get('synced_at');
if (date) {
@@ -241,6 +238,6 @@
attrs.syncTime = date.toLocaleTimeString();
}
return attrs;
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -17,7 +14,9 @@
if (number) {
this.$('input.number').val(number);
}
this.phoneView = new Whisper.PhoneInputView({el: this.$('#phone-number-input')});
this.phoneView = new Whisper.PhoneInputView({
el: this.$('#phone-number-input'),
});
this.$('#error').hide();
},
events: {
@@ -29,24 +28,37 @@
},
verifyCode: function(e) {
var number = this.phoneView.validateNumber();
var verificationCode = $('#code').val().replace(/\D+/g, '');
var verificationCode = $('#code')
.val()
.replace(/\D+/g, '');
this.accountManager.registerSingleDevice(number, verificationCode).then(function() {
this.accountManager
.registerSingleDevice(number, verificationCode)
.then(
function() {
this.$el.trigger('openInbox');
}.bind(this)).catch(this.log.bind(this));
}.bind(this)
)
.catch(this.log.bind(this));
},
log: function(s) {
console.log(s);
this.$('#status').text(s);
},
validateCode: function() {
var verificationCode = $('#code').val().replace(/\D/g, '');
var verificationCode = $('#code')
.val()
.replace(/\D/g, '');
if (verificationCode.length == 6) {
return verificationCode;
}
},
displayError: function(error) {
this.$('#error').hide().text(error).addClass('in').fadeIn();
this.$('#error')
.hide()
.text(error)
.addClass('in')
.fadeIn();
},
onValidation: function() {
if (this.$('#number-container').hasClass('valid')) {
@@ -67,8 +79,12 @@
this.$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this));
this.$('#step2').addClass('in').fadeIn();
this.accountManager
.requestVoiceVerification(number)
.catch(this.displayError.bind(this));
this.$('#step2')
.addClass('in')
.fadeIn();
} else {
this.$('#number-container').addClass('invalid');
}
@@ -78,11 +94,15 @@
$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
this.accountManager.requestSMSVerification(number).catch(this.displayError.bind(this));
this.$('#step2').addClass('in').fadeIn();
this.accountManager
.requestSMSVerification(number)
.catch(this.displayError.bind(this));
this.$('#step2')
.addClass('in')
.fadeIn();
} else {
this.$('#number-container').addClass('invalid');
}
}
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -13,7 +10,7 @@
this.clearTimeout();
var millis_now = Date.now();
var millis = this.$el.data('timestamp');
if (millis === "") {
if (millis === '') {
return;
}
if (millis >= millis_now) {
@@ -27,7 +24,9 @@
var millis_since = millis_now - millis;
if (this.delay) {
if (this.delay < 0) { this.delay = 1000; }
if (this.delay < 0) {
this.delay = 1000;
}
this.timeout = setTimeout(this.update.bind(this), this.delay);
}
},
@@ -47,22 +46,34 @@
this.delay = null;
return timestamp.format(this._format.M);
} else if (timediff.days() > 0) {
this.delay = moment(timestamp).add(timediff.days() + 1,'d').diff(now);
this.delay = moment(timestamp)
.add(timediff.days() + 1, 'd')
.diff(now);
return timestamp.format(this._format.d);
} else if (timediff.hours() > 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.hours() === 1) {
this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now);
this.delay = moment(timestamp)
.add(timediff.hours() + 1, 'h')
.diff(now);
return this.relativeTime(timediff.hours(), 'h');
} else if (timediff.minutes() > 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else if (timediff.minutes() === 1) {
this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now);
this.delay = moment(timestamp)
.add(timediff.minutes() + 1, 'm')
.diff(now);
return this.relativeTime(timediff.minutes(), 'm');
} else {
this.delay = moment(timestamp).add(1,'m').diff(now);
this.delay = moment(timestamp)
.add(1, 'm')
.diff(now);
return this.relativeTime(timediff.seconds(), 's');
}
},
@@ -70,19 +81,19 @@
return moment.duration(number, string).humanize();
},
_format: {
y: "ll",
M: i18n('timestampFormat_M') || "MMM D",
d: "ddd"
}
y: 'll',
M: i18n('timestampFormat_M') || 'MMM D',
d: 'ddd',
},
});
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
relativeTime: function(number, string, isFuture) {
return moment.duration(-1 * number, string).humanize(string !== 's');
},
_format: {
y: "lll",
M: (i18n('timestampFormat_M') || "MMM D") + ' LT',
d: "ddd LT"
}
y: 'lll',
M: (i18n('timestampFormat_M') || 'MMM D') + ' LT',
d: 'ddd LT',
},
});
})();

View File

@@ -1,6 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -17,12 +14,14 @@
},
render: function() {
this.$el.html(Mustache.render(
this.$el.html(
Mustache.render(
_.result(this, 'template', ''),
_.result(this, 'render_attributes', '')
));
)
);
this.$el.show();
setTimeout(this.close.bind(this), 2000);
}
},
});
})();

View File

@@ -1,6 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*
* Whisper.View
*
* This is the base for most of our views. The Backbone view is extended
@@ -23,7 +21,8 @@
'use strict';
window.Whisper = window.Whisper || {};
Whisper.View = Backbone.View.extend({
Whisper.View = Backbone.View.extend(
{
constructor: function() {
Backbone.View.apply(this, arguments);
Mustache.parse(_.result(this, 'template'));
@@ -48,24 +47,28 @@
return this;
},
confirm: function(message, okText) {
return new Promise(function(resolve, reject) {
return new Promise(
function(resolve, reject) {
var dialog = new Whisper.ConfirmationDialogView({
message: message,
okText: okText,
resolve: resolve,
reject: reject
reject: reject,
});
this.$el.append(dialog.el);
}.bind(this));
}.bind(this)
);
},
i18n_with_links: function() {
var args = Array.prototype.slice.call(arguments);
for (var i = 1; i < args.length; ++i) {
args[i] = 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
args[i] =
'class="link" href="' + encodeURI(args[i]) + '" target="_blank"';
}
return i18n(args[0], args.slice(1));
}
},{
},
},
{
// Class attributes
Templates: (function() {
var templates = {};
@@ -75,6 +78,7 @@
templates[id] = $el.html();
});
return templates;
}())
});
})(),
}
);
})();

View File

@@ -1,8 +1,4 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function () {
(function() {
'use strict';
window.Whisper = window.Whisper || {};
@@ -11,7 +7,7 @@
var events;
function checkTime() {
var currentTime = Date.now();
if (currentTime > (lastTime + interval * 2)) {
if (currentTime > lastTime + interval * 2) {
events.trigger('timetravel');
}
lastTime = currentTime;
@@ -22,6 +18,6 @@
events = _events;
lastTime = Date.now();
setInterval(checkTime, interval);
}
},
};
}());
})();

View File

@@ -1,8 +1,3 @@
/*
* vim: ts=4:sw=4:expandtab
*/
;(function () {
'use strict';
window.textsecure = window.textsecure || {};

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