mobile: gateway handoff for Chat and Drive below 1024px; pin Hub theme in the SSO bootstrap

Panel consensus (the branding panel): don't mount full-app iframes on
mobile — nested scrolling, keyboard collapse and double chrome break both
Element and oCIS under the bottom tab bar. Chat and Drive render a gateway
card (same-tab handoff, matrix: deep link, signed-in-as) below 1024px; the
iframe stays on desktop. hub-sso.html pins mx_local_settings.theme to
custom-OccupyHub Dark once (version-flagged) — a device's earlier picker
choice would otherwise beat config.json's default_theme.
This commit is contained in:
glass beadian 2026-10-13 17:41:05 +00:00
parent 3463b0a4f4
commit c01d121748
3 changed files with 72 additions and 0 deletions

View file

@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { EmbeddedApp } from "../../../components/embed/embedded-app";
import { api, errorMessage } from "../../../lib/api-client";
import { useApiResource } from "../../../lib/use-api";
import { useIsMobile } from "../../../hooks/use-is-mobile";
/**
* In-app Chat: Element Web, full height inside the Hub shell, signed in as the
@ -59,6 +60,7 @@ function originOf(url: string): string | null {
export default function ChatPage(): React.ReactElement {
const identity = useApiResource<MatrixIdentity, MatrixIdentity | null>("/matrix/identity", identityView, null);
const isMobile = useIsMobile();
const frameRef = useRef<HTMLIFrameElement>(null);
const mints = useRef(0);
const tokenSent = useRef(false);
@ -127,6 +129,39 @@ export default function ChatPage(): React.ReactElement {
? ssoUser
: undefined;
if (isMobile) {
// Mobile: a framed three-pane Element under the bottom tab bar breaks
// (nested scrolling, keyboard collapse, double chrome) — hand off to the
// standalone app instead (panel consensus: gateway pattern).
return (
<div className="flex h-full flex-col justify-center px-6 py-10 text-center">
<h1 className="text-lg font-bold">Newsroom chat</h1>
<p className="mx-auto mt-2 max-w-xs text-xs leading-relaxed text-text-dim">
Chat runs best as its own app on mobile — the full three-pane interface gets cramped inside the Hub frame.
</p>
<div className="mx-auto mt-8 flex w-full max-w-xs flex-col gap-3">
<a
href={identity.error === null ? `${elementOrigin}/` : FALLBACK_CHAT_URL}
className="flex min-h-12 items-center justify-center rounded-xl bg-accent px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90"
>
Open chat ↗
</a>
{ssoUser !== null && (
<a
href={`matrix:u/${ssoUser.slice(1)}`}
className="flex min-h-12 items-center justify-center rounded-xl border border-border bg-surface px-4 text-sm font-medium text-text transition-colors hover:border-accent"
>
Open in a Matrix app
</a>
)}
</div>
<p className="mt-4 text-2xs text-text-faint">
{ssoUser !== null ? `Signed in as ${ssoUser}` : "Sign in on desktop to link your account."}
</p>
</div>
);
}
return (
<EmbeddedApp
title="Chat"

View file

@ -3,6 +3,7 @@
import { EmbeddedApp } from "../../../../components/embed/embedded-app";
import { useFilesLang } from "../../../../components/files/i18n";
import { useApiResource } from "../../../../lib/use-api";
import { useIsMobile } from "../../../../hooks/use-is-mobile";
/**
* Drive — the oCIS web UI (personal space, shares, office documents) embedded
@ -29,6 +30,7 @@ const driveView = (session: DriveSession): DriveSession | null => session;
export default function DrivePage(): React.ReactElement {
const { t } = useFilesLang();
const drive = useApiResource<DriveSession, DriveSession | null>("/drive/session", driveView, null);
const isMobile = useIsMobile();
const driveUrl = drive.source === "sample" ? FALLBACK_DRIVE_URL : drive.data?.driveUrl ?? null;
const notConfigured = drive.source === "live" && drive.error === null && driveUrl === null;
@ -43,6 +45,26 @@ export default function DrivePage(): React.ReactElement {
es: "Inicia sesión una vez dentro de la unidad y quedará abierta. Los archivos de los colectivos están en la pestaña Biblioteca.",
});
if (isMobile) {
// Mobile: the oCIS file browser under the bottom tab bar breaks (nested
// scrolling, double chrome) — hand off to the standalone drive instead
// (panel consensus: gateway pattern). The Library tab is fully native.
return (
<div className="flex h-full flex-col justify-center px-6 py-10 text-center">
<h1 className="text-lg font-bold">{t({ en: "Drive", es: "Unidad" })}</h1>
<p className="mx-auto mt-2 max-w-xs text-xs leading-relaxed text-text-dim">{error ?? notice}</p>
<div className="mx-auto mt-8 w-full max-w-xs">
<a
href={`${driveUrl ?? FALLBACK_DRIVE_URL}/`}
className="flex min-h-12 items-center justify-center rounded-xl bg-accent px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90"
>
{t({ en: "Open drive ↗", es: "Abrir unidad ↗" })}
</a>
</div>
</div>
);
}
return (
<EmbeddedApp
title={t({ en: "Drive", es: "Unidad" })}

View file

@ -31,6 +31,21 @@
// Element and the homeserver share this host (/_matrix is proxied here).
var HOMESERVER = location.origin;
// Pin the Hub theme before any handover. config.json's default_theme only
// applies to devices that never picked a theme: Element stores the choice
// in mx_local_settings on THIS origin, and this bootstrap page is the one
// place the Hub can set it. Version-flagged so a later choice in Element's
// theme picker is respected.
try {
if (localStorage.getItem("tmbh_theme_pin_v1") !== "1") {
var raw = localStorage.getItem("mx_local_settings");
var settings = raw ? JSON.parse(raw) : {};
settings.theme = "custom-OccupyHub Dark";
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
localStorage.setItem("tmbh_theme_pin_v1", "1");
}
} catch (e) {}
function openElement(query) {
location.replace("/" + query);
}