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%;