Commit 410d4ae025d39ac726357f5020b563cf604ba6f8

Authored by Jan Jongboom
1 parent 0a18f56785

Improve usability and speed

Showing 13 changed files with 133 additions and 74 deletions

app/index.html View file @ 410d4ae
... ... @@ -19,6 +19,8 @@
19 19 </div><!-- main page -->
20 20 </div><!-- main wrapper -->
21 21  
  22 + <div id="cached-elements"></div>
  23 +
22 24 <script data-main="scripts/main" src="components/requirejs/require.js">
23 25 </script>
24 26 </body>
app/scripts/localisations/translation.js View file @ 410d4ae
... ... @@ -314,6 +314,7 @@
314 314 migrationPleaseWait: 'Please wait',
315 315 conversationLastSeen: 'Last seen:',
316 316 conversationIsOnline: 'Online',
  317 + conversationIsTyping: 'Typing...',
317 318 loadingParticipants: 'Loading contacts…',
318 319 invite: 'Invite to WhatsApp',
319 320 tellAFriendText: 'Check out WhatsApp Messenger for Android, iPhone, ' +
app/scripts/models/background-service.js View file @ 410d4ae
... ... @@ -79,18 +79,24 @@
79 79 }
80 80  
81 81 var nextAlarm = new Date(Date.now() + awakePeriod);
82   - var alarmRequest = alarms.add(nextAlarm, 'ignoreTimezone');
83   - alarmRequest.onsuccess = function () {
84   - var alarmId = alarmRequest.result;
85   - localStorage.alarmId = alarmId;
86   - console.log(
87   - '[background service] Next awake signal with id',
88   - alarmId, 'by', nextAlarm.toString()
89   - );
90   - };
91   - alarmRequest.onerror = function () {
92   - console.error('Impossible to set an alarm to keep the program alive.');
93   - };
  82 + try {
  83 + var alarmRequest = alarms.add(nextAlarm, 'ignoreTimezone');
  84 + alarmRequest.onsuccess = function () {
  85 + var alarmId = alarmRequest.result;
  86 + localStorage.alarmId = alarmId;
  87 + console.log(
  88 + '[background service] Next awake signal with id',
  89 + alarmId, 'by', nextAlarm.toString()
  90 + );
  91 + };
  92 + alarmRequest.onerror = function () {
  93 + console.error(
  94 + 'Impossible to set an alarm to keep the program alive.');
  95 + };
  96 + }
  97 + catch (ex) {
  98 + console.error('Could not set new alarm');
  99 + }
94 100 }
95 101 });
96 102  
app/scripts/templates/conversation.hbs View file @ 410d4ae
... ... @@ -16,6 +16,7 @@
16 16 <h2 class="last-seen">{{translate 'conversationLastSeen'}} <time></time></h2>
17 17 <h2 class="is-online">{{translate 'conversationIsOnline'}}</h2>
18 18 <h2 class="participants">{{translate 'loadingParticipants'}}</h2>
  19 + <h2 class="is-typing">{{translate 'conversationIsTyping'}}</h2>
19 20 </header>
20 21 <div class="progressContainer">
21 22 <span>{{translate 'sendingImage'}}</span>
app/scripts/templates/helpers.js View file @ 410d4ae
... ... @@ -20,7 +20,7 @@
20 20 var day = null;
21 21  
22 22 if (now.toDateString() === date.toDateString()) {
23   - day = Helpers._translate('today');
  23 + day = '';
24 24 }
25 25 else if (yesterday.toDateString() === date.toDateString()) {
26 26 day = Helpers._translate('yesterday');
app/scripts/utils/contact-picker.js View file @ 410d4ae
... ... @@ -5,7 +5,7 @@
5 5 ], function (global, Contact, PhoneNumber) {
6 6 'use strict';
7 7  
8   - function pickContact(callback) {
  8 + function pickContact(onContact, confirmCallback) {
9 9 var pick = new window.MozActivity({
10 10 name: 'pick',
11 11 data: {
12 12  
13 13  
14 14  
15 15  
... ... @@ -14,27 +14,28 @@
14 14 });
15 15  
16 16 pick.onsuccess = function () {
17   - if (!this.result) { return callback(null, null); }
  17 + if (!this.result) { return confirmCallback(null, null); }
18 18  
19 19 var activityContact = this.result;
20 20 var phone = activityContact.number;
21   - global.client.confirmContacts([phone],
22   - function (err, details) {
23 21  
24   - var contact = new Contact({
25   - 'displayName': activityContact.name[0] || ''
26   - });
  22 + var parsed = PhoneNumber.parse(phone);
  23 + var msisdn = parsed ? parsed.full : phone;
27 24  
  25 + var contact = new Contact({
  26 + displayName: activityContact.name[0] || '',
  27 + id: msisdn,
  28 + confirmed: false,
  29 + phone: msisdn
  30 + });
  31 +
  32 + onContact(contact);
  33 +
  34 + global.client.confirmContacts([phone],
  35 + function (err, details) {
28 36 if (err) {
29 37 console.warn('The contact ' + phone + ' can not be confirmed.');
30   - var parsed = PhoneNumber.parse(phone);
31   - var msisdn = parsed ? parsed.full : phone;
32   - contact.set({
33   - 'id': msisdn,
34   - 'confirmed': false,
35   - 'phone': msisdn
36   - });
37   - return callback('contact-not-confirmed', contact);
  38 + return confirmCallback('contact-not-confirmed', contact);
38 39 }
39 40  
40 41 contact.set({
41 42  
... ... @@ -44,13 +45,13 @@
44 45 'phone': details[0].n
45 46 });
46 47  
47   - callback(null, contact);
  48 + confirmCallback(null, contact);
48 49 }
49 50 );
50 51 };
51 52  
52 53 pick.onerror = function (evt) {
53   - callback(evt.target.error);
  54 + confirmCallback(evt.target.error);
54 55 };
55 56 }
56 57  
app/scripts/views/compose-message.js View file @ 410d4ae
... ... @@ -17,7 +17,8 @@
17 17 events: {
18 18 'click #conversation-send-button' : '_createTextMessage',
19 19 'click #insert-emoji': '_toggleEmojiList',
20   - 'click #message-text-input': '_hideEmojiList'
  20 + 'click #message-text-input': '_hideEmojiList',
  21 + 'keydown #message-text-input': '_handleKeyDown'
21 22 },
22 23  
23 24 initialize: function () {
... ... @@ -36,6 +37,7 @@
36 37  
37 38 clear: function () {
38 39 this.stopListening();
  40 + this._emojiSelector.clear();
39 41 },
40 42  
41 43 render: function () {
42 44  
... ... @@ -47,10 +49,20 @@
47 49 this._emojiSelector = new EmojiSelector({
48 50 composer: this.$messageComposer.get(0)
49 51 });
50   - $('#conversation').after(this._emojiSelector.render().el);
  52 + var emojiEl = this._emojiSelector.render().el;
  53 + var convEl = $('#conversation')[0];
  54 + convEl.parentNode.insertBefore(emojiEl, convEl.nextSibling);
51 55 this._emojiSelector.changeTab('people');
52 56 },
53 57  
  58 + _handleKeyDown: function (event) {
  59 + console.log('handleKeydown', event.keyCode);
  60 + if (event.keyCode === 13) {
  61 + this._createTextMessage(event);
  62 + return false;
  63 + }
  64 + },
  65 +
54 66 _createTextMessage: function (event) {
55 67 event.preventDefault();
56 68 this._hideEmojiList();
... ... @@ -70,6 +82,14 @@
70 82 this.$messageComposer.html('<br>');
71 83 this._emojiSelector.clearCaretPosition();
72 84 this.trigger('compose:message:text', newModel);
  85 +
  86 + // workaround https://bugzilla.mozilla.org/show_bug.cgi?id=1059163
  87 + if (document.activeElement === this.$messageComposer[0]) {
  88 + this.$messageComposer[0].blur();
  89 + setTimeout(function () {
  90 + this.$messageComposer[0].focus();
  91 + }.bind(this));
  92 + }
73 93 },
74 94  
75 95 _emojiToUnicode: function (html) {
app/scripts/views/conversation.js View file @ 410d4ae
... ... @@ -37,11 +37,12 @@
37 37 'click a.location': 'showComposeLocation',
38 38 'click a.multimedia': 'showComposeImage',
39 39 'click form#conversation-compose button.back': 'hideComposeText',
40   - 'keyup input#message-text-input': 'sendTypingActive',
  40 + 'keyup #message-text-input': 'sendTypingActive',
41 41 'click li.location img': 'goToImageViewer',
42 42 'click header > a': 'goToInbox',
43 43 'contextmenu .messages': '_showContextMenu',
44   - 'contextmenu .page-wrapper': '_chooseWallpaper'
  44 + 'contextmenu .page-wrapper': '_chooseWallpaper',
  45 + 'focus #message-text-input': 'focusComposeText'
45 46 },
46 47  
47 48 initialize: function (options) {
... ... @@ -91,6 +92,7 @@
91 92 clearTimeout(this.participantsTimeout);
92 93 this.stopListening();
93 94 this.sendTypingIdle();
  95 + this.composeMessageView.clear();
94 96 if (this.imageCompose) { this.imageCompose.clear(); }
95 97 },
96 98  
... ... @@ -98,7 +100,6 @@
98 100 this.$el.html(this.template(this.model.toJSON()));
99 101 this._scrollView = this.$el.find('.page-wrapper').get(0);
100 102 $(this._scrollView).on('scroll', this._onScroll.bind(this));
101   -
102 103 if (this.model.get('isGroup')) {
103 104 this.$el.find('#conversation').addClass('group');
104 105 }
105 106  
... ... @@ -581,15 +582,13 @@
581 582  
582 583 _setContactTypingActive: function (from) {
583 584 if (from.msisdn === this.contact.get('phone')) {
584   - this.$el.find('ul.messages').removeClass('idle');
585   - this.$el.find('ul.messages').addClass('active');
  585 + this.$el.find('.connection-status').addClass('typing');
586 586 }
587 587 },
588 588  
589 589 _setContactTypingIdle: function (from) {
590 590 if (from.msisdn === this.contact.get('phone')) {
591   - this.$el.find('ul.messages').removeClass('active');
592   - this.$el.find('ul.messages').addClass('idle');
  591 + this.$el.find('.connection-status').removeClass('typing');
593 592 }
594 593 },
595 594  
596 595  
... ... @@ -654,9 +653,19 @@
654 653 this.$el.find('.progressContainer').css('display', 'none');
655 654 }
656 655 this.$el.find('.progressContainer progress.pack-activity').val(value);
  656 + },
  657 +
  658 + focusComposeText: function () {
  659 + var self = this;
  660 + // The keyboard will pop up after the focus event, and resize the window
  661 + // Now the scroll position should be set to the last message
  662 + window.addEventListener('resize', function resize() {
  663 + window.removeEventListener('resize', resize);
  664 + console.log('window.resize event fired, scrollToLastMessage');
  665 + self.scrollToLastMessage();
  666 + });
657 667 }
658 668 });
659   -
660 669 return Conversation;
661 670 });
app/scripts/views/emoji-selector.js View file @ 410d4ae
... ... @@ -7,10 +7,13 @@
7 7 ], function (Backbone, Handlebars, $, global, templates) {
8 8 'use strict';
9 9  
10   - var EmojiSelector = Backbone.View.extend({
  10 + var templateNode = document.createElement('div');
  11 + templateNode.innerHTML = templates['emoji-selector']();
  12 + templateNode.id = 'cached-emoji';
  13 + document.querySelector('#cached-elements').appendChild(templateNode);
  14 + templateNode = templateNode.firstChild;
11 15  
12   - template: templates['emoji-selector'],
13   -
  16 + var EmojiSelector = Backbone.View.extend({
14 17 events: {
15 18 'click ul.categories button': '_onTabClick',
16 19 'click ul.content': '_selectEmoji'
17 20  
... ... @@ -25,8 +28,13 @@
25 28 },
26 29  
27 30 render: function () {
28   - this.setElement(this.template());
  31 + this.setElement(templateNode);
29 32 return this;
  33 + },
  34 +
  35 + clear: function () {
  36 + // Move the template element back for further use
  37 + document.querySelector('#cached-emoji').appendChild(templateNode);
30 38 },
31 39  
32 40 _onTabClick: function (evt) {
app/scripts/views/inbox.js View file @ 410d4ae
... ... @@ -110,13 +110,17 @@
110 110 });
111 111 }
112 112  
  113 + // Prevent clicks leaking through
  114 + global.router.trigger('contextmenu:open');
113 115 if (window.confirm(message)) {
  116 + this.trigger('contextmenu:closed');
114 117 if (isGroup) {
115 118 global.client.leaveGroup(conversationId);
116 119 }
117 120 global.historyCollection.removeConversation(conversationId);
118 121 listitem.parentNode.removeChild(listitem);
119 122 }
  123 + global.router.trigger('contextmenu:close');
120 124 },
121 125  
122 126 _checkCentinelVisibility: function () {
123 127  
124 128  
... ... @@ -300,20 +304,19 @@
300 304 var _this = this;
301 305 event.preventDefault();
302 306  
303   - pickContact(function (err, contact) {
  307 + pickContact(function onContact(contact) {
  308 + _this._addPickedContact(contact);
  309 + }, function onConfirm(err, contact) {
304 310 if (err && err.name === 'canceled') {
305 311 return;
306 312 }
307 313  
308 314 if (err === 'contact-not-confirmed') {
309   - return _this._addPickedContact(contact);
  315 + return;
310 316 }
311 317  
312 318 if (!err && contact) {
313   - if (contact.get('confirmed')) {
314   - _this._addPickedContact(contact);
315   - }
316   - else {
  319 + if (!contact.get('confirmed')) {
317 320 _this._tellAFriend(contact.get('phone'));
318 321 }
319 322 } else {
320 323  
... ... @@ -324,16 +327,16 @@
324 327 },
325 328  
326 329 _addPickedContact: function (contact) {
327   - global.historyCollection.findOrCreate(contact.id, null,
328   - function (err, result) {
329   - var conversation = result.conversation;
330   - global.router.navigate(
331   - 'conversation/' + conversation.get('id'), { trigger: true });
332   - }
333   - );
  330 + global.router.navigate(
  331 + 'conversation/' + contact.get('id'), { trigger: true });
  332 +
  333 + console.time('addPickedContact');
  334 + global.historyCollection.findOrCreate(contact.id, null, function () { });
334 335 },
335 336  
336 337 _tellAFriend: function (number) {
  338 + global.router.navigate('inbox', { trigger: true });
  339 +
337 340 new window.MozActivity({
338 341 name: 'new',
339 342 data: {
app/scripts/views/mini-conversation.js View file @ 410d4ae
... ... @@ -22,6 +22,8 @@
22 22 'click li > a' : '_openConversation'
23 23 },
24 24  
  25 + contextmenuState: 'close',
  26 +
25 27 initialize: function () {
26 28 this.contactPhotoURL = null;
27 29 this.listenTo(this.model, 'change', this._refreshRender);
... ... @@ -29,6 +31,13 @@
29 31 if (messages) {
30 32 this.listenTo(messages, 'add', this._refreshRender);
31 33 }
  34 +
  35 + global.router.on('contextmenu:open', function () {
  36 + this.contextmenuState = 'open';
  37 + }.bind(this));
  38 + global.router.on('contextmenu:close', function () {
  39 + this.contextmenuState = 'close';
  40 + }.bind(this));
32 41 },
33 42  
34 43 clear: function () {
... ... @@ -71,6 +80,10 @@
71 80 },
72 81  
73 82 _openConversation: function (event) {
  83 + // if context menu is open we should ignore the click
  84 + if (this.contextmenuState === 'open') {
  85 + return false;
  86 + }
74 87 event.preventDefault();
75 88 global.router.navigate('conversation/' + this.model.get('id'),
76 89 {trigger: true});
app/styles/_common.sass View file @ 410d4ae
... ... @@ -146,4 +146,7 @@
146 146  
147 147 .not-square
148 148 color: #FF3939
  149 +
  150 +#cached-elements
  151 + display: none
app/styles/_conversation.sass View file @ 410d4ae
... ... @@ -37,6 +37,15 @@
37 37 #conversation > header.connection-status .hide
38 38 display: none
39 39  
  40 +#conversation > header.connection-status > h2.is-typing
  41 + display: none
  42 +
  43 +#conversation > header.connection-status.typing > h2
  44 + display: none !important
  45 +
  46 +#conversation > header.connection-status.typing > h2.is-typing
  47 + display: inherit !important
  48 +
40 49 #conversation > header h1 span.emoji
41 50 vertical-align: middle
42 51  
... ... @@ -55,23 +64,6 @@
55 64 #conversation ul.messages
56 65 padding: 0
57 66 pointer-events: none
58   - // idle & active indicator
59   - &:after
60   - content: ""
61   - display: block
62   - margin-top: 1rem
63   - width: 2.8rem
64   - height: 0.6rem
65   - content: '•••'
66   - font-size: 22px
67   - font-weight: bolder
68   - color: #979ca3
69   - letter-spacing: 3px
70   - opacity: 0
71   - transition: opacity 0.2 ease
72   - margin-left: 0.7rem
73   - &.active:after
74   - opacity: 1
75 67  
76 68 #conversation .participants
77 69 overflow: hidden