fix(mobile-guide): German HealthChat guide for phones, proven in Chromium

Phones and tablets were bounced to the healthchat.ch website, where the
visitor had no way back into the app. They now land on the mobile guide
again, which pointed at Element X and spoke English while the rest of
HealthChat web is German.

The guide carries the HealthChat store links and German copy as static
HTML, not only via JS: Safari reads the Smart App Banner meta tag at
parse time. A charset meta is required now that the page has umlauts,
since nginx sends text/html without one. Step 2 and the F-Droid line
stay hidden because HealthChat has neither a mobile.element.io deep link
nor an F-Droid build.

Verified in Chromium at phone width with iPhone and Android user agents:
redirect, store links, the way back via the cookie, and the no-JS view.
This commit is contained in:
2026-10-25 09:49:42 +01:00
parent dcfd82b773
commit b1e36ff959
6 changed files with 78 additions and 72 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ import ToastStore from "../stores/ToastStore";
import SdkConfig from "../SdkConfig";
const onAccept = (): void => {
window.location.href = "https://healthchat.ch";
window.location.href = "mobile_guide/";
};
const onReject = (): void => {
+1 -1
View File
@@ -149,7 +149,7 @@ async function start(): Promise<void> {
const isAndroid = /Android/.test(navigator.userAgent);
if (isIos || isAndroid) {
if (document.cookie.indexOf("element_mobile_redirect_to_guide=false") === -1) {
window.location.href = "https://healthchat.ch";
window.location.href = "mobile_guide/";
return;
}
}
@@ -20,31 +20,31 @@
<feGaussianBlur stdDeviation="16.3415" result="effect1_foregroundBlur_382_1634"/>
</filter>
<linearGradient id="paint0_linear_382_1634" x1="141.636" y1="133.796" x2="141.636" y2="32.6829" gradientUnits="userSpaceOnUse">
<stop stop-color="#062993"/>
<stop offset="0.040404" stop-color="#02389D"/>
<stop offset="0.0808081" stop-color="#0045A6"/>
<stop offset="0.121212" stop-color="#0051AD"/>
<stop offset="0.161616" stop-color="#005DB4"/>
<stop offset="0.20202" stop-color="#0069BA"/>
<stop offset="0.242424" stop-color="#0075BB"/>
<stop offset="0.282828" stop-color="#0081BB"/>
<stop offset="0.323232" stop-color="#008CB9"/>
<stop offset="0.363636" stop-color="#0098B7"/>
<stop offset="0.40404" stop-color="#00A3B3"/>
<stop offset="0.444444" stop-color="#00AEAD"/>
<stop offset="0.484848" stop-color="#00B8A4"/>
<stop offset="0.525253" stop-color="#00C2A0"/>
<stop offset="0.565657" stop-color="#00CC99"/>
<stop offset="0.606061" stop-color="#3AD396"/>
<stop offset="0.646465" stop-color="#5DD898"/>
<stop offset="0.686869" stop-color="#79DD99"/>
<stop offset="0.727273" stop-color="#92E29B"/>
<stop offset="0.767677" stop-color="#A8E69F"/>
<stop offset="0.808081" stop-color="#BBEAA5"/>
<stop offset="0.848485" stop-color="#CDEEAE"/>
<stop offset="0.888889" stop-color="#DCF2B9"/>
<stop offset="0.929293" stop-color="#EAF6C7"/>
<stop offset="0.969697" stop-color="#F5FBD5"/>
<stop stop-color="#12325C"/>
<stop offset="0.040404" stop-color="#143561"/>
<stop offset="0.0808081" stop-color="#153966"/>
<stop offset="0.121212" stop-color="#173C6C"/>
<stop offset="0.161616" stop-color="#194071"/>
<stop offset="0.20202" stop-color="#1A4376"/>
<stop offset="0.242424" stop-color="#1C467B"/>
<stop offset="0.282828" stop-color="#1D4A80"/>
<stop offset="0.323232" stop-color="#1F4D85"/>
<stop offset="0.363636" stop-color="#225187"/>
<stop offset="0.40404" stop-color="#255588"/>
<stop offset="0.444444" stop-color="#28588A"/>
<stop offset="0.484848" stop-color="#2B5C8B"/>
<stop offset="0.525253" stop-color="#2E608D"/>
<stop offset="0.565657" stop-color="#31638E"/>
<stop offset="0.606061" stop-color="#346790"/>
<stop offset="0.646465" stop-color="#3B6D93"/>
<stop offset="0.686869" stop-color="#527E9F"/>
<stop offset="0.727273" stop-color="#698FAB"/>
<stop offset="0.767677" stop-color="#81A0B8"/>
<stop offset="0.808081" stop-color="#98B1C4"/>
<stop offset="0.848485" stop-color="#AFC2D0"/>
<stop offset="0.888889" stop-color="#C6D3DC"/>
<stop offset="0.929293" stop-color="#DEE4E9"/>
<stop offset="0.969697" stop-color="#F5F5F5"/>
</linearGradient>
<clipPath id="clip0_382_1634">
<rect width="375" height="178" fill="white"/>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+31 -24
View File
@@ -3,11 +3,12 @@
Please note: This page is similar to the one at mobile.element.io so any changes made here can potentially
be applied to that site as well. They still share the mobile-apps.ts file for updating the app metadata.
-->
<html lang="en">
<html lang="de">
<head>
<title>Element Mobile Guide</title>
<meta charset="utf-8" />
<title>HealthChat für Smartphone und Tablet</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-itunes-app" content="app-id=id1631335820" />
<meta name="apple-itunes-app" content="app-id=id6743060078" />
</head>
<body>
@@ -19,10 +20,15 @@
<header>
<div class="mx_HomePage_header">
<div>
<img src="./assets/element-logo.svg" alt="Element Logo" width="148px" height="32px" />
<img
src="../themes/element/img/logos/element-logo.png"
alt="HealthChat Logo"
width="32px"
height="32px"
/>
</div>
<div>
<p id="header_title">Join Element.</p>
<p id="header_title">HealthChat auf dem Smartphone</p>
</div>
</div>
<hr />
@@ -31,8 +37,8 @@
<main>
<div class="mx_HomePage_col mx_Spacer">
<p>
The <a id="back_to_element_button" href="#" class="mx_DesktopLink">desktop site</a> does
<b>not</b> work on mobile, please download the app.
Die Web-Version ist für den Desktop gedacht und funktioniert auf Smartphone und Tablet
<b>nicht</b>. Für Smartphone und Tablet gibt es die HealthChat-App.
</p>
</div>
<div class="mx_HomePage_col mx_Spacer">
@@ -41,44 +47,38 @@
<span>1</span>
</div>
<div>
<h1 id="step1_heading">Download Element</h1>
<h1 id="step1_heading">HealthChat-App laden</h1>
<div class="mx_StoreLinks">
<a
href="https://apps.apple.com/app/element-x-secure-chat-call/id1631335820"
href="https://apps.apple.com/ch/app/healthchat/id6743060078"
target="_blank"
class="mx_StoreBadge"
id="app_store_link"
>
<img
src="./assets/app-store-badge.svg"
alt="Download on the App Store."
alt="Im App Store laden."
width="120px"
height="40px"
/>
</a>
<a
href="https://play.google.com/store/apps/details?id=io.element.android.x"
href="https://play.google.com/store/apps/details?id=ch.healthchat.element"
target="_blank"
class="mx_StoreBadge"
id="play_store_link"
>
<img
src="./assets/google-play-badge.svg"
alt="Get it on Google Play."
alt="Jetzt bei Google Play."
width="135px"
height="40px"
/>
</a>
</div>
<p id="f_droid_section">
Also available on
<a
href="https://f-droid.org/packages/io.element.android.x"
target="_blank"
class="mx_ClearDecoration"
id="f_droid_link"
>F-Droid</a
>
<p id="f_droid_section" style="display: none">
Auch verfügbar auf
<a href="#" target="_blank" class="mx_ClearDecoration" id="f_droid_link">F-Droid</a>
</p>
</div>
</div>
@@ -89,12 +89,19 @@
<span>2</span>
</div>
<div>
<h1>Come back here to sign in</h1>
<p id="step2_description">This will set up your app</p>
<a class="mx_Button" id="deep_link_button" href="#">Sign in</a>
<h1>Danach hier anmelden</h1>
<p id="step2_description">Damit wird die App eingerichtet</p>
<a class="mx_Button" id="deep_link_button" href="#">Anmelden</a>
</div>
</div>
</div>
<div class="mx_HomePage_col mx_Spacer">
<p>
<a id="back_to_element_button" href="#" class="mx_DesktopLink"
>Trotzdem die Web-Version öffnen</a
>
</p>
</div>
</main>
</div>
+8 -8
View File
@@ -23,8 +23,8 @@ function onBackToElementClick(): void {
// NEVER pass user-controlled content to this function! Hardcoded strings only please.
function renderConfigError(message: string): void {
const contactMsg =
"If this is unexpected, please contact your system administrator " + "or technical support representative.";
message = `<h2>Error loading Element</h2><p>${message}</p><p>${contactMsg}</p>`;
"Falls das unerwartet ist, bitte den Systemadministrator " + "oder den technischen Support kontaktieren.";
message = `<h2>HealthChat konnte nicht geladen werden</h2><p>${message}</p><p>${contactMsg}</p>`;
const toHide = document.getElementsByClassName("mx_HomePage_container");
const errorContainers = document.getElementsByClassName(
@@ -59,12 +59,12 @@ async function initPage(): Promise<void> {
const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i);
if (defaultHsUrl && (wkConfig || serverName)) {
return renderConfigError(
"Invalid configuration: a default_hs_url can't be specified along with default_server_name " +
"or default_server_config",
"Ungültige Konfiguration: default_hs_url kann nicht zusammen mit default_server_name " +
"oder default_server_config angegeben werden",
);
}
if (incompatibleOptions.length < 1) {
return renderConfigError("Invalid configuration: no default server specified.");
return renderConfigError("Ungültige Konfiguration: kein Standard-Server angegeben.");
}
let hsUrl: string | undefined;
@@ -100,7 +100,7 @@ async function initPage(): Promise<void> {
}
} else {
logger.error(e);
return renderConfigError("Unable to fetch homeserver configuration");
return renderConfigError("Homeserver-Konfiguration konnte nicht geladen werden");
}
}
}
@@ -111,7 +111,7 @@ async function initPage(): Promise<void> {
}
if (!hsUrl) {
return renderConfigError("Unable to locate homeserver");
return renderConfigError("Homeserver konnte nicht gefunden werden");
}
if (hsUrl && !hsUrl.endsWith("/")) hsUrl += "/";
@@ -131,7 +131,7 @@ async function initPage(): Promise<void> {
// Not part of updateMobilePage as the link is only shown on mobile_guide and not on mobile.element.io
document.getElementById("back_to_element_button")!.onclick = onBackToElementClick;
updateMobilePage(metadata, deepLinkUrl, serverName ?? hsUrl);
updateMobilePage(metadata, deepLinkUrl);
}
void initPage();
+12 -13
View File
@@ -38,12 +38,11 @@ export const mobileApps: Record<MobileAppVariant, MobileAppMetadata> = {
isProApp: false,
},
[MobileAppVariant.X]: {
name: "Element X",
appleAppId: "id1631335820",
appStoreUrl: "https://apps.apple.com/app/element-x-secure-chat-call/id1631335820",
playStoreUrl: "https://play.google.com/store/apps/details?id=io.element.android.x",
fDroidUrl: "https://f-droid.org/packages/io.element.android.x",
deepLinkPath: "/element",
name: "HealthChat",
appleAppId: "id6743060078",
appStoreUrl: "https://apps.apple.com/ch/app/healthchat/id6743060078",
playStoreUrl: "https://play.google.com/store/apps/details?id=ch.healthchat.element",
deepLinkPath: "",
usesLegacyDeepLink: false,
isProApp: false,
},
@@ -58,18 +57,17 @@ export const mobileApps: Record<MobileAppVariant, MobileAppMetadata> = {
},
};
export function updateMobilePage(metadata: MobileAppMetadata, deepLinkUrl: string, server: string | undefined): void {
export function updateMobilePage(metadata: MobileAppMetadata, deepLinkUrl: string): void {
const appleMeta = document.querySelector('meta[name="apple-itunes-app"]') as Element;
appleMeta.setAttribute("content", `app-id=${metadata.appleAppId}`);
if (server) {
(document.getElementById("header_title") as HTMLHeadingElement).innerText = `Join ${server} on Element`;
}
(document.getElementById("header_title") as HTMLHeadingElement).innerText = `${metadata.name} auf dem Smartphone`;
(document.getElementById("app_store_link") as HTMLAnchorElement).href = metadata.appStoreUrl;
(document.getElementById("play_store_link") as HTMLAnchorElement).href = metadata.playStoreUrl;
if (metadata.fDroidUrl) {
(document.getElementById("f_droid_link") as HTMLAnchorElement).href = metadata.fDroidUrl;
document.getElementById("f_droid_section")!.style.display = "";
} else {
document.getElementById("f_droid_section")!.style.display = "none";
}
@@ -77,11 +75,12 @@ export function updateMobilePage(metadata: MobileAppMetadata, deepLinkUrl: strin
const step1Heading = document.getElementById("step1_heading")!;
step1Heading.innerHTML = step1Heading!.innerHTML.replace("Element", metadata.name);
// Step 2 is only shown on the mobile guide, not on mobile.element.io
if (document.getElementById("step2_container")) {
// Step 2 is only shown on the mobile guide, not on mobile.element.io,
// and only for apps that offer a sign-in deep link
if (document.getElementById("step2_container") && (metadata.deepLinkPath || metadata.usesLegacyDeepLink)) {
document.getElementById("step2_container")!.style.display = "block";
if (metadata.isProApp) {
document.getElementById("step2_description")!.innerHTML = "Use your work email to join";
document.getElementById("step2_description")!.innerHTML = "Mit der Arbeits-E-Mail-Adresse anmelden";
}
(document.getElementById("deep_link_button") as HTMLAnchorElement).href = deepLinkUrl;
}