summaryrefslogtreecommitdiff
path: root/packages/web-util/src/hooks/useNotifications.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src/hooks/useNotifications.ts')
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts337
1 files changed, 337 insertions, 0 deletions
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
new file mode 100644
index 000000000..103b88c86
--- /dev/null
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -0,0 +1,337 @@
+import {
+ AbsoluteTime,
+ Duration,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ OperationResult,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+import { ButtonHandler, OnOperationFailReturnType, OnOperationSuccesReturnType } from "../components/Button.js";
+import {
+ InternationalizationAPI,
+ memoryMap,
+ useTranslationContext,
+} from "../index.browser.js";
+
+export type NotificationMessage = ErrorNotification | InfoNotification;
+
+export interface ErrorNotification {
+ type: "error";
+ title: TranslatedString;
+ ack?: boolean;
+ timeout?: boolean;
+ description?: TranslatedString;
+ debug?: any;
+ when: AbsoluteTime;
+}
+export interface InfoNotification {
+ type: "info";
+ title: TranslatedString;
+ ack?: boolean;
+ timeout?: boolean;
+ when: AbsoluteTime;
+}
+
+const storage = memoryMap<Map<string, NotificationMessage>>();
+const NOTIFICATION_KEY = "notification";
+
+export const GLOBAL_NOTIFICATION_TIMEOUT = Duration.fromSpec({
+ seconds: 5,
+});
+
+function updateInStorage(n: NotificationMessage) {
+ const h = hash(n);
+ const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = new Map(mem);
+ newState.set(h, n);
+ storage.set(NOTIFICATION_KEY, newState);
+}
+
+export function notify(notif: NotificationMessage): void {
+ const currentState: Map<string, NotificationMessage> =
+ storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = currentState.set(hash(notif), notif);
+
+ if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") {
+ setTimeout(() => {
+ notif.timeout = true;
+ updateInStorage(notif);
+ }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms);
+ }
+
+ storage.set(NOTIFICATION_KEY, newState);
+}
+export function notifyError(
+ title: TranslatedString,
+ description: TranslatedString | undefined,
+ debug?: any,
+) {
+ notify({
+ type: "error" as const,
+ title,
+ description,
+ debug,
+ when: AbsoluteTime.now(),
+ });
+}
+export function notifyException(title: TranslatedString, ex: Error) {
+ notify({
+ type: "error" as const,
+ title,
+ description: ex.message as TranslatedString,
+ debug: ex.stack,
+ when: AbsoluteTime.now(),
+ });
+}
+export function notifyInfo(title: TranslatedString) {
+ notify({
+ type: "info" as const,
+ title,
+ when: AbsoluteTime.now(),
+ });
+}
+
+export type Notification = {
+ message: NotificationMessage;
+ acknowledge: () => void;
+};
+
+export function useNotifications(): Notification[] {
+ const [, setLastUpdate] = useState<number>();
+ const value = storage.get(NOTIFICATION_KEY) ?? new Map();
+
+ useEffect(() => {
+ return storage.onUpdate(NOTIFICATION_KEY, () => {
+ setLastUpdate(Date.now())
+ // const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+ // setter(structuredClone(mem));
+ });
+ });
+
+ return Array.from(value.values()).map((message, idx) => {
+ return {
+ message,
+ acknowledge: () => {
+ message.ack = true;
+ updateInStorage(message);
+ },
+ };
+ });
+}
+
+function hashCode(str: string): string {
+ if (str.length === 0) return "0";
+ let hash = 0;
+ let chr;
+ for (let i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash.toString(16);
+}
+
+function hash(msg: NotificationMessage): string {
+ let str = (msg.type + ":" + msg.title) as string;
+ if (msg.type === "error") {
+ if (msg.description) {
+ str += ":" + msg.description;
+ }
+ if (msg.debug) {
+ str += ":" + msg.debug;
+ }
+ }
+ return hashCode(str);
+}
+
+function errorMap<T extends OperationFail<unknown>>(
+ resp: T,
+ map: (d: T["case"]) => TranslatedString,
+): void {
+ notify({
+ type: "error",
+ title: map(resp.case),
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+}
+
+export type ErrorNotificationHandler = (
+ cb: (notify: typeof errorMap) => Promise<void>,
+) => Promise<void>;
+
+/**
+ * @deprecated use useLocalNotificationHandler
+ *
+ * @returns
+ */
+export function useLocalNotification(): [
+ Notification | undefined,
+ (n: NotificationMessage) => void,
+ ErrorNotificationHandler,
+] {
+ const { i18n } = useTranslationContext();
+
+ const [value, setter] = useState<NotificationMessage>();
+ const notif = !value
+ ? undefined
+ : {
+ message: value,
+ acknowledge: () => {
+ setter(undefined);
+ },
+ };
+
+ async function errorHandling(cb: (notify: typeof errorMap) => Promise<void>) {
+ try {
+ return await cb(errorMap);
+ } catch (error: unknown) {
+ if (error instanceof TalerError) {
+ notify(buildUnifiedRequestErrorMessage(i18n, error));
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString,
+ );
+ }
+ }
+ }
+ return [notif, setter, errorHandling];
+}
+
+type HandlerMaker = <T extends OperationResult<A, B>, A, B>(
+ onClick: () => Promise<T | undefined>,
+ onOperationSuccess: OnOperationSuccesReturnType<T>,
+ onOperationFail?: OnOperationFailReturnType<T>,
+ onOperationComplete?: () => void,
+) => ButtonHandler<T, A, B>;
+
+export function useLocalNotificationHandler(): [
+ Notification | undefined,
+ HandlerMaker,
+ (n: NotificationMessage) => void,
+] {
+ const [value, setter] = useState<NotificationMessage>();
+ const notif = !value
+ ? undefined
+ : {
+ message: value,
+ acknowledge: () => {
+ setter(undefined);
+ },
+ };
+
+ function makeHandler<T extends OperationResult<A, B>, A, B>(
+ onClick: () => Promise<T | undefined>,
+ onOperationSuccess:OnOperationSuccesReturnType<T>,
+ onOperationFail?: OnOperationFailReturnType<T>,
+ onOperationComplete?: () => void,
+ ): ButtonHandler<T, A, B> {
+ return {
+ onClick,
+ onNotification: setter,
+ onOperationFail,
+ onOperationSuccess,
+ onOperationComplete,
+ };
+ }
+
+ return [notif, makeHandler, setter];
+}
+
+export function buildUnifiedRequestErrorMessage(
+ i18n: InternationalizationAPI,
+ cause: TalerError,
+): ErrorNotification {
+ let result: ErrorNotification;
+ switch (cause.errorDetail.code) {
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Request cancelled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ result = {
+ type: "error",
+ title: i18n.str`Request throttled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ result = {
+ type: "error",
+ title: i18n.str`Malformed response`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Network error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected request error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ default: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ }
+ return result;
+}