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
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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; }
|
||||
|
|
Loading…
Reference in a new issue