feat: add dependency injection using tanstack context (#680)

* feat: add dep injection using tanstack context

* fixed small typo
This commit is contained in:
Dan Ditomaso
2025-06-24 18:19:26 -04:00
committed by GitHub
parent 6c676fa8da
commit e2f03aaf81
9 changed files with 95 additions and 30 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ stats.html
.vercel
.vite
dev-dist
__screenshots__*
__screenshots__*
*.diff

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/core/connection"]
path = src/core/connection
url = https://github.com/meshtastic/js.git

View File

@@ -82,6 +82,7 @@ export const NodeDetailsDialog = ({
function handleDirectMessage() {
if (!node) return;
navigate({ to: `/messages/direct/${node.num}` });
setDialogOpen("nodeDetails", false);
}
function handleRequestPosition() {

View File

@@ -51,7 +51,7 @@ interface MessageItemProps {
export const MessageItem = ({ message }: MessageItemProps) => {
const { getNode } = useDevice();
const { getMyNodeNum } = useMessageStore();
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation("messages");
const MESSAGE_STATUS_MAP = useMemo(
(): Record<MessageState, MessageStatusInfo> => ({
@@ -78,7 +78,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
);
const UNKNOWN_STATUS = useMemo((): MessageStatusInfo => ({
displayText: t("delveryStatus.unknown.displayText"),
displayText: t("deliveryStatus.unknown.displayText"),
icon: AlertCircle,
ariaLabel: t("deliveryStatus.unknown.label"),
iconClassName: "text-red-500 dark:text-red-400",
@@ -99,7 +99,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const { displayName, shortName, isFavorite } = useMemo(() => {
const userIdHex = message.from.toString(16).toUpperCase().padStart(2, "0");
const last4 = userIdHex.slice(-4);
const fallbackName = t("message_item_fallbackName_withLastFour", { last4 });
const fallbackName = t("fallbackName", { last4 });
const longName = messageUser?.user?.longName;
const derivedShortName = messageUser?.user?.shortName || fallbackName;
const derivedDisplayName = longName || derivedShortName;

View File

@@ -70,7 +70,7 @@ const Generator = (
key: "bit8",
},
{
text: t("security.empty"),
text: t("security.0bit"),
value: "0",
key: "bit0",
},

View File

@@ -114,6 +114,8 @@ export interface Device {
hasNodeError: (nodeNum: number) => boolean;
incrementUnread: (nodeNum: number) => void;
resetUnread: (nodeNum: number) => void;
getUnreadCount: (nodeNum: number) => number;
getAllUnreadCount: () => number;
getNodes: (
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
) => Protobuf.Mesh.NodeInfo[];
@@ -664,6 +666,24 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}),
);
},
getUnreadCount: (nodeNum: number): number => {
const device = get().devices.get(id);
if (!device) {
return 0;
}
return device.unreadCounts.get(nodeNum) ?? 0;
},
getAllUnreadCount: (): number => {
const device = get().devices.get(id);
if (!device) {
return 0;
}
let totalUnread = 0;
device.unreadCounts.forEach((count) => {
totalUnread += count;
});
return totalUnread;
},
resetUnread: (nodeNum: number) => {
set(
produce<PrivateDeviceState>((draft) => {

View File

@@ -1,11 +1,15 @@
import React from "react";
import "@app/index.css";
import { enableMapSet } from "immer";
import "maplibre-gl/dist/maplibre-gl.css";
import { StrictMode, Suspense } from "react";
import { Suspense } from "react";
import { createRoot } from "react-dom/client";
import "./i18n/config.ts";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "@app/routes.tsx";
import { router } from "@app/routes.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { useTranslation } from "react-i18next";
declare module "@tanstack/react-router" {
interface Register {
@@ -16,16 +20,27 @@ declare module "@tanstack/react-router" {
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);
enableMapSet();
function IndexPage() {
enableMapSet();
const appStore = useAppStore();
const messageStore = useMessageStore();
const translation = useTranslation();
const router = createRouter({
routeTree,
});
const context = React.useMemo(() => ({
stores: {
app: appStore,
message: messageStore,
},
i18n: translation,
}), [appStore, messageStore]);
root.render(
<StrictMode>
<Suspense fallback={null}>
<RouterProvider router={router} />
</Suspense>
</StrictMode>,
);
return (
<React.StrictMode>
<Suspense fallback={null}>
<RouterProvider router={router} context={context} />
</Suspense>
</React.StrictMode>
);
}
root.render(<IndexPage />);

View File

@@ -47,7 +47,7 @@ export const MessagesPage = () => {
getNodes,
getNode,
hasNodeError,
unreadCounts,
getUnreadCount,
resetUnread,
connection,
} = useDevice();
@@ -105,7 +105,7 @@ export const MessagesPage = () => {
})
.map((node) => ({
...node,
unreadCount: unreadCounts.get(node.num) ?? 0,
unreadCount: getUnreadCount(node.num) ?? 0,
}))
.sort((a, b) => {
const diff = b.unreadCount - a.unreadCount;
@@ -211,7 +211,7 @@ export const MessagesPage = () => {
{filteredChannels?.map((channel) => (
<SidebarButton
key={channel.index}
count={unreadCounts.get(channel.index)}
count={getUnreadCount(channel.index)}
label={channel.settings?.name ||
(channel.index === 0
? t("page.broadcastLabel", { ns: "channels" })
@@ -236,10 +236,10 @@ export const MessagesPage = () => {
</Sidebar>
), [
filteredChannels,
unreadCounts,
numericChatId,
chatType,
isCollapsed,
getUnreadCount,
navigateToChat,
resetUnread,
t,
@@ -249,7 +249,7 @@ export const MessagesPage = () => {
() => (
<SidebarSection
label=""
className="px-0 flex flex-col h-full overflow-y-auto"
className="px-0 flex-col h-full overflow-y-auto"
>
<label className="p-2 block">
<Input

View File

@@ -1,17 +1,32 @@
import { createRoute, redirect } from "@tanstack/react-router";
import {
createRootRouteWithContext,
createRoute,
createRouter,
redirect,
} from "@tanstack/react-router";
import { Dashboard } from "@pages/Dashboard/index.tsx";
import MessagesPage from "@pages/Messages.tsx";
import MapPage from "@pages/Map/index.tsx";
import ConfigPage from "@pages/Config/index.tsx";
import ChannelsPage from "@pages/Channels.tsx";
import NodesPage from "@pages/Nodes/index.tsx";
import { createRootRoute } from "@tanstack/react-router";
import { App } from "./App.tsx";
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
import { z } from "zod";
import { useAppStore } from "@core/stores/appStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { App } from "./App.tsx";
import { useTranslation } from "react-i18next";
const rootRoute = createRootRoute({
component: App,
interface AppContext {
stores: {
app: ReturnType<typeof useAppStore>;
message: ReturnType<typeof useMessageStore>;
};
i18n: ReturnType<typeof useTranslation>;
}
export const rootRoute = createRootRouteWithContext<AppContext>()({
component: () => <App />,
});
const indexRoute = createRoute({
@@ -96,7 +111,7 @@ const dialogWithParamsRoute = createRoute({
component: DialogManager,
});
export const routeTree = rootRoute.addChildren([
const routeTree = rootRoute.addChildren([
indexRoute,
messagesRoute,
messagesWithParamsRoute,
@@ -107,4 +122,14 @@ export const routeTree = rootRoute.addChildren([
dialogWithParamsRoute,
]);
export { rootRoute };
const router = createRouter({
routeTree,
context: {
stores: {
app: {} as ReturnType<typeof useAppStore>,
message: {} as ReturnType<typeof useMessageStore>,
},
i18n: {} as ReturnType<typeof import("react-i18next").useTranslation>,
},
});
export { router };