Backbone message storage and views
Adds Backbone-based Whisper.Messages model/collection with local storage extension. Saves sent and received messages in Whisper.Messages instead of message map. This will assign a unique id to the message and save it to localStorage. Adds Backbone-based view to popup.html Automatically updates itself when new messages are saved to Whisper.Messages db from the background page. Added some shiny new styles, and started splitting up css into multiple files for sanity's sake.
This commit is contained in:
parent
170257dafb
commit
b852e68290
14 changed files with 3832 additions and 323 deletions
|
@ -30,6 +30,10 @@
|
|||
<script type="text/javascript" src="js-deps/Long.min.js"></script>
|
||||
<script type="text/javascript" src="js-deps/ByteBuffer.min.js"></script>
|
||||
<script type="text/javascript" src="js-deps/ProtoBuf.min.js"></script>
|
||||
<script type="text/javascript" src="js-deps/underscore.js"></script>
|
||||
<script type="text/javascript" src="js-deps/backbone.js"></script>
|
||||
<script type="text/javascript" src="js-deps/backbone.localStorage.js"></script>
|
||||
<script type="text/javascript" src="js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="js/helpers.js"></script>
|
||||
<script type="text/javascript" src="js/api.js"></script>
|
||||
<script type="text/javascript" src="js/background.js"></script>
|
||||
|
|
26
css/buttons.css
Normal file
26
css/buttons.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
.btn span {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
border: 2px solid #7fd0ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
padding: 2px;
|
||||
border: none;
|
||||
}
|
||||
.btn:hover, .btn:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: #f1fafd;
|
||||
}
|
||||
.btn:active {
|
||||
outline: 2px dashed #acdbf5;
|
||||
}
|
||||
.btn.selected span,
|
||||
.btn:active span {
|
||||
background-color: #7fd0ed;
|
||||
border: 2px solid #acdbf5;
|
||||
color: #fff;
|
||||
}
|
176
css/conversation.css
Normal file
176
css/conversation.css
Normal file
|
@ -0,0 +1,176 @@
|
|||
.conversation {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #acdbf5;
|
||||
background-color: #7fd0ed;
|
||||
color: #fff;
|
||||
margin-bottom: 0.5em;
|
||||
-webkit-animation-duration: 1s;
|
||||
-webkit-animation-name: convoopen;
|
||||
}
|
||||
|
||||
.conversation.closed {
|
||||
background-color: #fff;
|
||||
color: #7fd0ed;
|
||||
-webkit-animation-duration: 1s;
|
||||
-webkit-animation-name: convoclose;
|
||||
}
|
||||
@-webkit-keyframes convoclose {
|
||||
from { background-color: #7fd0ed; }
|
||||
to { background-color: #fff; }
|
||||
}
|
||||
@-webkit-keyframes convoopen {
|
||||
from { background-color: #fff; }
|
||||
to { background-color: #7fd0ed; }
|
||||
}
|
||||
|
||||
.conversation.closed:hover {
|
||||
background-color: #f1fafd;
|
||||
cursor: pointer;
|
||||
-webkit-animation-duration: 1s;
|
||||
-webkit-animation-name: hovercolorfadein;
|
||||
}
|
||||
.conversation.closed:not(hover) {
|
||||
-webkit-animation-duration: 1s;
|
||||
-webkit-animation-name: hovercolorfadeout;
|
||||
}
|
||||
@-webkit-keyframes hovercolorfadein {
|
||||
from { background-color: #fff; }
|
||||
to { background-color: #f1fafd; }
|
||||
}
|
||||
@-webkit-keyframes hovercolorfadeout {
|
||||
from { background-color: #f1fafd; }
|
||||
to { background-color: #fff; }
|
||||
}
|
||||
|
||||
.conversation .header {
|
||||
padding: 0.3em 0.6em 0.3em 46px;
|
||||
}
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
border: 2px solid #acdbf5;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.conversation .header span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.collapsable {
|
||||
background-color: #fff;
|
||||
border: 2px solid #acdbf5;
|
||||
padding: 1em 0em;
|
||||
line-height: 1.2em;
|
||||
font-family: sans-serif;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.messages + form {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.conversation form {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
display: block;
|
||||
padding: 0.5em 0.6em 0em;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: block;
|
||||
color: #cccccc;
|
||||
font-size: 0.70em;
|
||||
padding: 0.2em 0.6em;
|
||||
visibility: hidden;
|
||||
}
|
||||
.bubble:hover .metadata {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
display: inline-block;
|
||||
background-color: #fafafa;
|
||||
color: #333333;
|
||||
border: 2px solid #7fd0ed;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sending .bubble {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.incoming .bubble {
|
||||
background-color: #ffffff;
|
||||
float: left;
|
||||
}
|
||||
.incoming .bubble:after, .incoming .bubble:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 11px;
|
||||
left: -0.8em;
|
||||
border-right: solid 0.5em #ffffff;
|
||||
border-top: solid 7px transparent;
|
||||
border-left: solid 0.4em transparent;
|
||||
border-bottom: solid 7px transparent;
|
||||
}
|
||||
|
||||
.outgoing .bubble {
|
||||
float: right;
|
||||
background-color: #f5feff;
|
||||
}
|
||||
.outgoing .bubble:after, .outgoing .bubble:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 11px;
|
||||
right: -0.8em;
|
||||
border-top: solid 7px transparent;
|
||||
border-right: solid 0.4em transparent;
|
||||
border-bottom: solid 7px transparent;
|
||||
}
|
||||
|
||||
.outgoing .bubble:after {
|
||||
border-left: solid 0.5em #f5feff;
|
||||
}
|
||||
.outgoing .bubble:before {
|
||||
border-left: solid 0.5em #7fd0ed;
|
||||
right: -0.9em;
|
||||
}
|
||||
|
||||
.incoming .bubble:before {
|
||||
border-right: solid 0.5em #7fd0ed;
|
||||
left: -0.9em;
|
||||
}
|
||||
|
||||
.outgoing .bubble, .outgoing .metadata {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.3em 11.63636px;
|
||||
}
|
||||
|
||||
.message:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
25
css/forms.css
Normal file
25
css/forms.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
input[type=text], textarea {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 7px;
|
||||
border: 2px solid #7fd0ed;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
input[type=text]:focus {
|
||||
outline: 2px dashed #acdbf5;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
box-sizing: border-box;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
99
css/popup.css
Normal file
99
css/popup.css
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
|
||||
.clearfix:after { clear: both; }
|
||||
.clearfix { zoom: 1; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 400px;
|
||||
min-height: 500px;
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
form.compose {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
form.compose input[type=text], form.compose textarea {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
#send input[type=submit] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#popup_send_numbers {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#popup_send_numbers:focus + .contacts,
|
||||
.contacts:hover {
|
||||
display: block;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.contacts {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.contact {
|
||||
border: solid 1px #ccc;
|
||||
background: #fff;
|
||||
font-size: 88%;
|
||||
padding-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact .pic {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.contact .name, .contact .number {
|
||||
line-height: 30px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Formatting */
|
1700
js-deps/backbone.js
Normal file
1700
js-deps/backbone.js
Normal file
File diff suppressed because it is too large
Load diff
247
js-deps/backbone.localStorage.js
Normal file
247
js-deps/backbone.localStorage.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* Backbone localStorage Adapter
|
||||
* Version 1.1.7
|
||||
*
|
||||
* 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.
|
||||
|
||||
// Hold reference to Underscore.js and Backbone.js in the closure in order
|
||||
// to make things work even if they are removed from the global namespace
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = (store && store.split(",")) || [];
|
||||
};
|
||||
|
||||
extend(Backbone.LocalStorage.prototype, {
|
||||
|
||||
// Save the current state of the **Store** to *localStorage*.
|
||||
save: function() {
|
||||
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 = guid();
|
||||
model.set(model.idAttribute, model.id);
|
||||
}
|
||||
this.localStorage().setItem(this.name+"-"+model.id, this.serializer.serialize(model));
|
||||
this.records.push(model.id.toString());
|
||||
this.save();
|
||||
return this.find(model) !== false;
|
||||
},
|
||||
|
||||
// Update a model by replacing its copy in `this.data`.
|
||||
update: function(model) {
|
||||
this.localStorage().setItem(this.name+"-"+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) !== false;
|
||||
},
|
||||
|
||||
// Retrieve a model from `this.data` by id.
|
||||
find: function(model) {
|
||||
return this.serializer.deserialize(this.localStorage().getItem(this.name+"-"+model.id));
|
||||
},
|
||||
|
||||
// Return the array of all models currently in storage.
|
||||
findAll: function() {
|
||||
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.name+"-"+id));
|
||||
if (data != null) result.push(data);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// Delete a model from `this.data`, returning it.
|
||||
destroy: function(model) {
|
||||
if (model.isNew())
|
||||
return false
|
||||
this.localStorage().removeItem(this.name+"-"+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);
|
||||
}
|
||||
}
|
||||
|
||||
this.records.length = 0;
|
||||
},
|
||||
|
||||
// Size of localStorage.
|
||||
_storageSize: function() {
|
||||
return this.localStorage().length;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 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 = model.localStorage || 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;
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
if (error.code === 22 && store._storageSize() === 0)
|
||||
errorMessage = "Private browsing is unsupported";
|
||||
else
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} else {
|
||||
errorMessage = errorMessage ? errorMessage
|
||||
: "Record Not Found";
|
||||
|
||||
if (options && options.error)
|
||||
if (Backbone.VERSION === "0.9.10") {
|
||||
options.error(model, errorMessage, options);
|
||||
} else {
|
||||
options.error(errorMessage);
|
||||
}
|
||||
|
||||
if (syncDfd)
|
||||
syncDfd.reject(errorMessage);
|
||||
}
|
||||
|
||||
// add compatibility with $.ajax
|
||||
// always execute callback for success and error
|
||||
if (options && options.complete) options.complete(resp);
|
||||
|
||||
return syncDfd && syncDfd.promise();
|
||||
};
|
||||
|
||||
Backbone.ajaxSync = Backbone.sync;
|
||||
|
||||
Backbone.getSyncMethod = function(model) {
|
||||
if(model.localStorage || (model.collection && model.collection.localStorage)) {
|
||||
return Backbone.localSync;
|
||||
}
|
||||
|
||||
return Backbone.ajaxSync;
|
||||
};
|
||||
|
||||
// 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]);
|
||||
};
|
||||
|
||||
return Backbone.LocalStorage;
|
||||
}));
|
1352
js-deps/underscore.js
Normal file
1352
js-deps/underscore.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -362,26 +362,6 @@ function isRegistrationDone() {
|
|||
return storage.getUnencrypted("registration_done") !== undefined;
|
||||
}
|
||||
|
||||
function getMessageMap() {
|
||||
return storage.getEncrypted("messageMap", {});
|
||||
}
|
||||
|
||||
function storeMessage(messageObject) {
|
||||
var messageMap = getMessageMap();
|
||||
var conversation = messageMap[messageObject.pushMessage.source]; //TODO: Also support Group message IDs here
|
||||
if (conversation === undefined) {
|
||||
conversation = [];
|
||||
messageMap[messageObject.pushMessage.source] = conversation;
|
||||
}
|
||||
|
||||
conversation[conversation.length] = { message: messageObject.message.body != null && getString(messageObject.message.body),
|
||||
sender: messageObject.pushMessage.source,
|
||||
timestamp: messageObject.pushMessage.timestamp.div(dcodeIO.Long.fromNumber(1000)).toNumber() };
|
||||
storage.putEncrypted("messageMap", messageMap);
|
||||
chrome.runtime.sendMessage(conversation[conversation.length - 1]);
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
*** NaCL Interface ***
|
||||
**********************/
|
||||
|
@ -493,7 +473,7 @@ window.textsecure.subscribeToPush = function() {
|
|||
promises[i] = handleAttachment(decrypted.message.attachments[i]);
|
||||
}
|
||||
return Promise.all(promises).then(function() {
|
||||
storeMessage(decrypted);
|
||||
Whisper.Messages.addIncomingMessage(decrypted);
|
||||
message_callback(decrypted);
|
||||
});
|
||||
})
|
||||
|
|
32
js/models/messages.js
Normal file
32
js/models/messages.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
var Whisper = Whisper || {};
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var Message = Backbone.Model.extend();
|
||||
Whisper.Messages = new (Backbone.Collection.extend({
|
||||
localStorage: new Backbone.LocalStorage("Messages"),
|
||||
model: Message,
|
||||
comparator: 'timestamp',
|
||||
|
||||
addIncomingMessage: function(decrypted) {
|
||||
Whisper.Messages.add({
|
||||
sender: decrypted.pushMessage.source,
|
||||
group: decrypted.message.group,
|
||||
body: decrypted.message.body,
|
||||
type: 'incoming',
|
||||
timestamp: decrypted.message.timestamp
|
||||
}).save();
|
||||
},
|
||||
|
||||
addOutgoingMessage: function(messageProto, sender) {
|
||||
Whisper.Messages.add({
|
||||
sender: sender,
|
||||
body: messageProto.body,
|
||||
type: 'outgoing',
|
||||
timestamp: new Date().getTime()
|
||||
}).save();
|
||||
}
|
||||
}))();
|
||||
|
||||
})()
|
76
js/popup.js
76
js/popup.js
|
@ -1,4 +1,4 @@
|
|||
/* vim: ts=4:sw=4
|
||||
/* vim: ts=4:sw=4:noexpandtab:
|
||||
*
|
||||
* 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
|
||||
|
@ -14,81 +14,25 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$('#inbox_link').click(function() {
|
||||
$('#inbox').show();
|
||||
$('#inbox_link').click(function(e) {
|
||||
$('#send').hide();
|
||||
$('#send_link').removeClass('selected');
|
||||
$('#inbox').show();
|
||||
$('#inbox_link').addClass('selected');
|
||||
});
|
||||
$('#send_link').click(function() {
|
||||
$('#send_link').click(function(e) {
|
||||
$('#inbox').hide();
|
||||
$('#inbox_link').removeClass('selected');
|
||||
$('#send').show();
|
||||
$('#send_link').addClass('selected');
|
||||
});
|
||||
|
||||
textsecure.registerOnLoadFunction(function() {
|
||||
if (storage.getUnencrypted("number_id") === undefined) {
|
||||
chrome.tabs.create({url: "options.html"});
|
||||
} else {
|
||||
function fillMessages() {
|
||||
var MAX_MESSAGES_PER_CONVERSATION = 4;
|
||||
var MAX_CONVERSATIONS = 5;
|
||||
|
||||
var conversations = [];
|
||||
|
||||
var messageMap = getMessageMap();
|
||||
for (conversation in messageMap) {
|
||||
var messages = messageMap[conversation];
|
||||
messages.sort(function(a, b) { return b.timestamp - a.timestamp; });
|
||||
conversations[conversations.length] = messages;
|
||||
}
|
||||
|
||||
conversations.sort(function(a, b) { return b[0].timestamp - a[0].timestamp });
|
||||
|
||||
var ul = $('#conversations');
|
||||
ul.html('');
|
||||
for (var i = 0; i < MAX_CONVERSATIONS && i < conversations.length; i++) {
|
||||
var conversation = conversations[i];
|
||||
var messages = $('<ul class="conversation">');
|
||||
for (var j = 0; j < MAX_MESSAGES_PER_CONVERSATION && j < conversation.length; j++) {
|
||||
var message = conversation[j];
|
||||
$('<li class="message incoming container">').
|
||||
append($('<div class="avatar">')).
|
||||
append($('<div class="bubble">').
|
||||
append($('<span class="message-text">').text(message.message)).
|
||||
append($('<span class="metadata">').text("From: " + message.sender + ", at: " + timestampToHumanReadable(message.timestamp)))
|
||||
).appendTo(messages);
|
||||
}
|
||||
var button = $('<button id="button' + i + '">').text('Send');
|
||||
var input = $('<input id="text' + i + '">');
|
||||
$('<li>').
|
||||
append(messages).
|
||||
append($("<form class='container'>").append(input).append(button)).
|
||||
appendTo(ul);
|
||||
button.click(function() {
|
||||
button.attr("disabled", "disabled");
|
||||
button.text("Sending");
|
||||
|
||||
var sendDestinations = [conversation[0].sender];
|
||||
if (conversation[0].group)
|
||||
sendDestinations = conversation[0].group.members;
|
||||
|
||||
var messageProto = new PushMessageContentProtobuf();
|
||||
messageProto.body = input.val();
|
||||
|
||||
textsecure.sendMessage(sendDestinations, messageProto, function(result) {
|
||||
console.log(result);
|
||||
button.removeAttr("disabled");
|
||||
button.text("Send");
|
||||
input.val("");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(window).bind('storage', function(e) {
|
||||
console.log("Got localStorage update for key " + e.key);
|
||||
if (event.key == "emessageMap")//TODO: Fix when we get actual encryption
|
||||
fillMessages();
|
||||
});
|
||||
fillMessages();
|
||||
$(window).bind('storage', function(e) { Whisper.Messages.fetch(); });
|
||||
Whisper.Messages.fetch();
|
||||
$('.my-number').text(storage.getUnencrypted("number_id").split(".")[0]);
|
||||
storage.putUnencrypted("unreadCount", 0);
|
||||
chrome.browserAction.setBadgeText({text: ""});
|
||||
|
|
145
js/views/messages.js
Normal file
145
js/views/messages.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
var Whisper = Whisper || {};
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var MessageView = Backbone.View.extend({
|
||||
tagName: "li",
|
||||
className: "message",
|
||||
|
||||
initialize: function() {
|
||||
this.$el.
|
||||
append($('<div class="bubble">').
|
||||
append($('<span class="message-text">')).
|
||||
append($('<span class="metadata">'))
|
||||
);
|
||||
this.$el.addClass(this.model.get('type'));
|
||||
this.listenTo(this.model, 'change:completed', this.render); // auto update
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.find('.message-text').text(this.model.get('body'));
|
||||
this.$el.find('.metadata').text(this.formatTimestamp());
|
||||
return this;
|
||||
},
|
||||
|
||||
formatTimestamp: function() {
|
||||
var timestamp = this.model.get('timestamp');
|
||||
var now = new Date().getTime() / 1000;
|
||||
var date = new Date();
|
||||
date.setTime(timestamp*1000);
|
||||
if (now - timestamp > 60*60*24*7) {
|
||||
return date.toLocaleDateString({month: 'short', day: 'numeric'});
|
||||
}
|
||||
if (now - timestamp > 60*60*24) {
|
||||
return date.toLocaleDateString({weekday: 'short'});
|
||||
}
|
||||
return date.toTimeString();
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationView = Backbone.View.extend({
|
||||
tagName: 'li',
|
||||
className: 'conversation',
|
||||
|
||||
initialize: function(options) {
|
||||
this.$el.addClass('closed');
|
||||
this.$header = $('<div class="header">').
|
||||
append($('<span>').text(options.sender)).appendTo(this.$el);
|
||||
this.$header.prepend($('<div class="avatar">'));
|
||||
this.$collapsable = $('<div class="collapsable">').hide();
|
||||
this.$messages = $('<ul>').addClass('messages').appendTo(this.$collapsable);
|
||||
|
||||
this.$button = $('<button class="btn">').attr('id', 'button' + this.id).
|
||||
append($('<span>').text('Send'));
|
||||
this.$input = $('<input type="text" id="text' + options.threadId + '">').
|
||||
attr('autocomplete','off');
|
||||
this.$form = $("<form class='container'>").append(this.$input, this.$button);
|
||||
this.$form.appendTo(this.$collapsable);
|
||||
this.$collapsable.appendTo(this.$el);
|
||||
|
||||
this.$header.click(function(e) {
|
||||
var $conversation = $(e.target).closest('.conversation');
|
||||
if (!$conversation.hasClass('closed')) {
|
||||
$conversation.addClass('closed');
|
||||
$conversation.find('.collapsable').slideUp(600);
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.click(function(e) {
|
||||
var $conversation = $(e.target).closest('.conversation');
|
||||
if ($conversation.hasClass('closed')) {
|
||||
$conversation.removeClass('closed');
|
||||
$conversation.find('.collapsable').slideDown(600);
|
||||
$conversation.find('input').focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.$button.click(function(e) {
|
||||
var $button = $(e.target).closest('.btn');
|
||||
var $input = $button.closest('form').find('input');
|
||||
$button.attr("disabled", "disabled");
|
||||
$button.find('span').text("Sending");
|
||||
|
||||
var sendDestinations = [options.sender];
|
||||
|
||||
var messageProto = new PushMessageContentProtobuf();
|
||||
messageProto.body = $input.val();
|
||||
|
||||
Whisper.Messages.addOutgoingMessage(messageProto, options.sender);
|
||||
|
||||
textsecure.sendMessage(sendDestinations, messageProto, function(result) {
|
||||
console.log(result);
|
||||
$button.removeAttr("disabled");
|
||||
$button.find('span').text("Send");
|
||||
$input.val("");
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addMessage: function (message) {
|
||||
var view = new MessageView({ model: message });
|
||||
this.$messages.append(view.render().el);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Whisper.ConversationListView = new (Backbone.View.extend({ // singleton
|
||||
|
||||
tagName: 'ul',
|
||||
id: 'conversations',
|
||||
initialize: function() {
|
||||
this.views = [];
|
||||
this.messages = Whisper.Messages;
|
||||
this.listenTo(this.messages, 'change:completed', this.render);
|
||||
this.listenTo(this.messages, 'add', this.addMessage);
|
||||
this.listenTo(this.messages, 'reset', this.addAll);
|
||||
this.listenTo(this.messages, 'all', this.render);
|
||||
|
||||
// Suppresses 'add' events with {reset: true} and prevents the app view
|
||||
// from being re-rendered for every model. Only renders when the 'reset'
|
||||
// event is triggered at the end of the fetch.
|
||||
//this.messages.fetch({reset: true});
|
||||
|
||||
this.$el.appendTo($('#inbox'));
|
||||
},
|
||||
|
||||
addMessage: function (message) {
|
||||
// todo: find the right existing view
|
||||
var threadId = message.get('sender'); // TODO: groups
|
||||
if (this.views[threadId] === undefined) {
|
||||
this.views[threadId] = new ConversationView({threadId: threadId, sender: message.get('sender')});
|
||||
this.$el.append(this.views[threadId].render().el);
|
||||
}
|
||||
|
||||
this.views[threadId].addMessage(message);
|
||||
},
|
||||
|
||||
// Add all items in the collection at once
|
||||
addAll: function () {
|
||||
this.$el.html('');
|
||||
this.messages.each(this.addMessage, this);
|
||||
},
|
||||
}))();
|
||||
})();
|
228
popup.css
228
popup.css
|
@ -1,228 +0,0 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
|
||||
.clearfix:after { clear: both; }
|
||||
.clearfix { zoom: 1; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 300px;
|
||||
min-height: 500px;
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: ccc;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
form.compose {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
input[type=text], textarea {
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddf;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
box-sizing: border-box;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
#send input[type=submit] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#popup_send_numbers {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#popup_send_numbers:focus + .contacts,
|
||||
.contacts:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.contacts {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.contact {
|
||||
border: solid 1px #ccc;
|
||||
background: #fff;
|
||||
font-size: 88%;
|
||||
padding-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact .pic {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.contact .name, .contact .number {
|
||||
line-height: 30px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* TS styles */
|
||||
.conversation {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background-color: #fafafa;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.incoming .bubble {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.sending .bubble {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Style variants */
|
||||
.blue-background {
|
||||
background: #3a7ef2;
|
||||
}
|
||||
|
||||
/* Formatting */
|
||||
.message-text {
|
||||
display: block;
|
||||
padding: 0.5em 0.6em 0em;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: block;
|
||||
font-size: 0.70em;
|
||||
padding: 0.2em 0.6em;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
background-color: #d0d0da;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
border-radius: 4.36364px;
|
||||
max-width: 75%;
|
||||
border-bottom: 2.25px solid #dddddd;
|
||||
}
|
||||
|
||||
.incoming .bubble {
|
||||
float: left;
|
||||
}
|
||||
.incoming .bubble:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 11px;
|
||||
left: -0.8em;
|
||||
border-right: solid 0.5em #ffffff;
|
||||
border-top: solid 7px transparent;
|
||||
border-left: solid 0.4em transparent;
|
||||
border-bottom: solid 7px transparent;
|
||||
}
|
||||
|
||||
.outgoing .bubble {
|
||||
float: right;
|
||||
}
|
||||
.outgoing .bubble:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 11px;
|
||||
right: -0.8em;
|
||||
border-left: solid 0.5em #ffffff;
|
||||
border-top: solid 7px transparent;
|
||||
border-right: solid 0.4em transparent;
|
||||
border-bottom: solid 7px transparent;
|
||||
}
|
||||
|
||||
.outgoing .bubble, .outgoing .metadata {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.3em 11.63636px;
|
||||
}
|
||||
|
||||
.message:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* debug styles */
|
||||
/*
|
||||
.conversation { border: 1px solid red; }
|
||||
.message { border: 1px solid blue; }
|
||||
*/
|
||||
|
23
popup.html
23
popup.html
|
@ -1,3 +1,5 @@
|
|||
<!-- vim: ts=4:sw=4:noexpandtab:
|
||||
--!>
|
||||
<!--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
|
||||
|
@ -14,13 +16,16 @@
|
|||
|
||||
<html>
|
||||
<head>
|
||||
<link type='text/css' rel='stylesheet' href='popup.css'>
|
||||
<link type='text/css' rel='stylesheet' href='css/forms.css'>
|
||||
<link type='text/css' rel='stylesheet' href='css/buttons.css'>
|
||||
<link type='text/css' rel='stylesheet' href='css/conversation.css'>
|
||||
<link type='text/css' rel='stylesheet' href='css/popup.css'>
|
||||
</head>
|
||||
<body data-name="curve25519" data-tools="pnacl" data-configs="Debug Release" data-path="pnacl/{config}">
|
||||
<header class="clearfix">
|
||||
<div class='container'>
|
||||
<button id="inbox_link">Inbox</button>
|
||||
<button id="send_link">Compose</button>
|
||||
<button class='btn selected' id='inbox_link'><span>Inbox</span></button>
|
||||
<button class='btn' id='send_link'><span>Compose</span></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class='container'>
|
||||
|
@ -40,16 +45,18 @@
|
|||
<button id="popup_send_button">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="inbox">
|
||||
</div>
|
||||
</div>
|
||||
<div id="inbox">
|
||||
<ul id="conversations">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="js/webcrypto.js"></script>
|
||||
<script type="text/javascript" src="js/crypto.js"></script>
|
||||
<script type="text/javascript" src="js-deps/nacl-common.js"></script>
|
||||
<script type="text/javascript" src="js-deps/jquery.js"></script>
|
||||
<script type="text/javascript" src="js-deps/underscore.js"></script>
|
||||
<script type="text/javascript" src="js-deps/backbone.js"></script>
|
||||
<script type="text/javascript" src="js-deps/backbone.localStorage.js"></script>
|
||||
<script type="text/javascript" src="js/models/messages.js"></script>
|
||||
<script type="text/javascript" src="js/views/messages.js"></script>
|
||||
<script type="text/javascript" src="js-deps/core.js"></script>
|
||||
<script type="text/javascript" src="js-deps/enc-base64.js"></script>
|
||||
<script type="text/javascript" src="js-deps/cipher-core.js"></script>
|
||||
|
|
Loading…
Reference in a new issue