Skip to content

Commit

Permalink
Add versions and update page (aeharding#13)
Browse files Browse the repository at this point in the history
* Add versions and update page

* Fix annoying broken transitions bug, polish updates page

* Hide pesky installation badge for various configurations
  • Loading branch information
aeharding authored Jun 27, 2023
1 parent ff7ee69 commit 15299d9
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@
"env": {
"node": true
}
},
{
"files": ["src/**"],
"globals": {
"APP_VERSION": true
}
}
]
}
4 changes: 2 additions & 2 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000",
"background_color": "#000",
"theme_color": "#000000",
"background_color": "#000000",
"description": "wefwef is a beautiful mobile web client for lemmy. Enjoy a seamless experience browsing the fediverse.",
"categories": ["social", "news"],
"screenshots": [
Expand Down
17 changes: 10 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Auth from "./Auth";
import { AppContextProvider } from "./features/auth/AppContext";
import Router from "./Router";
import BeforeInstallPromptProvider from "./BeforeInstallPromptProvider";
import { UpdateContextProvider } from "./pages/settings/update/UpdateContext";

setupIonicReact({
rippleEffect: false,
Expand All @@ -39,13 +40,15 @@ export default function App() {
<AppContextProvider>
<Provider store={store}>
<BeforeInstallPromptProvider>
<IonApp>
<Router>
<Auth>
<TabbedRoutes />
</Auth>
</Router>
</IonApp>
<UpdateContextProvider>
<IonApp>
<Router>
<Auth>
<TabbedRoutes />
</Auth>
</Router>
</IonApp>
</UpdateContextProvider>
</BeforeInstallPromptProvider>
</Provider>
</AppContextProvider>
Expand Down
16 changes: 14 additions & 2 deletions src/TabbedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { useContext, useRef } from "react";
import { AppContext } from "./features/auth/AppContext";
import UserPage from "./pages/shared/UserPage";
import InstallAppPage from "./pages/settings/InstallAppPage";
import { isInstalled } from "./helpers/device";
import SearchPage from "./pages/search/SearchPage";
import SearchPostsResultsPage from "./pages/search/results/SearchFeedResultsPage";
import ProfileFeedItemsPage from "./pages/profile/ProfileFeedItemsPage";
Expand All @@ -47,6 +46,9 @@ import InboxPage from "./pages/inbox/InboxPage";
import { PageContext } from "./features/auth/PageContext";
import { IonRouterOutletCustomEvent } from "@ionic/core";
import InboxAuthRequired from "./pages/inbox/InboxAuthRequired";
import UpdateAppPage from "./pages/settings/UpdateAppPage";
import useShouldInstall from "./features/pwa/useShouldInstall";
import { UpdateContext } from "./pages/settings/update/UpdateContext";

const Interceptor = styled.div`
position: absolute;
Expand All @@ -62,6 +64,11 @@ export default function TabbedRoutes() {
const router = useIonRouter();
const jwt = useAppSelector(jwtSelector);
const totalUnread = useAppSelector(totalUnreadSelector);
const { status: updateStatus } = useContext(UpdateContext);
const shouldInstall = useShouldInstall();

const settingsNotificationCount =
(shouldInstall ? 1 : 0) + (updateStatus === "outdated" ? 1 : 0);

const pageRef = useRef<IonRouterOutletCustomEvent<unknown>["target"]>(null);

Expand Down Expand Up @@ -304,6 +311,9 @@ export default function TabbedRoutes() {
<Route exact path="/settings/install">
<InstallAppPage />
</Route>
<Route exact path="/settings/update">
<UpdateAppPage />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton
Expand Down Expand Up @@ -348,7 +358,9 @@ export default function TabbedRoutes() {
<IonTabButton tab="settings" href="/settings">
<IonIcon aria-hidden="true" icon={cog} />
<IonLabel>Settings</IonLabel>
{!isInstalled() && <IonBadge color="danger">1</IonBadge>}
{settingsNotificationCount ? (
<IonBadge color="danger">{settingsNotificationCount}</IonBadge>
) : undefined}
</IonTabButton>
</IonTabBar>
</IonTabs>
Expand Down
18 changes: 18 additions & 0 deletions src/features/pwa/useShouldInstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext } from "react";
import { BeforeInstallPromptContext } from "../../BeforeInstallPromptProvider";
import {
isAppleDeviceInstallable,
isInstalled,
isTouchDevice,
} from "../../helpers/device";

export default function useShouldInstall() {
const { event } = useContext(BeforeInstallPromptContext);

if (isInstalled()) return false;
if (isAppleDeviceInstallable()) return true;
if (!isTouchDevice()) return false;
if (event) return true;

return false;
}
8 changes: 8 additions & 0 deletions src/helpers/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ export const isInstallable =
export function isAppleDeviceInstalledToHomescreen(): boolean {
return ua.getDevice().vendor === "Apple" && isInstalled();
}

export function isAppleDeviceInstallable(): boolean {
return ua.getDevice().vendor === "Apple" && isTouchDevice();
}

export function isTouchDevice() {
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
}
21 changes: 19 additions & 2 deletions src/pages/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ import {
logoGithub,
mailOutline,
openOutline,
reload,
shieldCheckmarkOutline,
} from "ionicons/icons";
import { isInstalled } from "../../helpers/device";
import { useContext, useEffect } from "react";
import { UpdateContext } from "./update/UpdateContext";
import useShouldInstall from "../../features/pwa/useShouldInstall";

export default function SettingsPage() {
const { status: updateStatus, checkForUpdates } = useContext(UpdateContext);
const shouldInstall = useShouldInstall();

useEffect(() => {
checkForUpdates();
}, [checkForUpdates]);

return (
<IonPage className="grey-bg">
<IonHeader>
Expand All @@ -37,7 +47,14 @@ export default function SettingsPage() {
<InsetIonItem routerLink="/settings/install">
<IonIcon icon={apps} color="primary" />
<SettingLabel>Install app</SettingLabel>
{!isInstalled() && <IonBadge color="danger">1</IonBadge>}
{shouldInstall && <IonBadge color="danger">1</IonBadge>}
</InsetIonItem>
<InsetIonItem routerLink="/settings/update">
<IonIcon icon={reload} color="primary" />
<SettingLabel>Check for updates</SettingLabel>
{updateStatus === "outdated" && (
<IonBadge color="danger">1</IonBadge>
)}
</InsetIonItem>
</IonList>

Expand Down
124 changes: 124 additions & 0 deletions src/pages/settings/UpdateAppPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
IonBackButton,
IonBadge,
IonButtons,
IonContent,
IonHeader,
IonLabel,
IonList,
IonPage,
IonRefresher,
IonRefresherContent,
IonTitle,
IonToolbar,
} from "@ionic/react";
import { MaxWidthContainer } from "../../features/shared/AppContent";
import { InsetIonItem, SettingLabel } from "../profile/ProfileFeedItemsPage";
import { useContext, useEffect } from "react";
import { PageContentIonSpinner } from "../shared/UserPage";
import styled from "@emotion/styled";
import { UpdateContext } from "./update/UpdateContext";

const UpToDateText = styled.div`
margin: auto;
text-align: center;
padding: 5rem 1rem;
color: var(--ion-color-medium);
`;

const Container = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
`;

export default function UpdateAppPage() {
const { status, checkForUpdates, updateServiceWorker } =
useContext(UpdateContext);

useEffect(() => {
checkForUpdates();
}, [checkForUpdates]);

return (
<IonPage className="grey-bg">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/settings" text="Settings" />
</IonButtons>

<IonTitle>Updates</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonRefresher
slot="fixed"
onIonRefresh={async (e) => {
try {
await checkForUpdates();
} finally {
e.detail.complete();
}
}}
>
<IonRefresherContent />
</IonRefresher>
<Container>
<MaxWidthContainer>
<IonList inset color="primary">
<InsetIonItem>
<IonLabel>Current version</IonLabel>
<SettingLabel slot="end" color="medium">
{APP_VERSION}
</SettingLabel>
</InsetIonItem>
<InsetIonItem
href="https://github.com/aeharding/wefwef/releases"
target="_blank"
rel="noopener noreferrer"
>
<IonLabel>Release notes</IonLabel>
</InsetIonItem>
</IonList>

{status === "outdated" && (
<IonList inset color="primary">
<InsetIonItem
detail
onClick={async () => {
await updateServiceWorker();
location.reload();
}}
>
<IonLabel>Install new update</IonLabel>
<IonBadge color="danger">1</IonBadge>
</InsetIonItem>
</IonList>
)}
</MaxWidthContainer>

{status === "loading" && <PageContentIonSpinner />}
{status === "not-enabled" && (
<UpToDateText>Not installed.</UpToDateText>
)}
{status === "error" && (
<UpToDateText>
Error checking for updates.
<br />
<br />
Are you connected to the internet?
</UpToDateText>
)}
{status === "current" && (
<UpToDateText>wefwef is up to date</UpToDateText>
)}
</Container>
</IonContent>
</IonPage>
);
}
91 changes: 91 additions & 0 deletions src/pages/settings/update/UpdateContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { createContext, useEffect, useRef, useState } from "react";
import { useInterval } from "usehooks-ts";
import { useRegisterSW } from "virtual:pwa-register/react";
import usePageVisibility from "../../../helpers/usePageVisibility";

type UpdateStatus =
| "not-enabled"
| "loading"
| "current"
| "outdated"
| "error";

interface IUpdateContext {
// used for determining whether page needs to be scrolled up first
checkForUpdates: () => Promise<void>;
updateServiceWorker: () => Promise<void>;
status: UpdateStatus;
}

export const UpdateContext = createContext<IUpdateContext>({
checkForUpdates: async () => {},
updateServiceWorker: async () => {},

status: "loading",
});

export function UpdateContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const [status, setStatus] = useState<UpdateStatus>("not-enabled");
const pageVisibility = usePageVisibility();

const registration = useRef<ServiceWorkerRegistration>();

const registerSW = useRegisterSW({
onRegistered(r) {
setStatus("loading");
if (!r) return;

registration.current = r;
checkForUpdates();
},
onRegisterError() {
setStatus("error");
},
});

useInterval(() => {
checkForUpdates();
}, 1_000 * 60 * 60);

useEffect(() => {
if (!pageVisibility) return;

checkForUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageVisibility]);

async function checkForUpdates() {
const r = registration.current;

if (!r) {
if (status === "not-enabled") return;
setStatus("error");
return;
}

try {
await r.update();
} catch (error) {
setStatus("error");
throw error;
}

setStatus(!!(r.waiting || r.installing) ? "outdated" : "current");
}

return (
<UpdateContext.Provider
value={{
status,
checkForUpdates,
updateServiceWorker: registerSW.updateServiceWorker,
}}
>
{children}
</UpdateContext.Provider>
);
}
3 changes: 3 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/// <reference types="vite/client" />
/// <reference types="@emotion/react/types/css-prop" />
/// <reference types="vite-plugin-pwa/react" />

declare const APP_VERSION: string;
Loading

0 comments on commit 15299d9

Please sign in to comment.