feat: frontend notification manager

This commit is contained in:
2025-05-10 16:43:30 +01:00
parent 64439d9041
commit 90b863b6cf
4 changed files with 119 additions and 6 deletions

View File

@ -1,8 +1,15 @@
import { type Component, type JSX, Show } from "solid-js";
import {
type Component,
type JSX,
ParentProps,
Show,
useContext,
} from "solid-js";
import { jwtDecode } from "jwt-decode";
import { Navigate } from "@solidjs/router";
import { save_token } from "tauri-plugin-ios-shared-token-api";
import { platform } from "@tauri-apps/plugin-os";
import { Notifications, NotificationsContext } from "./notifications";
export const isTokenValid = (): boolean => {
const token = localStorage.getItem("access");
@ -19,9 +26,26 @@ export const isTokenValid = (): boolean => {
}
};
export const ProtectedRoute: Component<{ children?: JSX.Element }> = (
props,
) => {
const WithNotifications: Component<ParentProps> = (props) => {
const notifications = Notifications();
return (
<NotificationsContext.Provider value={notifications}>
{props.children}
</NotificationsContext.Provider>
);
};
export const useNotifications = () => {
const notifications = useContext(NotificationsContext);
if (notifications == null) {
throw new Error("Notifications must be defined when using this hook");
}
return notifications;
};
export const ProtectedRoute: Component<ParentProps> = (props) => {
const isValid = isTokenValid();
if (isValid) {
@ -42,7 +66,7 @@ export const ProtectedRoute: Component<{ children?: JSX.Element }> = (
return (
<Show when={isValid} fallback={<Navigate href="/login" />}>
{props.children}
<WithNotifications>{props.children}</WithNotifications>
</Show>
);
};

View File

@ -17,12 +17,16 @@ import { base, type UserImage } from "./network";
import { useSearchImageContext } from "./contexts/SearchImageContext";
import { A } from "@solidjs/router";
import { useSetEntity } from "./WithEntityDialog";
import { useNotifications } from "./ProtectedRoute";
export const Search = () => {
const [searchResults, setSearchResults] = createSignal<UserImage[]>([]);
const [searchQuery, setSearchQuery] = createSignal("");
const setEntity = useSetEntity();
const notifications = useNotifications();
console.log(notifications);
const { images, imagesWithProperties, onRefetchImages } =
useSearchImageContext();

View File

@ -20,7 +20,7 @@ type BaseRequestParams = Partial<{
method: "GET" | "POST";
}>;
export const base = "https://haystack.johncosta.tech";
export const base = "http://localhost:3040";
const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => {
return new Request(`${base}/${path}`, {

View File

@ -0,0 +1,85 @@
import { createStore } from "solid-js/store";
import {
type InferOutput,
literal,
pipe,
safeParse,
strictObject,
string,
union,
uuid,
} from "valibot";
import { base } from "../network";
import { createContext } from "solid-js";
const processingImagesValidator = strictObject({
ImageID: pipe(string(), uuid()),
Status: union([literal("in-process"), literal("complete")]),
});
type NotificationState = {
ProcessingImages: Record<
string,
InferOutput<typeof processingImagesValidator>["Status"]
>;
};
export const Notifications = () => {
const [state, setState] = createStore<NotificationState>({
ProcessingImages: {},
});
const access = localStorage.getItem("access");
if (access == null) {
throw new Error("Access token not defined");
}
const dataEventListener = (e: MessageEvent<unknown>) => {
const processingImage = safeParse(processingImagesValidator, e.data);
if (!processingImage.success) {
console.error("Processing image could not be parsed.", e.data);
return;
}
const { ImageID, Status } = processingImage.output;
if (ImageID in state.ProcessingImages) {
const localImageStatus = state.ProcessingImages[ImageID];
if (localImageStatus !== "in-process" || Status !== "complete") {
console.error(
"Invalid state, an image present must always be in process",
);
return;
}
setState("ProcessingImages", (images) => {
delete images[ImageID];
// TODO: this might not work because it's not a new object.
return images;
});
} else {
if (Status !== "in-process") {
console.error(
"Invalid state, at this point a new image that isn't present should be in-progress",
);
return;
}
setState("ProcessingImages", {
[ImageID]: Status,
});
}
};
const events = new EventSource(`${base}/notifications?token=${access}`);
events.addEventListener("data", dataEventListener);
return {
state,
};
};
export const NotificationsContext =
createContext<ReturnType<typeof Notifications>>();