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 <n0-0ne+github@mailbox.org>
This commit is contained in:
Johannes Marbach 2021-03-16 19:34:34 +01:00
parent f4bb95f459
commit 14ed5fd1e8
5 changed files with 69 additions and 7 deletions

View file

@ -351,7 +351,11 @@ async function buildCssLegacy(entryPath, urlMapper = null) {
const preCss = await fs.readFile(entryPath, "utf8"); const preCss = await fs.readFile(entryPath, "utf8");
const options = [ const options = [
postcssImport, postcssImport,
cssvariables(), cssvariables({
preserve: (declaration) => {
return declaration.value.indexOf("var(--ios-") == 0;
}
}),
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}), autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
flexbugsFixes() flexbugsFixes()
]; ];

View file

@ -35,6 +35,7 @@ import {WorkerPool} from "./dom/WorkerPool.js";
import {BlobHandle} from "./dom/BlobHandle.js"; import {BlobHandle} from "./dom/BlobHandle.js";
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js"; import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
import {downloadInIframe} from "./dom/download.js"; import {downloadInIframe} from "./dom/download.js";
import {Disposables} from "../../utils/Disposables.js";
function addScript(src) { function addScript(src) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@ -83,6 +84,44 @@ async function loadOlmWorker(config) {
return olmWorker; 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 { export class Platform {
constructor(container, config, cryptoExtras = null, options = null) { constructor(container, config, cryptoExtras = null, options = null) {
this._config = config; this._config = config;
@ -115,6 +154,10 @@ export class Platform {
} }
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
this.isIE11 = isIE11; 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() { get updateService() {
@ -135,6 +178,13 @@ export class Platform {
if (this.isIE11) { if (this.isIE11) {
this._container.className += " legacy"; 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; window.__hydrogenViewModel = vm;
const view = new RootView(vm); const view = new RootView(vm);
this._container.appendChild(view.mount()); this._container.appendChild(view.mount());
@ -152,7 +202,7 @@ export class Platform {
if (navigator.msSaveBlob) { if (navigator.msSaveBlob) {
navigator.msSaveBlob(blobHandle.nativeBlob, filename); navigator.msSaveBlob(blobHandle.nativeBlob, filename);
} else { } 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() { get version() {
return window.HYDROGEN_VERSION; return window.HYDROGEN_VERSION;
} }
dispose() {
this._disposables.dispose();
}
} }

View file

@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// From https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885 export async function downloadInIframe(container, iframeSrc, blobHandle, filename, isIOS) {
const isIOS = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) && !window.MSStream;
export async function downloadInIframe(container, iframeSrc, blobHandle, filename) {
let iframe = container.querySelector("iframe.downloadSandbox"); let iframe = container.querySelector("iframe.downloadSandbox");
if (!iframe) { if (!iframe) {
iframe = document.createElement("iframe"); iframe = document.createElement("iframe");

View file

@ -54,6 +54,13 @@ main {
min-width: 0; 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 */ /* hide back button in middle section by default */
.middle .close-middle { display: none; } .middle .close-middle { display: none; }
/* mobile layout */ /* mobile layout */

View file

@ -40,7 +40,7 @@ function viewClassForEntry(entry) {
export class TimelineList extends ListView { export class TimelineList extends ListView {
constructor(viewModel) { constructor(viewModel) {
const options = { const options = {
className: "Timeline", className: "Timeline bottom-aligned-scroll",
list: viewModel.tiles, list: viewModel.tiles,
} }
super(options, entry => { super(options, entry => {