Move message and conversation storage to IndexedDB
Getting up and running with IndexedDB was pretty easy, thanks to backbone. The tricky part was making reads and writes asynchronous. In that process I did some refactoring on Whisper.Threads, which has been renamed Conversations for consistency with the view names. This change also adds the unlimitedStorage permission.
This commit is contained in:
parent
5638b20046
commit
ced295a630
24 changed files with 1663 additions and 324 deletions
|
@ -16,6 +16,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" src="js/components.js"></script>
|
||||
<script type="text/javascript" src="js/database.js"></script>
|
||||
|
||||
<script type="text/javascript" src="js/protobufs.js"></script>
|
||||
<script type="text/javascript" src="js/curve25519_compiled.js"></script>
|
||||
|
@ -29,7 +30,7 @@
|
|||
<script type="text/javascript" src="js/crypto.js"></script>
|
||||
<script type="text/javascript" src="js/protocol.js"></script>
|
||||
<script type="text/javascript" src="js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="js/models/threads.js"></script>
|
||||
<script type="text/javascript" src="js/models/conversations.js"></script>
|
||||
<script type="text/javascript" src="js/api.js"></script>
|
||||
<script type="text/javascript" src="js/sendmessage.js"></script>
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"cryptojs": "svn+http://crypto-js.googlecode.com/svn/#~3.1.2",
|
||||
"libphonenumber-api": "git://github.com/codedust/libphonenumber-api",
|
||||
"backbone.localstorage": "liliakai/Backbone.localStorage#master",
|
||||
"momentjs": "~2.8.3"
|
||||
"momentjs": "~2.8.3",
|
||||
"indexeddb-backbonejs-adapter": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "~2.0.1",
|
||||
|
@ -79,6 +80,9 @@
|
|||
],
|
||||
"momentjs": [
|
||||
"moment.js"
|
||||
],
|
||||
"indexeddb-backbonejs-adapter": [
|
||||
"backbone-indexeddb.js"
|
||||
]
|
||||
},
|
||||
"concat": {
|
||||
|
@ -90,7 +94,7 @@
|
|||
"mustache",
|
||||
"underscore",
|
||||
"backbone",
|
||||
"backbone.localstorage",
|
||||
"indexeddb-backbonejs-adapter",
|
||||
"qrcode",
|
||||
"libphonenumber-api",
|
||||
"momentjs",
|
||||
|
|
625
components/indexeddb-backbonejs-adapter/backbone-indexeddb.js
Normal file
625
components/indexeddb-backbonejs-adapter/backbone-indexeddb.js
Normal file
|
@ -0,0 +1,625 @@
|
|||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['backbone', 'underscore'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory(require('backbone'), require('underscore'));
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.returnExports = factory(root.Backbone, root._);
|
||||
}
|
||||
}(this, function (Backbone, _) {
|
||||
|
||||
// Generate four random hex digits.
|
||||
function S4() {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
}
|
||||
|
||||
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
||||
function guid() {
|
||||
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
|
||||
}
|
||||
|
||||
if ( _(indexedDB).isUndefined() ) { return; }
|
||||
|
||||
// Driver object
|
||||
// That's the interesting part.
|
||||
// There is a driver for each schema provided. The schema is a te combination of name (for the database), a version as well as migrations to reach that
|
||||
// version of the database.
|
||||
function Driver(schema, ready, nolog, onerror) {
|
||||
this.schema = schema;
|
||||
this.ready = ready;
|
||||
this.error = null;
|
||||
this.transactions = []; // Used to list all transactions and keep track of active ones.
|
||||
this.db = null;
|
||||
this.nolog = nolog;
|
||||
this.onerror = onerror;
|
||||
var lastMigrationPathVersion = _.last(this.schema.migrations).version;
|
||||
if (!this.nolog) debugLog("opening database " + this.schema.id + " in version #" + lastMigrationPathVersion);
|
||||
this.dbRequest = indexedDB.open(this.schema.id,lastMigrationPathVersion); //schema version need to be an unsigned long
|
||||
|
||||
this.launchMigrationPath = function(dbVersion) {
|
||||
var transaction = this.dbRequest.transaction;
|
||||
var clonedMigrations = _.clone(schema.migrations);
|
||||
this.migrate(transaction, clonedMigrations, dbVersion, {
|
||||
error: function (event) {
|
||||
this.error = "Database not up to date. " + dbVersion + " expected was " + lastMigrationPathVersion;
|
||||
}.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
this.dbRequest.onblocked = function(event){
|
||||
if (!this.nolog) debugLog("connection to database blocked");
|
||||
}
|
||||
|
||||
this.dbRequest.onsuccess = function (e) {
|
||||
this.db = e.target.result; // Attach the connection ot the queue.
|
||||
var currentIntDBVersion = (parseInt(this.db.version) || 0); // we need convert beacuse chrome store in integer and ie10 DP4+ in int;
|
||||
var lastMigrationInt = (parseInt(lastMigrationPathVersion) || 0); // And make sure we compare numbers with numbers.
|
||||
|
||||
if (currentIntDBVersion === lastMigrationInt) { //if support new event onupgradeneeded will trigger the ready function
|
||||
// No migration to perform!
|
||||
this.ready();
|
||||
} else if (currentIntDBVersion < lastMigrationInt ) {
|
||||
// We need to migrate up to the current migration defined in the database
|
||||
this.launchMigrationPath(currentIntDBVersion);
|
||||
} else {
|
||||
// Looks like the IndexedDB is at a higher version than the current driver schema.
|
||||
this.error = "Database version is greater than current code " + currentIntDBVersion + " expected was " + lastMigrationInt;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
|
||||
|
||||
this.dbRequest.onerror = function (e) {
|
||||
// Failed to open the database
|
||||
this.error = "Couldn't not connect to the database"
|
||||
if (!this.nolog) debugLog("Couldn't not connect to the database");
|
||||
this.onerror();
|
||||
}.bind(this);
|
||||
|
||||
this.dbRequest.onabort = function (e) {
|
||||
// Failed to open the database
|
||||
this.error = "Connection to the database aborted"
|
||||
if (!this.nolog) debugLog("Connection to the database aborted");
|
||||
this.onerror();
|
||||
}.bind(this);
|
||||
|
||||
|
||||
|
||||
this.dbRequest.onupgradeneeded = function(iDBVersionChangeEvent){
|
||||
this.db =iDBVersionChangeEvent.target.result;
|
||||
|
||||
var newVersion = iDBVersionChangeEvent.newVersion;
|
||||
var oldVersion = iDBVersionChangeEvent.oldVersion;
|
||||
|
||||
// Fix Safari 8 and iOS 8 bug
|
||||
// at the first connection oldVersion is equal to 9223372036854776000
|
||||
// but the real value is 0
|
||||
if (oldVersion > 99999999999)
|
||||
oldVersion = 0;
|
||||
|
||||
if (!this.nolog) debugLog("onupgradeneeded = " + oldVersion + " => " + newVersion);
|
||||
this.launchMigrationPath(oldVersion);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
function debugLog(str) {
|
||||
if (typeof window !== "undefined" && typeof window.console !== "undefined" && typeof window.console.log !== "undefined") {
|
||||
window.console.log(str);
|
||||
}
|
||||
else if(console.log !== "undefined") {
|
||||
console.log(str)
|
||||
}
|
||||
}
|
||||
|
||||
// Driver Prototype
|
||||
Driver.prototype = {
|
||||
|
||||
// Tracks transactions. Mostly for debugging purposes. TO-IMPROVE
|
||||
_track_transaction: function(transaction) {
|
||||
this.transactions.push(transaction);
|
||||
function removeIt() {
|
||||
var idx = this.transactions.indexOf(transaction);
|
||||
if (idx !== -1) {this.transactions.splice(idx); }
|
||||
};
|
||||
transaction.oncomplete = removeIt.bind(this);
|
||||
transaction.onabort = removeIt.bind(this);
|
||||
transaction.onerror = removeIt.bind(this);
|
||||
},
|
||||
|
||||
// Performs all the migrations to reach the right version of the database.
|
||||
migrate: function (transaction, migrations, version, options) {
|
||||
transaction.onerror = options.error;
|
||||
transaction.onabort = options.error;
|
||||
|
||||
if (!this.nolog) debugLog("migrate begin version from #" + version);
|
||||
var that = this;
|
||||
var migration = migrations.shift();
|
||||
if (migration) {
|
||||
if (!version || version < migration.version) {
|
||||
// We need to apply this migration-
|
||||
if (typeof migration.before == "undefined") {
|
||||
migration.before = function (next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
if (typeof migration.after == "undefined") {
|
||||
migration.after = function (next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
// First, let's run the before script
|
||||
if (!this.nolog) debugLog("migrate begin before version #" + migration.version);
|
||||
migration.before(function () {
|
||||
if (!this.nolog) debugLog("migrate done before version #" + migration.version);
|
||||
|
||||
if (!this.nolog) debugLog("migrate begin migrate version #" + migration.version);
|
||||
|
||||
migration.migrate(transaction, function () {
|
||||
if (!this.nolog) debugLog("migrate done migrate version #" + migration.version);
|
||||
// Migration successfully appliedn let's go to the next one!
|
||||
if (!this.nolog) debugLog("migrate begin after version #" + migration.version);
|
||||
migration.after(function () {
|
||||
if (!this.nolog) debugLog("migrate done after version #" + migration.version);
|
||||
if (!this.nolog) debugLog("Migrated to " + migration.version);
|
||||
|
||||
//last modification occurred, need finish
|
||||
if(migrations.length ==0) {
|
||||
if (!this.nolog) {
|
||||
debugLog("migrate setting transaction.oncomplete to finish version #" + migration.version);
|
||||
transaction.oncomplete = function() {
|
||||
debugLog("migrate done transaction.oncomplete version #" + migration.version);
|
||||
debugLog("Done migrating");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.nolog) debugLog("migrate end from version #" + version + " to " + migration.version);
|
||||
that.migrate(transaction, migrations, version, options);
|
||||
}
|
||||
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
} else {
|
||||
// No need to apply this migration
|
||||
if (!this.nolog) debugLog("Skipping migration " + migration.version);
|
||||
this.migrate(transaction, migrations, version, options);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This is the main method, called by the ExecutionQueue when the driver is ready (database open and migration performed)
|
||||
execute: function (storeName, method, object, options) {
|
||||
if (!this.nolog) debugLog("execute : " + method + " on " + storeName + " for " + object.id);
|
||||
switch (method) {
|
||||
case "create":
|
||||
this.create(storeName, object, options);
|
||||
break;
|
||||
case "read":
|
||||
if (object.id || object.cid) {
|
||||
this.read(storeName, object, options); // It's a model
|
||||
} else {
|
||||
this.query(storeName, object, options); // It's a collection
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
this.update(storeName, object, options); // We may want to check that this is not a collection. TOFIX
|
||||
break;
|
||||
case "delete":
|
||||
if (object.id || object.cid) {
|
||||
this.delete(storeName, object, options);
|
||||
} else {
|
||||
this.clear(storeName, object, options);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Hum what?
|
||||
}
|
||||
},
|
||||
|
||||
// Writes the json to the storeName in db. It is a create operations, which means it will fail if the key already exists
|
||||
// options are just success and error callbacks.
|
||||
create: function (storeName, object, options) {
|
||||
var writeTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(writeTransaction);
|
||||
var store = writeTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
var writeRequest;
|
||||
|
||||
if (json[idAttribute] === undefined && !store.autoIncrement) json[idAttribute] = guid();
|
||||
|
||||
writeTransaction.onerror = function (e) {
|
||||
options.error(e);
|
||||
};
|
||||
writeTransaction.oncomplete = function (e) {
|
||||
options.success(json);
|
||||
};
|
||||
|
||||
if (!store.keyPath)
|
||||
writeRequest = store.add(json, json[idAttribute]);
|
||||
else
|
||||
writeRequest = store.add(json);
|
||||
},
|
||||
|
||||
// Writes the json to the storeName in db. It is an update operation, which means it will overwrite the value if the key already exist
|
||||
// options are just success and error callbacks.
|
||||
update: function (storeName, object, options) {
|
||||
var writeTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(writeTransaction);
|
||||
var store = writeTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
var writeRequest;
|
||||
|
||||
if (!json[idAttribute]) json[idAttribute] = guid();
|
||||
|
||||
if (!store.keyPath)
|
||||
writeRequest = store.put(json, json[idAttribute]);
|
||||
else
|
||||
writeRequest = store.put(json);
|
||||
|
||||
writeRequest.onerror = function (e) {
|
||||
options.error(e);
|
||||
};
|
||||
writeTransaction.oncomplete = function (e) {
|
||||
options.success(json);
|
||||
};
|
||||
},
|
||||
|
||||
// Reads from storeName in db with json.id if it's there of with any json.xxxx as long as xxx is an index in storeName
|
||||
read: function (storeName, object, options) {
|
||||
var readTransaction = this.db.transaction([storeName], "readonly");
|
||||
this._track_transaction(readTransaction);
|
||||
|
||||
var store = readTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
|
||||
var getRequest = null;
|
||||
if (json[idAttribute]) {
|
||||
getRequest = store.get(json[idAttribute]);
|
||||
} else if(options.index) {
|
||||
var index = store.index(options.index.name);
|
||||
getRequest = index.get(options.index.value);
|
||||
} else {
|
||||
// We need to find which index we have
|
||||
var cardinality = 0; // try to fit the index with most matches
|
||||
_.each(store.indexNames, function (key, index) {
|
||||
index = store.index(key);
|
||||
if(typeof index.keyPath === 'string' && 1 > cardinality) {
|
||||
// simple index
|
||||
if (json[index.keyPath] !== undefined) {
|
||||
getRequest = index.get(json[index.keyPath]);
|
||||
cardinality = 1;
|
||||
}
|
||||
} else if(typeof index.keyPath === 'object' && index.keyPath.length > cardinality) {
|
||||
// compound index
|
||||
var valid = true;
|
||||
var keyValue = _.map(index.keyPath, function(keyPart) {
|
||||
valid = valid && json[keyPart] !== undefined;
|
||||
return json[keyPart];
|
||||
});
|
||||
if(valid) {
|
||||
getRequest = index.get(keyValue);
|
||||
cardinality = index.keyPath.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (getRequest) {
|
||||
getRequest.onsuccess = function (event) {
|
||||
if (event.target.result) {
|
||||
options.success(event.target.result);
|
||||
} else {
|
||||
options.error("Not Found");
|
||||
}
|
||||
};
|
||||
getRequest.onerror = function () {
|
||||
options.error("Not Found"); // We couldn't find the record.
|
||||
}
|
||||
} else {
|
||||
options.error("Not Found"); // We couldn't even look for it, as we don't have enough data.
|
||||
}
|
||||
},
|
||||
|
||||
// Deletes the json.id key and value in storeName from db.
|
||||
delete: function (storeName, object, options) {
|
||||
var deleteTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(deleteTransaction);
|
||||
|
||||
var store = deleteTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
|
||||
var deleteRequest = store.delete(json[idAttribute]);
|
||||
|
||||
deleteTransaction.oncomplete = function (event) {
|
||||
options.success(null);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
options.error("Not Deleted");
|
||||
};
|
||||
},
|
||||
|
||||
// Clears all records for storeName from db.
|
||||
clear: function (storeName, object, options) {
|
||||
var deleteTransaction = this.db.transaction([storeName], "readwrite");
|
||||
//this._track_transaction(deleteTransaction);
|
||||
|
||||
var store = deleteTransaction.objectStore(storeName);
|
||||
|
||||
var deleteRequest = store.clear();
|
||||
deleteRequest.onsuccess = function (event) {
|
||||
options.success(null);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
options.error("Not Cleared");
|
||||
};
|
||||
},
|
||||
|
||||
// Performs a query on storeName in db.
|
||||
// options may include :
|
||||
// - conditions : value of an index, or range for an index
|
||||
// - range : range for the primary key
|
||||
// - limit : max number of elements to be yielded
|
||||
// - offset : skipped items.
|
||||
query: function (storeName, collection, options) {
|
||||
var elements = [];
|
||||
var skipped = 0, processed = 0;
|
||||
var queryTransaction = this.db.transaction([storeName], "readonly");
|
||||
//this._track_transaction(queryTransaction);
|
||||
|
||||
var idAttribute = _.result(collection.model.prototype, 'idAttribute');
|
||||
var readCursor = null;
|
||||
var store = queryTransaction.objectStore(storeName);
|
||||
var index = null,
|
||||
lower = null,
|
||||
upper = null,
|
||||
bounds = null;
|
||||
|
||||
if (options.conditions) {
|
||||
// We have a condition, we need to use it for the cursor
|
||||
_.each(store.indexNames, function (key) {
|
||||
if (!readCursor) {
|
||||
index = store.index(key);
|
||||
if (options.conditions[index.keyPath] instanceof Array) {
|
||||
lower = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][1] : options.conditions[index.keyPath][0];
|
||||
upper = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][0] : options.conditions[index.keyPath][1];
|
||||
bounds = IDBKeyRange.bound(lower, upper, true, true);
|
||||
|
||||
if (options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1]) {
|
||||
// Looks like we want the DESC order
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.PREV || "prev");
|
||||
} else {
|
||||
// We want ASC order
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
}
|
||||
} else if (typeof options.conditions[index.keyPath] === 'object' && ('$gt' in options.conditions[index.keyPath] || '$gte' in options.conditions[index.keyPath])) {
|
||||
if('$gt' in options.conditions[index.keyPath])
|
||||
bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gt'], true);
|
||||
else
|
||||
bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gte']);
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
} else if (typeof options.conditions[index.keyPath] === 'object' && ('$lt' in options.conditions[index.keyPath] || '$lte' in options.conditions[index.keyPath])) {
|
||||
if('$lt' in options.conditions[index.keyPath])
|
||||
bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lt'], true);
|
||||
else
|
||||
bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lte']);
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
} else if (options.conditions[index.keyPath] != undefined) {
|
||||
bounds = IDBKeyRange.only(options.conditions[index.keyPath]);
|
||||
readCursor = index.openCursor(bounds);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No conditions, use the index
|
||||
if (options.range) {
|
||||
lower = options.range[0] > options.range[1] ? options.range[1] : options.range[0];
|
||||
upper = options.range[0] > options.range[1] ? options.range[0] : options.range[1];
|
||||
bounds = IDBKeyRange.bound(lower, upper);
|
||||
if (options.range[0] > options.range[1]) {
|
||||
readCursor = store.openCursor(bounds, window.IDBCursor.PREV || "prev");
|
||||
} else {
|
||||
readCursor = store.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
}
|
||||
} else {
|
||||
readCursor = store.openCursor();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof (readCursor) == "undefined" || !readCursor) {
|
||||
options.error("No Cursor");
|
||||
} else {
|
||||
readCursor.onerror = function(e){
|
||||
options.error("readCursor error", e);
|
||||
};
|
||||
// Setup a handler for the cursor’s `success` event:
|
||||
readCursor.onsuccess = function (e) {
|
||||
var cursor = e.target.result;
|
||||
if (!cursor) {
|
||||
if (options.addIndividually || options.clear) {
|
||||
// nothing!
|
||||
// We need to indicate that we're done. But, how?
|
||||
collection.trigger("reset");
|
||||
} else {
|
||||
options.success(elements); // We're done. No more elements.
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Cursor is not over yet.
|
||||
if (options.limit && processed >= options.limit) {
|
||||
// Yet, we have processed enough elements. So, let's just skip.
|
||||
if (bounds && options.conditions[index.keyPath]) {
|
||||
cursor.continue(options.conditions[index.keyPath][1] + 1); /* We need to 'terminate' the cursor cleany, by moving to the end */
|
||||
} else {
|
||||
cursor.continue(); /* We need to 'terminate' the cursor cleany, by moving to the end */
|
||||
}
|
||||
}
|
||||
else if (options.offset && options.offset > skipped) {
|
||||
skipped++;
|
||||
cursor.continue(); /* We need to Moving the cursor forward */
|
||||
} else {
|
||||
// This time, it looks like it's good!
|
||||
if (options.addIndividually) {
|
||||
collection.add(cursor.value);
|
||||
} else if (options.clear) {
|
||||
var deleteRequest = store.delete(cursor.value[idAttribute]);
|
||||
deleteRequest.onsuccess = function (event) {
|
||||
elements.push(cursor.value);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
elements.push(cursor.value);
|
||||
};
|
||||
|
||||
} else {
|
||||
elements.push(cursor.value);
|
||||
}
|
||||
processed++;
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
close :function(){
|
||||
if(this.db){
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ExecutionQueue object
|
||||
// The execution queue is an abstraction to buffer up requests to the database.
|
||||
// It holds a "driver". When the driver is ready, it just fires up the queue and executes in sync.
|
||||
function ExecutionQueue(schema,next,nolog) {
|
||||
this.driver = new Driver(schema, this.ready.bind(this), nolog, this.error.bind(this));
|
||||
this.started = false;
|
||||
this.failed = false;
|
||||
this.stack = [];
|
||||
this.version = _.last(schema.migrations).version;
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
// ExecutionQueue Prototype
|
||||
ExecutionQueue.prototype = {
|
||||
// Called when the driver is ready
|
||||
// It just loops over the elements in the queue and executes them.
|
||||
ready: function () {
|
||||
this.started = true;
|
||||
_.each(this.stack, function (message) {
|
||||
this.execute(message);
|
||||
}.bind(this));
|
||||
this.stack = []; // fix memory leak
|
||||
this.next();
|
||||
},
|
||||
|
||||
error: function() {
|
||||
this.failed = true;
|
||||
_.each(this.stack, function (message) {
|
||||
this.execute(message);
|
||||
}.bind(this));
|
||||
this.stack = [];
|
||||
this.next();
|
||||
},
|
||||
|
||||
// Executes a given command on the driver. If not started, just stacks up one more element.
|
||||
execute: function (message) {
|
||||
if (this.started) {
|
||||
this.driver.execute(message[2].storeName || message[1].storeName, message[0], message[1], message[2]); // Upon messages, we execute the query
|
||||
} else if (this.failed) {
|
||||
message[2].error();
|
||||
} else {
|
||||
this.stack.push(message);
|
||||
}
|
||||
},
|
||||
|
||||
close : function(){
|
||||
this.driver.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Method used by Backbone for sync of data with data store. It was initially designed to work with "server side" APIs, This wrapper makes
|
||||
// it work with the local indexedDB stuff. It uses the schema attribute provided by the object.
|
||||
// The wrapper keeps an active Executuon Queue for each "schema", and executes querues agains it, based on the object type (collection or
|
||||
// single model), but also the method... etc.
|
||||
// Keeps track of the connections
|
||||
var Databases = {};
|
||||
|
||||
function sync(method, object, options) {
|
||||
|
||||
if(method == "closeall"){
|
||||
_.each(Databases,function(database){
|
||||
database.close();
|
||||
});
|
||||
// Clean up active databases object.
|
||||
Databases = {};
|
||||
return Backbone.$.Deferred().resolve();
|
||||
}
|
||||
|
||||
// If a model or a collection does not define a database, fall back on ajaxSync
|
||||
if (!object || !_.isObject(object.database)) {
|
||||
return Backbone.ajaxSync(method, object, options);
|
||||
}
|
||||
|
||||
var schema = object.database;
|
||||
if (Databases[schema.id]) {
|
||||
if(Databases[schema.id].version != _.last(schema.migrations).version){
|
||||
Databases[schema.id].close();
|
||||
delete Databases[schema.id];
|
||||
}
|
||||
}
|
||||
|
||||
var promise;
|
||||
|
||||
if (typeof Backbone.$ === 'undefined' || typeof Backbone.$.Deferred === 'undefined') {
|
||||
var noop = function() {};
|
||||
var resolve = noop;
|
||||
var reject = noop;
|
||||
} else {
|
||||
var dfd = Backbone.$.Deferred();
|
||||
var resolve = dfd.resolve;
|
||||
var reject = dfd.reject;
|
||||
|
||||
promise = dfd.promise();
|
||||
}
|
||||
|
||||
var success = options.success;
|
||||
options.success = function(resp) {
|
||||
if (success) success(resp);
|
||||
resolve();
|
||||
object.trigger('sync', object, resp, options);
|
||||
};
|
||||
|
||||
var error = options.error;
|
||||
options.error = function(resp) {
|
||||
if (error) error(resp);
|
||||
reject();
|
||||
object.trigger('error', object, resp, options);
|
||||
};
|
||||
|
||||
var next = function(){
|
||||
Databases[schema.id].execute([method, object, options]);
|
||||
};
|
||||
|
||||
if (!Databases[schema.id]) {
|
||||
Databases[schema.id] = new ExecutionQueue(schema,next,schema.nolog);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
Backbone.ajaxSync = Backbone.sync;
|
||||
Backbone.sync = sync;
|
||||
|
||||
return { sync: sync, debugLog: debugLog};
|
||||
}));
|
|
@ -126,6 +126,7 @@
|
|||
</form>
|
||||
</script>
|
||||
<script type="text/javascript" src="js/components.js"></script>
|
||||
<script type="text/javascript" src="js/database.js"></script>
|
||||
|
||||
<script type="text/javascript" src="components/bootstrap-tagsinput/dist/bootstrap-tagsinput.js"></script>
|
||||
|
||||
|
@ -143,7 +144,7 @@
|
|||
<script type="text/javascript" src="js/protocol.js"></script>
|
||||
|
||||
<script type="text/javascript" src="js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="js/models/threads.js"></script>
|
||||
<script type="text/javascript" src="js/models/conversations.js"></script>
|
||||
<script type="text/javascript" src="js/api.js"></script>
|
||||
<script type="text/javascript" src="js/sendmessage.js"></script>
|
||||
|
||||
|
|
|
@ -23,8 +23,9 @@
|
|||
extension.navigator.tabs.create("options.html");
|
||||
} else {
|
||||
if (textsecure.registration.isDone()) {
|
||||
var conversations = new Whisper.ConversationCollection();
|
||||
textsecure.subscribeToPush(function(message) {
|
||||
Whisper.Threads.addIncomingMessage(message);
|
||||
conversations.addIncomingMessage(message);
|
||||
console.log("Got message from " + message.pushMessage.source + "." + message.pushMessage.sourceDevice +
|
||||
': "' + getString(message.message.body) + '"');
|
||||
var newUnreadCount = textsecure.storage.getUnencrypted("unreadCount", 0) + 1;
|
||||
|
|
847
js/components.js
847
js/components.js
|
@ -21356,267 +21356,630 @@ return jQuery;
|
|||
|
||||
}));
|
||||
|
||||
/**
|
||||
* Backbone localStorage Adapter
|
||||
* Version 1.1.14
|
||||
*
|
||||
* https://github.com/jeromegn/Backbone.localStorage
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof exports === 'object' && typeof require === 'function') {
|
||||
module.exports = factory(require("backbone"));
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(["backbone"], function(Backbone) {
|
||||
// Use global variables if the locals are undefined.
|
||||
return factory(Backbone || root.Backbone);
|
||||
});
|
||||
} else {
|
||||
factory(Backbone);
|
||||
}
|
||||
}(this, function(Backbone) {
|
||||
// A simple module to replace `Backbone.sync` with *localStorage*-based
|
||||
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
|
||||
// as that.
|
||||
|
||||
// Generate four random hex digits.
|
||||
function S4() {
|
||||
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
|
||||
};
|
||||
|
||||
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
||||
function guid() {
|
||||
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
|
||||
};
|
||||
|
||||
function isObject(item) {
|
||||
return item === Object(item);
|
||||
}
|
||||
|
||||
function contains(array, item) {
|
||||
var i = array.length;
|
||||
while (i--) if (array[i] === item) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function extend(obj, props) {
|
||||
for (var key in props) obj[key] = props[key]
|
||||
return obj;
|
||||
}
|
||||
|
||||
function result(object, property) {
|
||||
if (object == null) return void 0;
|
||||
var value = object[property];
|
||||
return (typeof value === 'function') ? object[property]() : value;
|
||||
}
|
||||
|
||||
// Our Store is represented by a single JS object in *localStorage*. Create it
|
||||
// with a meaningful name, like the name you'd give a table.
|
||||
// window.Store is deprectated, use Backbone.LocalStorage instead
|
||||
Backbone.LocalStorage = window.Store = function(name, serializer) {
|
||||
if( !this.localStorage ) {
|
||||
throw "Backbone.localStorage: Environment does not support localStorage."
|
||||
}
|
||||
this.name = name;
|
||||
this.serializer = serializer || {
|
||||
serialize: function(item) {
|
||||
return isObject(item) ? JSON.stringify(item) : item;
|
||||
},
|
||||
// fix for "illegal access" error on Android when JSON.parse is passed null
|
||||
deserialize: function (data) {
|
||||
return data && JSON.parse(data);
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['backbone', 'underscore'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory(require('backbone'), require('underscore'));
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.returnExports = factory(root.Backbone, root._);
|
||||
}
|
||||
};
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = (store && store.split(",")) || [];
|
||||
};
|
||||
}(this, function (Backbone, _) {
|
||||
|
||||
extend(Backbone.LocalStorage.prototype, {
|
||||
|
||||
// Save the current state of the **Store** to *localStorage*.
|
||||
save: function() {
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = _.union(this.records, store && store.split(","));
|
||||
this.localStorage().setItem(this.name, this.records.join(","));
|
||||
},
|
||||
|
||||
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
|
||||
// have an id of it's own.
|
||||
create: function(model) {
|
||||
if (!model.id && model.id !== 0) {
|
||||
model.id = guid();
|
||||
model.set(model.idAttribute, model.id);
|
||||
}
|
||||
this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
|
||||
this.records.push(model.id.toString());
|
||||
this.save();
|
||||
return this.find(model);
|
||||
},
|
||||
|
||||
// Update a model by replacing its copy in `this.data`.
|
||||
update: function(model) {
|
||||
this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
|
||||
var modelId = model.id.toString();
|
||||
if (!contains(this.records, modelId)) {
|
||||
this.records.push(modelId);
|
||||
this.save();
|
||||
}
|
||||
return this.find(model);
|
||||
},
|
||||
|
||||
// Retrieve a model from `this.data` by id.
|
||||
find: function(model) {
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = (store && store.split(",")) || [];
|
||||
return this.serializer.deserialize(this.localStorage().getItem(this._itemName(model.id)));
|
||||
},
|
||||
|
||||
// Return the array of all models currently in storage.
|
||||
findAll: function() {
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = (store && store.split(",")) || [];
|
||||
var result = [];
|
||||
for (var i = 0, id, data; i < this.records.length; i++) {
|
||||
id = this.records[i];
|
||||
data = this.serializer.deserialize(this.localStorage().getItem(this._itemName(id)));
|
||||
if (data != null) result.push(data);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// Delete a model from `this.data`, returning it.
|
||||
destroy: function(model) {
|
||||
this.localStorage().removeItem(this._itemName(model.id));
|
||||
var modelId = model.id.toString();
|
||||
for (var i = 0, id; i < this.records.length; i++) {
|
||||
if (this.records[i] === modelId) {
|
||||
this.records.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.save();
|
||||
return model;
|
||||
},
|
||||
|
||||
localStorage: function() {
|
||||
return localStorage;
|
||||
},
|
||||
|
||||
// Clear localStorage for specific collection.
|
||||
_clear: function() {
|
||||
var local = this.localStorage(),
|
||||
itemRe = new RegExp("^" + this.name + "-");
|
||||
|
||||
// Remove id-tracking item (e.g., "foo").
|
||||
local.removeItem(this.name);
|
||||
|
||||
// Match all data items (e.g., "foo-ID") and remove.
|
||||
for (var k in local) {
|
||||
if (itemRe.test(k)) {
|
||||
local.removeItem(k);
|
||||
}
|
||||
// Generate four random hex digits.
|
||||
function S4() {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
}
|
||||
|
||||
this.records.length = 0;
|
||||
},
|
||||
|
||||
// Size of localStorage.
|
||||
_storageSize: function() {
|
||||
return this.localStorage().length;
|
||||
},
|
||||
|
||||
_itemName: function(id) {
|
||||
return this.name+"-"+id;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// localSync delegate to the model or collection's
|
||||
// *localStorage* property, which should be an instance of `Store`.
|
||||
// window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead
|
||||
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
|
||||
var store = result(model, 'localStorage') || result(model.collection, 'localStorage');
|
||||
|
||||
var resp, errorMessage;
|
||||
//If $ is having Deferred - use it.
|
||||
var syncDfd = Backbone.$ ?
|
||||
(Backbone.$.Deferred && Backbone.$.Deferred()) :
|
||||
(Backbone.Deferred && Backbone.Deferred());
|
||||
|
||||
try {
|
||||
|
||||
switch (method) {
|
||||
case "read":
|
||||
resp = model.id != undefined ? store.find(model) : store.findAll();
|
||||
break;
|
||||
case "create":
|
||||
resp = store.create(model);
|
||||
break;
|
||||
case "update":
|
||||
resp = store.update(model);
|
||||
break;
|
||||
case "delete":
|
||||
resp = store.destroy(model);
|
||||
break;
|
||||
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
||||
function guid() {
|
||||
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
if (error.code === 22 && store._storageSize() === 0)
|
||||
errorMessage = "Private browsing is unsupported";
|
||||
else
|
||||
errorMessage = error.message;
|
||||
}
|
||||
if ( _(indexedDB).isUndefined() ) { return; }
|
||||
|
||||
if (resp) {
|
||||
if (options && options.success) {
|
||||
if (Backbone.VERSION === "0.9.10") {
|
||||
options.success(model, resp, options);
|
||||
} else {
|
||||
options.success(resp);
|
||||
}
|
||||
}
|
||||
if (syncDfd) {
|
||||
syncDfd.resolve(resp);
|
||||
// Driver object
|
||||
// That's the interesting part.
|
||||
// There is a driver for each schema provided. The schema is a te combination of name (for the database), a version as well as migrations to reach that
|
||||
// version of the database.
|
||||
function Driver(schema, ready, nolog, onerror) {
|
||||
this.schema = schema;
|
||||
this.ready = ready;
|
||||
this.error = null;
|
||||
this.transactions = []; // Used to list all transactions and keep track of active ones.
|
||||
this.db = null;
|
||||
this.nolog = nolog;
|
||||
this.onerror = onerror;
|
||||
var lastMigrationPathVersion = _.last(this.schema.migrations).version;
|
||||
if (!this.nolog) debugLog("opening database " + this.schema.id + " in version #" + lastMigrationPathVersion);
|
||||
this.dbRequest = indexedDB.open(this.schema.id,lastMigrationPathVersion); //schema version need to be an unsigned long
|
||||
|
||||
this.launchMigrationPath = function(dbVersion) {
|
||||
var transaction = this.dbRequest.transaction;
|
||||
var clonedMigrations = _.clone(schema.migrations);
|
||||
this.migrate(transaction, clonedMigrations, dbVersion, {
|
||||
error: function (event) {
|
||||
this.error = "Database not up to date. " + dbVersion + " expected was " + lastMigrationPathVersion;
|
||||
}.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
this.dbRequest.onblocked = function(event){
|
||||
if (!this.nolog) debugLog("connection to database blocked");
|
||||
}
|
||||
|
||||
this.dbRequest.onsuccess = function (e) {
|
||||
this.db = e.target.result; // Attach the connection ot the queue.
|
||||
var currentIntDBVersion = (parseInt(this.db.version) || 0); // we need convert beacuse chrome store in integer and ie10 DP4+ in int;
|
||||
var lastMigrationInt = (parseInt(lastMigrationPathVersion) || 0); // And make sure we compare numbers with numbers.
|
||||
|
||||
if (currentIntDBVersion === lastMigrationInt) { //if support new event onupgradeneeded will trigger the ready function
|
||||
// No migration to perform!
|
||||
this.ready();
|
||||
} else if (currentIntDBVersion < lastMigrationInt ) {
|
||||
// We need to migrate up to the current migration defined in the database
|
||||
this.launchMigrationPath(currentIntDBVersion);
|
||||
} else {
|
||||
// Looks like the IndexedDB is at a higher version than the current driver schema.
|
||||
this.error = "Database version is greater than current code " + currentIntDBVersion + " expected was " + lastMigrationInt;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
|
||||
|
||||
this.dbRequest.onerror = function (e) {
|
||||
// Failed to open the database
|
||||
this.error = "Couldn't not connect to the database"
|
||||
if (!this.nolog) debugLog("Couldn't not connect to the database");
|
||||
this.onerror();
|
||||
}.bind(this);
|
||||
|
||||
this.dbRequest.onabort = function (e) {
|
||||
// Failed to open the database
|
||||
this.error = "Connection to the database aborted"
|
||||
if (!this.nolog) debugLog("Connection to the database aborted");
|
||||
this.onerror();
|
||||
}.bind(this);
|
||||
|
||||
|
||||
|
||||
this.dbRequest.onupgradeneeded = function(iDBVersionChangeEvent){
|
||||
this.db =iDBVersionChangeEvent.target.result;
|
||||
|
||||
var newVersion = iDBVersionChangeEvent.newVersion;
|
||||
var oldVersion = iDBVersionChangeEvent.oldVersion;
|
||||
|
||||
// Fix Safari 8 and iOS 8 bug
|
||||
// at the first connection oldVersion is equal to 9223372036854776000
|
||||
// but the real value is 0
|
||||
if (oldVersion > 99999999999)
|
||||
oldVersion = 0;
|
||||
|
||||
if (!this.nolog) debugLog("onupgradeneeded = " + oldVersion + " => " + newVersion);
|
||||
this.launchMigrationPath(oldVersion);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
} else {
|
||||
errorMessage = errorMessage ? errorMessage
|
||||
: "Record Not Found";
|
||||
function debugLog(str) {
|
||||
if (typeof window !== "undefined" && typeof window.console !== "undefined" && typeof window.console.log !== "undefined") {
|
||||
window.console.log(str);
|
||||
}
|
||||
else if(console.log !== "undefined") {
|
||||
console.log(str)
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.error)
|
||||
if (Backbone.VERSION === "0.9.10") {
|
||||
options.error(model, errorMessage, options);
|
||||
} else {
|
||||
options.error(errorMessage);
|
||||
}
|
||||
// Driver Prototype
|
||||
Driver.prototype = {
|
||||
|
||||
if (syncDfd)
|
||||
syncDfd.reject(errorMessage);
|
||||
}
|
||||
// Tracks transactions. Mostly for debugging purposes. TO-IMPROVE
|
||||
_track_transaction: function(transaction) {
|
||||
this.transactions.push(transaction);
|
||||
function removeIt() {
|
||||
var idx = this.transactions.indexOf(transaction);
|
||||
if (idx !== -1) {this.transactions.splice(idx); }
|
||||
};
|
||||
transaction.oncomplete = removeIt.bind(this);
|
||||
transaction.onabort = removeIt.bind(this);
|
||||
transaction.onerror = removeIt.bind(this);
|
||||
},
|
||||
|
||||
// add compatibility with $.ajax
|
||||
// always execute callback for success and error
|
||||
if (options && options.complete) options.complete(resp);
|
||||
// Performs all the migrations to reach the right version of the database.
|
||||
migrate: function (transaction, migrations, version, options) {
|
||||
transaction.onerror = options.error;
|
||||
transaction.onabort = options.error;
|
||||
|
||||
return syncDfd && syncDfd.promise();
|
||||
};
|
||||
if (!this.nolog) debugLog("migrate begin version from #" + version);
|
||||
var that = this;
|
||||
var migration = migrations.shift();
|
||||
if (migration) {
|
||||
if (!version || version < migration.version) {
|
||||
// We need to apply this migration-
|
||||
if (typeof migration.before == "undefined") {
|
||||
migration.before = function (next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
if (typeof migration.after == "undefined") {
|
||||
migration.after = function (next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
// First, let's run the before script
|
||||
if (!this.nolog) debugLog("migrate begin before version #" + migration.version);
|
||||
migration.before(function () {
|
||||
if (!this.nolog) debugLog("migrate done before version #" + migration.version);
|
||||
|
||||
Backbone.ajaxSync = Backbone.sync;
|
||||
if (!this.nolog) debugLog("migrate begin migrate version #" + migration.version);
|
||||
|
||||
Backbone.getSyncMethod = function(model) {
|
||||
if(model.localStorage || (model.collection && model.collection.localStorage)) {
|
||||
return Backbone.localSync;
|
||||
}
|
||||
migration.migrate(transaction, function () {
|
||||
if (!this.nolog) debugLog("migrate done migrate version #" + migration.version);
|
||||
// Migration successfully appliedn let's go to the next one!
|
||||
if (!this.nolog) debugLog("migrate begin after version #" + migration.version);
|
||||
migration.after(function () {
|
||||
if (!this.nolog) debugLog("migrate done after version #" + migration.version);
|
||||
if (!this.nolog) debugLog("Migrated to " + migration.version);
|
||||
|
||||
return Backbone.ajaxSync;
|
||||
};
|
||||
//last modification occurred, need finish
|
||||
if(migrations.length ==0) {
|
||||
if (!this.nolog) {
|
||||
debugLog("migrate setting transaction.oncomplete to finish version #" + migration.version);
|
||||
transaction.oncomplete = function() {
|
||||
debugLog("migrate done transaction.oncomplete version #" + migration.version);
|
||||
debugLog("Done migrating");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.nolog) debugLog("migrate end from version #" + version + " to " + migration.version);
|
||||
that.migrate(transaction, migrations, version, options);
|
||||
}
|
||||
|
||||
// Override 'Backbone.sync' to default to localSync,
|
||||
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
|
||||
Backbone.sync = function(method, model, options) {
|
||||
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
|
||||
};
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
} else {
|
||||
// No need to apply this migration
|
||||
if (!this.nolog) debugLog("Skipping migration " + migration.version);
|
||||
this.migrate(transaction, migrations, version, options);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return Backbone.LocalStorage;
|
||||
// This is the main method, called by the ExecutionQueue when the driver is ready (database open and migration performed)
|
||||
execute: function (storeName, method, object, options) {
|
||||
if (!this.nolog) debugLog("execute : " + method + " on " + storeName + " for " + object.id);
|
||||
switch (method) {
|
||||
case "create":
|
||||
this.create(storeName, object, options);
|
||||
break;
|
||||
case "read":
|
||||
if (object.id || object.cid) {
|
||||
this.read(storeName, object, options); // It's a model
|
||||
} else {
|
||||
this.query(storeName, object, options); // It's a collection
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
this.update(storeName, object, options); // We may want to check that this is not a collection. TOFIX
|
||||
break;
|
||||
case "delete":
|
||||
if (object.id || object.cid) {
|
||||
this.delete(storeName, object, options);
|
||||
} else {
|
||||
this.clear(storeName, object, options);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Hum what?
|
||||
}
|
||||
},
|
||||
|
||||
// Writes the json to the storeName in db. It is a create operations, which means it will fail if the key already exists
|
||||
// options are just success and error callbacks.
|
||||
create: function (storeName, object, options) {
|
||||
var writeTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(writeTransaction);
|
||||
var store = writeTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
var writeRequest;
|
||||
|
||||
if (json[idAttribute] === undefined && !store.autoIncrement) json[idAttribute] = guid();
|
||||
|
||||
writeTransaction.onerror = function (e) {
|
||||
options.error(e);
|
||||
};
|
||||
writeTransaction.oncomplete = function (e) {
|
||||
options.success(json);
|
||||
};
|
||||
|
||||
if (!store.keyPath)
|
||||
writeRequest = store.add(json, json[idAttribute]);
|
||||
else
|
||||
writeRequest = store.add(json);
|
||||
},
|
||||
|
||||
// Writes the json to the storeName in db. It is an update operation, which means it will overwrite the value if the key already exist
|
||||
// options are just success and error callbacks.
|
||||
update: function (storeName, object, options) {
|
||||
var writeTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(writeTransaction);
|
||||
var store = writeTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
var writeRequest;
|
||||
|
||||
if (!json[idAttribute]) json[idAttribute] = guid();
|
||||
|
||||
if (!store.keyPath)
|
||||
writeRequest = store.put(json, json[idAttribute]);
|
||||
else
|
||||
writeRequest = store.put(json);
|
||||
|
||||
writeRequest.onerror = function (e) {
|
||||
options.error(e);
|
||||
};
|
||||
writeTransaction.oncomplete = function (e) {
|
||||
options.success(json);
|
||||
};
|
||||
},
|
||||
|
||||
// Reads from storeName in db with json.id if it's there of with any json.xxxx as long as xxx is an index in storeName
|
||||
read: function (storeName, object, options) {
|
||||
var readTransaction = this.db.transaction([storeName], "readonly");
|
||||
this._track_transaction(readTransaction);
|
||||
|
||||
var store = readTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
|
||||
var getRequest = null;
|
||||
if (json[idAttribute]) {
|
||||
getRequest = store.get(json[idAttribute]);
|
||||
} else if(options.index) {
|
||||
var index = store.index(options.index.name);
|
||||
getRequest = index.get(options.index.value);
|
||||
} else {
|
||||
// We need to find which index we have
|
||||
var cardinality = 0; // try to fit the index with most matches
|
||||
_.each(store.indexNames, function (key, index) {
|
||||
index = store.index(key);
|
||||
if(typeof index.keyPath === 'string' && 1 > cardinality) {
|
||||
// simple index
|
||||
if (json[index.keyPath] !== undefined) {
|
||||
getRequest = index.get(json[index.keyPath]);
|
||||
cardinality = 1;
|
||||
}
|
||||
} else if(typeof index.keyPath === 'object' && index.keyPath.length > cardinality) {
|
||||
// compound index
|
||||
var valid = true;
|
||||
var keyValue = _.map(index.keyPath, function(keyPart) {
|
||||
valid = valid && json[keyPart] !== undefined;
|
||||
return json[keyPart];
|
||||
});
|
||||
if(valid) {
|
||||
getRequest = index.get(keyValue);
|
||||
cardinality = index.keyPath.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (getRequest) {
|
||||
getRequest.onsuccess = function (event) {
|
||||
if (event.target.result) {
|
||||
options.success(event.target.result);
|
||||
} else {
|
||||
options.error("Not Found");
|
||||
}
|
||||
};
|
||||
getRequest.onerror = function () {
|
||||
options.error("Not Found"); // We couldn't find the record.
|
||||
}
|
||||
} else {
|
||||
options.error("Not Found"); // We couldn't even look for it, as we don't have enough data.
|
||||
}
|
||||
},
|
||||
|
||||
// Deletes the json.id key and value in storeName from db.
|
||||
delete: function (storeName, object, options) {
|
||||
var deleteTransaction = this.db.transaction([storeName], 'readwrite');
|
||||
//this._track_transaction(deleteTransaction);
|
||||
|
||||
var store = deleteTransaction.objectStore(storeName);
|
||||
var json = object.toJSON();
|
||||
var idAttribute = _.result(object, 'idAttribute');
|
||||
|
||||
var deleteRequest = store.delete(json[idAttribute]);
|
||||
|
||||
deleteTransaction.oncomplete = function (event) {
|
||||
options.success(null);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
options.error("Not Deleted");
|
||||
};
|
||||
},
|
||||
|
||||
// Clears all records for storeName from db.
|
||||
clear: function (storeName, object, options) {
|
||||
var deleteTransaction = this.db.transaction([storeName], "readwrite");
|
||||
//this._track_transaction(deleteTransaction);
|
||||
|
||||
var store = deleteTransaction.objectStore(storeName);
|
||||
|
||||
var deleteRequest = store.clear();
|
||||
deleteRequest.onsuccess = function (event) {
|
||||
options.success(null);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
options.error("Not Cleared");
|
||||
};
|
||||
},
|
||||
|
||||
// Performs a query on storeName in db.
|
||||
// options may include :
|
||||
// - conditions : value of an index, or range for an index
|
||||
// - range : range for the primary key
|
||||
// - limit : max number of elements to be yielded
|
||||
// - offset : skipped items.
|
||||
query: function (storeName, collection, options) {
|
||||
var elements = [];
|
||||
var skipped = 0, processed = 0;
|
||||
var queryTransaction = this.db.transaction([storeName], "readonly");
|
||||
//this._track_transaction(queryTransaction);
|
||||
|
||||
var idAttribute = _.result(collection.model.prototype, 'idAttribute');
|
||||
var readCursor = null;
|
||||
var store = queryTransaction.objectStore(storeName);
|
||||
var index = null,
|
||||
lower = null,
|
||||
upper = null,
|
||||
bounds = null;
|
||||
|
||||
if (options.conditions) {
|
||||
// We have a condition, we need to use it for the cursor
|
||||
_.each(store.indexNames, function (key) {
|
||||
if (!readCursor) {
|
||||
index = store.index(key);
|
||||
if (options.conditions[index.keyPath] instanceof Array) {
|
||||
lower = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][1] : options.conditions[index.keyPath][0];
|
||||
upper = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][0] : options.conditions[index.keyPath][1];
|
||||
bounds = IDBKeyRange.bound(lower, upper, true, true);
|
||||
|
||||
if (options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1]) {
|
||||
// Looks like we want the DESC order
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.PREV || "prev");
|
||||
} else {
|
||||
// We want ASC order
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
}
|
||||
} else if (typeof options.conditions[index.keyPath] === 'object' && ('$gt' in options.conditions[index.keyPath] || '$gte' in options.conditions[index.keyPath])) {
|
||||
if('$gt' in options.conditions[index.keyPath])
|
||||
bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gt'], true);
|
||||
else
|
||||
bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gte']);
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
} else if (typeof options.conditions[index.keyPath] === 'object' && ('$lt' in options.conditions[index.keyPath] || '$lte' in options.conditions[index.keyPath])) {
|
||||
if('$lt' in options.conditions[index.keyPath])
|
||||
bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lt'], true);
|
||||
else
|
||||
bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lte']);
|
||||
readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
} else if (options.conditions[index.keyPath] != undefined) {
|
||||
bounds = IDBKeyRange.only(options.conditions[index.keyPath]);
|
||||
readCursor = index.openCursor(bounds);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No conditions, use the index
|
||||
if (options.range) {
|
||||
lower = options.range[0] > options.range[1] ? options.range[1] : options.range[0];
|
||||
upper = options.range[0] > options.range[1] ? options.range[0] : options.range[1];
|
||||
bounds = IDBKeyRange.bound(lower, upper);
|
||||
if (options.range[0] > options.range[1]) {
|
||||
readCursor = store.openCursor(bounds, window.IDBCursor.PREV || "prev");
|
||||
} else {
|
||||
readCursor = store.openCursor(bounds, window.IDBCursor.NEXT || "next");
|
||||
}
|
||||
} else {
|
||||
readCursor = store.openCursor();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof (readCursor) == "undefined" || !readCursor) {
|
||||
options.error("No Cursor");
|
||||
} else {
|
||||
readCursor.onerror = function(e){
|
||||
options.error("readCursor error", e);
|
||||
};
|
||||
// Setup a handler for the cursor’s `success` event:
|
||||
readCursor.onsuccess = function (e) {
|
||||
var cursor = e.target.result;
|
||||
if (!cursor) {
|
||||
if (options.addIndividually || options.clear) {
|
||||
// nothing!
|
||||
// We need to indicate that we're done. But, how?
|
||||
collection.trigger("reset");
|
||||
} else {
|
||||
options.success(elements); // We're done. No more elements.
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Cursor is not over yet.
|
||||
if (options.limit && processed >= options.limit) {
|
||||
// Yet, we have processed enough elements. So, let's just skip.
|
||||
if (bounds && options.conditions[index.keyPath]) {
|
||||
cursor.continue(options.conditions[index.keyPath][1] + 1); /* We need to 'terminate' the cursor cleany, by moving to the end */
|
||||
} else {
|
||||
cursor.continue(); /* We need to 'terminate' the cursor cleany, by moving to the end */
|
||||
}
|
||||
}
|
||||
else if (options.offset && options.offset > skipped) {
|
||||
skipped++;
|
||||
cursor.continue(); /* We need to Moving the cursor forward */
|
||||
} else {
|
||||
// This time, it looks like it's good!
|
||||
if (options.addIndividually) {
|
||||
collection.add(cursor.value);
|
||||
} else if (options.clear) {
|
||||
var deleteRequest = store.delete(cursor.value[idAttribute]);
|
||||
deleteRequest.onsuccess = function (event) {
|
||||
elements.push(cursor.value);
|
||||
};
|
||||
deleteRequest.onerror = function (event) {
|
||||
elements.push(cursor.value);
|
||||
};
|
||||
|
||||
} else {
|
||||
elements.push(cursor.value);
|
||||
}
|
||||
processed++;
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
close :function(){
|
||||
if(this.db){
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ExecutionQueue object
|
||||
// The execution queue is an abstraction to buffer up requests to the database.
|
||||
// It holds a "driver". When the driver is ready, it just fires up the queue and executes in sync.
|
||||
function ExecutionQueue(schema,next,nolog) {
|
||||
this.driver = new Driver(schema, this.ready.bind(this), nolog, this.error.bind(this));
|
||||
this.started = false;
|
||||
this.failed = false;
|
||||
this.stack = [];
|
||||
this.version = _.last(schema.migrations).version;
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
// ExecutionQueue Prototype
|
||||
ExecutionQueue.prototype = {
|
||||
// Called when the driver is ready
|
||||
// It just loops over the elements in the queue and executes them.
|
||||
ready: function () {
|
||||
this.started = true;
|
||||
_.each(this.stack, function (message) {
|
||||
this.execute(message);
|
||||
}.bind(this));
|
||||
this.stack = []; // fix memory leak
|
||||
this.next();
|
||||
},
|
||||
|
||||
error: function() {
|
||||
this.failed = true;
|
||||
_.each(this.stack, function (message) {
|
||||
this.execute(message);
|
||||
}.bind(this));
|
||||
this.stack = [];
|
||||
this.next();
|
||||
},
|
||||
|
||||
// Executes a given command on the driver. If not started, just stacks up one more element.
|
||||
execute: function (message) {
|
||||
if (this.started) {
|
||||
this.driver.execute(message[2].storeName || message[1].storeName, message[0], message[1], message[2]); // Upon messages, we execute the query
|
||||
} else if (this.failed) {
|
||||
message[2].error();
|
||||
} else {
|
||||
this.stack.push(message);
|
||||
}
|
||||
},
|
||||
|
||||
close : function(){
|
||||
this.driver.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Method used by Backbone for sync of data with data store. It was initially designed to work with "server side" APIs, This wrapper makes
|
||||
// it work with the local indexedDB stuff. It uses the schema attribute provided by the object.
|
||||
// The wrapper keeps an active Executuon Queue for each "schema", and executes querues agains it, based on the object type (collection or
|
||||
// single model), but also the method... etc.
|
||||
// Keeps track of the connections
|
||||
var Databases = {};
|
||||
|
||||
function sync(method, object, options) {
|
||||
|
||||
if(method == "closeall"){
|
||||
_.each(Databases,function(database){
|
||||
database.close();
|
||||
});
|
||||
// Clean up active databases object.
|
||||
Databases = {};
|
||||
return Backbone.$.Deferred().resolve();
|
||||
}
|
||||
|
||||
// If a model or a collection does not define a database, fall back on ajaxSync
|
||||
if (!object || !_.isObject(object.database)) {
|
||||
return Backbone.ajaxSync(method, object, options);
|
||||
}
|
||||
|
||||
var schema = object.database;
|
||||
if (Databases[schema.id]) {
|
||||
if(Databases[schema.id].version != _.last(schema.migrations).version){
|
||||
Databases[schema.id].close();
|
||||
delete Databases[schema.id];
|
||||
}
|
||||
}
|
||||
|
||||
var promise;
|
||||
|
||||
if (typeof Backbone.$ === 'undefined' || typeof Backbone.$.Deferred === 'undefined') {
|
||||
var noop = function() {};
|
||||
var resolve = noop;
|
||||
var reject = noop;
|
||||
} else {
|
||||
var dfd = Backbone.$.Deferred();
|
||||
var resolve = dfd.resolve;
|
||||
var reject = dfd.reject;
|
||||
|
||||
promise = dfd.promise();
|
||||
}
|
||||
|
||||
var success = options.success;
|
||||
options.success = function(resp) {
|
||||
if (success) success(resp);
|
||||
resolve();
|
||||
object.trigger('sync', object, resp, options);
|
||||
};
|
||||
|
||||
var error = options.error;
|
||||
options.error = function(resp) {
|
||||
if (error) error(resp);
|
||||
reject();
|
||||
object.trigger('error', object, resp, options);
|
||||
};
|
||||
|
||||
var next = function(){
|
||||
Databases[schema.id].execute([method, object, options]);
|
||||
};
|
||||
|
||||
if (!Databases[schema.id]) {
|
||||
Databases[schema.id] = new ExecutionQueue(schema,next,schema.nolog);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
Backbone.ajaxSync = Backbone.sync;
|
||||
Backbone.sync = sync;
|
||||
|
||||
return { sync: sync, debugLog: debugLog};
|
||||
}));
|
||||
|
||||
/**
|
||||
|
|
37
js/database.js
Normal file
37
js/database.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.Database = window.Whisper.Database || {};
|
||||
window.Whisper.Database.id = window.Whisper.Database.id || 'signal';
|
||||
|
||||
Whisper.Database.migrations = [
|
||||
{
|
||||
version: "1.0",
|
||||
migrate: function(transaction, next) {
|
||||
console.log('migratetion 1.0');
|
||||
var messages = transaction.db.createObjectStore("messages");
|
||||
messages.createIndex("conversation", "conversationId", { unique: false });
|
||||
|
||||
var conversations = transaction.db.createObjectStore("conversations");
|
||||
conversations.createIndex("timestamp", "timestamp", { unique: false });
|
||||
next();
|
||||
}
|
||||
}
|
||||
];
|
||||
}());
|
14
js/index.js
14
js/index.js
|
@ -22,10 +22,15 @@
|
|||
this.contacts = $('#contacts');
|
||||
this.resize();
|
||||
|
||||
new Whisper.ConversationListView({el: $('#contacts')});
|
||||
window.addEventListener('resize', this.resize.bind(this));
|
||||
window.addEventListener('storage', function () {Whisper.Threads.fetch(); });
|
||||
Whisper.Threads.fetch({reset: true});
|
||||
|
||||
new Whisper.ConversationListView({el: $('#contacts'), collection: Whisper.Conversations});
|
||||
Whisper.Conversations.fetch({reset: true}).then(function() {
|
||||
if (Whisper.Conversations.length) {
|
||||
Whisper.Conversations.at(0).trigger('render');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
events: {
|
||||
'click #new-message': 'new_message',
|
||||
|
@ -69,8 +74,5 @@
|
|||
} else {
|
||||
textsecure.storage.putUnencrypted("unreadCount", 0);
|
||||
extension.navigator.setBadgeText("");
|
||||
if (Whisper.Threads.length) {
|
||||
Whisper.Threads.at(0).trigger('render');
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
// vim: ts=2:sw=2:expandtab:
|
||||
|
||||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
@ -20,7 +33,9 @@
|
|||
}));
|
||||
};
|
||||
|
||||
var Thread = Backbone.Model.extend({
|
||||
var Conversation = Whisper.Conversation = Backbone.Model.extend({
|
||||
database: Whisper.Database,
|
||||
storeName: 'conversations',
|
||||
defaults: function() {
|
||||
return {
|
||||
name: 'New Conversation',
|
||||
|
@ -31,20 +46,26 @@
|
|||
};
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.messageCollection = new Whisper.MessageCollection();
|
||||
},
|
||||
|
||||
validate: function(attributes, options) {
|
||||
var required = ['type', 'timestamp', 'image', 'name'];
|
||||
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
||||
if (missing.length) { return "Thread must have " + missing; }
|
||||
if (missing.length) { return "Conversation must have " + missing; }
|
||||
},
|
||||
|
||||
sendMessage: function(message, attachments) {
|
||||
return encodeAttachments(attachments).then(function(base64_attachments) {
|
||||
var timestamp = Date.now();
|
||||
this.messages().add({ type: 'outgoing',
|
||||
body: message,
|
||||
threadId: this.id,
|
||||
attachments: base64_attachments,
|
||||
timestamp: timestamp }).save();
|
||||
this.messages().add({ body: message,
|
||||
timestamp: timestamp,
|
||||
conversationId: this.id,
|
||||
conversationType: this.get('type'),
|
||||
type: 'outgoing',
|
||||
attachments: base64_attachments
|
||||
}).save();
|
||||
|
||||
this.save({ timestamp: timestamp,
|
||||
unreadCount: 0,
|
||||
|
@ -64,16 +85,17 @@
|
|||
},
|
||||
|
||||
receiveMessage: function(decrypted) {
|
||||
var thread = this;
|
||||
var conversation = this;
|
||||
encodeAttachments(decrypted.message.attachments).then(function(base64_attachments) {
|
||||
var timestamp = decrypted.pushMessage.timestamp.toNumber();
|
||||
var m = this.messages().add({
|
||||
person: decrypted.pushMessage.source,
|
||||
threadId: this.id,
|
||||
body: decrypted.message.body,
|
||||
timestamp: timestamp,
|
||||
conversationId: this.id,
|
||||
conversationType: this.get('type'),
|
||||
attachments: base64_attachments,
|
||||
type: 'incoming',
|
||||
timestamp: timestamp
|
||||
sender: decrypted.pushMessage.source
|
||||
});
|
||||
m.save();
|
||||
|
||||
|
@ -85,28 +107,24 @@
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
fetch: function() {
|
||||
return this.messageCollection.fetch({conditions: {conversationId: this.id }});
|
||||
},
|
||||
|
||||
messages: function() {
|
||||
if (!this.messageCollection) {
|
||||
this.messageCollection = new Whisper.MessageCollection([], {threadId: this.id});
|
||||
}
|
||||
return this.messageCollection;
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.Threads = new (Backbone.Collection.extend({
|
||||
localStorage: new Backbone.LocalStorage("Threads"),
|
||||
model: Thread,
|
||||
Whisper.ConversationCollection = Backbone.Collection.extend({
|
||||
database: Whisper.Database,
|
||||
storeName: 'conversations',
|
||||
model: Conversation,
|
||||
|
||||
comparator: function(m) {
|
||||
return -m.get('timestamp');
|
||||
},
|
||||
|
||||
findOrCreate: function(attributes) {
|
||||
var thread = Whisper.Threads.add(attributes, {merge: true});
|
||||
thread.save();
|
||||
return thread;
|
||||
},
|
||||
|
||||
createGroup: function(recipients, name) {
|
||||
var attributes = {};
|
||||
attributes = {
|
||||
|
@ -114,13 +132,13 @@
|
|||
numbers : recipients,
|
||||
type : 'group',
|
||||
};
|
||||
var thread = this.findOrCreate(attributes);
|
||||
var conversation = this.add(attributes, {merge: true});
|
||||
return textsecure.messaging.createGroup(recipients, name).then(function(groupId) {
|
||||
thread.save({
|
||||
conversation.save({
|
||||
id : getString(groupId),
|
||||
groupId : getString(groupId)
|
||||
});
|
||||
return thread;
|
||||
return conversation;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -131,10 +149,12 @@
|
|||
name : recipient,
|
||||
type : 'private',
|
||||
};
|
||||
return this.findOrCreate(attributes);
|
||||
var conversation = this.add(attributes, {merge: true});
|
||||
conversation.save();
|
||||
return conversation;
|
||||
},
|
||||
|
||||
findOrCreateForIncomingMessage: function(decrypted) {
|
||||
addIncomingMessage: function(decrypted) {
|
||||
var attributes = {};
|
||||
if (decrypted.message.group) {
|
||||
attributes = {
|
||||
|
@ -150,12 +170,18 @@
|
|||
type : 'private'
|
||||
};
|
||||
}
|
||||
return this.findOrCreate(attributes);
|
||||
var conversation = this.add(attributes, {merge: true});
|
||||
conversation.receiveMessage(decrypted);
|
||||
},
|
||||
|
||||
addIncomingMessage: function(decrypted) {
|
||||
var thread = Whisper.Threads.findOrCreateForIncomingMessage(decrypted);
|
||||
return thread.receiveMessage(decrypted);
|
||||
destroyAll: function () {
|
||||
return Promise.all(this.models.map(function(m) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
m.destroy().then(resolve).fail(reject);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}))();
|
||||
});
|
||||
|
||||
Whisper.Conversations = new Whisper.ConversationCollection();
|
||||
})();
|
|
@ -1,25 +1,51 @@
|
|||
var Whisper = Whisper || {};
|
||||
|
||||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
var Message = Backbone.Model.extend({
|
||||
validate: function(attributes, options) {
|
||||
var required = ['timestamp', 'threadId'];
|
||||
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
||||
if (missing.length) { console.log("Message missing attributes: " + missing); }
|
||||
},
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
thread: function() {
|
||||
return Whisper.Threads.get(this.get('threadId'));
|
||||
}
|
||||
});
|
||||
var Message = Backbone.Model.extend({
|
||||
database: Whisper.Database,
|
||||
storeName: 'messages',
|
||||
defaults: function() { return { timestamp: new Date().getTime() }; },
|
||||
validate: function(attributes, options) {
|
||||
var required = ['timestamp', 'conversationId'];
|
||||
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
||||
if (missing.length) {
|
||||
console.log("Message missing attributes: " + missing);
|
||||
}
|
||||
},
|
||||
|
||||
Whisper.MessageCollection = Backbone.Collection.extend({
|
||||
model: Message,
|
||||
comparator: 'timestamp',
|
||||
initialize: function(models, options) {
|
||||
this.localStorage = new Backbone.LocalStorage("Messages-" + options.threadId);
|
||||
}
|
||||
});
|
||||
conversation: function() {
|
||||
return Whisper.Conversations.get(this.get('conversationId'));
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.MessageCollection = Backbone.Collection.extend({
|
||||
model: Message,
|
||||
database: Whisper.Database,
|
||||
storeName: 'messages',
|
||||
comparator: function(m) { return -m.get('timestamp'); },
|
||||
destroyAll: function () {
|
||||
return Promise.all(this.models.map(function(m) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
m.destroy().then(resolve).fail(reject);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
})()
|
||||
|
|
|
@ -7,7 +7,7 @@ var Whisper = Whisper || {};
|
|||
tagName: 'div',
|
||||
id: 'contacts',
|
||||
itemView: Whisper.ConversationListItemView,
|
||||
collection: Whisper.Threads,
|
||||
collection: Whisper.Conversations,
|
||||
|
||||
events: {
|
||||
'click .contact': 'select',
|
||||
|
|
|
@ -45,11 +45,11 @@ var Whisper = Whisper || {};
|
|||
e.preventDefault();
|
||||
var input = this.$el.find('.send input');
|
||||
var message = input.val();
|
||||
var thread = this.model;
|
||||
var convo = this.model;
|
||||
|
||||
if (message.length > 0 || this.fileInput.hasFiles()) {
|
||||
this.fileInput.getFiles().then(function(attachments) {
|
||||
thread.sendMessage(message, attachments);
|
||||
convo.sendMessage(message, attachments);
|
||||
});
|
||||
input.val("");
|
||||
}
|
||||
|
|
|
@ -13,5 +13,12 @@ var Whisper = Whisper || {};
|
|||
scrollToBottom: function() {
|
||||
this.$el.scrollTop(this.el.scrollHeight);
|
||||
},
|
||||
addAll: function() {
|
||||
this.$el.html('');
|
||||
this.collection.each(function(model) {
|
||||
var view = new this.itemView({model: model});
|
||||
this.$el.prepend(view.render().el);
|
||||
});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -25,7 +25,7 @@ var Whisper = Whisper || {};
|
|||
timestamp: moment(this.model.get('timestamp')).fromNow(),
|
||||
attachments: this.model.get('attachments'),
|
||||
bubble_class: this.model.get('type') === 'outgoing' ? 'sent' : 'incoming',
|
||||
sender: this.model.thread().get('type') === 'group' ? this.model.get('person') : ''
|
||||
sender: this.model.get('conversationType') === 'group' ? this.model.get('sender') : ''
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -67,17 +67,17 @@ var Whisper = Whisper || {};
|
|||
e.preventDefault();
|
||||
var number = this.input.verifyNumber();
|
||||
if (number) {
|
||||
var thread = Whisper.Threads.findOrCreateForRecipient(number);
|
||||
var convo = Whisper.Conversations.findOrCreateForRecipient(number);
|
||||
var message_input = this.$el.find('input.send-message');
|
||||
var message = message_input.val();
|
||||
if (message.length > 0 || this.fileInput.hasFiles()) {
|
||||
this.fileInput.getFiles().then(function(attachments) {
|
||||
thread.sendMessage(message, attachments);
|
||||
convo.sendMessage(message, attachments);
|
||||
});
|
||||
message_input.val("");
|
||||
}
|
||||
this.remove();
|
||||
thread.trigger('render');
|
||||
convo.trigger('render');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -37,10 +37,10 @@ var Whisper = Whisper || {};
|
|||
var numbers = this.$el.find('input.numbers').val().split(',');
|
||||
var name = this.$el.find('input.name').val();
|
||||
var view = this;
|
||||
Whisper.Threads.createGroup(numbers, name).then(function(thread){
|
||||
thread.sendMessage(view.$el.find('input.send-message').val());
|
||||
Whisper.Conversations.createGroup(numbers, name).then(function(convo){
|
||||
convo.sendMessage(view.$el.find('input.send-message').val());
|
||||
view.remove();
|
||||
thread.trigger('render');
|
||||
convo.trigger('render');
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
"version": "0.0.1",
|
||||
"offline_enabled": false,
|
||||
|
||||
"permissions": [
|
||||
"unlimitedStorage"
|
||||
],
|
||||
|
||||
"icons": { "128": "icon.png" },
|
||||
|
||||
"browser_action": {
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="js/components.js"></script>
|
||||
<script type="text/javascript" src="js/database.js"></script>
|
||||
|
||||
<script type="text/javascript" src="js/protobufs.js"></script>
|
||||
<script type="text/javascript" src="js/curve25519_compiled.js"></script>
|
||||
|
@ -108,7 +109,7 @@
|
|||
<script type="text/javascript" src="js/crypto.js"></script>
|
||||
<script type="text/javascript" src="js/protocol.js"></script>
|
||||
<script type="text/javascript" src="js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="js/models/threads.js"></script>
|
||||
<script type="text/javascript" src="js/models/conversations.js"></script>
|
||||
<script type="text/javascript" src="js/api.js"></script>
|
||||
<script type="text/javascript" src="js/sendmessage.js"></script>
|
||||
|
||||
|
|
|
@ -53,6 +53,11 @@ window.assert = chai.assert;
|
|||
mocha.reporter(SauceReporter);
|
||||
}());
|
||||
|
||||
// Override the database id.
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.Database = window.Whisper.Database || {};
|
||||
Whisper.Database.id = 'test';
|
||||
|
||||
/*
|
||||
* global helpers for tests
|
||||
*/
|
||||
|
|
|
@ -134,8 +134,9 @@
|
|||
<script type="text/javascript" src="../js/webcrypto.js"></script>
|
||||
<script type="text/javascript" src="../js/crypto.js"></script>
|
||||
<script type="text/javascript" src="../js/protocol.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/database.js"></script>
|
||||
<script type="text/javascript" src="../js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="../js/models/threads.js"></script>
|
||||
<script type="text/javascript" src="../js/models/conversations.js"></script>
|
||||
<script type="text/javascript" src="../js/api.js"></script>
|
||||
<script type="text/javascript" src="../js/sendmessage.js" data-cover></script>
|
||||
|
||||
|
@ -161,7 +162,8 @@
|
|||
<script type="text/javascript" src="helpers_test.js"></script>
|
||||
<script type="text/javascript" src="views/message_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/threads_test.js"></script>
|
||||
<script type="text/javascript" src="models/conversations_test.js"></script>
|
||||
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||
<script type="text/javascript" src="protocol_test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
149
test/models/conversations_test.js
Normal file
149
test/models/conversations_test.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
function clear(done) {
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
return convos.fetch().then(function() {
|
||||
convos.destroyAll().then(function() {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
return messages.fetch().then(function() {
|
||||
messages.destroyAll().then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var attributes = { type: 'outgoing',
|
||||
body: 'hi',
|
||||
conversationId: 'foo',
|
||||
attachments: [],
|
||||
timestamp: new Date().getTime() };
|
||||
|
||||
describe('ConversationCollection', function() {
|
||||
before(clear);
|
||||
after(clear);
|
||||
|
||||
it('adds without saving', function() {
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
var message = convos.add(attributes);
|
||||
assert.notEqual(convos.length, 0);
|
||||
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
assert.strictEqual(convos.length, 0);
|
||||
});
|
||||
|
||||
it('saves asynchronously', function(done) {
|
||||
new Whisper.ConversationCollection().add(attributes).save().then(done);
|
||||
});
|
||||
|
||||
it('fetches persistent convos', function(done) {
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
assert.strictEqual(convos.length, 0);
|
||||
convos.fetch().then(function() {
|
||||
var m = convos.at(0).attributes;
|
||||
_.each(attributes, function(val, key) {
|
||||
assert.deepEqual(m[key], val);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('destroys persistent convos', function(done) {
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
convos.fetch().then(function() {
|
||||
convos.destroyAll().then(function() {
|
||||
var convos = new Whisper.ConversationCollection();
|
||||
convos.fetch().then(function() {
|
||||
assert.strictEqual(convos.length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be ordered newest to oldest', function() {
|
||||
var conversations = new Whisper.ConversationCollection();
|
||||
// Timestamps
|
||||
var today = new Date();
|
||||
var tomorrow = new Date();
|
||||
tomorrow.setDate(today.getDate()+1);
|
||||
|
||||
// Add convos
|
||||
conversations.add({ timestamp: today });
|
||||
conversations.add({ timestamp: tomorrow });
|
||||
|
||||
var models = conversations.models;
|
||||
var firstTimestamp = models[0].get('timestamp').getTime();
|
||||
var secondTimestamp = models[1].get('timestamp').getTime();
|
||||
|
||||
// Compare timestamps
|
||||
assert(firstTimestamp > secondTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conversation', function() {
|
||||
var attributes = { type: 'private', id: 'foobar' };
|
||||
before(function(done) {
|
||||
var convo = new Whisper.ConversationCollection().add(attributes);
|
||||
convo.save().then(function() {
|
||||
var message = convo.messages().add({body: 'hello world', conversationId: convo.id});
|
||||
message.save().then(done)
|
||||
});
|
||||
});
|
||||
after(clear);
|
||||
|
||||
it('contains its own messages', function (done) {
|
||||
var convo = new Whisper.ConversationCollection().add({id: 'foobar'});
|
||||
convo.fetch().then(function() {
|
||||
assert.notEqual(convo.messages().length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('contains only its own messages', function (done) {
|
||||
var convo = new Whisper.ConversationCollection().add({id: 'barfoo'});
|
||||
convo.fetch().then(function() {
|
||||
assert.strictEqual(convo.messages().length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('has most recent messages first', function(done) {
|
||||
var convo = new Whisper.ConversationCollection().add({id: 'barfoo'});
|
||||
convo.messages().add({
|
||||
body: 'first message',
|
||||
conversationId: convo.id,
|
||||
timestamp: new Date().getTime() - 5000
|
||||
}).save().then(function() {
|
||||
convo.messages().add({
|
||||
body: 'second message',
|
||||
conversationId: convo.id
|
||||
}).save().then(function() {
|
||||
convo.fetch().then(function() {
|
||||
assert.strictEqual(convo.messages().at(0).get('body'), 'second message');
|
||||
assert.strictEqual(convo.messages().at(1).get('body'), 'first message');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})();;
|
76
test/models/messages_test.js
Normal file
76
test/models/messages_test.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function clear(done) {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
return messages.fetch().then(function() {
|
||||
messages.destroyAll();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
var attributes = { type: 'outgoing',
|
||||
body: 'hi',
|
||||
conversationId: 'foo',
|
||||
attachments: [],
|
||||
timestamp: new Date().getTime() };
|
||||
|
||||
describe('MessageCollection', function() {
|
||||
before(clear);
|
||||
after(clear);
|
||||
|
||||
it('adds without saving', function() {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
var message = messages.add(attributes);
|
||||
assert.notEqual(messages.length, 0);
|
||||
|
||||
var messages = new Whisper.MessageCollection();
|
||||
assert.strictEqual(messages.length, 0);
|
||||
});
|
||||
|
||||
it('saves asynchronously', function(done) {
|
||||
new Whisper.MessageCollection().add(attributes).save().then(done);
|
||||
});
|
||||
|
||||
it('fetches persistent messages', function(done) {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
assert.strictEqual(messages.length, 0);
|
||||
messages.fetch().then(function() {
|
||||
assert.notEqual(messages.length, 0);
|
||||
var m = messages.at(0).attributes;
|
||||
_.each(attributes, function(val, key) {
|
||||
assert.deepEqual(m[key], val);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('destroys persistent messages', function(done) {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
messages.fetch().then(function() {
|
||||
messages.destroyAll().then(function() {
|
||||
var messages = new Whisper.MessageCollection();
|
||||
messages.fetch().then(function() {
|
||||
assert.strictEqual(messages.length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -10927,6 +10927,11 @@ window.assert = chai.assert;
|
|||
mocha.reporter(SauceReporter);
|
||||
}());
|
||||
|
||||
// Override the database id.
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.Database = window.Whisper.Database || {};
|
||||
Whisper.Database.id = 'test';
|
||||
|
||||
/*
|
||||
* global helpers for tests
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
describe('MessageView', function() {
|
||||
var thread = Whisper.Threads.add({id: 'foo'});
|
||||
var message = thread.messages().add({
|
||||
threadId: thread.id,
|
||||
before(function(done) {
|
||||
Whisper.Conversations.fetch().then(done);
|
||||
});
|
||||
|
||||
var convo = Whisper.Conversations.add({id: 'foo'});
|
||||
var message = convo.messages().add({
|
||||
conversationId: convo.id,
|
||||
body: 'hello world',
|
||||
type: 'outgoing',
|
||||
timestamp: new Date().getTime()
|
||||
|
|
Loading…
Reference in a new issue