diff --git a/src/hooks/useEqualSplitUndo.ts b/src/hooks/useEqualSplitUndo.ts index 4b5ee77..04f14c6 100644 --- a/src/hooks/useEqualSplitUndo.ts +++ b/src/hooks/useEqualSplitUndo.ts @@ -9,12 +9,16 @@ interface RecipientRow { export function useEqualSplitUndo(recipients: RecipientRow[]) { const [previousState, setPreviousState] = useState(null); + const [undoCount, setUndoCount] = useState(0); + const [redoCount, setRedoCount] = useState(0); const splitEqually = useCallback( (totalAmount: number) => { if (recipients.length <= 1) return false; setPreviousState([...recipients]); + setUndoCount((count) => count + 1); + setRedoCount(0); const perRecipient = Math.floor(100 / recipients.length); const remainder = 100 - (perRecipient * recipients.length); @@ -39,10 +43,14 @@ export function useEqualSplitUndo(recipients: RecipientRow[]) { const undo = useCallback(() => { const state = previousState; setPreviousState(null); + if (state !== null) { + setUndoCount((count) => Math.max(0, count - 1)); + setRedoCount((count) => count + 1); + } return state; }, [previousState]); const hasUndo = previousState !== null; - return { splitEqually, undo, hasUndo }; + return { splitEqually, undo, hasUndo, undoCount, redoCount }; } diff --git a/src/hooks/useFeeSpikeDetection.ts b/src/hooks/useFeeSpikeDetection.ts index 5e95477..083680b 100644 --- a/src/hooks/useFeeSpikeDetection.ts +++ b/src/hooks/useFeeSpikeDetection.ts @@ -1,20 +1,22 @@ "use client"; -import { useEffect, useState } from "react"; -import { NORMAL_FEE_THRESHOLD } from "@/lib/stellar"; +import { useEffect, useRef, useState } from "react"; export interface FeeSpikeState { p95AcceptedFee: number | null; isSpike: boolean; loading: boolean; + sensitivityPercent: number; } -export function useFeeSpikeDetection(): FeeSpikeState { +export function useFeeSpikeDetection(sensitivityPercent = 50): FeeSpikeState { const [state, setState] = useState({ p95AcceptedFee: null, isSpike: false, loading: true, + sensitivityPercent, }); + const historyRef = useRef([]); useEffect(() => { let cancelled = false; @@ -27,10 +29,21 @@ export function useFeeSpikeDetection(): FeeSpikeState { const json = await response.json(); const p95AcceptedFee = Number(json.p95_accepted_fee); if (!cancelled) { + const history = historyRef.current; + const rollingAverage = + history.length > 0 + ? history.reduce((sum, fee) => sum + fee, 0) / history.length + : p95AcceptedFee; + const isSpike = + rollingAverage > 0 && + p95AcceptedFee > rollingAverage * (1 + sensitivityPercent / 100); + history.push(p95AcceptedFee); + if (history.length > 10) history.shift(); setState({ p95AcceptedFee, - isSpike: p95AcceptedFee > NORMAL_FEE_THRESHOLD * 2, + isSpike, loading: false, + sensitivityPercent, }); } } catch { @@ -51,7 +64,7 @@ export function useFeeSpikeDetection(): FeeSpikeState { window.clearInterval(intervalId); document.removeEventListener("visibilitychange", handleVisibilityChange); }; - }, []); + }, [sensitivityPercent]); return state; } diff --git a/src/hooks/useInvoiceSelection.ts b/src/hooks/useInvoiceSelection.ts index 5d3a913..23a8a05 100644 --- a/src/hooks/useInvoiceSelection.ts +++ b/src/hooks/useInvoiceSelection.ts @@ -8,6 +8,7 @@ interface UseInvoiceSelectionResult { toggleSelecting: () => void; toggleInvoice: (id: string) => void; selectAll: (ids: string[]) => void; + selectAllVisible: (visibleIds: string[]) => void; deselectAll: () => void; isSelected: (id: string) => boolean; selectedCount: number; @@ -40,6 +41,10 @@ export function useInvoiceSelection(): UseInvoiceSelectionResult { setSelectedIds(new Set(ids)); }, []); + const selectAllVisible = useCallback((visibleIds: string[]) => { + setSelectedIds(new Set(visibleIds)); + }, []); + const deselectAll = useCallback(() => { setSelectedIds(new Set()); }, []); @@ -55,6 +60,7 @@ export function useInvoiceSelection(): UseInvoiceSelectionResult { toggleSelecting, toggleInvoice, selectAll, + selectAllVisible, deselectAll, isSelected, selectedCount: selectedIds.size, diff --git a/src/hooks/usePushNotifications.ts b/src/hooks/usePushNotifications.ts index 6c9f851..08f39cd 100644 --- a/src/hooks/usePushNotifications.ts +++ b/src/hooks/usePushNotifications.ts @@ -4,8 +4,11 @@ import { useCallback, useEffect, useState } from "react"; export type PushNotificationStatus = "unsupported" | "off" | "denied" | "active"; +export type PushNotificationPermissionStatus = "granted" | "denied" | "default"; + export interface UsePushNotificationsResult { status: PushNotificationStatus; + permissionStatus: PushNotificationPermissionStatus; subscribe: () => Promise; unsubscribe: () => Promise; } @@ -32,12 +35,16 @@ function isSupported(): boolean { */ export function usePushNotifications(invoiceId: string): UsePushNotificationsResult { const [status, setStatus] = useState("off"); + const [permissionStatus, setPermissionStatus] = useState( + isSupported() ? Notification.permission : "default" + ); const refreshStatus = useCallback(async () => { if (!isSupported()) { setStatus("unsupported"); return; } + setPermissionStatus(Notification.permission); if (Notification.permission === "denied") { setStatus("denied"); return; @@ -59,6 +66,7 @@ export function usePushNotifications(invoiceId: string): UsePushNotificationsRes if (!isSupported()) return false; const permission = await Notification.requestPermission(); + setPermissionStatus(permission); if (permission !== "granted") { setStatus(permission === "denied" ? "denied" : "off"); return false; @@ -100,5 +108,5 @@ export function usePushNotifications(invoiceId: string): UsePushNotificationsRes setStatus("off"); }, [invoiceId]); - return { status, subscribe, unsubscribe }; + return { status, permissionStatus, subscribe, unsubscribe }; }