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:
parent
7414828bb3
commit
f70c22f898
8 changed files with 221 additions and 9 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}());
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
94
js/views/conversation_search_view.js
Normal file
94
js/views/conversation_search_view.js
Normal 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]*$/);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
|
@ -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();
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue