From 14ed5fd1e863c3916906e5256df7415e3386ff3c Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 16 Mar 2021 19:34:34 +0100 Subject: [PATCH] Manually adapt UI when keyboard shows or hides on mobile Safari Mobile Safari seems to be the only browser that does *not* resize the viewport when the keyboard shows and hides. Instead the window is moved to make room for the keyboard which moves content at the top off screen. This uses the VisualViewport API to manually resize the `SessionView` in response to keyboard display events. Additionally, if a DOM element exists that has the `bottom-aligned-scroll` CSS class, its scroll position is retained. Currently this only applies to the `Timeline`. Note that the VisualViewport API was only introduced with iOS 13. According to [statista.com], versions below 13 made up for 19% of all iOS users in summer 2020, with the share continuing to fall off. As a result, this seems like an acceptable workaround. Fixes: #181 [statista.com]: https://www.statista.com/statistics/565270/apple-devices-ios-version-share-worldwide/ Signed-off-by: Johannes Marbach --- scripts/build.mjs | 6 +- src/platform/web/Platform.js | 56 ++++++++++++++++++- src/platform/web/dom/download.js | 5 +- src/platform/web/ui/css/layout.css | 7 +++ .../web/ui/session/room/TimelineList.js | 2 +- 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 44d58fe6..8e3eb6c6 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -351,7 +351,11 @@ async function buildCssLegacy(entryPath, urlMapper = null) { const preCss = await fs.readFile(entryPath, "utf8"); const options = [ postcssImport, - cssvariables(), + cssvariables({ + preserve: (declaration) => { + return declaration.value.indexOf("var(--ios-") == 0; + } + }), autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}), flexbugsFixes() ]; diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 747855de..49b9df35 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -35,6 +35,7 @@ import {WorkerPool} from "./dom/WorkerPool.js"; import {BlobHandle} from "./dom/BlobHandle.js"; import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js"; import {downloadInIframe} from "./dom/download.js"; +import {Disposables} from "../../utils/Disposables.js"; function addScript(src) { return new Promise(function (resolve, reject) { @@ -83,6 +84,44 @@ async function loadOlmWorker(config) { return olmWorker; } +// needed for mobile Safari which shifts the layout viewport up without resizing it +// when the keyboard shows (see https://bugs.webkit.org/show_bug.cgi?id=141832) +function adaptUIOnVisualViewportResize(container) { + if (!window.visualViewport) { + return; + } + const handler = () => { + const sessionView = container.querySelector('.SessionView'); + if (!sessionView) { + return; + } + + const scrollable = container.querySelector('.bottom-aligned-scroll'); + let scrollTopBefore, heightBefore, heightAfter; + + if (scrollable) { + scrollTopBefore = scrollable.scrollTop; + heightBefore = scrollable.offsetHeight; + } + + // Ideally we'd use window.visualViewport.offsetTop but that seems to occasionally lag + // behind (last tested on iOS 14.4 simulator) so we have to compute the offset manually + const offsetTop = sessionView.offsetTop + sessionView.offsetHeight - window.visualViewport.height; + + container.style.setProperty('--ios-viewport-height', window.visualViewport.height.toString() + 'px'); + container.style.setProperty('--ios-viewport-top', offsetTop.toString() + 'px'); + + if (scrollable) { + heightAfter = scrollable.offsetHeight; + scrollable.scrollTop = scrollTopBefore + heightBefore - heightAfter; + } + }; + window.visualViewport.addEventListener('resize', handler); + return () => { + window.visualViewport.removeEventListener('resize', handler); + }; +} + export class Platform { constructor(container, config, cryptoExtras = null, options = null) { this._config = config; @@ -115,6 +154,10 @@ export class Platform { } const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; this.isIE11 = isIE11; + // From https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885 + const isIOS = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) && !window.MSStream; + this.isIOS = isIOS; + this._disposables = new Disposables(); } get updateService() { @@ -135,6 +178,13 @@ export class Platform { if (this.isIE11) { this._container.className += " legacy"; } + if (this.isIOS) { + this._container.className += " ios"; + const disposable = adaptUIOnVisualViewportResize(this._container); + if (disposable) { + this._disposables.track(disposable); + } + } window.__hydrogenViewModel = vm; const view = new RootView(vm); this._container.appendChild(view.mount()); @@ -152,7 +202,7 @@ export class Platform { if (navigator.msSaveBlob) { navigator.msSaveBlob(blobHandle.nativeBlob, filename); } else { - downloadInIframe(this._container, this._config.downloadSandbox, blobHandle, filename); + downloadInIframe(this._container, this._config.downloadSandbox, blobHandle, filename, this.isIOS); } } @@ -201,4 +251,8 @@ export class Platform { get version() { return window.HYDROGEN_VERSION; } + + dispose() { + this._disposables.dispose(); + } } diff --git a/src/platform/web/dom/download.js b/src/platform/web/dom/download.js index 41d3c29d..2476d3df 100644 --- a/src/platform/web/dom/download.js +++ b/src/platform/web/dom/download.js @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// From https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885 -const isIOS = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) && !window.MSStream; - -export async function downloadInIframe(container, iframeSrc, blobHandle, filename) { +export async function downloadInIframe(container, iframeSrc, blobHandle, filename, isIOS) { let iframe = container.querySelector("iframe.downloadSandbox"); if (!iframe) { iframe = document.createElement("iframe"); diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index 9917ca74..765bff3c 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -54,6 +54,13 @@ main { min-width: 0; } +/* resize and reposition session view to account for mobile Safari which shifts +the layout viewport up without resizing it when the keyboard shows */ +.hydrogen.ios .SessionView { + height: var(--ios-viewport-height, 100%); + top: var(--ios-viewport-top, 0); +} + /* hide back button in middle section by default */ .middle .close-middle { display: none; } /* mobile layout */ diff --git a/src/platform/web/ui/session/room/TimelineList.js b/src/platform/web/ui/session/room/TimelineList.js index 414649be..791e23a3 100644 --- a/src/platform/web/ui/session/room/TimelineList.js +++ b/src/platform/web/ui/session/room/TimelineList.js @@ -40,7 +40,7 @@ function viewClassForEntry(entry) { export class TimelineList extends ListView { constructor(viewModel) { const options = { - className: "Timeline", + className: "Timeline bottom-aligned-scroll", list: viewModel.tiles, } super(options, entry => {