diff --git a/README.md b/README.md
index 60a1cdf4..0d5cd8c0 100644
--- a/README.md
+++ b/README.md
@@ -52,17 +52,18 @@ You can now access the client at http://local.revolt.chat:3000.
## CLI Commands
-| Command | Description |
-| ------------------- | -------------------------------------------- |
-| `yarn pull` | Setup assets required for Revite. |
-| `yarn dev` | Start the Revolt client in development mode. |
-| `yarn build` | Build the Revolt client. |
-| `yarn preview` | Start a local server with the built client. |
-| `yarn lint` | Run ESLint on the client. |
-| `yarn fmt` | Run Prettier on the client. |
-| `yarn typecheck` | Run TypeScript type checking on the client. |
-| `yarn start` | Start a local sirv server with built client. |
-| `yarn start:inject` | Inject a given API URL and start server. |
+| Command | Description |
+| --------------------------------------- | -------------------------------------------- |
+| `yarn pull` | Setup assets required for Revite. |
+| `yarn dev` | Start the Revolt client in development mode. |
+| `yarn build` | Build the Revolt client. |
+| `yarn preview` | Start a local server with the built client. |
+| `yarn lint` | Run ESLint on the client. |
+| `yarn fmt` | Run Prettier on the client. |
+| `yarn typecheck` | Run TypeScript type checking on the client. |
+| `yarn start` | Start a local sirv server with built client. |
+| `yarn start:inject` | Inject a given API URL and start server. |
+| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. |
## License
diff --git a/external/lang b/external/lang
index a38d0dc7..30964859 160000
--- a/external/lang
+++ b/external/lang
@@ -1 +1 @@
-Subproject commit a38d0dc72a39ea2b5a6f54c1c999f2021b899e50
+Subproject commit 309648592801a3bb5c1fa1702753f8dadde56cae
diff --git a/package.json b/package.json
index 0f158e83..86dc0c7e 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,8 @@
{
"varsIgnorePattern": "^_"
}
- ]
+ ],
+ "react/jsx-no-literals": "warn"
}
},
"dependencies": {
@@ -92,6 +93,7 @@
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
+ "@types/semver": "^7",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
@@ -132,6 +134,7 @@
"revolt.js": "6.0.3",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
+ "semver": "^7.3.7",
"shade-blend-color": "^1.0.0",
"stacktrace-js": "^2.0.2",
"styled-components": "^5.3.0",
diff --git a/src/components/common/UpdateIndicator.tsx b/src/components/common/UpdateIndicator.tsx
index 52ef1ebe..cb3093df 100644
--- a/src/components/common/UpdateIndicator.tsx
+++ b/src/components/common/UpdateIndicator.tsx
@@ -9,7 +9,7 @@ import { internalSubscribe } from "../../lib/eventEmitter";
import { useApplicationState } from "../../mobx/State";
-import { updateSW } from "../../main";
+import { updateSW } from "../../updateWorker";
import Tooltip from "./Tooltip";
let pendingUpdate = false;
diff --git a/src/context/modals/components/OutOfDate.tsx b/src/context/modals/components/OutOfDate.tsx
new file mode 100644
index 00000000..6368e56e
--- /dev/null
+++ b/src/context/modals/components/OutOfDate.tsx
@@ -0,0 +1,47 @@
+import { Text } from "preact-i18n";
+
+import { Modal } from "@revoltchat/ui";
+
+import { noop, noopTrue } from "../../../lib/js";
+
+import { APP_VERSION } from "../../../version";
+import { ModalProps } from "../types";
+
+export default function OutOfDate({
+ onClose,
+ version,
+}: ModalProps<"out_of_date">) {
+ return (
+ }
+ description={
+ <>
+
+
+
+ >
+ }
+ actions={[
+ {
+ palette: "plain",
+ onClick: noop,
+ children: (
+
+ ),
+ },
+ {
+ palette: "plain-secondary",
+ onClick: noopTrue,
+ children: (
+
+ ),
+ },
+ ]}
+ onClose={onClose}
+ nonDismissable
+ />
+ );
+}
diff --git a/src/context/modals/index.tsx b/src/context/modals/index.tsx
index 852271ec..824c31f2 100644
--- a/src/context/modals/index.tsx
+++ b/src/context/modals/index.tsx
@@ -11,6 +11,7 @@ import { ulid } from "ulid";
import MFAEnableTOTP from "./components/MFAEnableTOTP";
import MFAFlow from "./components/MFAFlow";
import MFARecovery from "./components/MFARecovery";
+import OutOfDate from "./components/OutOfDate";
import Test from "./components/Test";
import { Modal } from "./types";
@@ -120,5 +121,6 @@ export const modalController = new ModalControllerExtended({
mfa_flow: MFAFlow,
mfa_recovery: MFARecovery,
mfa_enable_totp: MFAEnableTOTP,
+ out_of_date: OutOfDate,
test: Test,
});
diff --git a/src/context/modals/types.ts b/src/context/modals/types.ts
index 4f67b9bf..bbe43c37 100644
--- a/src/context/modals/types.ts
+++ b/src/context/modals/types.ts
@@ -24,6 +24,10 @@ export type Modal = {
secret: string;
callback: (code?: string) => void;
}
+ | {
+ type: "out_of_date";
+ version: string;
+ }
| {
type: "test";
}
diff --git a/src/main.tsx b/src/main.tsx
index 28081391..2ba0dd7e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,21 +1,8 @@
-import { registerSW } from "virtual:pwa-register";
-
import "./styles/index.scss";
import { render } from "preact";
-import { internalEmit } from "./lib/eventEmitter";
-
import { App } from "./pages/app";
-
-export const updateSW = registerSW({
- onNeedRefresh() {
- internalEmit("PWA", "update");
- },
- onOfflineReady() {
- console.info("Ready to work offline.");
- // show a ready to work offline to user
- },
-});
+import "./updateWorker";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
render(, document.getElementById("app")!);
diff --git a/src/updateWorker.ts b/src/updateWorker.ts
new file mode 100644
index 00000000..4976fe0c
--- /dev/null
+++ b/src/updateWorker.ts
@@ -0,0 +1,64 @@
+import semver from "semver";
+import { ulid } from "ulid";
+import { registerSW } from "virtual:pwa-register";
+
+import { internalEmit } from "./lib/eventEmitter";
+
+import { modalController } from "./context/modals";
+
+import { APP_VERSION } from "./version";
+
+const INTERVAL_HOUR = 36e5;
+
+let forceUpdate = false;
+let registration: ServiceWorkerRegistration | undefined;
+
+export const updateSW = registerSW({
+ onNeedRefresh() {
+ if (forceUpdate) {
+ updateSW(true);
+ } else {
+ internalEmit("PWA", "update");
+ }
+ },
+ onOfflineReady() {
+ console.info("Ready to work offline.");
+ // show a ready to work offline to user
+ },
+ onRegistered(r) {
+ registration = r;
+
+ // Check for updates every hour
+ setInterval(() => r!.update(), INTERVAL_HOUR);
+ },
+});
+
+/**
+ * Check whether the client is out of date
+ */
+async function checkVersion() {
+ const { version } = (await fetch("https://api.revolt.chat/release").then(
+ (res) => res.json(),
+ )) as { version: string };
+
+ if (!semver.satisfies(APP_VERSION, version)) {
+ // Let the worker know we should immediately refresh
+ forceUpdate = true;
+
+ // Prompt service worker to update
+ registration?.update();
+
+ // Push information that the client is out of date
+ modalController.push({
+ key: ulid(),
+ type: "out_of_date",
+ version,
+ });
+ }
+}
+
+if (import.meta.env.VITE_API_URL === "https://api.revolt.chat") {
+ // Check for critical updates hourly
+ checkVersion();
+ setInterval(checkVersion, INTERVAL_HOUR);
+}
diff --git a/yarn.lock b/yarn.lock
index 6e582177..52c11951 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2730,6 +2730,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/semver@npm:^7":
+ version: 7.3.9
+ resolution: "@types/semver@npm:7.3.9"
+ checksum: 60bfcfdfa7f937be2c6f4b37ddb6714fb0f27b05fe4cbdfdd596a97d35ed95d13ee410efdd88e72a66449d0384220bf20055ab7d6b5df10de4990fbd20e5cbe0
+ languageName: node
+ linkType: hard
+
"@types/styled-components@npm:^5.1.10":
version: 5.1.13
resolution: "@types/styled-components@npm:5.1.13"
@@ -3551,6 +3558,7 @@ __metadata:
"@types/react-helmet": ^6.1.1
"@types/react-router-dom": ^5.1.7
"@types/react-scroll": ^1.8.2
+ "@types/semver": ^7
"@types/styled-components": ^5.1.10
"@types/twemoji": ^12.1.1
"@typescript-eslint/eslint-plugin": ^4.27.0
@@ -3593,6 +3601,7 @@ __metadata:
revolt.js: 6.0.3
rimraf: ^3.0.2
sass: ^1.35.1
+ semver: ^7.3.7
shade-blend-color: ^1.0.0
sirv-cli: ^1.0.14
stacktrace-js: ^2.0.2