feat: page update notifications (#2074)

* feat: watchers notification and email preferences

* fix: email copy

* digests

* clean up

* fix

* clean up

* move backlinks queue-up to history processor

* fix

* fix keys

* feat: group notifications

* filter

* adjust email digest window
This commit is contained in:
Philip Okugbe
2026-03-31 16:03:59 +01:00
committed by GitHub
parent c180d0e487
commit 879aa2c3d8
39 changed files with 983 additions and 73 deletions
@@ -3,6 +3,8 @@ import {
IconArrowRight,
IconArrowsHorizontal,
IconDots,
IconEye,
IconEyeOff,
IconFileExport,
IconHistory,
IconLink,
@@ -40,6 +42,11 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
import { PageShareModal } from "@/ee/page-permission";
import {
useWatchStatusQuery,
useWatchPageMutation,
useUnwatchPageMutation,
} from "@/features/page/queries/watcher-query";
interface PageHeaderMenuProps {
readOnly?: boolean;
@@ -123,6 +130,9 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
] = useDisclosure(false);
const [pageEditor] = useAtom(pageEditorAtom);
const pageUpdatedAt = useTimeAgo(page?.updatedAt);
const { data: watchStatus } = useWatchStatusQuery(page?.id);
const watchPage = useWatchPageMutation();
const unwatchPage = useUnwatchPageMutation();
const handleCopyLink = () => {
const pageUrl =
@@ -185,6 +195,23 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
>
{t("Copy as Markdown")}
</Menu.Item>
{watchStatus?.watching ? (
<Menu.Item
leftSection={<IconEyeOff size={16} />}
onClick={() => unwatchPage.mutate(page.id)}
>
{t("Stop watching")}
</Menu.Item>
) : (
<Menu.Item
leftSection={<IconEye size={16} />}
onClick={() => watchPage.mutate(page.id)}
>
{t("Watch page")}
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
@@ -0,0 +1,43 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
watchPage,
unwatchPage,
getWatchStatus,
} from "@/features/page/services/watcher-service";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
const WATCHER_KEY = "watcher";
export function useWatchStatusQuery(pageId: string) {
return useQuery({
queryKey: [WATCHER_KEY, pageId],
queryFn: () => getWatchStatus(pageId),
enabled: !!pageId,
staleTime: 60_000,
});
}
export function useWatchPageMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (pageId: string) => watchPage(pageId),
onSuccess: (_data, pageId) => {
queryClient.setQueryData([WATCHER_KEY, pageId], { watching: true });
notifications.show({ message: t("You are now watching this page") });
},
});
}
export function useUnwatchPageMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (pageId: string) => unwatchPage(pageId),
onSuccess: (_data, pageId) => {
queryClient.setQueryData([WATCHER_KEY, pageId], { watching: false });
notifications.show({ message: t("You are no longer watching this page") });
},
});
}
@@ -0,0 +1,16 @@
import api from "@/lib/api-client";
export async function watchPage(pageId: string): Promise<{ watching: boolean }> {
const req = await api.post<{ watching: boolean }>("/pages/watch", { pageId });
return req.data;
}
export async function unwatchPage(pageId: string): Promise<{ watching: boolean }> {
const req = await api.post<{ watching: boolean }>("/pages/unwatch", { pageId });
return req.data;
}
export async function getWatchStatus(pageId: string): Promise<{ watching: boolean }> {
const req = await api.post<{ watching: boolean }>("/pages/watch-status", { pageId });
return req.data;
}