mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 07:12:36 -05:00
Move notifications to root loader
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
32
app/root.tsx
32
app/root.tsx
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user