From f060c9b3ff4569166eb6a4380037eca1de53014a Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 11 Apr 2019 14:11:26 -0400 Subject: [PATCH] UI: improvements to footer nav for app and PWAs Adds support for iPad and Chrome PWAs Better scroll direction logic when reaching bottom of the page --- ...mobile-footer.js.es6 => footer-nav.js.es6} | 22 +++++---- .../discourse/controllers/application.js.es6 | 10 ++-- .../discourse/lib/utilities.js.es6 | 9 ++++ .../mixins/mobile-scroll-direction.js.es6 | 46 +++++++++++-------- .../discourse/templates/application.hbs | 4 +- ...le-footer-nav.js.es6 => footer-nav.js.es6} | 19 ++++++-- .../components/footer-nav.scss} | 30 ++++++++---- .../common/foundation/variables.scss | 4 +- app/assets/stylesheets/mobile.scss | 1 - 9 files changed, 97 insertions(+), 48 deletions(-) rename app/assets/javascripts/discourse/components/{mobile-footer.js.es6 => footer-nav.js.es6} (85%) rename app/assets/javascripts/discourse/widgets/{mobile-footer-nav.js.es6 => footer-nav.js.es6} (74%) rename app/assets/stylesheets/{mobile/footer.scss => common/components/footer-nav.scss} (57%) diff --git a/app/assets/javascripts/discourse/components/mobile-footer.js.es6 b/app/assets/javascripts/discourse/components/footer-nav.js.es6 similarity index 85% rename from app/assets/javascripts/discourse/components/mobile-footer.js.es6 rename to app/assets/javascripts/discourse/components/footer-nav.js.es6 index cecfbd47a03..3fe473baabe 100644 --- a/app/assets/javascripts/discourse/components/mobile-footer.js.es6 +++ b/app/assets/javascripts/discourse/components/footer-nav.js.es6 @@ -5,14 +5,14 @@ import { observes } from "ember-addons/ember-computed-decorators"; const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150; -const MobileFooterComponent = MountWidget.extend( +const FooterNavComponent = MountWidget.extend( Scrolling, MobileScrollDirection, { - widget: "mobile-footer-nav", + widget: "footer-nav", mobileScrollDirection: null, scrollEventDisabled: false, - classNames: ["mobile-footer", "visible"], + classNames: ["footer-nav", "visible"], routeHistory: [], currentRouteIndex: 0, canGoBack: false, @@ -28,20 +28,22 @@ const MobileFooterComponent = MountWidget.extend( didInsertElement() { this._super(...arguments); - this.bindScrolling({ name: "mobile-footer" }); - $(window).on("resize.mobile-footer-on-scroll", () => this.scrolled()); + this.bindScrolling({ name: "footer-nav" }); + $(window).on("resize.footer-nav-on-scroll", () => this.scrolled()); this.appEvents.on("page:changed", this, "_routeChanged"); this.appEvents.on("composer:opened", this, "_composerOpened"); this.appEvents.on("composer:closed", this, "_composerClosed"); + $("body").addClass("with-footer-nav"); }, willDestroyElement() { this._super(...arguments); - this.unbindScrolling("mobile-footer"); - $(window).unbind("resize.mobile-footer-on-scroll"); + this.unbindScrolling("footer-nav"); + $(window).unbind("resize.footer-nav-on-scroll"); this.appEvents.off("page:changed", this, "_routeChanged"); this.appEvents.off("composer:opened", this, "_composerOpened"); this.appEvents.off("composer:closed", this, "_composerClosed"); + $("body").removeClass("with-footer-nav"); }, // The user has scrolled the window, or it is finished rendering and ready for processing. @@ -75,7 +77,7 @@ const MobileFooterComponent = MountWidget.extend( ); // body class used to adjust positioning of #topic-progress-wrapper $("body").toggleClass( - "mobile-footer-nav-visible", + "footer-nav-visible", this.mobileScrollDirection === null ? true : false ); }, @@ -119,10 +121,10 @@ const MobileFooterComponent = MountWidget.extend( setBackForward() { let index = this.get("currentRouteIndex"); - this.set("canGoBack", index > 1 ? true : false); + this.set("canGoBack", (index > 1 || document.referrer) ? true : false); this.set("canGoForward", index < this.routeHistory.length ? true : false); } } ); -export default MobileFooterComponent; +export default FooterNavComponent; diff --git a/app/assets/javascripts/discourse/controllers/application.js.es6 b/app/assets/javascripts/discourse/controllers/application.js.es6 index 62ef324792e..12459fd2882 100644 --- a/app/assets/javascripts/discourse/controllers/application.js.es6 +++ b/app/assets/javascripts/discourse/controllers/application.js.es6 @@ -1,5 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; -import { isAppWebview, isiOSPWA } from "discourse/lib/utilities"; +import { isAppWebview, isiOSPWA, isChromePWA } from "discourse/lib/utilities"; export default Ember.Controller.extend({ showTop: true, @@ -20,7 +20,11 @@ export default Ember.Controller.extend({ }, @computed - showMobileFooterNav() { - return isAppWebview() || isiOSPWA(); + showFooterNav() { + return ( + isAppWebview() || + isiOSPWA() || + (!this.site.isMobileDevice && isChromePWA()) + ); } }); diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 171378b723e..06f6cc40812 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -653,5 +653,14 @@ export function isiOSPWA() { export function isAppWebview() { return window.ReactNativeWebView !== undefined; } + +export function isChromePWA() { + // Watch out: this doesn't distinguish between mobile or desktop PWAs + return ( + window.matchMedia("(display-mode: standalone)").matches && + navigator.userAgent.match(/(Chrome)/g) + ); +} + // This prevents a mini racer crash export default {}; diff --git a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 index e22bed2e13e..3c389a141b6 100644 --- a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 +++ b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 @@ -2,11 +2,12 @@ const MOBILE_SCROLL_TOLERANCE = 5; export default Ember.Mixin.create({ - _mobileLastScroll: null, + _lastScroll: null, + _bottomHit: 0, calculateDirection(offset) { // Difference between this scroll and the one before it. - const delta = Math.floor(offset - this._mobileLastScroll); + const delta = Math.floor(offset - this._lastScroll); // This is a tiny scroll, so we ignore it. if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE) @@ -15,26 +16,35 @@ export default Ember.Mixin.create({ const prevDirection = this.mobileScrollDirection; const currDirection = delta > 0 ? "down" : null; - // Handle Safari overscroll first - if (offset < 0) { - this.set("mobileScrollDirection", null); - } else if (currDirection !== prevDirection) { - this.set("mobileScrollDirection", currDirection); - } - - // We store this to compare against it the next time the user scrolls - this._mobileLastScroll = Math.floor(offset); - - // If the user reaches the very bottom of the topic, we want to reset the - // scroll direction in order for the header to switch back. const distanceToBottom = Math.floor( $("body").height() - offset - $(window).height() ); - // Not at the bottom yet - if (distanceToBottom > 0) return; + // Handle Safari top overscroll first + if (offset < 0) { + this.set("mobileScrollDirection", null); + } else if (currDirection !== prevDirection && distanceToBottom > 0) { + this.set("mobileScrollDirection", currDirection); + } - // We're at the bottom now, so we reset the direction. - this.set("mobileScrollDirection", null); + // We store this to compare against it the next time the user scrolls + this._lastScroll = Math.floor(offset); + + // Not at the bottom yet + if (distanceToBottom > 0) { + this._bottomHit = 0; + return; + } + + // If the user reaches the very bottom of the topic, we only want to reset + // this scroll direction after a second scrolldown. This is a nicer event + // similar to what Safari and Chrome do. + Ember.run.debounce(() => { + this._bottomHit = 1; + }, 1000); + + if (this._bottomHit === 1) { + this.set("mobileScrollDirection", null); + } } }); diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index 8a4621398d4..ad010cbbf65 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -34,6 +34,6 @@ {{topic-entrance}} {{outlet "composer"}} -{{#if showMobileFooterNav}} - {{mobile-footer}} +{{#if showFooterNav}} + {{footer-nav}} {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/mobile-footer-nav.js.es6 b/app/assets/javascripts/discourse/widgets/footer-nav.js.es6 similarity index 74% rename from app/assets/javascripts/discourse/widgets/mobile-footer-nav.js.es6 rename to app/assets/javascripts/discourse/widgets/footer-nav.js.es6 index 0047d4d2b9b..b139daaa845 100644 --- a/app/assets/javascripts/discourse/widgets/mobile-footer-nav.js.es6 +++ b/app/assets/javascripts/discourse/widgets/footer-nav.js.es6 @@ -1,8 +1,8 @@ import { createWidget } from "discourse/widgets/widget"; -import { isAppWebview } from "discourse/lib/utilities"; +import { isAppWebview, isChromePWA } from "discourse/lib/utilities"; -createWidget("mobile-footer-nav", { - tagName: "div.mobile-footer-nav", +createWidget("footer-nav", { + tagName: "div.footer-nav-widget", html(attrs) { const buttons = []; @@ -43,6 +43,15 @@ createWidget("mobile-footer-nav", { ); } + if (isChromePWA()) { + buttons.push( + this.attach("flat-button", { + action: "refresh", + icon: "sync", + className: "btn-large" + }) + ); + } return buttons; }, @@ -54,5 +63,9 @@ createWidget("mobile-footer-nav", { window.ReactNativeWebView.postMessage( JSON.stringify({ shareUrl: window.location.href }) ); + }, + + refresh() { + window.location.reload(); } }); diff --git a/app/assets/stylesheets/mobile/footer.scss b/app/assets/stylesheets/common/components/footer-nav.scss similarity index 57% rename from app/assets/stylesheets/mobile/footer.scss rename to app/assets/stylesheets/common/components/footer-nav.scss index 95e4151e535..a4f2049702c 100644 --- a/app/assets/stylesheets/mobile/footer.scss +++ b/app/assets/stylesheets/common/components/footer-nav.scss @@ -1,38 +1,42 @@ // -------------------------------------------------- -// Mobile footer (displayed in DiscourseHub app and PWAs) +// Footer nav bar (displayed in DiscourseHub app and PWAs) // -------------------------------------------------- $footer-nav-height: 55px; -body.mobile-footer-nav-visible { +body.with-footer-nav { padding-bottom: $footer-nav-height + 15; +} + +body.footer-nav-visible { #topic-progress-wrapper, #reply-control.draft { bottom: $footer-nav-height; } } -.mobile-footer { - background-color: $header_background; - box-shadow: shadow("mobile-footer"); +.footer-nav { + background-color: rgba($header_background, 0.9); + box-shadow: shadow("footer-nav"); height: $footer-nav-height; position: fixed; bottom: -$footer-nav-height; left: 0; width: 100%; - z-index: z("mobile-footer"); - transition: all linear 0.15s; + z-index: z("footer-nav"); + transition: all linear 0.1s; .d-icon { - color: $header_primary-low-mid; + color: $header_primary-medium; } &.visible { bottom: 0px; + padding-bottom: env(safe-area-inset-bottom); } - .mobile-footer-nav { + .footer-nav-widget { display: flex; justify-content: "space-evenly"; @include unselectable; @@ -45,3 +49,11 @@ body.mobile-footer-nav-visible { } } } + +@supports (-webkit-backdrop-filter: blur(10px)) { + .footer-nav { + background-color: rgba($header_background, 0.7); + -webkit-backdrop-filter: blur(10px); + } +} + diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index e784213d53b..e79550cc2e7 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -80,7 +80,7 @@ $z-layers: ( "overlay": 1200 ), "fullscreen": 1150, - "mobile-footer": 1140, + "footer-nav": 1140, "mobile-composer": 1100, "header": 1000, "tooltip": 600, @@ -130,7 +130,7 @@ $box-shadow: ( "card": 0 4px 14px rgba(0, 0, 0, 0.15), "dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2), "header": 0 2px 4px -1px rgba(0, 0, 0, 0.25), - "mobile-footer": 0 2px 4px 1px rgba(0, 0, 0, 0.25), + "footer-nav": 0 2px 4px 1px rgba(0, 0, 0, 0.25), "kbd": ( 0 2px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px dark-light-choose(#fff, #000) inset diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index f1f4bcb6799..244c90102a9 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -30,7 +30,6 @@ @import "mobile/admin_report_counters"; @import "mobile/menu-panel"; @import "mobile/reviewables"; -@import "mobile/footer"; // Import all component-specific files @import "mobile/components/*";