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
This commit is contained in:
Penar Musaraj 2019-04-11 14:11:26 -04:00
parent d21dd521d2
commit f060c9b3ff
9 changed files with 97 additions and 48 deletions

View File

@ -5,14 +5,14 @@ import { observes } from "ember-addons/ember-computed-decorators";
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150; const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150;
const MobileFooterComponent = MountWidget.extend( const FooterNavComponent = MountWidget.extend(
Scrolling, Scrolling,
MobileScrollDirection, MobileScrollDirection,
{ {
widget: "mobile-footer-nav", widget: "footer-nav",
mobileScrollDirection: null, mobileScrollDirection: null,
scrollEventDisabled: false, scrollEventDisabled: false,
classNames: ["mobile-footer", "visible"], classNames: ["footer-nav", "visible"],
routeHistory: [], routeHistory: [],
currentRouteIndex: 0, currentRouteIndex: 0,
canGoBack: false, canGoBack: false,
@ -28,20 +28,22 @@ const MobileFooterComponent = MountWidget.extend(
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
this.bindScrolling({ name: "mobile-footer" }); this.bindScrolling({ name: "footer-nav" });
$(window).on("resize.mobile-footer-on-scroll", () => this.scrolled()); $(window).on("resize.footer-nav-on-scroll", () => this.scrolled());
this.appEvents.on("page:changed", this, "_routeChanged"); this.appEvents.on("page:changed", this, "_routeChanged");
this.appEvents.on("composer:opened", this, "_composerOpened"); this.appEvents.on("composer:opened", this, "_composerOpened");
this.appEvents.on("composer:closed", this, "_composerClosed"); this.appEvents.on("composer:closed", this, "_composerClosed");
$("body").addClass("with-footer-nav");
}, },
willDestroyElement() { willDestroyElement() {
this._super(...arguments); this._super(...arguments);
this.unbindScrolling("mobile-footer"); this.unbindScrolling("footer-nav");
$(window).unbind("resize.mobile-footer-on-scroll"); $(window).unbind("resize.footer-nav-on-scroll");
this.appEvents.off("page:changed", this, "_routeChanged"); this.appEvents.off("page:changed", this, "_routeChanged");
this.appEvents.off("composer:opened", this, "_composerOpened"); this.appEvents.off("composer:opened", this, "_composerOpened");
this.appEvents.off("composer:closed", this, "_composerClosed"); 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. // 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 class used to adjust positioning of #topic-progress-wrapper
$("body").toggleClass( $("body").toggleClass(
"mobile-footer-nav-visible", "footer-nav-visible",
this.mobileScrollDirection === null ? true : false this.mobileScrollDirection === null ? true : false
); );
}, },
@ -119,10 +121,10 @@ const MobileFooterComponent = MountWidget.extend(
setBackForward() { setBackForward() {
let index = this.get("currentRouteIndex"); 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); this.set("canGoForward", index < this.routeHistory.length ? true : false);
} }
} }
); );
export default MobileFooterComponent; export default FooterNavComponent;

View File

@ -1,5 +1,5 @@
import computed from "ember-addons/ember-computed-decorators"; 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({ export default Ember.Controller.extend({
showTop: true, showTop: true,
@ -20,7 +20,11 @@ export default Ember.Controller.extend({
}, },
@computed @computed
showMobileFooterNav() { showFooterNav() {
return isAppWebview() || isiOSPWA(); return (
isAppWebview() ||
isiOSPWA() ||
(!this.site.isMobileDevice && isChromePWA())
);
} }
}); });

View File

@ -653,5 +653,14 @@ export function isiOSPWA() {
export function isAppWebview() { export function isAppWebview() {
return window.ReactNativeWebView !== undefined; 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 // This prevents a mini racer crash
export default {}; export default {};

View File

@ -2,11 +2,12 @@
const MOBILE_SCROLL_TOLERANCE = 5; const MOBILE_SCROLL_TOLERANCE = 5;
export default Ember.Mixin.create({ export default Ember.Mixin.create({
_mobileLastScroll: null, _lastScroll: null,
_bottomHit: 0,
calculateDirection(offset) { calculateDirection(offset) {
// Difference between this scroll and the one before it. // 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. // This is a tiny scroll, so we ignore it.
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE) if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
@ -15,26 +16,35 @@ export default Ember.Mixin.create({
const prevDirection = this.mobileScrollDirection; const prevDirection = this.mobileScrollDirection;
const currDirection = delta > 0 ? "down" : null; 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( const distanceToBottom = Math.floor(
$("body").height() - offset - $(window).height() $("body").height() - offset - $(window).height()
); );
// Not at the bottom yet // Handle Safari top overscroll first
if (distanceToBottom > 0) return; 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. // We store this to compare against it the next time the user scrolls
this.set("mobileScrollDirection", null); 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);
}
} }
}); });

View File

@ -34,6 +34,6 @@
{{topic-entrance}} {{topic-entrance}}
{{outlet "composer"}} {{outlet "composer"}}
{{#if showMobileFooterNav}} {{#if showFooterNav}}
{{mobile-footer}} {{footer-nav}}
{{/if}} {{/if}}

View File

@ -1,8 +1,8 @@
import { createWidget } from "discourse/widgets/widget"; import { createWidget } from "discourse/widgets/widget";
import { isAppWebview } from "discourse/lib/utilities"; import { isAppWebview, isChromePWA } from "discourse/lib/utilities";
createWidget("mobile-footer-nav", { createWidget("footer-nav", {
tagName: "div.mobile-footer-nav", tagName: "div.footer-nav-widget",
html(attrs) { html(attrs) {
const buttons = []; 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; return buttons;
}, },
@ -54,5 +63,9 @@ createWidget("mobile-footer-nav", {
window.ReactNativeWebView.postMessage( window.ReactNativeWebView.postMessage(
JSON.stringify({ shareUrl: window.location.href }) JSON.stringify({ shareUrl: window.location.href })
); );
},
refresh() {
window.location.reload();
} }
}); });

View File

@ -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; $footer-nav-height: 55px;
body.mobile-footer-nav-visible { body.with-footer-nav {
padding-bottom: $footer-nav-height + 15; padding-bottom: $footer-nav-height + 15;
}
body.footer-nav-visible {
#topic-progress-wrapper, #topic-progress-wrapper,
#reply-control.draft { #reply-control.draft {
bottom: $footer-nav-height; bottom: $footer-nav-height;
} }
} }
.mobile-footer { .footer-nav {
background-color: $header_background; background-color: rgba($header_background, 0.9);
box-shadow: shadow("mobile-footer"); box-shadow: shadow("footer-nav");
height: $footer-nav-height; height: $footer-nav-height;
position: fixed; position: fixed;
bottom: -$footer-nav-height; bottom: -$footer-nav-height;
left: 0; left: 0;
width: 100%; width: 100%;
z-index: z("mobile-footer"); z-index: z("footer-nav");
transition: all linear 0.15s; transition: all linear 0.1s;
.d-icon { .d-icon {
color: $header_primary-low-mid; color: $header_primary-medium;
} }
&.visible { &.visible {
bottom: 0px; bottom: 0px;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
.mobile-footer-nav { .footer-nav-widget {
display: flex; display: flex;
justify-content: "space-evenly"; justify-content: "space-evenly";
@include unselectable; @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);
}
}

View File

@ -80,7 +80,7 @@ $z-layers: (
"overlay": 1200 "overlay": 1200
), ),
"fullscreen": 1150, "fullscreen": 1150,
"mobile-footer": 1140, "footer-nav": 1140,
"mobile-composer": 1100, "mobile-composer": 1100,
"header": 1000, "header": 1000,
"tooltip": 600, "tooltip": 600,
@ -130,7 +130,7 @@ $box-shadow: (
"card": 0 4px 14px rgba(0, 0, 0, 0.15), "card": 0 4px 14px rgba(0, 0, 0, 0.15),
"dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2), "dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2),
"header": 0 2px 4px -1px rgba(0, 0, 0, 0.25), "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": ( "kbd": (
0 2px 0 rgba(0, 0, 0, 0.2), 0 2px 0 rgba(0, 0, 0, 0.2),
0 0 0 1px dark-light-choose(#fff, #000) inset 0 0 0 1px dark-light-choose(#fff, #000) inset

View File

@ -30,7 +30,6 @@
@import "mobile/admin_report_counters"; @import "mobile/admin_report_counters";
@import "mobile/menu-panel"; @import "mobile/menu-panel";
@import "mobile/reviewables"; @import "mobile/reviewables";
@import "mobile/footer";
// Import all component-specific files // Import all component-specific files
@import "mobile/components/*"; @import "mobile/components/*";