From c10739630c6f69fd17da06f312550d838452eacf Mon Sep 17 00:00:00 2001
From: Pecacheu <3608878+Pecacheu@users.noreply.github.com>
Date: Sat, 21 Feb 2026 15:26:33 -0500
Subject: [PATCH 1/2] feat: TryPWA install dialog
Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com>
---
packages/client/components/client/index.tsx | 45 +++++++++++------
packages/client/components/modal/modals.tsx | 3 ++
.../client/components/modal/modals/TryPWA.tsx | 49 +++++++++++++++++++
packages/client/components/modal/types.ts | 3 ++
packages/client/components/state/index.tsx | 5 ++
.../components/state/stores/Settings.ts | 6 +++
packages/client/src/index.tsx | 4 ++
7 files changed, 99 insertions(+), 16 deletions(-)
create mode 100644 packages/client/components/modal/modals/TryPWA.tsx
diff --git a/packages/client/components/client/index.tsx b/packages/client/components/client/index.tsx
index 47741e438..007780dc9 100644
--- a/packages/client/components/client/index.tsx
+++ b/packages/client/components/client/index.tsx
@@ -26,30 +26,43 @@ const clientContext = createContext(null! as ClientController);
* Mount the modal controller
*/
export function ClientContext(props: { state: State; children: JSXElement }) {
- const { openModal } = useModals();
+ const { openModal, isOpen } = useModals();
// eslint-disable-next-line solid/reactivity
const controller = new ClientController(props.state);
onCleanup(() => controller.dispose());
createEffect(() => {
- const lastIndex = props.state.settings.getValue("changelog:last_index");
- if (controller.lifecycle.state() === LifecycleState.Ready) return;
+ const cycleState = controller.lifecycle.state();
+
+ //Show Changelog modal
+ if (cycleState !== LifecycleState.Ready) {
+ const lastIndex = props.state.settings.getValue("changelog:last_index");
+
+ if (
+ lastIndex !== CHANGELOG_MODAL_CONST.index &&
+ new Date() < CHANGELOG_MODAL_CONST.until
+ ) {
+ openModal({
+ type: "changelog",
+ initial: CHANGELOG_MODAL_CONST.index,
+ });
+
+ props.state.settings.setValue(
+ "changelog:last_index",
+ CHANGELOG_MODAL_CONST.index,
+ );
+ }
+ }
+ //Show TryPWA modal
if (
- lastIndex !== CHANGELOG_MODAL_CONST.index &&
- new Date() < CHANGELOG_MODAL_CONST.until
- ) {
- openModal({
- type: "changelog",
- initial: CHANGELOG_MODAL_CONST.index,
- });
-
- props.state.settings.setValue(
- "changelog:last_index",
- CHANGELOG_MODAL_CONST.index,
- );
- }
+ props.state.isMobile &&
+ cycleState === LifecycleState.Connected &&
+ !props.state.settings.getValue("pwa:shown") &&
+ !isOpen("try_pwa")
+ )
+ openModal({ type: "try_pwa" });
});
createEffect(
diff --git a/packages/client/components/modal/modals.tsx b/packages/client/components/modal/modals.tsx
index 9da2b50ee..0ddacd3be 100644
--- a/packages/client/components/modal/modals.tsx
+++ b/packages/client/components/modal/modals.tsx
@@ -53,6 +53,7 @@ import { ServerInfoModal } from "./modals/ServerInfo";
import { SettingsModal } from "./modals/Settings";
import { SignOutSessionsModal } from "./modals/SignOutSessions";
import { SignedOutModal } from "./modals/SignedOut";
+import { TryPWAModal } from "./modals/TryPWA";
import { UserProfileModal } from "./modals/UserProfile";
import { UserProfileMutualFriendsModal } from "./modals/UserProfileMutualFriends";
import { UserProfileMutualGroupsModal } from "./modals/UserProfileMutualGroups";
@@ -183,6 +184,8 @@ export function RenderModal(props: ActiveModal & { onClose: () => void }) {
return ;
case "edit_category":
return ;
+ case "try_pwa":
+ return ;
default:
console.error(
diff --git a/packages/client/components/modal/modals/TryPWA.tsx b/packages/client/components/modal/modals/TryPWA.tsx
new file mode 100644
index 000000000..62ead9097
--- /dev/null
+++ b/packages/client/components/modal/modals/TryPWA.tsx
@@ -0,0 +1,49 @@
+import { Trans } from "@lingui-solid/solid/macro";
+
+import { Dialog, DialogProps, iconSize } from "@revolt/ui";
+
+import MdInfo from "@material-design-icons/svg/outlined/error.svg?component-solid";
+
+import { t } from "@lingui/core/macro";
+import { useState } from "@revolt/state";
+import { Modals } from "../types";
+
+export function TryPWAModal(props: DialogProps & Modals & { type: "try_pwa" }) {
+ const state = useState();
+
+ return (
+ }
+ show={props.show}
+ onClose={() => {
+ state.settings.setValue("pwa:shown", true);
+ props.onClose();
+ }}
+ title={Did you know?}
+ actions={[
+ { text: Close },
+ {
+ text: Install,
+ onClick: () => {
+ // @ts-expect-error PWA event not recognized
+ if (state.pwaPrompt) state.pwaPrompt.prompt();
+ else
+ alert(
+ t`Sorry, your device doesn't support auto-install. Check your browser's menu for the 'Add to Home Screen' option.`,
+ );
+ },
+ },
+ ]}
+ >
+
+ You can install this Web App on your devive for conveient access
+ from the Home Screen, just like a real app. It even hides the huge menu
+ bar!
+
+
+ To install, tap the button below or open your browser's menu and choose
+ Add to Home Screen.
+
+
+ );
+}
diff --git a/packages/client/components/modal/types.ts b/packages/client/components/modal/types.ts
index a6c6a020e..48c7cc048 100644
--- a/packages/client/components/modal/types.ts
+++ b/packages/client/components/modal/types.ts
@@ -314,4 +314,7 @@ export type Modals =
type: "edit_category";
server: Server;
category: CategoryData;
+ }
+ | {
+ type: "try_pwa";
};
diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx
index bfdbb55db..ab32d8b09 100644
--- a/packages/client/components/state/index.tsx
+++ b/packages/client/components/state/index.tsx
@@ -11,6 +11,7 @@ import { SetStoreFunction, createStore } from "solid-js/store";
import equal from "fast-deep-equal";
import localforage from "localforage";
+import { isMobileBrowser } from "@livekit/components-core";
import { AbstractStore, Store } from "./stores";
import { Auth } from "./stores/Auth";
import { Draft } from "./stores/Draft";
@@ -47,6 +48,9 @@ export class State {
private setStore: SetStoreFunction;
private writeQueue: Record;
+ isMobile: boolean;
+ pwaPrompt: Event | undefined;
+
// define all stores
auth = new Auth(this);
draft = new Draft(this);
@@ -99,6 +103,7 @@ export class State {
this.store = store as never;
this.setStore = setStore;
this.writeQueue = {};
+ this.isMobile = isMobileBrowser();
}
/**
diff --git a/packages/client/components/state/stores/Settings.ts b/packages/client/components/state/stores/Settings.ts
index 1aee0a92e..8748d890d 100644
--- a/packages/client/components/state/stores/Settings.ts
+++ b/packages/client/components/state/stores/Settings.ts
@@ -68,6 +68,11 @@ interface SettingsDefinition {
* Last read changelog index
*/
"changelog:last_index": number;
+
+ /**
+ * Whether the user has seen the TryPWA dialog
+ */
+ "pwa:shown": boolean;
}
/**
@@ -95,6 +100,7 @@ const EXPECTED_TYPES: { [K in keyof SettingsDefinition]: ValueType } = {
"advanced:copy_id": "boolean",
"advanced:admin_panel": "boolean",
"changelog:last_index": "number",
+ "pwa:shown": "boolean",
};
/**
diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx
index 23a11e744..9115416eb 100644
--- a/packages/client/src/index.tsx
+++ b/packages/client/src/index.tsx
@@ -108,8 +108,12 @@ function BotRedirect() {
return ;
}
+let pwaPrompt: Event;
+addEventListener("beforeinstallprompt", (e) => (pwaPrompt = e));
+
function MountContext(props: { children?: JSX.Element }) {
const state = useState();
+ state.pwaPrompt = pwaPrompt;
/**
* Tanstack Query client
From e482c2169d31f63847cbce53980adfe77178ea46 Mon Sep 17 00:00:00 2001
From: Pecacheu <3608878+Pecacheu@users.noreply.github.com>
Date: Sat, 21 Mar 2026 23:02:35 -0400
Subject: [PATCH 2/2] fix: Make dialog less wordy
Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com>
---
packages/client/components/modal/modals/TryPWA.tsx | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/packages/client/components/modal/modals/TryPWA.tsx b/packages/client/components/modal/modals/TryPWA.tsx
index 62ead9097..b4591f6bf 100644
--- a/packages/client/components/modal/modals/TryPWA.tsx
+++ b/packages/client/components/modal/modals/TryPWA.tsx
@@ -37,12 +37,8 @@ export function TryPWAModal(props: DialogProps & Modals & { type: "try_pwa" }) {
>
You can install this Web App on your devive for conveient access
- from the Home Screen, just like a real app. It even hides the huge menu
- bar!
-
-
- To install, tap the button below or open your browser's menu and choose
- Add to Home Screen.
+ from the Home Screen. Tap the button below or open your browser's menu
+ and choose Add to Home Screen.
);