Move notifications to root loader
Some checks failed
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled

This commit is contained in:
Kalle
2025-04-04 21:10:27 +03:00
parent 2136cc56bb
commit 3ac6712091
10 changed files with 24 additions and 90 deletions

View File

@@ -1,29 +1,34 @@
import { useLocation } from "@remix-run/react";
import { useLocation, useMatches, useRevalidator } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useUser } from "~/features/auth/core/user";
import {
NotificationItem,
NotificationItemDivider,
NotificationsList,
} from "~/features/notifications/components/NotificationList";
import { NOTIFICATIONS } from "~/features/notifications/notifications-contants";
import { useNotifications } from "~/hooks/swr";
import type { RootLoaderData } from "~/root";
import { NOTIFICATIONS_URL } from "~/utils/urls";
import { useMarkNotificationsAsSeen } from "../../features/notifications/notifications-hooks";
import type { LoaderNotification } from "../../features/notifications/routes/notifications.peek";
import { LinkButton } from "../Button";
import { SendouButton } from "../elements/Button";
import { SendouPopover } from "../elements/Popover";
import { BellIcon } from "../icons/Bell";
import { RefreshIcon } from "../icons/Refresh";
import styles from "./NotificationPopover.module.css";
export type LoaderNotification = NonNullable<
RootLoaderData["notifications"]
>[number];
export function NotificationPopover() {
const location = useLocation();
const user = useUser();
const { notifications, isLoading, refresh } = useNotifications();
const [root] = useMatches();
const notifications = (root.data as RootLoaderData | undefined)
?.notifications;
const unseenIds = React.useMemo(
() =>
@@ -33,7 +38,7 @@ export function NotificationPopover() {
[notifications],
);
if (!user) {
if (!notifications) {
return null;
}
@@ -56,8 +61,6 @@ export function NotificationPopover() {
<NotificationContent
notifications={notifications ?? []}
unseenIds={unseenIds}
isLoading={isLoading}
refresh={refresh}
/>
</SendouPopover>
</div>
@@ -67,15 +70,12 @@ export function NotificationPopover() {
function NotificationContent({
notifications,
unseenIds,
refresh,
isLoading,
}: {
notifications: LoaderNotification[];
unseenIds: number[];
refresh: () => void;
isLoading: boolean;
}) {
const { t } = useTranslation(["common"]);
const { revalidate, state } = useRevalidator();
useMarkNotificationsAsSeen(unseenIds);
@@ -89,16 +89,14 @@ function NotificationContent({
icon={<RefreshIcon />}
variant="minimal"
className={styles.refreshButton}
onPress={refresh}
isDisabled={isLoading}
onPress={revalidate}
isDisabled={state !== "idle"}
/>
</div>
<hr className={styles.divider} />
{notifications.length === 0 ? (
<div className={styles.noNotifications}>
{isLoading
? t("common:notifications.loading")
: t("common:notifications.empty")}
{t("common:notifications.empty")}
</div>
) : (
<NotificationsList>

View File

@@ -2,11 +2,11 @@ import { Link } from "@remix-run/react";
import { formatDistance } from "date-fns";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import type { LoaderNotification } from "~/components/layout/NotificationPopover";
import {
notificationLink,
notificationNavIcon,
} from "~/features/notifications/notifications-utils";
import type { LoaderNotification } from "~/features/notifications/routes/notifications.peek";
import { databaseTimestampToDate } from "~/utils/dates";
import { navIconUrl } from "~/utils/urls";
import styles from "./NotificationList.module.css";

View File

@@ -1,19 +0,0 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { requireUser } from "~/features/auth/core/user.server";
import type { SerializeFrom } from "~/utils/remix";
import * as NotificationRepository from "../NotificationRepository.server";
import { NOTIFICATIONS } from "../notifications-contants";
export type NotificationsLoaderData = SerializeFrom<typeof loader>;
export type LoaderNotification =
NotificationsLoaderData["notifications"][number];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const notifications = await NotificationRepository.findByUserId(user.id, {
limit: NOTIFICATIONS.PEEK_COUNT,
});
return { notifications };
};

View File

@@ -1,19 +1,15 @@
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
import type { EventsWithMapPoolsLoaderData } from "~/features/calendar/routes/map-pool-events";
import type { PatronsListLoaderData } from "~/features/front-page/routes/patrons-list";
import type { NotificationsLoaderData } from "~/features/notifications/routes/notifications.peek";
import type { TrustersLoaderData } from "~/features/sendouq/routes/trusters";
import type { WeaponUsageLoaderData } from "~/features/sendouq/routes/weapon-usage";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import {
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
GET_TRUSTERS_ROUTE,
NOTIFICATIONS_PEAK_ROUTE,
PATRONS_LIST_ROUTE,
getWeaponUsage,
} from "~/utils/urls";
import { useUser } from "../features/auth/core/user";
// TODO: replace with useFetcher after proper errr handling is implemented https://github.com/remix-run/react-router/discussions/10013
@@ -83,18 +79,3 @@ export function usePatrons() {
isError: error,
};
}
export function useNotifications() {
const user = useUser();
const { data, error, mutate, isValidating } = useSWR<NotificationsLoaderData>(
user ? NOTIFICATIONS_PEAK_ROUTE : null,
fetcher(NOTIFICATIONS_PEAK_ROUTE),
);
return {
notifications: data?.notifications,
isLoading: isValidating || (!error && !data),
isError: error,
refresh: () => mutate(),
};
}

View File

@@ -15,7 +15,6 @@ import {
useMatches,
useNavigate,
useNavigation,
useRevalidator,
useSearchParams,
} from "@remix-run/react";
import generalI18next from "i18next";
@@ -25,6 +24,8 @@ import { I18nProvider } from "react-aria-components";
import { ErrorBoundary as ClientErrorBoundary } from "react-error-boundary";
import { useTranslation } from "react-i18next";
import { useChangeLanguage } from "remix-i18next/react";
import * as NotificationRepository from "~/features/notifications/NotificationRepository.server";
import { NOTIFICATIONS } from "~/features/notifications/notifications-contants";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { Catcher } from "./components/Catcher";
import { SendouToastRegion, toastQueue } from "./components/elements/Toast";
@@ -42,7 +43,6 @@ import {
} from "./features/theme/core/provider";
import { getThemeSession } from "./features/theme/core/session.server";
import { useIsMounted } from "./hooks/useIsMounted";
import { useVisibilityChange } from "./hooks/useVisibilityChange";
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
import i18next, { i18nCookie } from "./modules/i18n/i18next.server";
import type { Namespace } from "./modules/i18n/resources.server";
@@ -116,6 +116,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
languages: user.languages ? user.languages.split(",") : [],
}
: undefined,
notifications: user
? await NotificationRepository.findByUserId(user.id, {
limit: NOTIFICATIONS.PEEK_COUNT,
})
: undefined,
},
{
headers: { "Set-Cookie": await i18nCookie.serialize(locale) },
@@ -263,29 +268,6 @@ function usePreloadTranslation() {
}, []);
}
// @ts-expect-error to be used in the future
function useRevalidateOnRevisit() {
const visibility = useVisibilityChange();
const { revalidate } = useRevalidator();
const [lastUpdated, setLastUpdated] = React.useState<Date>();
React.useEffect(() => {
setLastUpdated(new Date());
}, []);
React.useEffect(() => {
if (visibility !== "visible" || !lastUpdated) return;
const sinceLastUpdated = new Date().getTime() - lastUpdated.getTime();
// 15 minutes
if (sinceLastUpdated < 1000 * 60 * 15) return;
setLastUpdated(new Date());
revalidate();
}, [visibility, revalidate, lastUpdated]);
}
function useCustomizedCSSVars() {
const matches = useMatches();

View File

@@ -10,10 +10,6 @@ export default [
route("/patrons-list", "features/front-page/routes/patrons-list.ts"),
route("/notifications", "features/notifications/routes/notifications.tsx"),
route(
"/notifications/peek",
"features/notifications/routes/notifications.peek.ts",
),
route(
"/notifications/seen",
"features/notifications/routes/notifications.seen.ts",

View File

@@ -140,7 +140,6 @@ export const GET_TRUSTERS_ROUTE = "/trusters";
export const PATRONS_LIST_ROUTE = "/patrons-list";
export const NOTIFICATIONS_URL = "/notifications";
export const NOTIFICATIONS_PEAK_ROUTE = "/notifications/peek";
export const NOTIFICATIONS_MARK_AS_SEEN_ROUTE = "/notifications/seen";
interface UserLinkArgs {

View File

@@ -43,7 +43,6 @@
"notifications.title": "Notifications",
"notifications.empty": "None yet, check back later",
"notifications.loading": "Loading...",
"notifications.seeAll": "See all",
"notifications.fullList.explanation": "Max of 100 notifications shown. Notifications older than 14 days are automatically deleted.",
"notifications.managePush": "Push notifications",

View File

@@ -43,7 +43,6 @@
"notifications.title": "Notification",
"notifications.empty": "Rien ici encore, revenez plus tard",
"notifications.loading": "Chargement...",
"notifications.seeAll": "Voir tout",
"notifications.fullList.explanation": "100 notifications au maximum peuvent être afficher. Les notifications sont effacer automatiquement au bout de 14 jours.",
"notifications.managePush": "Notifications push",

View File

@@ -43,7 +43,6 @@
"notifications.title": "Notifiche",
"notifications.empty": "Nessuna notifica, controlla più tardi",
"notifications.loading": "Caricamento...",
"notifications.seeAll": "Vedi tutte",
"notifications.fullList.explanation": "Un massimo di 100 notifiche sono state mostrate. Le notifiche più vecchie di 14 giorni vengono eliminate automaticamente.",
"notifications.managePush": "Notifiche push",