Files
kandev/apps/web/components/gitlab/subscription-toggle.tsx
T
2026-09-10 18:14:05 +01:00

152 lines
4.7 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { IconBell, IconBellOff } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import {
getIssueSubscription,
getMRSubscription,
setIssueSubscription,
setMRSubscription,
} from "@/lib/api/domains/gitlab-api";
import { useToast } from "@/components/toast-provider";
import { isCurrentIdentityRequest } from "@/hooks/domains/gitlab/request-identity";
import { useTranslation } from "react-i18next";
function requestIsActive(
generation: number,
requestIdentity: string,
currentGeneration: { current: number },
currentIdentity: { current: string },
) {
return isCurrentIdentityRequest(
generation,
currentGeneration.current,
requestIdentity,
currentIdentity.current,
);
}
export function subscriptionActionLabel(
subscribed: boolean,
t: (key: string, values?: Record<string, unknown>) => string,
): string {
return subscribed
? t("gitlab:unsubscribeFromNotifications")
: t("gitlab:subscribeToNotifications");
}
type SubscriptionIdentity = {
kind?: "mr" | "issue";
workspaceId: string;
host: string;
project: string;
iid: number;
};
function useGitLabSubscription({
kind = "mr",
workspaceId,
host,
project,
iid,
}: SubscriptionIdentity) {
const { t } = useTranslation();
const [subscribed, setSubscribed] = useState(false);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const requestGeneration = useRef(0);
const identity = `${kind}\0${workspaceId}\0${host}\0${project}\0${iid}`;
const currentIdentity = useRef(identity);
currentIdentity.current = identity;
useEffect(() => {
const generation = ++requestGeneration.current;
const requestIdentity = identity;
setSubscribed(false);
setLoading(true);
const getSubscription = kind === "issue" ? getIssueSubscription : getMRSubscription;
getSubscription({ workspaceId, host, project, iid })
.then((state) => {
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
setSubscribed(state.subscribed);
}
})
.catch((error: unknown) => {
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
toast({
title: t("gitlab:notificationStatusUnavailable"),
description:
error instanceof Error ? error.message : t("gitlab:gitlabRejectedTheRequest"),
variant: "error",
});
}
})
.finally(() => {
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
setLoading(false);
}
});
return () => {
if (requestGeneration.current === generation) requestGeneration.current += 1;
};
}, [kind, workspaceId, host, project, iid, identity, toast]);
const toggle = async () => {
const generation = ++requestGeneration.current;
const requestIdentity = identity;
const next = !subscribed;
setLoading(true);
try {
const setSubscription = kind === "issue" ? setIssueSubscription : setMRSubscription;
const state = await setSubscription({ workspaceId, host, project, iid, subscribed: next });
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
setSubscribed(state.subscribed);
toast({
description: state.subscribed
? t("gitlab:subscribedToGitlabNotifications")
: t("gitlab:unsubscribedFromGitlabNotifications"),
variant: "success",
});
}
} catch (error) {
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
toast({
title: t("gitlab:notificationUpdateFailed"),
description: error instanceof Error ? error.message : t("gitlab:gitlabRejectedTheAction"),
variant: "error",
});
}
} finally {
if (requestIsActive(generation, requestIdentity, requestGeneration, currentIdentity)) {
setLoading(false);
}
}
};
return { subscribed, loading, toggle };
}
export function SubscriptionToggle(identity: SubscriptionIdentity) {
const { t } = useTranslation();
const { subscribed, loading, toggle } = useGitLabSubscription(identity);
const label = subscriptionActionLabel(subscribed, t);
const Icon = subscribed ? IconBellOff : IconBell;
return (
<Button
variant="outline"
className="cursor-pointer gap-1.5"
disabled={loading}
aria-label={label}
title={label}
onClick={() => void toggle()}
>
<Icon className="h-4 w-4" />
<span className="hidden lg:inline">
{subscribed ? t("gitlab:unsubscribe") : t("gitlab:subscribe")}
</span>
</Button>
);
}