feat: frontend notification manager
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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}`, {
|
||||
|
85
frontend/src/notifications/index.ts
Normal file
85
frontend/src/notifications/index.ts
Normal 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>>();
|
Reference in New Issue
Block a user