Files
kandev/apps/web/hooks/use-task-color.ts
T
2026-09-06 17:18:09 +01:00

149 lines
5.0 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { useTranslation } from "react-i18next";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
import { compareUserSettingsRevisions } from "@/lib/settings/user-settings-revision";
import { createQueuedUserSettingsSyncWithResponse } from "@/lib/user-settings-sync";
import type { UserSettingsUpdatePayload } from "@/lib/types/http-user-settings";
import { type TaskColor } from "@/lib/task-colors";
type TaskColorMap = Record<string, TaskColor | null>;
function cloneTaskColorMap(colors: TaskColorMap): TaskColorMap {
return { ...colors };
}
function optimisticTaskColorSettings(
settings: ReturnType<ReturnType<typeof useAppStoreApi>["getState"]>["userSettings"],
taskId: string,
color: TaskColor | null,
) {
return {
...settings,
sidebarTaskColors: {
...settings.sidebarTaskColors,
[taskId]: color,
},
loaded: true,
};
}
function responseIsCurrent(
responseRevision: number | undefined,
currentRevision: number | null,
settingsAtSubmit: object,
currentSettings: object,
) {
const order = compareUserSettingsRevisions(responseRevision, currentRevision);
return order === null ? currentSettings === settingsAtSubmit : order >= 0;
}
export function useTaskColor(taskId: string | undefined): TaskColor | null {
return useAppStore((state) => {
if (!taskId) return null;
return state.userSettings.sidebarTaskColors[taskId] ?? null;
});
}
export function useSetTaskColor(): (taskId: string, color: TaskColor | null) => void {
const store = useAppStoreApi();
const settings = useAppStore((state) => state.userSettings);
const { toast } = useToast();
const { t } = useTranslation();
const confirmedColorsRef = useRef<TaskColorMap>(cloneTaskColorMap(settings.sidebarTaskColors));
const confirmedRevisionRef = useRef<number | null>(settings.revision);
const operationRef = useRef(0);
const pendingRef = useRef(0);
const sync = useMemo(
() =>
createQueuedUserSettingsSyncWithResponse<{
taskId: string;
color: TaskColor | null;
}>(
({ taskId, color }): UserSettingsUpdatePayload => ({
sidebar_task_color_patch: {
colors: { [taskId]: color },
if_missing: false,
},
}),
),
[],
);
useEffect(() => {
const current = store.getState().userSettings;
const order = compareUserSettingsRevisions(current.revision, confirmedRevisionRef.current);
if (pendingRef.current === 0) {
if (order === -1) return;
confirmedColorsRef.current = cloneTaskColorMap(current.sidebarTaskColors);
confirmedRevisionRef.current = current.revision;
return;
}
if (order === 1) {
confirmedColorsRef.current = cloneTaskColorMap(current.sidebarTaskColors);
confirmedRevisionRef.current = current.revision;
}
}, [settings, store]);
return useCallback(
(taskId: string, color: TaskColor | null) => {
if (!taskId) return;
const operation = ++operationRef.current;
pendingRef.current += 1;
const settingsAtSubmit = store.getState().userSettings;
store
.getState()
.setUserSettings(optimisticTaskColorSettings(settingsAtSubmit, taskId, color));
void sync({ taskId, color })
.then((response) => {
const latest = store.getState().userSettings;
const mapped = mapUserSettingsResponse(response, latest);
const responseOrder = compareUserSettingsRevisions(
mapped.revision,
confirmedRevisionRef.current,
);
if (responseOrder !== 1) return;
confirmedColorsRef.current = cloneTaskColorMap(mapped.sidebarTaskColors);
confirmedRevisionRef.current = mapped.revision;
if (
operation !== operationRef.current ||
!responseIsCurrent(
response.settings.revision,
latest.revision,
settingsAtSubmit,
latest,
)
) {
return;
}
store.getState().setUserSettings(mapped);
})
.catch(() => {
if (operation !== operationRef.current) return;
const latest = store.getState().userSettings;
const order = compareUserSettingsRevisions(latest.revision, confirmedRevisionRef.current);
if (order === 1) {
confirmedColorsRef.current = cloneTaskColorMap(latest.sidebarTaskColors);
confirmedRevisionRef.current = latest.revision;
}
store.getState().setUserSettings({
...latest,
sidebarTaskColors: cloneTaskColorMap(confirmedColorsRef.current),
loaded: true,
});
toast({ variant: "error", description: t("task:manualColorSaveError") });
})
.finally(() => {
pendingRef.current -= 1;
});
},
[store, sync, t, toast],
);
}