diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c41337eee42a..fc9e2308a61b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3,6 +3,18 @@ "message": "You left the group", "description": "Displayed when a user can't send a message because they have left the group" }, + "scrollDown": { + "message": "Scroll to bottom of conversation", + "description": "Alt text for button to take user down to bottom of conversation, shown when user scrolls up" + }, + "messageBelow": { + "message": "New message below", + "description": "Alt text for button to take user down to bottom of conversation with a new message out of screen" + }, + "messagesBelow": { + "message": "New messages below", + "description": "Alt text for button to take user down to bottom of conversation with more than one message out of screen" + }, "unreadMessage": { "message": "1 unread message", "description": "Text for unread message separator, just one message" diff --git a/background.html b/background.html index 242975df78c0..0df988dbc69b 100644 --- a/background.html +++ b/background.html @@ -42,6 +42,11 @@ + + diff --git a/images/down.svg b/images/down.svg new file mode 100644 index 000000000000..090f09cadd7d --- /dev/null +++ b/images/down.svg @@ -0,0 +1 @@ + diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 2fc33327595c..16c950c8fa0a 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -26,6 +26,7 @@ this.$('.menu-list').toggle(); } }); + var TimerMenuView = MenuView.extend({ initialize: function() { this.render(); @@ -151,10 +152,14 @@ 'click .back': 'resetPanel', 'click .microphone': 'captureAudio', 'click .disappearing-messages': 'enableDisappearingMessages', + 'click .scroll-down-button-view': 'scrollToBottom', 'focus .send-message': 'focusBottomBar', 'change .file-input': 'toggleMicrophone', 'blur .send-message': 'unfocusBottomBar', 'loadMore .message-list': 'fetchMessages', + 'newOffscreenMessage .message-list': 'addScrollDownButtonWithCount', + 'atBottom .message-list': 'hideScrollDownButton', + 'farFromBottom .message-list': 'addScrollDownButton', 'close .menu': 'closeMenu', 'select .message-list .entry': 'messageDetail', 'force-resize': 'forceUpdateMessageFieldSize', @@ -218,6 +223,35 @@ } }, + addScrollDownButtonWithCount: function() { + this.updateScrollDownButton(1); + }, + + addScrollDownButton: function() { + if (!this.scrollDownButton) { + this.updateScrollDownButton(); + } + }, + + updateScrollDownButton: function(count) { + if (this.scrollDownButton) { + this.scrollDownButton.increment(count); + } else { + this.scrollDownButton = new Whisper.ScrollDownButtonView({count: count}); + this.scrollDownButton.render(); + var container = this.$('.discussion-container'); + console.log('showscrollDownButton', container); + container.append(this.scrollDownButton.el); + } + }, + + hideScrollDownButton: function() { + if (this.scrollDownButton) { + this.scrollDownButton.remove(); + this.scrollDownButton = null; + } + }, + removeLastSeenIndicator: function() { if (this.lastSeenIndicator) { this.lastSeenIndicator.remove(); @@ -225,6 +259,10 @@ } }, + scrollToBottom: function() { + this.view.scrollToBottom(); + }, + updateLastSeenIndicator: function() { this.removeLastSeenIndicator(); @@ -239,6 +277,10 @@ unreadEl.insertBefore(this.$('#' + oldestUnread.get('id'))); var position = unreadEl[0].scrollIntoView(true); + + if (this.view.bottomOffset === 0) { + this.addScrollDownButtonWithCount(unreadCount); + } } }, diff --git a/js/views/message_list_view.js b/js/views/message_list_view.js index c1be8b40d105..1656e9d2c899 100644 --- a/js/views/message_list_view.js +++ b/js/views/message_list_view.js @@ -17,18 +17,24 @@ if (this.$el.scrollTop() === 0) { this.$el.trigger('loadMore'); } + if (this.bottomOffset === 0) { + this.$el.trigger('atBottom'); + } else if (this.bottomOffset > this.outerHeight) { + this.$el.trigger('farFromBottom'); + } }, measureScrollPosition: function() { if (this.el.scrollHeight === 0) { // hidden return; } - this.scrollPosition = this.$el.scrollTop() + this.$el.outerHeight(); + this.outerHeight = this.$el.outerHeight(); + this.scrollPosition = this.$el.scrollTop() + this.outerHeight; this.scrollHeight = this.el.scrollHeight; this.shouldStickToBottom = this.scrollPosition === this.scrollHeight; if (this.shouldStickToBottom) { this.bottomOffset = 0; } else { - this.bottomOffset = this.scrollHeight - this.$el.scrollTop(); + this.bottomOffset = this.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight(); } }, resetScrollPosition: function() { @@ -36,10 +42,13 @@ }, scrollToBottomIfNeeded: function() { if (this.bottomOffset === 0) { - this.$el.scrollTop(this.el.scrollHeight); - this.measureScrollPosition(); + this.scrollToBottom(); } }, + scrollToBottom: function() { + this.$el.scrollTop(this.el.scrollHeight); + this.measureScrollPosition(); + }, addOne: function(model) { var view; if (model.isExpirationTimerUpdate()) { @@ -54,6 +63,10 @@ var index = this.collection.indexOf(model); this.measureScrollPosition(); + if (model.get('unread') && this.bottomOffset > 0) { + this.$el.trigger('newOffscreenMessage'); + } + if (index === this.collection.length - 1) { // add to the bottom. this.$el.append(view.el); diff --git a/js/views/scroll_down_button_view.js b/js/views/scroll_down_button_view.js new file mode 100644 index 000000000000..1b79b40328de --- /dev/null +++ b/js/views/scroll_down_button_view.js @@ -0,0 +1,39 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +(function () { + 'use strict'; + window.Whisper = window.Whisper || {}; + + Whisper.ScrollDownButtonView = Whisper.View.extend({ + className: 'scroll-down-button-view', + templateName: 'scroll-down-button-view', + + initialize: function(options) { + options = options || {}; + this.count = options.count || 0; + }, + + increment: function(count) { + count = count || 0; + this.count += count; + this.render(); + }, + + render_attributes: function() { + var cssClass = this.count > 0 ? 'new-messages' : ''; + + var moreBelow = i18n('scrollDown'); + if (this.count > 1) { + moreBelow = i18n('messagesBelow'); + } else if (this.count === 1) { + moreBelow = i18n('messageBelow'); + } + + return { + cssClass: cssClass, + moreBelow: moreBelow + }; + } + }); +})(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index aed6f3523b9f..27b28aeeae7e 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -696,3 +696,31 @@ li.entry .error-icon-container { background-color: $grey_l2; } } + +.discussion-container .scroll-down-button-view { + position: absolute; + right: 20px; + bottom: 10px; + + button { + height: 44px; + width: 44px; + border-radius: 22px; + text-align: center; + background-color: $grey_l; + border: none; + + .icon { + @include color-svg('/images/down.svg', $grey_l3); + height: 100%; + width: 100%; + } + + &.new-messages { + background-color: $grey_l4; + .icon { + @include color-svg('/images/down.svg', black); + } + } + } +} diff --git a/stylesheets/android-dark.scss b/stylesheets/android-dark.scss index f4af1ca8a394..7baa8d7131f9 100644 --- a/stylesheets/android-dark.scss +++ b/stylesheets/android-dark.scss @@ -207,4 +207,21 @@ $text-dark: #CCCCCC; margin: 2em; background-color: $grey-dark_l2; } + + .discussion-container .scroll-down-button-view { + button { + background-color: $grey_l4; + + .icon { + @include color-svg('/images/down.svg', black); + } + + &.new-messages { + background-color: $grey_l2; + .icon { + @include color-svg('/images/down.svg', $grey_l4); + } + } + } + } } diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index c64e71b7593f..01ca08d33628 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -1506,6 +1506,30 @@ li.entry .error-icon-container { margin: 1em; background-color: #d9d9d9; } +.discussion-container .scroll-down-button-view { + position: absolute; + right: 20px; + bottom: 10px; } + .discussion-container .scroll-down-button-view button { + height: 44px; + width: 44px; + border-radius: 22px; + text-align: center; + background-color: #f3f3f3; + border: none; } + .discussion-container .scroll-down-button-view button .icon { + -webkit-mask: url("/images/down.svg") no-repeat center; + -webkit-mask-size: 100%; + background-color: silver; + height: 100%; + width: 100%; } + .discussion-container .scroll-down-button-view button.new-messages { + background-color: #8d8d8d; } + .discussion-container .scroll-down-button-view button.new-messages .icon { + -webkit-mask: url("/images/down.svg") no-repeat center; + -webkit-mask-size: 100%; + background-color: black; } + .ios #header { height: 64px; border-bottom: 1px solid rgba(0, 0, 0, 0.05); @@ -2113,5 +2137,17 @@ li.entry .error-icon-container { .android-dark .message-list .last-seen-indicator-view .text { margin: 2em; background-color: #292929; } + .android-dark .discussion-container .scroll-down-button-view button { + background-color: #8d8d8d; } + .android-dark .discussion-container .scroll-down-button-view button .icon { + -webkit-mask: url("/images/down.svg") no-repeat center; + -webkit-mask-size: 100%; + background-color: black; } + .android-dark .discussion-container .scroll-down-button-view button.new-messages { + background-color: #d9d9d9; } + .android-dark .discussion-container .scroll-down-button-view button.new-messages .icon { + -webkit-mask: url("/images/down.svg") no-repeat center; + -webkit-mask-size: 100%; + background-color: #8d8d8d; } /*# sourceMappingURL=manifest.css.map */ diff --git a/test/index.html b/test/index.html index 98bdc7f4f779..5117255a00fe 100644 --- a/test/index.html +++ b/test/index.html @@ -16,6 +16,11 @@
+ + - @@ -581,8 +586,12 @@ + + + + diff --git a/test/views/last_seen_indicator_view_test.js b/test/views/last_seen_indicator_view_test.js index 04ddfa4da56c..d293c6ddeb82 100644 --- a/test/views/last_seen_indicator_view_test.js +++ b/test/views/last_seen_indicator_view_test.js @@ -2,7 +2,7 @@ * vim: ts=4:sw=4:expandtab */ describe('LastSeenIndicatorView', function() { - // TODO: in electron branch, wheere we have access to real i18n, test rendered HTML + // TODO: in electron branch, where we have access to real i18n, test rendered HTML it('renders provided count', function() { var view = new Whisper.LastSeenIndicatorView({count: 10}); diff --git a/test/views/scroll_down_button_view_test.js b/test/views/scroll_down_button_view_test.js new file mode 100644 index 000000000000..e830885ede5f --- /dev/null +++ b/test/views/scroll_down_button_view_test.js @@ -0,0 +1,40 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +describe('ScrollDownButtonView', function() { + // TODO: in electron branch, where we have access to real i18n, uncomment assertions against real strings + + it('renders with count = 0', function() { + var view = new Whisper.ScrollDownButtonView(); + view.render(); + assert.equal(view.count, 0); + // assert.match(view.$el.html(), /Scroll to bottom/); + }); + + it('renders with count = 1', function() { + var view = new Whisper.ScrollDownButtonView({count: 1}); + view.render(); + assert.equal(view.count, 1); + assert.match(view.$el.html(), /new-messages/); + // assert.match(view.$el.html(), /New message below/); + }); + + it('renders with count = 2', function() { + var view = new Whisper.ScrollDownButtonView({count: 2}); + view.render(); + assert.equal(view.count, 2); + + assert.match(view.$el.html(), /new-messages/); + // assert.match(view.$el.html(), /New messages below/); + }); + + it('increments count and re-renders', function() { + var view = new Whisper.ScrollDownButtonView(); + view.render(); + assert.equal(view.count, 0); + assert.notMatch(view.$el.html(), /new-messages/); + view.increment(1); + assert.equal(view.count, 1); + assert.match(view.$el.html(), /new-messages/); + }); +});