mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-12 01:08:29 +03:00
fix: notifications not reloading and add site announcements
This commit is contained in:
+17
-14
@@ -2,6 +2,7 @@ import { MaterialThemeProvider } from "components/layout/misc/MaterialThemeProvi
|
||||
import { LoadingScreen } from "components/layout/screens/LoadingScreen";
|
||||
import { AllLUGPTStatsContextProvider } from "context/AllLUGPTStatsContext";
|
||||
import { BannedContextProvider } from "context/BannedContext";
|
||||
import { NotificationsContextProvider } from "context/NotificationsContext";
|
||||
import { SubheaderContextProvider } from "context/SubheaderContext";
|
||||
import { UserContextProvider } from "context/UserContext";
|
||||
import { UserSettingsContextProvider } from "context/UserSettingsContext";
|
||||
@@ -21,20 +22,22 @@ export default function App({ basename }: { basename: string }) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BannedContextProvider>
|
||||
<UserContextProvider>
|
||||
<UserSettingsContextProvider>
|
||||
<AllLUGPTStatsContextProvider>
|
||||
<LoadingScreen>
|
||||
<BrowserRouter basename={basename}>
|
||||
<MaterialThemeProvider>
|
||||
<Toaster />
|
||||
<SubheaderContextProvider>
|
||||
<Routes />
|
||||
</SubheaderContextProvider>
|
||||
</MaterialThemeProvider>
|
||||
</BrowserRouter>
|
||||
</LoadingScreen>
|
||||
</AllLUGPTStatsContextProvider>
|
||||
</UserSettingsContextProvider>
|
||||
<NotificationsContextProvider>
|
||||
<UserSettingsContextProvider>
|
||||
<AllLUGPTStatsContextProvider>
|
||||
<LoadingScreen>
|
||||
<BrowserRouter basename={basename}>
|
||||
<MaterialThemeProvider>
|
||||
<Toaster />
|
||||
<SubheaderContextProvider>
|
||||
<Routes />
|
||||
</SubheaderContextProvider>
|
||||
</MaterialThemeProvider>
|
||||
</BrowserRouter>
|
||||
</LoadingScreen>
|
||||
</AllLUGPTStatsContextProvider>
|
||||
</UserSettingsContextProvider>
|
||||
</NotificationsContextProvider>
|
||||
</UserContextProvider>
|
||||
</BannedContextProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -3,13 +3,12 @@ import { ErrorPage } from "app/pages/ErrorPage";
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import NotificationRow from "components/notifications/NotificationRow";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import { NotificationsContext } from "context/NotificationsContext";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import React, { useContext, useEffect, useMemo } from "react";
|
||||
import { Button, Col, Row } from "react-bootstrap";
|
||||
import { NotificationDocument, UserDocument } from "tachi-common";
|
||||
import { UserDocument } from "tachi-common";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const { user } = useContext(UserContext);
|
||||
@@ -24,34 +23,31 @@ export default function NotificationsPage() {
|
||||
}
|
||||
|
||||
function NotificationsInnerPage({ user }: { user: UserDocument }) {
|
||||
const { data, error } = useApiQuery<Array<NotificationDocument>>(
|
||||
`/users/${user.id}/notifications`
|
||||
);
|
||||
const { notifications, reload } = useContext(NotificationsContext);
|
||||
|
||||
const unread = useMemo(() => {
|
||||
if (!data) {
|
||||
if (!notifications) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.filter((e) => e.read === false).length;
|
||||
}, [data]);
|
||||
return notifications.filter((e) => e.read === false).length;
|
||||
}, [notifications]);
|
||||
|
||||
// mark all notifications the user has seen as being read
|
||||
// mark all notifications the user has seen as being read, this runs on mount
|
||||
// and never again
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
APIFetchV1(`/users/${user.id}/notifications/mark-all-read`, { method: "POST" });
|
||||
if (notifications) {
|
||||
APIFetchV1(`/users/${user.id}/notifications/mark-all-read`, { method: "POST" }).then(
|
||||
() => reload()
|
||||
);
|
||||
}
|
||||
}, [data]);
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <ApiError error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!notifications) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
if (notifications.length === 0) {
|
||||
return (
|
||||
<div className="d-flex w-100 justify-content-center">
|
||||
<div>You've got no mail!</div>
|
||||
@@ -63,7 +59,7 @@ function NotificationsInnerPage({ user }: { user: UserDocument }) {
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<MiniTable headers={["Read"]}>
|
||||
{data.map((e) => (
|
||||
{notifications.map((e) => (
|
||||
<NotificationRow notif={e} key={e.notifID} />
|
||||
))}
|
||||
</MiniTable>
|
||||
@@ -74,9 +70,11 @@ function NotificationsInnerPage({ user }: { user: UserDocument }) {
|
||||
<div style={{ flex: 1, display: "flex", justifyContent: "center" }}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await APIFetchV1(`/users/${user.id}/delete-all`, {
|
||||
await APIFetchV1(`/users/${user.id}/notifications/delete-all`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
reload();
|
||||
}}
|
||||
variant="outline-danger"
|
||||
>
|
||||
@@ -84,7 +82,7 @@ function NotificationsInnerPage({ user }: { user: UserDocument }) {
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ flex: 1, textAlign: "right" }}>
|
||||
{data.length} Notifications.
|
||||
{notifications.length} Notifications.
|
||||
{unread > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import Icon from "components/util/Icon";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import React, { useMemo } from "react";
|
||||
import { NotificationsContext } from "context/NotificationsContext";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NotificationDocument, UserDocument } from "tachi-common";
|
||||
|
||||
export function UserNotificationButton({ user }: { user: UserDocument }) {
|
||||
// don't care whether it errors or not, tbh
|
||||
const { data } = useApiQuery<Array<NotificationDocument>>(`/users/${user.id}/notifications`);
|
||||
const { notifications } = useContext(NotificationsContext);
|
||||
|
||||
const unread = useMemo(() => {
|
||||
if (!data) {
|
||||
if (!notifications) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.filter((e) => e.read === false).length;
|
||||
}, [data]);
|
||||
return notifications.filter((e) => e.read === false).length;
|
||||
}, [notifications]);
|
||||
|
||||
return (
|
||||
<div className="topbar-item">
|
||||
|
||||
@@ -28,9 +28,14 @@ export default function NotificationRow({ notif }: { notif: NotificationDocument
|
||||
|
||||
function NotifToURL(notif: NotificationDocument) {
|
||||
switch (notif.body.type) {
|
||||
case "QUEST_CHANGED":
|
||||
return "/idkyet";
|
||||
case "QUEST_CHANGED": {
|
||||
const { game, playtype, questID } = notif.body.content;
|
||||
|
||||
return `/dashboard/games/${game}/${playtype}/quests/${questID}`;
|
||||
}
|
||||
case "RIVALED_BY":
|
||||
return `/dashboard/users/${notif.body.content.userID}/games/${notif.body.content.game}/${notif.body.content.playtype}`;
|
||||
case "SITE_ANNOUNCEMENT":
|
||||
return `#`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { NotificationDocument } from "tachi-common";
|
||||
import { JustChildren } from "types/react";
|
||||
import { UserContext } from "./UserContext";
|
||||
|
||||
export const NotificationsContext = createContext<{
|
||||
notifications: Array<NotificationDocument>;
|
||||
reload: () => Promise<void>;
|
||||
}>({
|
||||
notifications: [],
|
||||
// eslint-disable-next-line require-await
|
||||
reload: async () => void 0,
|
||||
});
|
||||
|
||||
export function NotificationsContextProvider({ children }: JustChildren) {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const [notifications, setNotifications] = useState<Array<NotificationDocument>>([]);
|
||||
|
||||
const reload = async () => {
|
||||
if (!user) {
|
||||
setNotifications([]);
|
||||
return;
|
||||
}
|
||||
|
||||
await APIFetchV1<Array<NotificationDocument>>(`/users/${user.id}/notifications`).then(
|
||||
(r) => {
|
||||
if (!r.success) {
|
||||
setNotifications([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setNotifications(r.body);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// fetch the target subscriptions from the api.
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<NotificationsContext.Provider value={{ notifications, reload }}>
|
||||
{children}
|
||||
</NotificationsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1571,6 +1571,8 @@ export type NotificationBody =
|
||||
type: "QUEST_CHANGED"; // Emitted when a quest the user is subscribed to changed.
|
||||
content: {
|
||||
questID: string;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -1580,6 +1582,11 @@ export type NotificationBody =
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "SITE_ANNOUNCEMENT"; // Emitted as a site announcement
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
content: {};
|
||||
};
|
||||
|
||||
export type NotificationDocument = BaseNotification & {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SendNotification } from "./notifications";
|
||||
import { BulkSendNotification, SendNotification } from "./notifications";
|
||||
import db from "external/mongo/db";
|
||||
import { FormatGame } from "tachi-common";
|
||||
import type { Game, integer, Playtype, UserDocument } from "tachi-common";
|
||||
@@ -44,3 +44,27 @@ export async function SendSetRivalNotification(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function SendSiteAnnouncementNotification(
|
||||
title: string,
|
||||
maybeGame?: Game,
|
||||
maybePlaytype?: Playtype
|
||||
) {
|
||||
let toUserIDs = [];
|
||||
|
||||
if (maybeGame && maybePlaytype) {
|
||||
toUserIDs = (
|
||||
await db["game-stats"].find(
|
||||
{ game: maybeGame, playtype: maybePlaytype },
|
||||
{ projection: { userID: 1 } }
|
||||
)
|
||||
).map((e) => e.userID);
|
||||
} else {
|
||||
toUserIDs = (await db.users.find({}, { projection: { id: 1 } })).map((e) => e.id);
|
||||
}
|
||||
|
||||
return BulkSendNotification(title, toUserIDs, {
|
||||
type: "SITE_ANNOUNCEMENT",
|
||||
content: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,6 +319,8 @@ export async function UpdateQuestSubscriptions(questID: string) {
|
||||
type: "QUEST_CHANGED",
|
||||
content: {
|
||||
questID,
|
||||
game: maybeQuest.game,
|
||||
playtype: maybeQuest.playtype,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import { ONE_MINUTE } from "lib/constants/time";
|
||||
import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel } from "lib/logger/logger";
|
||||
import { SendSiteAnnouncementNotification } from "lib/notifications/notification-wrappers";
|
||||
import { DeleteMultipleScores, DeleteScore } from "lib/score-mutation/delete-scores";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import p from "prudence";
|
||||
@@ -300,4 +301,46 @@ router.post("/recalc", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Send an announcement to the site.
|
||||
*
|
||||
* @name POST /api/v1/admin/announcement
|
||||
*/
|
||||
router.post(
|
||||
"/announcement",
|
||||
prValidate({
|
||||
game: p.optional(p.isIn(TachiConfig.GAMES)),
|
||||
playtype: "*string",
|
||||
title: "string",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { game, playtype, title } = req.safeBody as {
|
||||
game?: Game;
|
||||
playtype?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let maybePlaytype: Playtype | undefined;
|
||||
|
||||
if (game && playtype) {
|
||||
if (!IsValidPlaytype(game, playtype)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid playtype '${playtype}' for game '${game}'.`,
|
||||
});
|
||||
}
|
||||
|
||||
maybePlaytype = playtype;
|
||||
}
|
||||
|
||||
await SendSiteAnnouncementNotification(title, game, maybePlaytype);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Sent notification '${title}'.`,
|
||||
body: {},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user