diff --git a/index.html b/index.html
index 1b24e1dd..8ed841b0 100644
--- a/index.html
+++ b/index.html
@@ -35,8 +35,10 @@
+
+
diff --git a/js/views/conversation_list_item_view.js b/js/views/conversation_list_item_view.js
index 4b3ca2b2..de66b5a5 100644
--- a/js/views/conversation_list_item_view.js
+++ b/js/views/conversation_list_item_view.js
@@ -25,8 +25,7 @@ var Whisper = Whisper || {};
className: 'contact',
events: {
- 'click': 'open',
- 'click .checkbox': 'checkbox'
+ 'click': 'open'
},
initialize: function() {
this.template = $('#contact').html();
@@ -41,14 +40,6 @@ var Whisper = Whisper || {};
this.$el.trigger('open', {modelId: this.model.id});
},
- checkbox: function(e) {
- e.stopPropagation();
- this.$el.trigger('checkbox', {
- modelId: this.model.id,
- checked: e.target.checked
- });
- },
-
render: function() {
this.$el.html(
Mustache.render(this.template, {
diff --git a/js/views/file_input_view.js b/js/views/file_input_view.js
index 0362c84c..6ed3d3b3 100644
--- a/js/views/file_input_view.js
+++ b/js/views/file_input_view.js
@@ -25,6 +25,7 @@ var Whisper = Whisper || {};
this.$input = this.$el.find('input[type=file]');
this.modal = new Whisper.ModalView({el: $('#file-modal')});
this.thumb = new Whisper.AttachmentPreviewView();
+ this.$el.addClass('file-input');
},
events: {
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index 1e9230a4..987c833c 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -38,7 +38,7 @@
this.$el.addClass('loading');
this.conversations.fetchActive({reset: true}).then(function() {
this.$el.removeClass('loading');
- window.conversations = this.conversations;
+ window.conversations = this.conversations; // debug
}.bind(this));
extension.on('message', function() {
@@ -51,20 +51,12 @@
'click .back button': 'hideCompose',
'click .fab': 'showCompose',
'open #contacts': 'openConversation',
- 'open .contacts': 'openConversation',
- 'open .new-group-update-form': 'openConversation',
- 'open .new-contact': 'createConversation',
+ 'open .new-conversation .contacts': 'openConversation'
},
openConversation: function(e, data) {
bg.openConversation(data.modelId);
this.hideCompose();
},
- createConversation: function(e, data) {
- this.newConversationView.new_contact.model.save().then(function() {
- bg.openConversation(data.modelId);
- });
- this.hideCompose();
- },
showCompose: function() {
this.$fab.hide();
this.$contacts.hide();
diff --git a/js/views/new_conversation_view.js b/js/views/new_conversation_view.js
index e1e9a99f..392b760e 100644
--- a/js/views/new_conversation_view.js
+++ b/js/views/new_conversation_view.js
@@ -18,7 +18,7 @@ var Whisper = Whisper || {};
(function () {
'use strict';
- var typeahead = Backbone.TypeaheadCollection.extend({
+ var ContactsTypeahead = Backbone.TypeaheadCollection.extend({
typeaheadAttributes: [
'name',
'e164_number',
@@ -30,72 +30,160 @@ var Whisper = Whisper || {};
model: Whisper.Conversation
});
+ Whisper.ContactPillView = Backbone.View.extend({
+ tagName: 'span',
+ className: 'recipient',
+ events: {
+ 'click .remove': 'removeModel'
+ },
+ initialize: function() {
+ this.template = $('#contact_pill').html();
+ Mustache.parse(this.template);
+
+ var error = this.model.validate(this.model.attributes);
+ if (error) {
+ this.$el.addClass('error');
+ }
+ },
+ removeModel: function() {
+ this.$el.trigger('remove', {modelId: this.model.id});
+ this.remove();
+ },
+ render: function() {
+ this.$el.html(
+ Mustache.render(this.template, { name: this.model.getTitle() })
+ );
+ return this;
+ }
+ });
+
+ Whisper.RecipientListView = Whisper.ListView.extend({
+ itemView: Whisper.ContactPillView
+ });
+
Whisper.NewConversationView = Backbone.View.extend({
className: 'new-conversation',
initialize: function() {
this.template = $('#new-conversation').html();
Mustache.parse(this.template);
this.$el.html($(Mustache.render(this.template)));
- this.$input = this.$el.find('input.new-message');
this.$group_update = this.$el.find('.new-group-update-form');
+ this.$buttons = this.$el.find('.buttons');
+ this.$input = this.$el.find('input.new-message');
- this.typeahead_collection = new typeahead();
+ // Collection of contacts to match user input against
+ this.typeahead = new ContactsTypeahead();
+ this.typeahead.fetch({ conditions: { type: 'private' } });
+
+ // View to display the matched contacts from typeahead
this.typeahead_view = new Whisper.ConversationListView({
- collection : new Whisper.ConversationCollection({
+ collection : new Whisper.ConversationCollection([], {
comparator: function(m) { return m.getTitle(); }
- }),
- className: 'typeahead'
- });
-
- this.typeahead_view.$el.appendTo(this.$el.find('.contacts'));
- this.typeahead_collection.fetch({
- conditions: { type: 'private' }
+ })
});
+ this.$el.find('.contacts').append(this.typeahead_view.el);
+ // View to display a new contact
this.new_contact = new Whisper.ConversationListItemView({
model: new Whisper.Conversation({
active_at: null,
type: 'private'
})
}).render();
-
- this.newGroupUpdateView = new Whisper.NewGroupUpdateView({
- model: new Whisper.Conversation({ type: 'group' }),
- el: this.$group_update
- });
- this.group_members = new Whisper.ConversationCollection();
this.$el.find('.new-contact').append(this.new_contact.el);
+
+ // Group avatar file input
+ this.avatarInput = new Whisper.FileInputView({
+ el: this.$el.find('.group-avatar')
+ });
+
+ // Collection of recipients selected for the new message
+ this.recipients = new Whisper.ConversationCollection([], {
+ comparator: false
+ });
+ // View to display the selected recipients
+ new Whisper.RecipientListView({
+ collection: this.recipients,
+ el: this.$el.find('.recipients')
+ });
},
events: {
'change input.new-message': 'filterContacts',
'keyup input.new-message': 'filterContacts',
- 'checkbox .contact': 'updateGroup',
- 'click .create-group': 'createGroup'
+ 'open .new-contact': 'addNewRecipient',
+ 'open .contacts': 'addRecipient',
+ 'remove .recipient': 'removeRecipient',
+ 'click .create': 'create'
},
- updateGroup: function(e, data) {
+ addNewRecipient: function(e, data) {
+ this.new_contact.model.newContact = true; // hack
+ this.recipients.add(this.new_contact.model);
+ this.new_contact.model = new Whisper.Conversation({
+ active_at: null,
+ type: 'private'
+ });
+ this.resetTypeahead();
+ this.updateControls();
+ },
+
+ addRecipient: function(e, data) {
+ this.recipients.add(this.typeahead.remove(data.modelId));
+ this.filterContacts();
+ this.updateControls();
+ },
+
+ removeRecipient: function(e, data) {
+ var model = this.recipients.remove(data.modelId);
+ if (!model.newContact) { // hack
+ this.typeahead.add(model);
+ }
+ this.filterContacts();
+ this.updateControls();
+ },
+
+ updateControls: function() {
+ if (this.recipients.length > 0) {
+ this.$buttons.slideDown();
+ } else {
+ this.$buttons.slideUp();
+ }
+ if (this.recipients.length > 1) {
+ this.$group_update.slideDown();
+ } else {
+ this.$group_update.slideUp();
+ }
this.$input.focus();
- if (data.checked) {
- this.group_members.add({id: data.modelId});
+ },
+
+ create: function() {
+ if (this.recipients.length > 1) {
+ this.createGroup();
} else {
- this.group_members.remove({id: data.modelId});
- }
- this.group_members
- if (this.group_members.length) {
- this.$group_update.show();
- } else {
- this.$group_update.hide();
+ this.createConversation();
}
},
+ createConversation: function() {
+ var conversation = new Whisper.Conversation({
+ id: this.recipients.at(0).id,
+ type: 'private'
+ });
+ conversation.fetch().fail(function() {
+ if (conversation.save()) {
+ this.$el.trigger('open', { modelId: conversation.id });
+ }
+ });
+ },
+
createGroup: function() {
- return this.newGroupUpdateView.avatarInput.getFiles().then(function(avatarFiles) {
+ return this.avatarInput.getFiles().then(function(avatarFiles) {
var attributes = {
type: 'group',
name: this.$el.find('.new-group-update-form .name').val(),
avatar: avatarFiles[0],
- members: this.group_members.pluck('id')
+ members: this.recipients.pluck('id')
};
return textsecure.messaging.createGroup(
attributes.members, attributes.name, attributes.avatar
@@ -109,11 +197,18 @@ var Whisper = Whisper || {};
}.bind(this));
},
- reset: function() {
+ resetTypeahead: function() {
this.new_contact.$el.hide();
this.$input.val('').focus();
- this.typeahead_view.collection.reset(this.typeahead_collection.models);
- this.group_members.reset([]);
+ this.typeahead_view.collection.reset(this.typeahead.models);
+ },
+
+ reset: function() {
+ this.$buttons.hide();
+ this.$group_update.hide();
+ this.typeahead.add(this.recipients.models);
+ this.recipients.reset([]);
+ this.resetTypeahead();
},
filterContacts: function() {
@@ -121,15 +216,15 @@ var Whisper = Whisper || {};
if (query.length) {
if (this.maybeNumber(query)) {
this.new_contact.model.set('id', query);
- this.new_contact.$el.show();
+ this.new_contact.render().$el.show();
} else {
this.new_contact.$el.hide();
}
this.typeahead_view.collection.reset(
- this.typeahead_collection.typeahead(query)
+ this.typeahead.typeahead(query)
);
} else {
- this.reset();
+ this.resetTypeahead();
}
},
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
index 30545df2..14a364c3 100644
--- a/stylesheets/_conversation.scss
+++ b/stylesheets/_conversation.scss
@@ -180,54 +180,8 @@ button {
}
.attachments {
- position: relative;
float: left;
height: 100%;
- width: 36px;
- margin-right: 10px;
-
- .paperclip {
- width: 100%;
- height: 100%;
- background: url('/images/paperclip.png') no-repeat;
- background-size: 90%;
- background-position: center 6px;
- }
-
- input[type=file] {
- display: none;
- position: absolute;
- width: 100%;
- height: 100%;
- opacity: 0;
- top: 0;
- left: 0;
- cursor: pointer;
- z-index: 1;
- }
-
- img.preview {
- max-width: 100%;
- }
-
- .close {
- font-family: sans-serif;
- color: white;
- position: absolute;
- top: -10px;
- left: 20px;
- text-align: center;
- cursor: default;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- padding: 0px;
-
- background: #666;
- color: #fff;
- text-align: center;
- }
-
}
.send-btn {
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index 2d05d58a..fe3a0e01 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -10,6 +10,15 @@ body {
font-size: 14px;
}
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: " ";
+}
+.clearfix:after {
+ clear: both;
+}
+
#header {
position: fixed;
top: 0;
@@ -51,3 +60,51 @@ body {
}
}
}
+
+.file-input {
+ position: relative;
+ width: 36px;
+ margin-right: 10px;
+
+ .paperclip {
+ width: 100%;
+ height: 100%;
+ background: url('/images/paperclip.png') no-repeat;
+ background-size: 90%;
+ background-position: center 6px;
+ }
+
+ input[type=file] {
+ display: none;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+ z-index: 1;
+ }
+
+ img.preview {
+ max-width: 100%;
+ }
+
+ .close {
+ font-family: sans-serif;
+ color: white;
+ position: absolute;
+ top: -10px;
+ left: 20px;
+ text-align: center;
+ cursor: default;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ padding: 0px;
+
+ background: #666;
+ color: #fff;
+ text-align: center;
+ }
+}
diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss
index e1f7b49f..9a743318 100644
--- a/stylesheets/_index.scss
+++ b/stylesheets/_index.scss
@@ -10,13 +10,17 @@
// TODO: spinner
}
-.contact .checkbox {
- display: none;
+.contact {
+ .number, .checkbox {
+ display: none;
+ }
}
input.new-message {
- box-sizing: border-box;
- width: 100%;
+ border: none;
+ padding: 0;
+ margin: 0;
+ outline: 0;
}
.back {
@@ -39,8 +43,50 @@ input.new-message {
}
}
-.new-conversation .new-group-update-form {
- display: none;
+.new-conversation {
+ .new-group-update-form {
+ display: none;
+
+ button.create-group {
+ float: right;
+ }
+
+ .group-avatar {
+ float: left;
+ height: 36px;
+ }
+ }
+
+ .buttons {
+ display: none;
+ }
+}
+
+.new-conversation {
+ .recipients-container {
+ background-color: white;
+ padding: 2px;
+ border-bottom: 1px solid #f2f2f2;
+ line-height: 24px;
+ }
+
+ .recipient {
+ display: inline-block;
+ margin: 0 2px 2px 0;
+ padding: 0 5px;
+ border-radius: 10px;
+ background-color: $blue;
+ color: white;
+
+ &.error {
+ background-color: #f00;
+ }
+
+ .remove {
+ margin-left: 5px;
+ padding: 0 2px;
+ }
+ }
}
.fab {
@@ -70,23 +116,32 @@ input.new-message {
font-size: smaller;
}
-.new-contact,
-.typeahead {
+.new-conversation {
.last-message, .last-timestamp {
display: none;
}
- .contact .checkbox {
- display: inline-block;
+ .contact {
+ .checkbox, .number {
+ display: inline-block;
+ }
+
+ .number {
+ color: $grey;
+ font-size: small;
+ }
}
}
-.new-contact .contact-details::before {
- content: 'Create new contact';
- display: block;
- font-style: italic;
- opacity: 0.7;
- padding-right: 8px;
+.new-contact {
+ .contact-name { display: none; }
+ .contact-details::before {
+ content: 'Create new contact';
+ display: block;
+ font-style: italic;
+ opacity: 0.7;
+ padding-right: 8px;
+ }
}
.index {
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index 13f50add..6e0c8243 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -15,6 +15,14 @@ body {
font-family: Roboto, "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 14px; }
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: " "; }
+
+.clearfix:after {
+ clear: both; }
+
#header {
position: fixed;
top: 0;
@@ -50,18 +58,58 @@ body {
white-space: nowrap;
padding: 5px 15px 5px 10px; }
+.file-input {
+ position: relative;
+ width: 36px;
+ margin-right: 10px; }
+ .file-input .paperclip {
+ width: 100%;
+ height: 100%;
+ background: url("/images/paperclip.png") no-repeat;
+ background-size: 90%;
+ background-position: center 6px; }
+ .file-input input[type=file] {
+ display: none;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+ z-index: 1; }
+ .file-input img.preview {
+ max-width: 100%; }
+ .file-input .close {
+ font-family: sans-serif;
+ color: white;
+ position: absolute;
+ top: -10px;
+ left: 20px;
+ text-align: center;
+ cursor: default;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ padding: 0px;
+ background: #666;
+ color: #fff;
+ text-align: center; }
+
.gutter {
margin-top: 36px; }
#contacts {
overflow-y: scroll; }
-.contact .checkbox {
+.contact .number, .contact .checkbox {
display: none; }
input.new-message {
- box-sizing: border-box;
- width: 100%; }
+ border: none;
+ padding: 0;
+ margin: 0;
+ outline: 0; }
.back {
display: none;
@@ -81,6 +129,31 @@ input.new-message {
.new-conversation .new-group-update-form {
display: none; }
+ .new-conversation .new-group-update-form button.create-group {
+ float: right; }
+ .new-conversation .new-group-update-form .group-avatar {
+ float: left;
+ height: 36px; }
+.new-conversation .buttons {
+ display: none; }
+
+.new-conversation .recipients-container {
+ background-color: white;
+ padding: 2px;
+ border-bottom: 1px solid #f2f2f2;
+ line-height: 24px; }
+.new-conversation .recipient {
+ display: inline-block;
+ margin: 0 2px 2px 0;
+ padding: 0 5px;
+ border-radius: 10px;
+ background-color: #2a92e7;
+ color: white; }
+ .new-conversation .recipient.error {
+ background-color: #f00; }
+ .new-conversation .recipient .remove {
+ margin-left: 5px;
+ padding: 0 2px; }
.fab {
z-index: 1;
@@ -105,14 +178,16 @@ input.new-message {
.last-timestamp {
font-size: smaller; }
-.new-contact .last-message, .new-contact .last-timestamp,
-.typeahead .last-message,
-.typeahead .last-timestamp {
+.new-conversation .last-message, .new-conversation .last-timestamp {
display: none; }
-.new-contact .contact .checkbox,
-.typeahead .contact .checkbox {
+.new-conversation .contact .checkbox, .new-conversation .contact .number {
display: inline-block; }
+.new-conversation .contact .number {
+ color: #616161;
+ font-size: small; }
+.new-contact .contact-name {
+ display: none; }
.new-contact .contact-details::before {
content: 'Create new contact';
display: block;
@@ -297,44 +372,8 @@ button {
font-size: 24px;
background: transparent; }
.bottom-bar .attachments {
- position: relative;
float: left;
- height: 100%;
- width: 36px;
- margin-right: 10px; }
- .bottom-bar .attachments .paperclip {
- width: 100%;
- height: 100%;
- background: url("/images/paperclip.png") no-repeat;
- background-size: 90%;
- background-position: center 6px; }
- .bottom-bar .attachments input[type=file] {
- display: none;
- position: absolute;
- width: 100%;
- height: 100%;
- opacity: 0;
- top: 0;
- left: 0;
- cursor: pointer;
- z-index: 1; }
- .bottom-bar .attachments img.preview {
- max-width: 100%; }
- .bottom-bar .attachments .close {
- font-family: sans-serif;
- color: white;
- position: absolute;
- top: -10px;
- left: 20px;
- text-align: center;
- cursor: default;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- padding: 0px;
- background: #666;
- color: #fff;
- text-align: center; }
+ height: 100%; }
.bottom-bar .send-btn {
float: right;
height: 100%;