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
|
@ -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);
|
||||
},
|
||||
}))();
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue