- {enabled && (
-
- )}
+
{
expect(systemBarShell).toContain(
'var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))'
);
+ expect(systemBarShell).toContain(' {
+ const indexCss = readWorkspaceFile('src/index.css');
+ const indexTsx = readWorkspaceFile('src/index.tsx');
+ const iosPwaViewport = readWorkspaceFile('src/app/utils/iosPwaViewport.ts');
+
+ expect(indexCss).toContain('@media (display-mode: standalone)');
+ expect(indexCss).toContain('@supports (-webkit-touch-callout: none)');
+ expect(indexCss).toContain('var(--sable-ios-pwa-viewport-height, 100dvh)');
+ expect(indexTsx).toContain('installIosPwaViewportHeight();');
+ expect(iosPwaViewport).toContain("window.matchMedia('(display-mode: standalone)').matches");
+ expect(iosPwaViewport).toContain('const MIN_KEYBOARD_HEIGHT = 100');
+ expect(iosPwaViewport).toContain('const keyboardOpen = fullHeight - visibleHeight');
+ expect(iosPwaViewport).toContain('const height = keyboardOpen ? visibleBottom : fullHeight');
+ expect(iosPwaViewport).toContain('window.setTimeout(updateHeight, 350)');
+ });
+
it('removes the scattered safe-area css consumers', () => {
const indexCss = readWorkspaceFile('src/index.css');
const pageStyles = readWorkspaceFile('src/app/components/page/style.css.ts');
diff --git a/src/app/utils/iosPwaViewport.ts b/src/app/utils/iosPwaViewport.ts
new file mode 100644
index 000000000..326884273
--- /dev/null
+++ b/src/app/utils/iosPwaViewport.ts
@@ -0,0 +1,49 @@
+const IOS_PWA_VIEWPORT_HEIGHT = '--sable-ios-pwa-viewport-height';
+const MIN_KEYBOARD_HEIGHT = 100;
+
+const isStandaloneIosPwa = (): boolean =>
+ window.matchMedia('(display-mode: standalone)').matches &&
+ CSS.supports('-webkit-touch-callout: none');
+
+export function installIosPwaViewportHeight(): void {
+ if (!isStandaloneIosPwa()) return;
+
+ let frame = 0;
+ let settleTimer = 0;
+ let fullHeight = 0;
+ let viewportWidth = window.innerWidth;
+
+ const updateHeight = () => {
+ frame = 0;
+ const viewport = window.visualViewport;
+ const visibleHeight = viewport?.height ?? window.innerHeight;
+ const visibleBottom = visibleHeight + (viewport?.offsetTop ?? 0);
+
+ if (window.innerWidth !== viewportWidth) {
+ viewportWidth = window.innerWidth;
+ fullHeight = visibleBottom;
+ }
+
+ const keyboardOpen = fullHeight - visibleHeight > MIN_KEYBOARD_HEIGHT;
+ if (!keyboardOpen) fullHeight = Math.max(fullHeight, visibleBottom);
+
+ const height = keyboardOpen ? visibleBottom : fullHeight;
+ document.documentElement.style.setProperty(IOS_PWA_VIEWPORT_HEIGHT, `${Math.round(height)}px`);
+ };
+
+ const scheduleUpdate = () => {
+ if (frame) cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(updateHeight);
+
+ window.clearTimeout(settleTimer);
+ settleTimer = window.setTimeout(updateHeight, 350);
+ };
+
+ updateHeight();
+ window.addEventListener('resize', scheduleUpdate);
+ window.addEventListener('orientationchange', scheduleUpdate);
+ window.visualViewport?.addEventListener('resize', scheduleUpdate);
+ window.visualViewport?.addEventListener('scroll', scheduleUpdate);
+ document.addEventListener('focusin', scheduleUpdate);
+ document.addEventListener('focusout', scheduleUpdate);
+}
diff --git a/src/index.css b/src/index.css
index b5e730a37..a80a37d10 100755
--- a/src/index.css
+++ b/src/index.css
@@ -58,6 +58,16 @@ body {
flex-direction: column;
}
+@media (display-mode: standalone) {
+ @supports (-webkit-touch-callout: none) {
+ html,
+ body,
+ #root {
+ height: var(--sable-ios-pwa-viewport-height, 100dvh);
+ }
+ }
+}
+
*,
*::before,
*::after {
diff --git a/src/index.tsx b/src/index.tsx
index 7a292d816..036d41cbb 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -23,6 +23,7 @@ import { registerMatrixUriProtocol } from './app/plugins/matrix-uri';
import { initTauriMediaSession } from './app/utils/tauriMediaAuth';
import { registerAppServiceWorker } from './serviceWorkerBootstrap';
import { hasServiceWorker } from './app/utils/platform';
+import { installIosPwaViewportHeight } from './app/utils/iosPwaViewport';
enableMapSet();
installConsolePasteScamWarning();
@@ -30,6 +31,7 @@ registerMatrixUriProtocol();
const log = createLogger('index');
document.body.classList.add(configClass, varsClass);
+installIosPwaViewportHeight();
registerAppServiceWorker();