fix: notifications not reloading and add site announcements

This commit is contained in:
zkldi
2022-11-26 16:33:22 +00:00
parent 8775f25acd
commit 708949bb13
9 changed files with 176 additions and 45 deletions
+17 -14
View File
@@ -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>
);
}
+7
View File
@@ -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: {},
});
}
+2
View File
@@ -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;