diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 00000000..0c83ac4e
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend-mock/apps.js b/backend-mock/apps.js
index 2fde99c1..b8902c93 100644
--- a/backend-mock/apps.js
+++ b/backend-mock/apps.js
@@ -1,11 +1,12 @@
const express = require("express");
const router = express.Router();
const util = require("./sse/util");
+const index = require("./index");
router.post("/install/:id", (req, res) => {
console.info("call to /api/apps/install for app", req.params.id);
// send information that btc-pay is currently installing
- util.sendSSE("install", {
+ util.sendData(index.ws, "install", {
id: "rtl",
mode: "on",
result: "running",
@@ -44,7 +45,7 @@ const installApp = () => {
console.info("call to installApp");
// inform Frontend that app finished installing
- util.sendSSE("install", {
+ util.sendData(index.ws, "install", {
id: "rtl",
mode: "on",
result: "win",
@@ -53,7 +54,7 @@ const installApp = () => {
details: "OK",
});
- util.sendSSE("installed_app_status", [
+ util.sendData(index.ws, "installed_app_status", [
{ id: "lnbits", installed: false, status: "offline", error: "" },
{ id: "thunderhub", installed: false, status: "offline", error: "" },
{ id: "btcpayserver", installed: false, status: "offline", error: "" },
diff --git a/backend-mock/index.js b/backend-mock/index.js
index 85c5b950..1f1ee3cb 100644
--- a/backend-mock/index.js
+++ b/backend-mock/index.js
@@ -1,5 +1,6 @@
const express = require("express");
const cors = require("cors");
+const WebSocket = require("ws");
const btcInfo = require("./sse/btc_info");
const lnInfo = require("./sse/ln_info");
const installedAppStatus = require("./sse/installed_app_status");
@@ -7,15 +8,20 @@ const systemInfo = require("./sse/system_info");
const hardwareInfo = require("./sse/hardware_info");
const walletBalance = require("./sse/wallet_balance");
const systemStartupInfo = require("./sse/system_startup_info");
-const util = require("./sse/util");
const setup = require("./setup");
const system = require("./system");
const apps = require("./apps");
const lightning = require("./lightning");
+const { createServer } = require("node:http");
require("dotenv").config();
const app = express();
+const server = createServer(app);
+
+const wss = new WebSocket.Server({ server, path: "/ws" });
+let ws = null;
+
app.use(
cors({ credentials: true, origin: "http://localhost:3000" }),
express.json(),
@@ -31,49 +37,48 @@ app.use("/api/lightning", lightning);
const PORT = 8000;
-app.listen(PORT, () => {
+server.listen(PORT, () => {
console.info(`Server listening on http://localhost:${PORT}`);
+ console.info(`WebSocket server is running on ws://localhost:${PORT}/ws`);
});
-// app.use('/', express.static('../build'));
-
/**
- * Main SSE Handler
+ * Main WebSocket Handler
*/
-const eventsHandler = (request, response) => {
- console.info("call to /api/sse/subscribe");
- const headers = {
- "Content-Type": "text/event-stream",
- Connection: "keep-alive",
- "Cache-Control": "no-cache",
- };
- response.writeHead(200, headers);
- const data = `data: null\n\n`;
+wss.on("connection", (ws) => {
+ this.ws = ws;
+ console.info("WebSocket connection established on /ws");
- response.write(data);
+ // Handle incoming messages
+ ws.on("message", (message) => {
+ console.info(`Received message: ${message}`);
+ });
- const id = util.currClientId++;
+ // Handle errors
+ ws.on("error", (error) => {
+ console.error("Error occurred:", error);
+ });
- util.clients.push({
- id,
- response,
+ // Handle disconnections
+ ws.on("close", () => {
+ console.info("Client disconnected");
});
- systemStartupInfo.systemStartupInfo();
- systemInfo.systemInfo();
- hardwareInfo.hardwareInfo();
- btcInfo.btcInfo();
- lnInfo.lnInfo();
- installedAppStatus.appStatus();
- walletBalance.walletBalance();
+ // Send data from SSE handlers to the client
+ const sendData = () => {
+ console.info("Sending data to the client");
+ systemStartupInfo.systemStartupInfo(ws);
+ systemInfo.systemInfo(ws);
+ hardwareInfo.hardwareInfo(ws);
+ btcInfo.btcInfo(ws);
+ lnInfo.lnInfo(ws);
+ installedAppStatus.appStatus(ws);
+ walletBalance.walletBalance(ws);
+ };
- request.on("close", () => {
- // do nothing
- });
-};
+ // Send initial data to the client
+ sendData();
+});
-/**
- * SSE Handler call
- */
-app.get("/api/sse/subscribe", eventsHandler);
+module.exports = { ws };
diff --git a/backend-mock/lightning.js b/backend-mock/lightning.js
index 1813c026..fa1b6245 100644
--- a/backend-mock/lightning.js
+++ b/backend-mock/lightning.js
@@ -2,6 +2,7 @@ const express = require("express");
const router = express.Router();
const transactions = require("./transactions");
const util = require("./sse/util");
+const index = require("./index");
let WALLET_LOCKED = true;
@@ -235,7 +236,7 @@ router.post("/unlock-wallet", (req, res) => {
if (req.body.password === "password") {
WALLET_LOCKED = false;
- util.sendSSE("system_startup_info", {
+ util.sendData(index.ws, "system_startup_info", {
bitcoin: "done",
bitcoin_msg: "",
lightning: "bootstrapping_after_unlock",
@@ -243,7 +244,7 @@ router.post("/unlock-wallet", (req, res) => {
});
setTimeout(() => {
- util.sendSSE("system_startup_info", {
+ util.sendData(index.ws, "system_startup_info", {
bitcoin: "done",
bitcoin_msg: "",
lightning: "done",
diff --git a/backend-mock/package-lock.json b/backend-mock/package-lock.json
index f7c2828c..1c16c3cc 100644
--- a/backend-mock/package-lock.json
+++ b/backend-mock/package-lock.json
@@ -12,7 +12,8 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
- "jsonwebtoken": "^9.0.1"
+ "jsonwebtoken": "^9.0.1",
+ "ws": "^8.18.0"
}
},
"node_modules/accepts": {
@@ -923,6 +924,27 @@
"engines": {
"node": ">= 0.8"
}
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/backend-mock/package.json b/backend-mock/package.json
index 09bdeb2b..e8586cfa 100644
--- a/backend-mock/package.json
+++ b/backend-mock/package.json
@@ -17,6 +17,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
- "jsonwebtoken": "^9.0.1"
+ "jsonwebtoken": "^9.0.1",
+ "ws": "^8.18.0"
}
}
diff --git a/backend-mock/sse/btc_info.js b/backend-mock/sse/btc_info.js
index 4fc45948..6cd322eb 100644
--- a/backend-mock/sse/btc_info.js
+++ b/backend-mock/sse/btc_info.js
@@ -1,9 +1,9 @@
const util = require("./util");
-const btcInfo = () => {
+const btcInfo = (ws) => {
console.info("sending btc_info");
- util.sendSSE("btc_info", {
+ util.sendData(ws, "btc_info", {
blocks: 25,
headers: 25,
verification_progress: 0.9999983702720613,
diff --git a/backend-mock/sse/hardware_info.js b/backend-mock/sse/hardware_info.js
index a55ed63e..53e476c0 100644
--- a/backend-mock/sse/hardware_info.js
+++ b/backend-mock/sse/hardware_info.js
@@ -1,9 +1,9 @@
const util = require("./util");
-const hardwareInfo = () => {
+const hardwareInfo = (ws) => {
console.info("sending hardware_info");
- util.sendSSE("hardware_info", {
+ util.sendData(ws, "hardware_info", {
cpu_overall_percent: 1.57,
cpu_per_cpu_percent: [1.64, 1.54, 1.54],
vram_total_bytes: 3844000000,
diff --git a/backend-mock/sse/installed_app_status.js b/backend-mock/sse/installed_app_status.js
index e52da86b..db9fd356 100644
--- a/backend-mock/sse/installed_app_status.js
+++ b/backend-mock/sse/installed_app_status.js
@@ -1,8 +1,8 @@
const util = require("./util");
-const appStatus = () => {
+const appStatus = (ws) => {
console.info("sending installed_app_status");
- util.sendSSE("installed_app_status", [
+ util.sendData(ws, "installed_app_status", [
{
id: "rtl",
installed: false,
diff --git a/backend-mock/sse/ln_info.js b/backend-mock/sse/ln_info.js
index 3c72fe55..79f4e229 100644
--- a/backend-mock/sse/ln_info.js
+++ b/backend-mock/sse/ln_info.js
@@ -1,9 +1,9 @@
const util = require("./util");
-const lnInfo = () => {
+const lnInfo = (ws) => {
console.info("sending ln_info");
- util.sendSSE("ln_info", {
+ util.sendData(ws, "ln_info", {
implementation: "LND_GRPC",
version: "0.17.3-beta commit=v0.17.3-beta",
commit_hash: "13aa7f99248c7ee63989d3b62e0cbfe86d7b0964",
diff --git a/backend-mock/sse/system_info.js b/backend-mock/sse/system_info.js
index dd526e27..dc407380 100644
--- a/backend-mock/sse/system_info.js
+++ b/backend-mock/sse/system_info.js
@@ -1,9 +1,9 @@
const util = require("./util");
-const systemInfo = () => {
+const systemInfo = (ws) => {
console.info("sending system_info");
- util.sendSSE("system_info", {
+ util.sendData(ws, "system_info", {
alias: "myBlitz",
color: "#3399ff",
platform: "raspiblitz",
diff --git a/backend-mock/sse/system_startup_info.js b/backend-mock/sse/system_startup_info.js
index d3098ddb..8dcb5c6c 100644
--- a/backend-mock/sse/system_startup_info.js
+++ b/backend-mock/sse/system_startup_info.js
@@ -1,14 +1,16 @@
const util = require("./util");
-const systemStartupInfo = () => {
+const systemStartupInfo = (ws) => {
console.info("sending system_startup_info");
- util.sendSSE("system_startup_info", {
+ const data = {
bitcoin: "done",
bitcoin_msg: "",
- lightning: "locked",
+ lightning: "",
lightning_msg: "Wallet locked, unlock it to enable full RPC access",
- });
+ };
+
+ util.sendData(ws, "system_startup_info", data);
};
module.exports = { systemStartupInfo };
diff --git a/backend-mock/sse/util.js b/backend-mock/sse/util.js
index b072d3c7..54b9c2ec 100644
--- a/backend-mock/sse/util.js
+++ b/backend-mock/sse/util.js
@@ -1,18 +1,7 @@
-/**
- * @type {{id: number, response: Response}[]}
- */
-let clients = [];
-let currClientId = 0;
-
-/**
- * @param {string} event the event to send
- * @param {any} data data of the event
- * @returns void
- */
-const sendSSE = (event, data) => {
- clients.forEach((client) =>
- client.response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
- );
+const util = {
+ sendData: (ws, type, data) => {
+ ws.send(JSON.stringify({ type, data }));
+ },
};
-module.exports = { clients, currClientId, sendSSE };
+module.exports = util;
diff --git a/backend-mock/sse/wallet_balance.js b/backend-mock/sse/wallet_balance.js
index 00a3030e..96a64d69 100644
--- a/backend-mock/sse/wallet_balance.js
+++ b/backend-mock/sse/wallet_balance.js
@@ -1,9 +1,9 @@
const util = require("./util");
-const walletBalance = () => {
+const walletBalance = (ws) => {
console.info("sending wallet_balance");
- util.sendSSE("wallet_balance", {
+ util.sendData(ws, "wallet_balance", {
onchain_confirmed_balance: 742363,
onchain_total_balance: 742363,
onchain_unconfirmed_balance: 200000000,
diff --git a/src/context/app-context.tsx b/src/context/app-context.tsx
index 5c40c17d..bf29d942 100644
--- a/src/context/app-context.tsx
+++ b/src/context/app-context.tsx
@@ -1,4 +1,4 @@
-import { SSEContext } from "./sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import {
ACCESS_TOKEN,
disableGutter,
@@ -53,7 +53,7 @@ export const AppContext = createContext(appContextDefault);
const AppContextProvider: FC = ({ children }) => {
const { i18n } = useTranslation();
- const { evtSource, setEvtSource } = useContext(SSEContext);
+ const { websocket } = useContext(WebSocketContext);
const [unit, setUnit] = useState(Unit.SAT);
const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -69,16 +69,15 @@ const AppContextProvider: FC = ({ children }) => {
localStorage.removeItem(ACCESS_TOKEN);
// close EventSource on logout
- if (evtSource) {
- evtSource.close();
- setEvtSource(null);
+ if (websocket) {
+ websocket.close();
}
setIsLoggedIn(false);
disableGutter();
setWindowAlias(null);
toast.dismiss();
navigate("/");
- }, [evtSource, setEvtSource, navigate]);
+ }, [websocket, navigate]);
useEffect(() => {
const settings = retrieveSettings();
diff --git a/src/context/sse-context.tsx b/src/context/ws-context.tsx
similarity index 57%
rename from src/context/sse-context.tsx
rename to src/context/ws-context.tsx
index 75a06762..5a6b7235 100644
--- a/src/context/sse-context.tsx
+++ b/src/context/ws-context.tsx
@@ -1,3 +1,4 @@
+import { WEBSOCKET_URL } from "@/hooks/use-ws";
import { AppStatus } from "@/models/app-status";
import { App } from "@/models/app.model";
import { BtcInfo } from "@/models/btc-info";
@@ -8,11 +9,17 @@ import { SystemStartupInfo } from "@/models/system-startup-info";
import { Transaction } from "@/models/transaction.model";
import { WalletBalance } from "@/models/wallet-balance";
import type { FC, PropsWithChildren } from "react";
-import { Dispatch, SetStateAction, createContext, useState } from "react";
+import {
+ createContext,
+ Dispatch,
+ SetStateAction,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
-export interface SSEContextType {
- evtSource: EventSource | null;
- setEvtSource: Dispatch>;
+export interface WebSocketContextType {
+ websocket: WebSocket | null;
systemInfo: SystemInfo;
setSystemInfo: Dispatch>;
btcInfo: BtcInfo;
@@ -21,7 +28,6 @@ export interface SSEContextType {
setLnInfo: Dispatch>;
balance: WalletBalance;
setBalance: Dispatch>;
-
appStatus: AppStatus[];
setAppStatus: Dispatch>;
availableApps: App[];
@@ -36,9 +42,8 @@ export interface SSEContextType {
setSystemStartupInfo: Dispatch>;
}
-export const sseContextDefault: SSEContextType = {
- evtSource: null,
- setEvtSource: () => {},
+export const websocketContextDefault: WebSocketContextType = {
+ websocket: null,
systemInfo: {} as SystemInfo,
setSystemInfo: () => {},
btcInfo: {} as BtcInfo,
@@ -61,12 +66,12 @@ export const sseContextDefault: SSEContextType = {
setSystemStartupInfo: () => {},
};
-export const SSEContext = createContext(sseContextDefault);
-
-export const SSE_URL = "/api/sse/subscribe";
+export const WebSocketContext = createContext(
+ websocketContextDefault,
+);
-const SSEContextProvider: FC = (props) => {
- const [evtSource, setEvtSource] = useState(null);
+const WebSocketContextProvider: FC = (props) => {
+ const [ws, setWs] = useState(null);
const [systemInfo, setSystemInfo] = useState({
alias: "",
color: "",
@@ -132,15 +137,99 @@ const SSEContextProvider: FC = (props) => {
const [systemStartupInfo, setSystemStartupInfo] =
useState(null);
- const contextValue: SSEContextType = {
- evtSource,
- setEvtSource,
+ const websocketRef = useRef(null);
+
+ useEffect(() => {
+ const ws = new WebSocket(WEBSOCKET_URL);
+
+ ws.onopen = () => {
+ console.log("WebSocket connected");
+ };
+
+ ws.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+
+ switch (data.type) {
+ case "apps":
+ setAvailableApps((prev) => {
+ const apps = data.payload;
+ if (prev.length === 0) {
+ return apps;
+ } else {
+ return prev.map(
+ (old) =>
+ apps.find((newApp: { id: string }) => old.id === newApp.id) ||
+ old,
+ );
+ }
+ });
+ break;
+ case "installed_app_status":
+ setAppStatus((prev) => {
+ const status = data.data;
+ if (prev.length === 0) {
+ return status;
+ } else {
+ const currentIds = status.map((item: { id: any }) => item.id);
+ return prev
+ .filter((item) => !currentIds.includes(item.id))
+ .concat(status);
+ }
+ });
+ break;
+ case "transactions":
+ setTransactions((prev) => [data.data, ...prev]);
+ break;
+ case "install":
+ // handle install message
+ break;
+ case "system_info":
+ setSystemInfo((prev) => ({ ...prev, ...data.data }));
+ break;
+ case "btc_info":
+ setBtcInfo((prev) => ({ ...prev, ...data.data }));
+ break;
+ case "ln_info":
+ setLnInfo((prev) => ({ ...prev, ...data.data }));
+ break;
+ case "wallet_balance":
+ setBalance((prev) => ({ ...prev, ...data.data }));
+ break;
+ case "hardware_info":
+ setHardwareInfo((prev) => ({ ...prev, ...data.data }));
+ break;
+ case "system_startup_info":
+ setSystemStartupInfo((prev) => ({ ...prev, ...data.data }));
+ break;
+ default:
+ console.warn("Unknown message type:", data.type);
+ }
+ };
+
+ ws.onerror = (error) => {
+ console.error("WebSocket error:", error);
+ // handle error
+ };
+
+ ws.onclose = () => {
+ console.log("WebSocket disconnected");
+ };
+
+ setWs(ws);
+
+ return () => {
+ ws.close();
+ };
+ }, []);
+
+ const contextValue: WebSocketContextType = {
+ websocket: websocketRef.current,
systemInfo,
setSystemInfo,
btcInfo,
setBtcInfo,
- lnInfo: lnInfo,
- setLnInfo: setLnInfo,
+ lnInfo,
+ setLnInfo,
balance,
setBalance,
appStatus,
@@ -158,10 +247,10 @@ const SSEContextProvider: FC = (props) => {
};
return (
-
+
{props.children}
-
+
);
};
-export default SSEContextProvider;
+export default WebSocketContextProvider;
diff --git a/src/hooks/use-sse.tsx b/src/hooks/use-sse.tsx
deleted file mode 100644
index a6c96507..00000000
--- a/src/hooks/use-sse.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { AppContext } from "@/context/app-context";
-import { SSE_URL, SSEContext } from "@/context/sse-context";
-import { AppStatus } from "@/models/app-status";
-import { App } from "@/models/app.model";
-import { BtcInfo } from "@/models/btc-info";
-import { HardwareInfo } from "@/models/hardware-info";
-import { InstallAppData } from "@/models/install-app";
-import { LnInfo } from "@/models/ln-info";
-import { SystemInfo } from "@/models/system-info";
-import { SystemStartupInfo } from "@/models/system-startup-info";
-import { WalletBalance } from "@/models/wallet-balance";
-import { setWindowAlias } from "@/utils";
-import { availableApps } from "@/utils/availableApps";
-import { useCallback, useContext, useEffect } from "react";
-import { useTranslation } from "react-i18next";
-import { toast } from "react-toastify";
-
-/**
- * Establishes a SSE connection if not available yet & attaches / removes event listeners
- * to the single events to update the SSEContext
- * Use useContext(SSEContext) to get the data, is only used in Layout.tsx
- * @returns the infos from the SSEContext
- */
-function useSSE() {
- const { t } = useTranslation();
- const sseCtx = useContext(SSEContext);
- const appCtx = useContext(AppContext);
- const { evtSource, setEvtSource } = sseCtx;
-
- const appInstallSuccessHandler = useCallback(
- (installData: InstallAppData, appName: string) => {
- if (installData.mode === "on") {
- toast.success(t("apps.install_success", { appName }));
- } else {
- toast.success(t("apps.uninstall_success", { appName }));
- }
- },
- [t],
- );
-
- const appInstallErrorHandler = useCallback(
- (installData: InstallAppData, appName: string) => {
- if (installData.mode === "on") {
- toast.error(
- t("apps.install_failure", {
- appName,
- details: installData.details,
- }),
- );
- } else {
- toast.error(
- t("apps.uninstall_failure", {
- appName,
- details: installData.details,
- }),
- );
- }
- },
- [t],
- );
-
- useEffect(() => {
- if (!evtSource) {
- setEvtSource(new EventSource(SSE_URL, { withCredentials: true }));
- }
-
- const setApps = (event: MessageEvent) => {
- sseCtx.setAvailableApps((prev: App[]) => {
- const apps = JSON.parse(event.data);
- if (prev.length === 0) {
- return apps;
- } else {
- return prev.map(
- (old: App) =>
- apps.find((newApp: App) => old.id === newApp.id) || old,
- );
- }
- });
- };
-
- const setAppStatus = (event: MessageEvent) => {
- sseCtx.setAppStatus((prev: AppStatus[]) => {
- const status: AppStatus[] = JSON.parse(event.data);
- if (prev.length === 0) {
- return status;
- } else {
- const currentIds = status.map((item) => item.id);
-
- // remove items which get updated and concat arrays
- return prev
- .filter((item) => !currentIds.includes(item.id))
- .concat(status);
- }
- });
- };
-
- const setTx = (event: MessageEvent) => {
- const t = JSON.parse(event.data);
- sseCtx.setTransactions((prev) => {
- // add the newest transaction to the beginning
- return [t, ...prev];
- });
- };
-
- const setInstall = (event: MessageEvent) => {
- toast.dismiss();
- const installAppData = JSON.parse(event.data) as InstallAppData;
- const appName = availableApps[installAppData.id]?.name || "";
- if (installAppData.result === "fail") {
- appInstallErrorHandler(installAppData, appName);
- sseCtx.setInstallingApp(null);
- return;
- }
- if (installAppData.result === "win") {
- appInstallSuccessHandler(installAppData, appName);
- sseCtx.setInstallingApp(null);
- return;
- }
- const installing = installAppData.mode === "on";
- toast(
- installing
- ? `${t("apps.installing")} ${appName}`
- : `${t("apps.uninstalling")} ${appName}`,
- {
- isLoading: true,
- autoClose: false,
- },
- );
- sseCtx.setInstallingApp(installAppData);
- };
-
- const setSystemInfo = (event: MessageEvent) => {
- const message = JSON.parse(event.data);
- if (message.alias) {
- setWindowAlias(message.alias);
- }
- sseCtx.setSystemInfo((prev: SystemInfo) => {
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const setBtcInfo = (event: MessageEvent) => {
- sseCtx.setBtcInfo((prev: BtcInfo) => {
- const message = JSON.parse(event.data);
-
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const setLnInfo = (event: MessageEvent) => {
- sseCtx.setLnInfo((prev: LnInfo) => {
- const message = JSON.parse(event.data);
-
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const setBalance = (event: MessageEvent) => {
- sseCtx.setBalance((prev: WalletBalance) => {
- const message = JSON.parse(event.data);
-
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const setHardwareInfo = (event: MessageEvent) => {
- sseCtx.setHardwareInfo((prev: HardwareInfo | null) => {
- const message = JSON.parse(event.data);
-
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const setSystemStartupInfo = (event: MessageEvent) => {
- sseCtx.setSystemStartupInfo((prev: SystemStartupInfo | null) => {
- const message = JSON.parse(event.data);
-
- return {
- ...prev,
- ...message,
- };
- });
- };
-
- const eventErrorHandler = (_event: Event) => {
- appCtx.logout();
- };
-
- if (evtSource) {
- evtSource.addEventListener("error", eventErrorHandler);
- evtSource.addEventListener("system_info", setSystemInfo);
- evtSource.addEventListener("btc_info", setBtcInfo);
- evtSource.addEventListener("ln_info", setLnInfo);
- evtSource.addEventListener("wallet_balance", setBalance);
- evtSource.addEventListener("transactions", setTx);
- evtSource.addEventListener("installed_app_status", setAppStatus);
- evtSource.addEventListener("apps", setApps);
- evtSource.addEventListener("install", setInstall);
- evtSource.addEventListener("hardware_info", setHardwareInfo);
- evtSource.addEventListener("system_startup_info", setSystemStartupInfo);
- }
-
- return () => {
- // cleanup
- if (evtSource) {
- evtSource.removeEventListener("error", eventErrorHandler);
- evtSource.removeEventListener("system_info", setSystemInfo);
- evtSource.removeEventListener("btc_info", setBtcInfo);
- evtSource.removeEventListener("ln_info", setLnInfo);
- evtSource.removeEventListener("wallet_balance", setBalance);
- evtSource.removeEventListener("transactions", setTx);
- evtSource.removeEventListener("installed_app_status", setAppStatus);
- evtSource.removeEventListener("apps", setApps);
- evtSource.removeEventListener("install", setInstall);
- evtSource.removeEventListener("hardware_info", setHardwareInfo);
- evtSource.removeEventListener(
- "system_startup_info",
- setSystemStartupInfo,
- );
- }
- };
- }, [
- t,
- evtSource,
- appCtx,
- sseCtx,
- setEvtSource,
- appInstallSuccessHandler,
- appInstallErrorHandler,
- ]);
-
- return {
- evtSource: sseCtx.evtSource,
- systemInfo: sseCtx.systemInfo,
- btcInfo: sseCtx.btcInfo,
- lnInfo: sseCtx.lnInfo,
- balance: sseCtx.balance,
- appStatus: sseCtx.appStatus,
- transactions: sseCtx.transactions,
- availableApps: sseCtx.availableApps,
- installingApp: sseCtx.installingApp,
- hardwareInfo: sseCtx.hardwareInfo,
- systemStartupInfo: sseCtx.systemStartupInfo,
- };
-}
-
-export default useSSE;
diff --git a/src/hooks/use-ws.tsx b/src/hooks/use-ws.tsx
new file mode 100644
index 00000000..5606fc4f
--- /dev/null
+++ b/src/hooks/use-ws.tsx
@@ -0,0 +1,119 @@
+import { WebSocketContext } from "@/context/ws-context";
+import { InstallAppData } from "@/models/install-app";
+import { SystemInfo } from "@/models/system-info";
+import { setWindowAlias } from "@/utils";
+import { useCallback, useContext } from "react";
+import { useTranslation } from "react-i18next";
+import { toast } from "react-toastify";
+
+export const WEBSOCKET_URL = "/ws";
+
+function useWebSocket() {
+ const { t } = useTranslation();
+ const wsCtx = useContext(WebSocketContext);
+
+ const {
+ systemInfo,
+ btcInfo,
+ lnInfo,
+ balance,
+ appStatus,
+ transactions,
+ availableApps,
+ installingApp,
+ hardwareInfo,
+ systemStartupInfo,
+ } = wsCtx;
+
+ const appInstallSuccessHandler = useCallback(
+ (installData: InstallAppData, appName: string) => {
+ if (installData.mode === "on") {
+ toast.success(t("apps.install_success", { appName }));
+ } else {
+ toast.success(t("apps.uninstall_success", { appName }));
+ }
+ },
+ [t],
+ );
+
+ const appInstallErrorHandler = useCallback(
+ (installData: InstallAppData, appName: string) => {
+ if (installData.mode === "on") {
+ toast.error(
+ t("apps.install_failure", {
+ appName,
+ details: installData.details,
+ }),
+ );
+ } else {
+ toast.error(
+ t("apps.uninstall_failure", {
+ appName,
+ details: installData.details,
+ }),
+ );
+ }
+ },
+ [t],
+ );
+
+ const handleInstall = useCallback(
+ (installAppData: InstallAppData) => {
+ toast.dismiss();
+ // @ts-ignore
+ const appName = availableApps[installAppData.id]?.name || "";
+ if (installAppData.result === "fail") {
+ appInstallErrorHandler(installAppData, appName);
+ wsCtx.setInstallingApp(null);
+ return;
+ }
+ if (installAppData.result === "win") {
+ appInstallSuccessHandler(installAppData, appName);
+ wsCtx.setInstallingApp(null);
+ return;
+ }
+ const installing = installAppData.mode === "on";
+ toast(
+ installing
+ ? `${t("apps.installing")} ${appName}`
+ : `${t("apps.uninstalling")} ${appName}`,
+ {
+ isLoading: true,
+ autoClose: false,
+ },
+ );
+ wsCtx.setInstallingApp(installAppData);
+ },
+ [wsCtx, appInstallSuccessHandler, appInstallErrorHandler, t],
+ );
+
+ const handleSystemInfo = useCallback(
+ (message: SystemInfo) => {
+ if (message.alias) {
+ setWindowAlias(message.alias);
+ }
+ wsCtx.setSystemInfo((prev: SystemInfo) => ({
+ ...prev,
+ ...message,
+ }));
+ },
+ [wsCtx],
+ );
+
+ return {
+ systemInfo,
+ btcInfo,
+ lnInfo,
+ balance,
+ appStatus,
+ transactions,
+ availableApps,
+ installingApp,
+ hardwareInfo,
+ systemStartupInfo,
+ handleInstall,
+ handleSystemInfo,
+ };
+}
+
+export default useWebSocket;
diff --git a/src/index.tsx b/src/index.tsx
index e3e17eea..c8f00e22 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,9 +1,9 @@
import App from "./App";
import AppContextProvider from "./context/app-context";
-import SSEContextProvider from "./context/sse-context";
import "./i18n/config";
import "./index.css";
import ErrorBoundary from "@/ErrorBoundary";
+import WebSocketContextProvider from "@/context/ws-context";
import { NextUIProvider } from "@nextui-org/react";
import "i18next";
import { StrictMode } from "react";
@@ -29,7 +29,7 @@ root.render(
-
+
{/* For persistent toasts over all pages */}
@@ -37,7 +37,7 @@ root.render(
-
+
,
diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx
index c0afea02..d928215f 100644
--- a/src/layouts/Header.tsx
+++ b/src/layouts/Header.tsx
@@ -1,13 +1,13 @@
import DropdownMenu from "./DropdownMenu";
import RaspiBlitzMobileLogo from "@/assets/RaspiBlitz_Logo_Icon.svg?react";
import RaspiBlitzLogoDark from "@/assets/RaspiBlitz_Logo_Main_Negative.svg?react";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import { Bars3Icon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useRef, useState } from "react";
import { NavLink } from "react-router-dom";
export default function Header() {
- const { systemInfo } = useContext(SSEContext);
+ const { systemInfo } = useContext(WebSocketContext);
const dropdown = useRef(null);
const menu = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx
index d4ec9181..902fbb74 100644
--- a/src/layouts/Layout.tsx
+++ b/src/layouts/Layout.tsx
@@ -1,12 +1,12 @@
import BottomNav from "./BottomNav";
import Header from "./Header";
import SideDrawer from "./SideDrawer";
-import useSSE from "@/hooks/use-sse";
+import useWs from "@/hooks/use-ws";
import type { FC, PropsWithChildren } from "react";
const Layout: FC = ({ children }) => {
// use SSE for all components after login
- useSSE();
+ useWs();
return (
<>
diff --git a/src/layouts/SideDrawer.tsx b/src/layouts/SideDrawer.tsx
index daeff0c0..c3912e1c 100644
--- a/src/layouts/SideDrawer.tsx
+++ b/src/layouts/SideDrawer.tsx
@@ -1,6 +1,6 @@
import AppStatusItem from "@/components/AppStatusItem";
import { AppContext } from "@/context/app-context";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import {
ArrowRightStartOnRectangleIcon,
Cog6ToothIcon,
@@ -21,7 +21,7 @@ const navIconClasses = "inline w-10 h-10";
export const SideDrawer: FC = () => {
const { logout } = useContext(AppContext);
- const { appStatus } = useContext(SSEContext);
+ const { appStatus } = useContext(WebSocketContext);
const { t } = useTranslation();
return (
diff --git a/src/pages/Apps/AppInfo.tsx b/src/pages/Apps/AppInfo.tsx
index 9ea52da6..50c1ff45 100644
--- a/src/pages/Apps/AppInfo.tsx
+++ b/src/pages/Apps/AppInfo.tsx
@@ -1,6 +1,6 @@
import ImageCarousel from "./ImageCarousel";
import AppIcon from "@/components/AppIcon";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import PageLoadingScreen from "@/layouts/PageLoadingScreen";
import { availableApps } from "@/utils/availableApps";
import { checkError } from "@/utils/checkError";
@@ -10,7 +10,7 @@ import {
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
-import { Link, Button } from "@nextui-org/react";
+import { Button, Link } from "@nextui-org/react";
import { FC, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
@@ -21,7 +21,7 @@ export const AppInfo: FC = () => {
const { appId } = useParams();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(true);
- const { appStatus, installingApp } = useContext(SSEContext);
+ const { appStatus, installingApp } = useContext(WebSocketContext);
const [imgs, setImgs] = useState([]);
const { name } = availableApps[appId!];
const { author, repository } = availableApps[appId!];
diff --git a/src/pages/Apps/AppPage.tsx b/src/pages/Apps/AppPage.tsx
index db656166..0241f105 100644
--- a/src/pages/Apps/AppPage.tsx
+++ b/src/pages/Apps/AppPage.tsx
@@ -1,4 +1,4 @@
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import PageLoadingScreen from "@/layouts/PageLoadingScreen";
import { getHrefFromApp } from "@/utils";
import { availableApps } from "@/utils/availableApps";
@@ -9,7 +9,7 @@ export const AppInfo: FC = () => {
const navigate = useNavigate();
const { appId } = useParams();
const [isLoading, setIsLoading] = useState(true);
- const { appStatus } = useContext(SSEContext);
+ const { appStatus } = useContext(WebSocketContext);
const { customComponent } = availableApps[appId!];
const app = appStatus.find((app) => app.id === appId);
diff --git a/src/pages/Apps/customApps/Electrs.tsx b/src/pages/Apps/customApps/Electrs.tsx
index 568ac8c3..1940e29b 100644
--- a/src/pages/Apps/customApps/Electrs.tsx
+++ b/src/pages/Apps/customApps/Electrs.tsx
@@ -1,5 +1,5 @@
import { Headline } from "@/components/Headline";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import PageLoadingScreen from "@/layouts/PageLoadingScreen";
import { AdvancedAppStatusElectron } from "@/models/advanced-app-status";
import { checkError } from "@/utils/checkError";
@@ -22,7 +22,7 @@ import { useNavigate } from "react-router-dom";
const Electrs = () => {
const navigate = useNavigate();
const { t } = useTranslation();
- const { appStatus } = useContext(SSEContext);
+ const { appStatus } = useContext(WebSocketContext);
const [isLoading, setIsLoading] = useState(true);
const [appData, setAppData] = useState(
null,
diff --git a/src/pages/Apps/index.tsx b/src/pages/Apps/index.tsx
index 94aa8dbc..2bf92394 100644
--- a/src/pages/Apps/index.tsx
+++ b/src/pages/Apps/index.tsx
@@ -1,6 +1,6 @@
import AppCardAlby from "./AppCardAlby";
import AppList from "./AppList";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import PageLoadingScreen from "@/layouts/PageLoadingScreen";
import { AppStatus } from "@/models/app-status";
import { enableGutter } from "@/utils";
@@ -13,7 +13,7 @@ import { toast } from "react-toastify";
export const Apps: FC = () => {
const { t } = useTranslation(["translation", "apps"]);
- const { appStatus } = useContext(SSEContext);
+ const { appStatus } = useContext(WebSocketContext);
useEffect(() => {
enableGutter();
diff --git a/src/pages/Home/BitcoinCard.tsx b/src/pages/Home/BitcoinCard.tsx
index e3a93ca5..ad16eeb6 100644
--- a/src/pages/Home/BitcoinCard.tsx
+++ b/src/pages/Home/BitcoinCard.tsx
@@ -1,12 +1,12 @@
import LoadingBox from "@/components/LoadingBox";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import { checkPropsUndefined } from "@/utils";
import { FC, useContext } from "react";
import { useTranslation } from "react-i18next";
export const BitcoinCard: FC = () => {
const { t } = useTranslation();
- const { btcInfo, systemInfo } = useContext(SSEContext);
+ const { btcInfo, systemInfo } = useContext(WebSocketContext);
if (checkPropsUndefined({ btcInfo, systemInfo })) {
return ;
diff --git a/src/pages/Home/ConnectionCard.tsx b/src/pages/Home/ConnectionCard.tsx
index dfd7439e..7d6e0ba0 100644
--- a/src/pages/Home/ConnectionCard.tsx
+++ b/src/pages/Home/ConnectionCard.tsx
@@ -1,6 +1,6 @@
import QRCodeModal from "./QRCodeModal";
import LoadingBox from "@/components/LoadingBox";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import useClipboard from "@/hooks/use-clipboard";
import {
ClipboardDocumentCheckIcon,
@@ -16,7 +16,7 @@ const HIDDEN_TEXT = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
export const ConnectionCard: FC = () => {
const { t } = useTranslation();
- const { systemInfo, lnInfo } = useContext(SSEContext);
+ const { systemInfo, lnInfo } = useContext(WebSocketContext);
const [showAddress, setShowAddress] = useState(true);
const [showModal, setShowModal] = useState(false);
diff --git a/src/pages/Home/HardwareCard.tsx b/src/pages/Home/HardwareCard.tsx
index 4757250f..4549c05d 100644
--- a/src/pages/Home/HardwareCard.tsx
+++ b/src/pages/Home/HardwareCard.tsx
@@ -1,5 +1,5 @@
import LoadingBox from "@/components/LoadingBox";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import { FC, useContext } from "react";
import { useTranslation } from "react-i18next";
@@ -11,7 +11,7 @@ const bytesToGB = (bytes: number | undefined): string => {
export const HardwareCard: FC = () => {
const { t } = useTranslation();
- const { hardwareInfo } = useContext(SSEContext);
+ const { hardwareInfo } = useContext(WebSocketContext);
if (!hardwareInfo) {
return (
diff --git a/src/pages/Home/LightningCard.tsx b/src/pages/Home/LightningCard.tsx
index b814f98b..21c6ddb3 100644
--- a/src/pages/Home/LightningCard.tsx
+++ b/src/pages/Home/LightningCard.tsx
@@ -1,6 +1,6 @@
import LoadingBox from "@/components/LoadingBox";
import { AppContext, Unit } from "@/context/app-context";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import { checkPropsUndefined } from "@/utils";
import { convertMSatToBtc, convertToString } from "@/utils/format";
import { FC, useContext } from "react";
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
export const LightningCard: FC = () => {
const { t } = useTranslation();
const { unit } = useContext(AppContext);
- const { lnInfo, balance } = useContext(SSEContext);
+ const { lnInfo, balance } = useContext(WebSocketContext);
const {
num_active_channels: activeChannels,
diff --git a/src/pages/Home/WalletCard.tsx b/src/pages/Home/WalletCard.tsx
index ec11fe5f..16c2bce4 100644
--- a/src/pages/Home/WalletCard.tsx
+++ b/src/pages/Home/WalletCard.tsx
@@ -1,5 +1,5 @@
import { AppContext, Unit } from "@/context/app-context";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import {
convertMSatToBtc,
convertSatToBtc,
@@ -34,7 +34,7 @@ export const WalletCard: FC = ({
}) => {
const { t } = useTranslation();
const { unit } = useContext(AppContext);
- const { balance } = useContext(SSEContext);
+ const { balance } = useContext(WebSocketContext);
const {
onchain_total_balance: onchainBalance,
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
index aa85af64..5fa3a739 100644
--- a/src/pages/Home/index.tsx
+++ b/src/pages/Home/index.tsx
@@ -11,7 +11,7 @@ import TransactionDetailModal from "./TransactionCard/TransactionDetailModal/Tra
import UnlockModal from "./UnlockModal";
import WalletCard from "./WalletCard";
import { AppContext } from "@/context/app-context";
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import { useInterval } from "@/hooks/use-interval";
import PageLoadingScreen from "@/layouts/PageLoadingScreen";
import { Transaction } from "@/models/transaction.model";
@@ -36,7 +36,7 @@ type ModalType =
const Home: FC = () => {
const { t } = useTranslation();
const { walletLocked, setWalletLocked } = useContext(AppContext);
- const { balance, lnInfo, systemStartupInfo } = useContext(SSEContext);
+ const { balance, lnInfo, systemStartupInfo } = useContext(WebSocketContext);
const [showModal, setShowModal] = useState(false);
const [detailTx, setDetailTx] = useState(null);
const [transactions, setTransactions] = useState([]);
diff --git a/src/pages/Settings/VersionBox.tsx b/src/pages/Settings/VersionBox.tsx
index 16bcab2e..1df1362f 100644
--- a/src/pages/Settings/VersionBox.tsx
+++ b/src/pages/Settings/VersionBox.tsx
@@ -1,4 +1,4 @@
-import { SSEContext } from "@/context/sse-context";
+import { WebSocketContext } from "@/context/ws-context";
import packageJson from "package.json";
import { FC, useContext } from "react";
import { useTranslation } from "react-i18next";
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
*/
const VersionBox: FC = () => {
const { t } = useTranslation();
- const { systemInfo } = useContext(SSEContext);
+ const { systemInfo } = useContext(WebSocketContext);
const { platform_version, api_version } = systemInfo;
diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx
index 9b75fe7c..6077740b 100644
--- a/src/utils/test-utils.tsx
+++ b/src/utils/test-utils.tsx
@@ -4,10 +4,10 @@ import {
AppContextType,
} from "@/context/app-context";
import {
- SSEContext,
- sseContextDefault,
- SSEContextType,
-} from "@/context/sse-context";
+ WebSocketContext,
+ websocketContextDefault,
+ WebSocketContextType,
+} from "@/context/ws-context";
import i18n from "@/i18n/test_config";
import { render, RenderOptions } from "@testing-library/react";
import { FC, PropsWithChildren, ReactElement } from "react";
@@ -15,7 +15,7 @@ import { I18nextProvider } from "react-i18next";
import { BrowserRouter } from "react-router-dom";
type Props = {
- sseProps: SSEContextType;
+ sseProps: WebSocketContextType;
appProps: AppContextType;
};
@@ -26,9 +26,9 @@ const AllTheProviders: FC> = ({
}) => {
return (
-
@@ -40,7 +40,7 @@ const AllTheProviders: FC> = ({
>
{children}
-
+
);
};
@@ -49,7 +49,7 @@ const customRender = (
ui: ReactElement,
options?: Omit & {
providerOptions?: {
- sseProps?: Partial;
+ sseProps?: Partial;
appProps?: Partial;
};
},
diff --git a/vite.config.ts b/vite.config.ts
index e9770563..a6479dbd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -32,6 +32,12 @@ export default defineConfig({
changeOrigin: true,
secure: false,
},
+ "/ws": {
+ target: "ws://localhost:8000",
+ changeOrigin: true,
+ secure: false,
+ ws: true,
+ },
},
},
test: {