diff --git a/background.html b/background.html
index eb926be7..bd987089 100644
--- a/background.html
+++ b/background.html
@@ -34,6 +34,7 @@
+
diff --git a/css/buttons.css b/css/buttons.css
index cec16d5e..7809c84f 100644
--- a/css/buttons.css
+++ b/css/buttons.css
@@ -1,26 +1,44 @@
-.btn span {
+.btn {
display: inline-block;
padding: 0.5em;
- border: 2px solid #7fd0ed;
- border-radius: 4px;
-}
-.btn {
+ border: 2px solid #acdbf5;
border-radius: 4px;
background-color: #fff;
- padding: 2px;
- border: none;
+ color: #7fd0ed;
+ font-weight: bold;
}
.btn:hover, .btn:focus {
cursor: pointer;
outline: none;
- background-color: #f1fafd;
+}
+.btn:hover {
+ background-color: #7fd0ed;
+ border-color: #acdbf5;
+ color: #fff;
}
.btn:active {
outline: 2px dashed #acdbf5;
+ outline-offset: 2px;
}
-.btn.selected span,
-.btn:active span {
+.btn.selected ,
+.btn:active {
background-color: #7fd0ed;
border: 2px solid #acdbf5;
color: #fff;
}
+.btn:active {
+ background-color: #f1fafd;
+ color: #7fd0ed;
+}
+
+.btn-square {
+ display: inline-block;
+ width: 32px;
+ line-height: 15px;
+}
+
+.btn-sm.btn-square {
+ padding: 0;
+ width: 25px;
+ height: 25px;
+}
diff --git a/css/conversation.css b/css/conversation.css
index 961ceab1..e479f0ff 100644
--- a/css/conversation.css
+++ b/css/conversation.css
@@ -1,6 +1,7 @@
.conversation {
+ box-sizing: border-box;
position: relative;
- max-width: 400px;
+ min-height: 64px;
margin: auto;
padding: 1em;
border-radius: 10px;
@@ -47,9 +48,12 @@
}
.conversation .header {
- padding: 0.3em 0.6em 0.3em 46px;
+ padding: 0.3em 0 0.3em 46px;
}
-.avatar {
+.conversation .btn.destroy {
+ float: right;
+}
+.conversation .image {
position: absolute;
top: 8px;
left: 10px;
@@ -69,18 +73,20 @@
.collapsable {
background-color: #fff;
border: 2px solid #acdbf5;
- padding: 1em 0em;
+ padding: 1em 0em 0em;
line-height: 1.2em;
font-family: sans-serif;
- border-radius: 30px;
+ border-radius: 20px 0;
}
-.messages + form {
- text-align: right;
+.collapsable form {
+ margin: 0;
+ padding: 1em;
}
-
-.conversation form {
- margin-top: 0.5em;
+.collapsable input[type=text] {
+ box-sizing: border-box;
+ width: 100%;
+ border: none;
}
.message-text {
diff --git a/css/forms.css b/css/forms.css
index 6d203151..a6378d43 100644
--- a/css/forms.css
+++ b/css/forms.css
@@ -1,7 +1,7 @@
input[type=text], textarea {
position: relative;
display: inline-block;
- padding: 7px;
+ padding: 0.5em;
border: 2px solid #7fd0ed;
border-radius: 4px;
background-color: #fafafa;
@@ -9,6 +9,7 @@ input[type=text], textarea {
}
+input[type=submit]:focus,
input[type=text]:focus {
outline: 2px dashed #acdbf5;
outline-offset: 2px;
diff --git a/css/popup.css b/css/popup.css
index bf29813f..1ef45a78 100644
--- a/css/popup.css
+++ b/css/popup.css
@@ -35,22 +35,19 @@ 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] {
+#compose-cancel {
float: right;
}
+#send .conversation {
+ padding: 0.3em 1em;
+}
+
#popup_send_numbers {
margin-bottom: 0;
}
@@ -95,5 +92,19 @@ ul {
li {
list-style: none;
}
-
-/* Formatting */
+#send_link ~ #new-chat-help,
+#new-group ~ #new-group-help {
+ display: none;
+}
+#send_link:hover ~ #new-chat-help,
+#new-group:hover ~ #new-group-help {
+ display: block;
+}
+.help {
+ display: inline-block;
+ position: fixed;
+ top: 10;
+ right: 10;
+ font-size: 0.8em;
+ color: #7fd0ed;
+}
diff --git a/js/models/messages.js b/js/models/messages.js
index d6c90cda..76cae14b 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -4,8 +4,18 @@ var Whisper = Whisper || {};
'use strict';
var Message = Backbone.Model.extend({
+ validate: function(attributes, options) {
+ var required = ['body', 'timestamp', 'threadId'];
+ var missing = _.filter(required, function(attr) { return !attributes[attr]; });
+ if (missing.length) { return "Message must have " + missing; }
+ },
+
toProto: function() {
return new textsecure.protos.PushMessageContentProtobuf({body: this.get('body')});
+ },
+
+ thread: function() {
+ return Whisper.Threads.get(this.get('threadId'));
}
});
@@ -20,28 +30,36 @@ var Whisper = Whisper || {};
for (var i = 0; i < decrypted.message.attachments.length; i++)
attachments[i] = "data:" + decrypted.message.attachments[i].contentType + ";base64," + btoa(getString(decrypted.message.attachments[i].decrypted));
+ var thread = Whisper.Threads.findOrCreateForIncomingMessage(decrypted);
var m = Whisper.Messages.add({
person: decrypted.pushMessage.source,
- group: decrypted.message.group,
+ threadId: thread.id,
body: decrypted.message.body,
attachments: attachments,
type: 'incoming',
timestamp: decrypted.message.timestamp
});
m.save();
+
+ if (decrypted.message.timestamp > thread.get('timestamp')) {
+ thread.set('timestamp', decrypted.message.timestamp);
+ thread.set('unreadCount', thread.get('unreadCount') + 1);
+ thread.save();
+ }
+ thread.trigger('message', m);
return m;
},
- addOutgoingMessage: function(message, recipients) {
+ addOutgoingMessage: function(message, thread) {
var m = Whisper.Messages.add({
- person: recipients[0], // TODO: groups
+ threadId: thread.id,
body: message,
type: 'outgoing',
timestamp: new Date().getTime()
});
m.save();
+ thread.trigger('message', m);
return m;
}
}))();
-
})()
diff --git a/js/models/threads.js b/js/models/threads.js
new file mode 100644
index 00000000..d947e9e5
--- /dev/null
+++ b/js/models/threads.js
@@ -0,0 +1,94 @@
+var Whisper = Whisper || {};
+
+(function () {
+ 'use strict';
+
+ var Thread = Backbone.Model.extend({
+ defaults: function() {
+ return {
+ image: '/images/default.png',
+ unreadCount: 0,
+ timestamp: new Date().getTime()
+ };
+ },
+
+ validate: function(attributes, options) {
+ var required = ['id', 'type', 'recipients', 'timestamp', 'image', 'name'];
+ var missing = _.filter(required, function(attr) { return !attributes[attr]; });
+ if (missing.length) { return "Thread must have " + missing; }
+ if (attributes.recipients.length === 0) {
+ return "No recipients for thread " + this.id;
+ }
+ for (var person in attributes.recipients) {
+ if (!person) return "Invalid recipient";
+ }
+ },
+
+ sendMessage: function(message) {
+ return new Promise(function(resolve) {
+ var m = Whisper.Messages.addOutgoingMessage(message, this);
+ textsecure.sendMessage(this.get('recipients'), m.toProto(),
+ function(result) {
+ console.log(result);
+ resolve();
+ }
+ );
+ }.bind(this));
+ },
+
+ messages: function() {
+ return Whisper.Messages.where({threadId: this.id});
+ },
+ });
+
+ Whisper.Threads = new (Backbone.Collection.extend({
+ localStorage: new Backbone.LocalStorage("Threads"),
+ model: Thread,
+ comparator: 'timestamp',
+ findOrCreate: function(attributes) {
+ var thread = Whisper.Threads.add(attributes, {merge: true});
+ thread.save();
+ return thread;
+ },
+
+ findOrCreateForRecipients: function(recipients) {
+ var attributes = {};
+ if (recipients.length > 1) {
+ attributes = {
+ //TODO group id formatting?
+ name : recipients,
+ recipients : recipients,
+ type : 'group',
+ };
+ } else {
+ attributes = {
+ id : recipients[0],
+ name : recipients[0],
+ recipients : recipients,
+ type : 'private',
+ };
+ }
+ return this.findOrCreate(attributes);
+ },
+
+ findOrCreateForIncomingMessage: function(decrypted) {
+ var attributes = {};
+ if (decrypted.message.group) {
+ attributes = {
+ id : decrypted.message.group.id,
+ name : decrypted.message.group.name,
+ recipients : decrypted.message.group.members,
+ type : 'group',
+ };
+ } else {
+ attributes = {
+ id : decrypted.pushMessage.source,
+ name : decrypted.pushMessage.source,
+ recipients : [decrypted.pushMessage.source],
+ type : 'private'
+ };
+ }
+ return this.findOrCreate(attributes);
+ }
+ }))();
+})();
diff --git a/js/popup.js b/js/popup.js
index ba98c1b8..87ec8fc2 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -14,49 +14,16 @@
* along with this program. If not, see