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 549d49c3..fea17ba1 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() { @@ -139,6 +182,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()); @@ -156,7 +206,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); } } @@ -205,4 +255,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 b4089d9b..9670afad 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 => {