Add search field to inbox

Using the search field produces a filtered view of all contacts and
groups containing the input. To make this fast and scalable, add an
index on a 'tokens' array containing words from the conversation name
and different forms of phone number.

Closes #365

// FREEBIE
This commit is contained in:
lilia 2015-10-15 12:10:03 -07:00
parent 7414828bb3
commit f70c22f898
8 changed files with 221 additions and 9 deletions

View file

@ -15,10 +15,13 @@
</div>
<div class='gutter'>
<div class='tool-bar clearfix'>
<input type='text' class='search' placeholder='Search'>
<button class='show-new-conversation'></button>
</div>
<div class='conversations scrollable'></div>
<span class='fab'></span>
<div class='conversations scrollable inbox'></div>
<div class='conversations scrollable search-results hide'>
<div class='new-contact contact hide'></div>
</div>
</div>
<div class='conversation-stack'></div>
</script>
@ -327,6 +330,7 @@
<script type="text/javascript" src="js/views/group_member_list_view.js"></script>
<script type="text/javascript" src="js/views/conversation_view.js"></script>
<script type="text/javascript" src="js/views/new_conversation_view.js"></script>
<script type="text/javascript" src="js/views/conversation_search_view.js"></script>
<script type="text/javascript" src="js/views/window_controls_view.js"></script>
<script type="text/javascript" src="js/views/inbox_view.js"></script>
<script type="text/javascript" src="js/views/confirmation_dialog_view.js"></script>

View file

@ -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();
});
});
}
}
];
}());

View file

@ -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: {

View file

@ -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);
},

View file

@ -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]*$/);
}
});
})();

View file

@ -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();
},

View file

@ -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 {

View file

@ -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; }