Merge branch 'development'

pull/19/head
Scott Nonnenberg 7 years ago
commit bff4b27fa7

@ -12,6 +12,7 @@ test/views/*.js
# Generated files
js/components.js
js/libtextsecure.js
js/util_worker.js
js/libsignal-protocol-worker.js
libtextsecure/components.js
libtextsecure/test/test.js

1
.gitignore vendored

@ -17,6 +17,7 @@ sql/
# generated files
js/components.js
js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js

@ -6,6 +6,7 @@ config/local-*.json
config/local.json
dist/**
js/components.js
js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js

@ -33,6 +33,14 @@ module.exports = grunt => {
src: components,
dest: 'js/components.js',
},
util_worker: {
src: [
'components/bytebuffer/dist/ByteBufferAB.js',
'components/long/dist/Long.js',
'js/util_worker_tasks.js',
],
dest: 'js/util_worker.js',
},
libtextsecurecomponents: {
src: libtextsecurecomponents,
dest: 'libtextsecure/components.js',

File diff suppressed because it is too large Load Diff

@ -11,8 +11,9 @@ module.exports = {
let initialized = false;
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
async function initialize({ configDir }) {
async function initialize({ configDir, cleanupOrphanedAttachments }) {
if (initialized) {
throw new Error('initialze: Already initialized!');
}
@ -29,8 +30,19 @@ async function initialize({ configDir }) {
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`sql-erase error: ${errorForDisplay}`);
console.log(`erase attachments error: ${errorForDisplay}`);
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`, error);
}
});
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
try {
await cleanupOrphanedAttachments();
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`cleanup orphaned attachments error: ${errorForDisplay}`);
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`, error);
}
});
}

@ -0,0 +1,58 @@
const fs = require('fs');
const _ = require('lodash');
const ENCODING = 'utf8';
module.exports = {
start,
};
function start(name, targetPath) {
let cachedValue = null;
try {
const text = fs.readFileSync(targetPath, ENCODING);
cachedValue = JSON.parse(text);
console.log(`config/get: Successfully read ${name} config file`);
if (!cachedValue) {
console.log(
`config/get: ${name} config value was falsy, cache is now empty object`
);
cachedValue = Object.create(null);
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
console.log(
`config/get: Did not find ${name} config file, cache is now empty object`
);
cachedValue = Object.create(null);
}
function get(keyPath) {
return _.get(cachedValue, keyPath);
}
function set(keyPath, value) {
_.set(cachedValue, keyPath, value);
console.log(`config/set: Saving ${name} config to disk`);
const text = JSON.stringify(cachedValue, null, ' ');
fs.writeFileSync(targetPath, text, ENCODING);
}
function remove() {
console.log(`config/remove: Deleting ${name} config from disk`);
fs.unlinkSync(targetPath);
cachedValue = Object.create(null);
}
return {
set,
get,
remove,
};
}

@ -0,0 +1,12 @@
const path = require('path');
const { app } = require('electron');
const { start } = require('./base_config');
const userDataPath = app.getPath('userData');
const targetPath = path.join(userDataPath, 'ephemeral.json');
const ephemeralConfig = start('ephemeral', targetPath);
module.exports = ephemeralConfig;

@ -1,5 +1,7 @@
const electron = require('electron');
const sql = require('./sql');
const { remove: removeUserConfig } = require('./user_config');
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
const { ipcMain } = electron;
@ -12,16 +14,12 @@ let initialized = false;
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';
function initialize({ userConfig }) {
function initialize() {
if (initialized) {
throw new Error('sqlChannels: already initialized!');
}
initialized = true;
if (!userConfig) {
throw new Error('initialize: userConfig is required!');
}
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try {
const fn = sql[callName];
@ -44,7 +42,8 @@ function initialize({ userConfig }) {
ipcMain.on(ERASE_SQL_KEY, async event => {
try {
userConfig.set('key', null);
removeUserConfig();
removeEphemeralConfig();
event.sender.send(`${ERASE_SQL_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;

@ -1,11 +1,11 @@
const path = require('path');
const { app } = require('electron');
const ElectronConfig = require('electron-config');
const { start } = require('./base_config');
const config = require('./config');
// use a separate data directory for development
// Use separate data directory for development
if (config.has('storageProfile')) {
const userData = path.join(
app.getPath('appData'),
@ -17,7 +17,9 @@ if (config.has('storageProfile')) {
console.log(`userData: ${app.getPath('userData')}`);
// this needs to be below our update to the appData path
const userConfig = new ElectronConfig();
const userDataPath = app.getPath('userData');
const targetPath = path.join(userDataPath, 'config.json');
const userConfig = start('user', targetPath);
module.exports = userConfig;

@ -408,6 +408,10 @@
);
window.log.info('Cleanup: complete');
if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments();
}
Views.Initialization.setMessage(window.i18n('loading'));
// Note: We are not invoking the second set of IndexedDB migrations because it is
@ -1105,9 +1109,10 @@
conversationId: data.destination,
type: 'outgoing',
sent: true,
expirationStartTimestamp: data.expirationStartTimestamp
? Math.min(data.expirationStartTimestamp, Date.now())
: null,
expirationStartTimestamp: Math.min(
data.expirationStartTimestamp || data.timestamp || Date.now(),
Date.now()
),
});
}

@ -166,15 +166,18 @@
this.updateLastMessage();
const removeMessage = () => {
const existing = this.messageCollection.get(message.id);
const { id } = message;
const existing = this.messageCollection.get(id);
if (!existing) {
return;
}
window.log.info('Remove expired message from collection', {
sentAt: message.get('sent_at'),
sentAt: existing.get('sent_at'),
});
this.messageCollection.remove(message.id);
this.messageCollection.remove(id);
existing.trigger('expired');
};
// If a fetch is in progress, then we need to wait until that's complete to

@ -83,6 +83,7 @@
this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire);
this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire();
},
idForLogging() {
@ -233,7 +234,9 @@
this.quotedMessage = null;
}
},
onExpired() {
this.hasExpired = true;
},
getPropsForTimerNotification() {
const { expireTimer, fromSync, source } = this.get(
'expirationTimerUpdate'
@ -424,6 +427,7 @@
attachment: this.getPropsForAttachment(firstAttachment),
quote: this.getPropsForQuote(),
authorAvatarPath,
isExpired: this.hasExpired,
expirationLength,
expirationTimestamp,
onReply: () => this.trigger('reply', this),
@ -875,17 +879,33 @@
promises.push(c.getProfiles());
}
} else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
// Note: In a partially-successful group send, we do not start
// the expiration timer.
// In groups, we don't treat unregistered users as a user-visible
// error. The message will look successful, but the details
// screen will show that we didn't send to these unregistered users.
const filteredErrors = _.reject(
result.errors,
error => error.name === 'UnregisteredUserError'
);
// We don't start the expiration timer if there are real errors
// left after filtering out all of the unregistered user errors.
const expirationStartTimestamp = filteredErrors.length
? null
: Date.now();
this.saveErrors(filteredErrors);
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp,
});
promises.push(this.sendSyncMessage());
} else {
this.saveErrors(result.errors);
}
promises = promises.concat(
_.map(result.errors, error => {

@ -22,6 +22,7 @@ const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const _jobs = Object.create(null);
const _DEBUG = false;
@ -64,6 +65,7 @@ module.exports = {
removeAll,
removeOtherData,
cleanupOrphanedAttachments,
// Returning plain JSON
getMessagesNeedingUpgrade,
@ -164,7 +166,7 @@ ipcRenderer.on(
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received job reply to job ${jobId}, but did not have it in our registry!`
`Received SQL channel reply to job ${jobId}, but did not have it in our registry!`
);
}
@ -172,7 +174,9 @@ ipcRenderer.on(
if (errorForDisplay) {
return reject(
new Error(`Error calling channel ${fnName}: ${errorForDisplay}`)
new Error(
`Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
@ -194,7 +198,8 @@ function makeChannel(fnName) {
});
setTimeout(
() => reject(new Error(`Request to ${fnName} timed out`)),
() =>
reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)),
DATABASE_UPDATE_TIMEOUT
);
});
@ -244,8 +249,7 @@ async function removeMessage(id, { Message }) {
// it needs to delete all associated on-disk files along with the database delete.
if (message) {
await channels.removeMessage(id);
const model = new Message(message);
await model.cleanup();
await message.cleanup();
}
}
@ -388,6 +392,10 @@ async function removeAll() {
await channels.removeAll();
}
async function cleanupOrphanedAttachments() {
await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
}
// Note: will need to restart the app after calling this, to set up afresh
async function removeOtherData() {
await Promise.all([

@ -1,6 +1,6 @@
/* global window, IDBKeyRange */
const { includes, isFunction, isString, last, forEach } = require('lodash');
const { includes, isFunction, isString, last, map } = require('lodash');
const {
saveMessages,
_removeMessages,
@ -83,23 +83,25 @@ async function migrateToSQL({
const status = await migrateStoreToSQLite({
db,
save: async array => {
forEach(array, item => {
// In the new database, we can't store ArrayBuffers, so we turn these two fields
// into strings like MessageReceiver now does before save.
await Promise.all(
map(array, async item => {
// In the new database, we can't store ArrayBuffers, so we turn these two
// fields into strings like MessageReceiver now does before save.
// Need to set it to version two, since we're using Base64 strings now
// eslint-disable-next-line no-param-reassign
item.version = 2;
if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// Need to set it to version two, since we're using Base64 strings now
// eslint-disable-next-line no-param-reassign
item.decrypted = arrayBufferToString(item.decrypted);
}
});
item.version = 2;
if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = await arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// eslint-disable-next-line no-param-reassign
item.decrypted = await arrayBufferToString(item.decrypted);
}
})
);
await saveUnprocesseds(array);
},
remove: removeUnprocessed,

@ -0,0 +1,44 @@
/* global dcodeIO */
/* eslint-disable strict */
'use strict';
const functions = {
stringToArrayBufferBase64,
arrayBufferToStringBase64,
};
onmessage = async e => {
const [jobId, fnName, ...args] = e.data;
try {
const fn = functions[fnName];
if (!fn) {
throw new Error(`Worker: job ${jobId} did not find function ${fnName}`);
}
const result = await fn(...args);
postMessage([jobId, null, result]);
} catch (error) {
const errorForDisplay = prepareErrorForPostMessage(error);
postMessage([jobId, errorForDisplay]);
}
};
function prepareErrorForPostMessage(error) {
if (!error) {
return null;
}
if (error.stack) {
return error.stack;
}
return error.message;
}
function stringToArrayBufferBase64(string) {
return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
}
function arrayBufferToStringBase64(arrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
}

@ -34,11 +34,15 @@
this.render();
try {
await Database.clear();
await Database.close();
window.log.info('All database connections closed. Starting delete.');
window.log.info(
'All database connections closed. Starting database drop.'
);
await Database.drop();
} catch (error) {
window.log.error(
'Something went wrong closing all database connections.'
'Something went wrong deleting IndexedDB data then dropping database.'
);
}
@ -46,15 +50,14 @@
},
async clearAllData() {
try {
await Promise.all([
Logs.deleteAll(),
Database.drop(),
window.Signal.Data.removeAll(),
window.Signal.Data.removeOtherData(),
]);
await Logs.deleteAll();
// SQLCipher
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
} catch (error) {
window.log.error(
'Something went wrong deleting all data:',

@ -15,6 +15,7 @@
this.listenTo(this.model, 'change', this.onChange);
this.listenTo(this.model, 'destroy', this.onDestroy);
this.listenTo(this.model, 'unload', this.onUnload);
this.listenTo(this.model, 'expired', this.onExpired);
},
onChange() {
this.addId();
@ -27,6 +28,9 @@
const { id } = this.model;
this.$el.attr('id', id);
},
onExpired() {
setTimeout(() => this.onUnload(), 1000);
},
onUnload() {
if (this.childView) {
this.childView.remove();
@ -93,6 +97,7 @@
};
this.listenTo(this.model, 'change', update);
this.listenTo(this.model, 'expired', update);
this.conversation = this.model.getConversation();
this.listenTo(this.conversation, 'change', update);

@ -9,9 +9,112 @@
/* global _: false */
/* global ContactBuffer: false */
/* global GroupBuffer: false */
/* global Worker: false */
/* eslint-disable more/no-then */
const WORKER_TIMEOUT = 60 * 1000; // one minute
const _utilWorker = new Worker('js/util_worker.js');
const _jobs = Object.create(null);
const _DEBUG = false;
let _jobCounter = 0;
function _makeJob(fnName) {
_jobCounter += 1;
const id = _jobCounter;
if (_DEBUG) {
window.log.info(`Worker job ${id} (${fnName}) started`);
}
_jobs[id] = {
fnName,
start: Date.now(),
};
return id;
}
function _updateJob(id, data) {
const { resolve, reject } = data;
const { fnName, start } = _jobs[id];
_jobs[id] = {
..._jobs[id],
...data,
resolve: value => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
);
return resolve(value);
},
reject: error => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) failed in ${end - start}ms`
);
return reject(error);
},
};
}
function _removeJob(id) {
if (_DEBUG) {
_jobs[id].complete = true;
} else {
delete _jobs[id];
}
}
function _getJob(id) {
return _jobs[id];
}
async function callWorker(fnName, ...args) {
const jobId = _makeJob(fnName);
return new Promise((resolve, reject) => {
_utilWorker.postMessage([jobId, fnName, ...args]);
_updateJob(jobId, {
resolve,
reject,
args: _DEBUG ? args : null,
});
setTimeout(
() => reject(new Error(`Worker job ${jobId} (${fnName}) timed out`)),
WORKER_TIMEOUT
);
});
}
_utilWorker.onmessage = e => {
const [jobId, errorForDisplay, result] = e.data;
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received worker reply to job ${jobId}, but did not have it in our registry!`
);
}
const { resolve, reject, fnName } = job;
if (errorForDisplay) {
return reject(
new Error(
`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
return resolve(result);
};
function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0;
@ -32,13 +135,14 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
}
MessageReceiver.stringToArrayBuffer = string =>
dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
MessageReceiver.arrayBufferToString = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
MessageReceiver.stringToArrayBufferBase64 = string =>
dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
callWorker('stringToArrayBufferBase64', string);
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
callWorker('arrayBufferToStringBase64', arrayBuffer);
MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({
@ -271,13 +375,8 @@ MessageReceiver.prototype.extend({
async queueAllCached() {
const items = await this.getAllFromCache();
for (let i = 0, max = items.length; i < max; i += 1) {
if (i > 0 && i % 20 === 0) {
window.log.info('queueAllCached: Giving event loop a rest');
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, 2000));
}
this.queueCached(items[i]);
// eslint-disable-next-line no-await-in-loop
await this.queueCached(items[i]);
}
},
async queueCached(item) {
@ -285,13 +384,13 @@ MessageReceiver.prototype.extend({
let envelopePlaintext = item.envelope;
if (item.version === 2) {
envelopePlaintext = MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext = await MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext
);
}
if (typeof envelopePlaintext === 'string') {
envelopePlaintext = MessageReceiver.stringToArrayBuffer(
envelopePlaintext = await MessageReceiver.stringToArrayBuffer(
envelopePlaintext
);
}
@ -302,13 +401,13 @@ MessageReceiver.prototype.extend({
let payloadPlaintext = decrypted;
if (item.version === 2) {
payloadPlaintext = MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext = await MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext
);
}
if (typeof payloadPlaintext === 'string') {
payloadPlaintext = MessageReceiver.stringToArrayBuffer(
payloadPlaintext = await MessageReceiver.stringToArrayBuffer(
payloadPlaintext
);
}
@ -375,12 +474,12 @@ MessageReceiver.prototype.extend({
);
});
},
addToCache(envelope, plaintext) {
async addToCache(envelope, plaintext) {
const id = this.getEnvelopeId(envelope);
const data = {
id,
version: 2,
envelope: MessageReceiver.arrayBufferToStringBase64(plaintext),
envelope: await MessageReceiver.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(),
attempts: 1,
};
@ -399,10 +498,13 @@ MessageReceiver.prototype.extend({
if (item.get('version') === 2) {
item.set(
'decrypted',
MessageReceiver.arrayBufferToStringBase64(plaintext)
await MessageReceiver.arrayBufferToStringBase64(plaintext)
);
} else {
item.set('decrypted', MessageReceiver.arrayBufferToString(plaintext));
item.set(
'decrypted',
await MessageReceiver.arrayBufferToString(plaintext)
);
}
return textsecure.storage.unprocessed.save(item.attributes);

@ -10,8 +10,12 @@ const _ = require('lodash');
const pify = require('pify');
const electron = require('electron');
const getRealPath = pify(fs.realpath);
const packageJson = require('./package.json');
const GlobalErrors = require('./app/global_errors');
GlobalErrors.addHandler();
const getRealPath = pify(fs.realpath);
const {
app,
BrowserWindow,
@ -22,26 +26,6 @@ const {
shell,
} = electron;
const packageJson = require('./package.json');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
// const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const GlobalErrors = require('./app/global_errors');
const logging = require('./app/logging');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
GlobalErrors.addHandler();
const appUserModelId = `org.whispersystems.${packageJson.name}`;
console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId,
@ -64,14 +48,32 @@ const usingTrayIcon =
const config = require('./app/config');
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
const importMode =
process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development';
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
// We generally want to pull in our own modules after this point, after the user
// data directory has been set.
const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config');
const logging = require('./app/logging');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
function showWindow() {
if (!mainWindow) {
@ -114,7 +116,14 @@ if (!process.mas) {
}
}
let windowConfig = userConfig.get('window');
const windowFromUserConfig = userConfig.get('window');
const windowFromEphemeral = ephemeralConfig.get('window');
let windowConfig = windowFromEphemeral || windowFromUserConfig;
if (windowFromUserConfig) {
userConfig.set('window', null);
ephemeralConfig.set('window', windowConfig);
}
const loadLocale = require('./app/locale').load;
// Both of these will be set after app fires the 'ready' event
@ -284,7 +293,7 @@ function createWindow() {
'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig)
);
userConfig.set('window', windowConfig);
ephemeralConfig.set('window', windowConfig);
}
const debouncedCaptureStats = _.debounce(captureAndSaveWindowStats, 500);
@ -618,24 +627,33 @@ app.on('ready', async () => {
locale = loadLocale({ appLocale, logger });
}
await attachmentChannel.initialize({ configDir: userDataPath });
let key = userConfig.get('key');
if (!key) {
console.log(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
key = crypto.randomBytes(32).toString('hex');
userConfig.set('key', key);
}
await sql.initialize({ configDir: userDataPath, key });
await sqlChannels.initialize({ userConfig });
// const allAttachments = await attachments.getAllAttachments(userDataPath);
// const orphanedAttachments = await sql.removeKnownAttachments(allAttachments);
// await attachments.deleteAll({
// userDataPath,
// attachments: orphanedAttachments,
// });
await sqlChannels.initialize();
async function cleanupOrphanedAttachments() {
const allAttachments = await attachments.getAllAttachments(userDataPath);
const orphanedAttachments = await sql.removeKnownAttachments(
allAttachments
);
await attachments.deleteAll({
userDataPath,
attachments: orphanedAttachments,
});
}
await attachmentChannel.initialize({
configDir: userDataPath,
cleanupOrphanedAttachments,
});
ready = true;

@ -3,7 +3,7 @@
"productName": "Signal",
"description": "Private messaging from your desktop",
"repository": "https://github.com/signalapp/Signal-Desktop.git",
"version": "1.15.5",
"version": "1.16.0-beta.1",
"license": "GPL-3.0",
"author": {
"name": "Open Whisper Systems",
@ -40,6 +40,7 @@
"styleguide": "styleguidist server"
},
"dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"@sindresorhus/is": "^0.8.0",
"archiver": "^2.1.1",
"backbone": "^1.3.3",
@ -49,10 +50,8 @@
"bunyan": "^1.8.12",
"classnames": "^2.2.5",
"config": "^1.28.1",
"electron-config": "^1.0.0",
"electron-editor-context-menu": "^1.1.1",
"electron-is-dev": "^0.3.0",
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"electron-unhandled": "https://github.com/scottnonnenberg-signal/electron-unhandled.git#7496187472aa561d39fcd4c843a54ffbef0a388c",
"electron-updater": "^2.21.10",
"emoji-datasource": "4.0.0",
@ -110,7 +109,7 @@
"asar": "^0.14.0",
"bower": "^1.8.2",
"chai": "^4.1.2",
"electron": "2.0.1",
"electron": "2.0.8",
"electron-builder": "^20.13.5",
"electron-icon-maker": "0.0.3",
"eslint": "^4.14.0",

@ -89,14 +89,14 @@
<hr>
<div class='spell-check-setting'>
<h3>{{ spellCheckHeader }}</h3>
<input type='checkbox' name='spell-check-setting' />
<input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
<label for='spell-check-setting'>{{ spellCheckDescription }}</label>
</div>
<hr>
<div class='permissions-setting'>
<h3>{{ permissions }}</h3>
<div class='media-permissions'>
<input type='checkbox' name='media-permissions' />
<input type='checkbox' name='media-permissions' id='media-permissions' />
<label for='media-permissions'>{{ mediaPermissionsDescription }}</label>
</div>
</div>

@ -125,6 +125,14 @@
color: $color-light-90;
}
.module-expire-timer {
background-color: $color-white-07;
}
.module-expire-timer--incoming {
background-color: $color-light-45;
}
.module-quote--incoming {
background-color: $color-signal-blue-025;
border-left-color: $color-signal-blue;
@ -286,6 +294,10 @@
color: $color-white;
}
.module-expire-timer--incoming {
background-color: $color-white-07;
}
.module-quote__primary__author {
color: $color-light-90;
}

@ -1,13 +1,14 @@
### Countdown at different rates
```jsx
<util.ConversationContext theme={util.theme}>
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="cyan"
direction="incoming"
text="10 second timer"
i18n={util.i18n}
timestamp={Date.now() + 10 * 1000}
expirationLength={10 * 1000}
expirationTimestamp={Date.now() + 10 * 1000}
/>
@ -18,6 +19,7 @@
authorColor="cyan"
text="30 second timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={30 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -28,6 +30,7 @@
direction="incoming"
text="1 minute timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -38,6 +41,7 @@
direction="incoming"
text="5 minute timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 60 * 1000}
expirationLength={5 * 60 * 1000}
expirationTimestamp={Date.now() + 5 * 60 * 1000}
/>
@ -48,13 +52,14 @@
### Timer calculations
```jsx
<util.ConversationContext theme={util.theme}>
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="cyan"
direction="incoming"
text="Full timer"
i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000}
/>
@ -65,6 +70,7 @@
status="delivered"
text="Full timer"
i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000}
/>
@ -75,6 +81,7 @@
direction="incoming"
text="55 timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -85,6 +92,7 @@
status="delivered"
text="55 timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -95,6 +103,7 @@
direction="incoming"
text="30 timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -105,6 +114,7 @@
status="delivered"
text="30 timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -115,6 +125,7 @@
direction="incoming"
text="5 timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000}
/>
@ -125,6 +136,7 @@
status="delivered"
text="5 timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000}
/>
@ -135,6 +147,7 @@
direction="incoming"
text="Expired timer"
i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000}
expirationTimestamp={Date.now()}
/>
@ -145,6 +158,7 @@
status="delivered"
text="Expired timer"
i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000}
expirationTimestamp={Date.now()}
/>
@ -155,6 +169,7 @@
direction="incoming"
text="Expiration is too far away"
i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000}
/>
@ -165,6 +180,7 @@
status="delivered"
text="Expiration is too far away"
i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000}
/>
@ -175,6 +191,7 @@
direction="incoming"
text="Already expired"
i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000}
/>
@ -185,6 +202,7 @@
status="delivered"
text="Already expired"
i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000}
/>

@ -81,6 +81,7 @@ export interface Props {
referencedMessageNotFound: boolean;
};
authorAvatarPath?: string;
isExpired: boolean;
expirationLength?: number;
expirationTimestamp?: number;
onClickAttachment?: () => void;
@ -211,15 +212,22 @@ export class Message extends React.Component<Props, State> {
}
}
public componentDidUpdate() {
this.checkExpired();
}
public checkExpired() {
const now = Date.now();
const { expirationTimestamp, expirationLength } = this.props;
const { isExpired, expirationTimestamp, expirationLength } = this.props;
if (!expirationTimestamp || !expirationLength) {
return;
}
if (this.expiredTimeout) {
return;
}
if (now >= expirationTimestamp) {
if (isExpired || now >= expirationTimestamp) {
this.setState({
expiring: true,
});

@ -1647,15 +1647,6 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
conf@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/conf/-/conf-1.1.1.tgz#238d0a3090ac4916ed2d40c7e81d7a11667bc7ba"
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
make-dir "^1.0.0"
pkg-up "^2.0.0"
config@^1.28.1:
version "1.28.1"
resolved "https://registry.yarnpkg.com/config/-/config-1.28.1.tgz#7625d2a1e4c90f131d8a73347982d93c3873282d"
@ -2423,12 +2414,6 @@ electron-chromedriver@~1.8.0:
electron-download "^4.1.0"
extract-zip "^1.6.5"
electron-config@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-config/-/electron-config-1.0.0.tgz#069d044cc794f04784ae72f12916725d3c8c39af"
dependencies:
conf "^1.0.0"
electron-download-tf@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165"
@ -2538,9 +2523,9 @@ electron-updater@^2.21.10:
semver "^5.5.0"
source-map-support "^0.5.5"
electron@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.1.tgz#d9defcc187862143b9027378be78490eddbfabf4"
electron@2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.8.tgz#6ec7113b356e09cc9899797e0d41ebff8163e962"
dependencies:
"@types/node" "^8.0.24"
electron-download "^3.0.1"
@ -6428,12 +6413,6 @@ pkg-dir@^2.0.0:
dependencies:
find-up "^2.1.0"
pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies:
find-up "^2.1.0"
pkginfo@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65"

Loading…
Cancel
Save