diff --git a/background.html b/background.html
index fa3edd17f..0ef363f73 100644
--- a/background.html
+++ b/background.html
@@ -15,10 +15,13 @@
@@ -327,6 +330,7 @@
+
diff --git a/js/database.js b/js/database.js
index 3924e609a..8f40c1a86 100644
--- a/js/database.js
+++ b/js/database.js
@@ -34,6 +34,21 @@
var items = transaction.db.createObjectStore("items");
next();
}
+ },
+ {
+ version: "2.0",
+ migrate: function(transaction, next) {
+ var conversations = transaction.objectStore("conversations");
+ conversations.createIndex("search", "tokens", { unique: false, multiEntry: true });
+
+ var all = new Whisper.ConversationCollection();
+ all.fetch().then(function() {
+ all.each(function(model) {
+ model.updateTokens();
+ model.save();
+ });
+ });
+ }
}
];
}());
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 9ae7e5112..7b0c8d407 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -42,6 +42,7 @@
conversation: this
});
+ this.on('change:id change:name', this.updateTokens);
this.on('change:avatar', this.updateAvatarUrl);
this.on('destroy', this.revokeAvatarUrl);
},
@@ -54,22 +55,33 @@
if (attributes.type !== 'private' && attributes.type !== 'group') {
return "Invalid conversation type: " + attributes.type;
}
+ },
+
+ updateTokens: function() {
+ var tokens = [];
+ var name = this.get('name');
+ if (typeof name === 'string') {
+ tokens = name.trim().toLowerCase().split(/[\s\-_\(\)\+]+/);
+ }
- // hack
if (this.isPrivate()) {
try {
this.id = libphonenumber.util.verifyNumber(this.id);
var number = libphonenumber.util.splitCountryCode(this.id);
+ var international_number = '' + number.country_code + number.national_number;
+ var national_number = '' + number.national_number;
this.set({
e164_number: this.id,
- national_number: '' + number.national_number,
- international_number: '' + number.country_code + number.national_number
+ national_number: national_number,
+ international_number: international_number
});
+ tokens = tokens.concat(national_number, international_number);
} catch(ex) {
return ex;
}
}
+ this.set({tokens: tokens});
},
sendMessage: function(body, attachments) {
@@ -332,6 +344,25 @@
}));
},
+ search: function(query) {
+ query = query.trim().toLowerCase();
+ if (query.length > 0) {
+ var lastCharCode = query.charCodeAt(query.length - 1);
+ var nextChar = String.fromCharCode(lastCharCode + 1);
+ var upper = query.slice(0, -1) + nextChar;
+ console.log('searching', query, ' -> ', upper);
+ return new Promise(function(resolve) {
+ this.fetch({
+ index: {
+ name: 'search', // 'search' index on tokens array
+ lower: query,
+ upper: upper
+ }
+ }).always(resolve);
+ }.bind(this));
+ }
+ },
+
fetchGroups: function(number) {
return this.fetch({
index: {
diff --git a/js/views/conversation_list_item_view.js b/js/views/conversation_list_item_view.js
index faba8a8f4..3597a86e4 100644
--- a/js/views/conversation_list_item_view.js
+++ b/js/views/conversation_list_item_view.js
@@ -18,13 +18,18 @@
initialize: function() {
this.listenTo(this.model, 'change', this.render); // auto update
this.listenTo(this.model, 'destroy', this.remove); // auto update
+ this.listenTo(this.model, 'opened', this.markSelected); // auto update
extension.windows.beforeUnload(function() {
this.stopListening();
}.bind(this));
},
- select: function(e) {
+ markSelected: function() {
this.$el.addClass('selected').siblings('.selected').removeClass('selected');
+ },
+
+ select: function(e) {
+ this.markSelected();
this.$el.trigger('select', this.model);
},
diff --git a/js/views/conversation_search_view.js b/js/views/conversation_search_view.js
new file mode 100644
index 000000000..9de916125
--- /dev/null
+++ b/js/views/conversation_search_view.js
@@ -0,0 +1,94 @@
+/*
+ * vim: ts=4:sw=4:expandtab
+ */
+(function () {
+ 'use strict';
+ window.Whisper = window.Whisper || {};
+
+ Whisper.ConversationSearchView = Whisper.View.extend({
+ className: 'conversation-search',
+ initialize: function(options) {
+ this.$input = options.input;
+ this.$new_contact = this.$('.new-contact');
+
+ this.typeahead = new Whisper.ConversationCollection();
+ // View to display the matched contacts from typeahead
+ this.typeahead_view = new Whisper.ConversationListView({
+ collection : new Whisper.ConversationCollection([], {
+ comparator: function(m) { return m.getTitle().toLowerCase(); }
+ })
+ });
+ this.$el.append(this.typeahead_view.el);
+ this.initNewContact();
+ //this.listenTo(this.collection, 'reset', this.filterContacts);
+
+ },
+
+ events: {
+ 'select .new-contact': 'createConversation',
+ 'select .contacts': 'open'
+ },
+
+ filterContacts: function(e) {
+ var query = this.$input.val();
+ if (query.length) {
+ if (this.maybeNumber(query)) {
+ this.new_contact_view.model.set('id', query);
+ this.new_contact_view.render().$el.show();
+ } else {
+ this.new_contact_view.$el.hide();
+ }
+ this.typeahead.search(query).then(function() {
+ this.typeahead_view.collection.reset(this.typeahead.models);
+ }.bind(this));
+ this.trigger('show');
+ } else {
+ this.resetTypeahead();
+ }
+ },
+
+ initNewContact: function() {
+ if (this.new_contact_view) {
+ this.new_contact_view.undelegateEvents();
+ this.new_contact_view.$el.hide();
+ }
+ // Creates a view to display a new contact
+ this.new_contact_view = new Whisper.ConversationListItemView({
+ el: this.$new_contact,
+ model: ConversationController.create({
+ type: 'private',
+ newContact: true
+ })
+ }).render();
+ },
+
+ createConversation: function() {
+ this.$el.trigger('open', this.new_contact_view.model);
+ this.initNewContact();
+ this.resetTypeahead();
+ },
+
+ open: function(e, conversation) {
+ this.$el.trigger('open', conversation);
+ },
+
+ reset: function() {
+ this.delegateEvents();
+ this.typeahead_view.delegateEvents();
+ this.new_contact_view.delegateEvents();
+ this.resetTypeahead();
+ },
+
+ resetTypeahead: function() {
+ this.new_contact_view.$el.hide();
+ this.$input.val('').focus();
+ this.typeahead_view.collection.reset([]);
+ this.trigger('hide');
+ },
+
+ maybeNumber: function(number) {
+ return number.match(/^\+?[0-9]*$/);
+ }
+ });
+
+})();
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index 61d996203..01aa7d815 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -67,6 +67,7 @@
});
});
conversation.markRead();
+ conversation.trigger('opened');
}
});
@@ -88,11 +89,30 @@
var inboxCollection = bg.getInboxCollection();
this.inboxListView = new Whisper.ConversationListView({
- el : this.$('.conversations'),
+ el : this.$('.inbox'),
collection : inboxCollection
}).render();
- this.inboxListView.listenTo(inboxCollection, 'add change:active_at', this.inboxListView.onChangeActiveAt);
+ this.inboxListView.listenTo(inboxCollection,
+ 'add change:active_at',
+ this.inboxListView.onChangeActiveAt);
+
+ this.searchView = new Whisper.ConversationSearchView({
+ el : this.$('.search-results'),
+ input : this.$('input.search')
+ });
+
+ this.searchView.$el.hide().insertAfter(this.inboxListView.el);
+
+ this.listenTo(this.searchView, 'hide', function() {
+ this.searchView.$el.hide();
+ this.inboxListView.$el.show();
+ });
+ this.listenTo(this.searchView, 'show', function() {
+ this.searchView.$el.show();
+ this.inboxListView.$el.hide();
+ });
+
new SocketView().render().$el.appendTo(this.$('.socket-status'));
@@ -109,9 +129,20 @@
'click .hamburger': 'toggleMenu',
'click .show-debug-log': 'showDebugLog',
'click .show-new-conversation': 'showCompose',
- 'select .gutter .contact': 'openConversation'
+ 'select .gutter .contact': 'openConversation',
+ 'input input.search': 'filterContacts'
+ },
+ filterContacts: function(e) {
+ this.searchView.filterContacts(e);
+ var input = this.$('input.search');
+ if (input.val().length > 0) {
+ input.addClass('active');
+ } else {
+ input.removeClass('active');
+ }
},
openConversation: function(e, conversation) {
+ conversation = ConversationController.create(conversation);
this.conversation_stack.open(conversation);
this.hideCompose();
},
diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss
index 730971a1e..9d4510304 100644
--- a/stylesheets/_index.scss
+++ b/stylesheets/_index.scss
@@ -106,6 +106,26 @@ input.search {
background-color: darken($grey_l, 3%);
}
}
+
+ input.search {
+ height: $header-height - 10px;
+ width: calc(100% - #{$header-height + 10px});
+ background: $grey_l;
+ margin: 5px;
+ padding: 5px;
+
+ &:before {
+ content: 'Search';
+ }
+
+ &.active, &:active, &:focus {
+ background: white;
+
+ &:before {
+ content: '';
+ }
+ }
+ }
}
.last-timestamp {
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index b7b0d4b40..8fed1d80b 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -462,6 +462,18 @@ input.search {
background: url("/images/pencil.png") no-repeat center center; }
.tool-bar button.show-new-conversation:hover {
background-color: #ebebeb; }
+.tool-bar input.search {
+ height: 26px;
+ width: calc(100% - 46px);
+ background: #f3f3f3;
+ margin: 5px;
+ padding: 5px; }
+ .tool-bar input.search:before {
+ content: 'Search'; }
+ .tool-bar input.search.active, .tool-bar input.search:active, .tool-bar input.search:focus {
+ background: white; }
+ .tool-bar input.search.active:before, .tool-bar input.search:active:before, .tool-bar input.search:focus:before {
+ content: ''; }
.last-timestamp {
font-size: smaller; }