mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-06 05:07:36 -05:00
parent
ae6401f429
commit
fb86b9f24d
|
|
@ -4,7 +4,6 @@ import { Link } from "react-router";
|
|||
import { Image } from "~/components/Image";
|
||||
import type { LoaderNotification } from "~/components/layout/NotificationPopover";
|
||||
import {
|
||||
mapMetaForTranslation,
|
||||
notificationLink,
|
||||
notificationNavIcon,
|
||||
} from "~/features/notifications/notifications-utils";
|
||||
|
|
@ -23,7 +22,7 @@ export function NotificationItem({
|
|||
notification: LoaderNotification;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["common"]);
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
@ -36,10 +35,7 @@ export function NotificationItem({
|
|||
{!notification.seen ? <div className={styles.unseenDot} /> : null}
|
||||
</NotificationImage>
|
||||
<div className={styles.itemHeader}>
|
||||
{t(
|
||||
`common:notifications.text.${notification.type}`,
|
||||
mapMetaForTranslation(notification, i18n.language),
|
||||
)}
|
||||
{t(`common:notifications.text.${notification.type}`, notification.meta)}
|
||||
</div>
|
||||
<div className={styles.timestamp}>
|
||||
{formatDistance(
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ describe("notify()", () => {
|
|||
userIds: [10, 11],
|
||||
notification: {
|
||||
type: "SCRIM_SCHEDULED",
|
||||
meta: { id: 1, at: 123 },
|
||||
meta: { id: 1, opponentTeamName: "Alpha" },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ describe("notify()", () => {
|
|||
userIds: [10, 11],
|
||||
notification: {
|
||||
type: "SCRIM_CANCELED",
|
||||
meta: { id: 1, at: 123 },
|
||||
meta: { id: 1, opponentTeamName: "Alpha" },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -346,7 +346,7 @@ describe("notify() - web push notifications", () => {
|
|||
expect(mockSendNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("formats timestamp for scrim notifications", async () => {
|
||||
test("includes opponent team name for scrim notifications", async () => {
|
||||
const mockSubscription = {
|
||||
endpoint: "https://fcm.googleapis.com/fcm/send/test",
|
||||
keys: {
|
||||
|
|
@ -367,13 +367,11 @@ describe("notify() - web push notifications", () => {
|
|||
|
||||
mockWebPushEnabled.value = true;
|
||||
|
||||
const testTimestamp = new Date("2024-01-15T15:30:00Z").getTime();
|
||||
|
||||
await notify({
|
||||
userIds: [1],
|
||||
notification: {
|
||||
type: "SCRIM_SCHEDULED",
|
||||
meta: { id: 1, at: testTimestamp },
|
||||
meta: { id: 1, opponentTeamName: "Sendou's pickup" },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -383,8 +381,6 @@ describe("notify() - web push notifications", () => {
|
|||
const payload = JSON.parse(callArgs);
|
||||
|
||||
expect(payload.title).toBe("Scrim Scheduled");
|
||||
expect(payload.body).toMatch(
|
||||
/New scrim scheduled at \d+\/\d+, \d+:\d+ (AM|PM)/,
|
||||
);
|
||||
expect(payload.body).toBe("New scrim scheduled vs. Sendou's pickup");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ import { i18next } from "../../../modules/i18n/i18next.server";
|
|||
import { logger } from "../../../utils/logger";
|
||||
import * as NotificationRepository from "../NotificationRepository.server";
|
||||
import type { Notification } from "../notifications-types";
|
||||
import {
|
||||
mapMetaForTranslation,
|
||||
notificationLink,
|
||||
} from "../notifications-utils";
|
||||
import { notificationLink } from "../notifications-utils";
|
||||
import webPush, { webPushEnabled } from "./webPush.server";
|
||||
|
||||
const NOTIFICATION_URGENCY: Record<Notification["type"], Urgency> = {
|
||||
|
|
@ -168,10 +165,12 @@ function pushNotificationOptions(
|
|||
): Parameters<ServiceWorkerRegistration["showNotification"]>[1] & {
|
||||
title: string;
|
||||
} {
|
||||
const meta = mapMetaForTranslation(notification, "en-US");
|
||||
return {
|
||||
title: t(`common:notifications.title.${notification.type}`),
|
||||
body: t(`common:notifications.text.${notification.type}`, meta),
|
||||
body: t(
|
||||
`common:notifications.text.${notification.type}`,
|
||||
notification.meta,
|
||||
),
|
||||
icon: notification.pictureUrl ?? "/static-assets/img/app-icon.png",
|
||||
data: { url: notificationLink(notification) },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -62,9 +62,15 @@ export type Notification =
|
|||
>
|
||||
| NotificationItem<"SEASON_STARTED", { seasonNth: number }>
|
||||
| NotificationItem<"SCRIM_NEW_REQUEST", { fromUsername: string }>
|
||||
| NotificationItem<"SCRIM_SCHEDULED", { id: number; at: number }>
|
||||
| NotificationItem<"SCRIM_CANCELED", { id: number; at: number }>
|
||||
| NotificationItem<"SCRIM_STARTING_SOON", { id: number; at: number }>
|
||||
| NotificationItem<
|
||||
"SCRIM_SCHEDULED",
|
||||
{ id: number; opponentTeamName: string }
|
||||
>
|
||||
| NotificationItem<"SCRIM_CANCELED", { id: number; opponentTeamName: string }>
|
||||
| NotificationItem<
|
||||
"SCRIM_STARTING_SOON",
|
||||
{ id: number; opponentTeamName: string }
|
||||
>
|
||||
| NotificationItem<"COMMISSIONS_CLOSED", { discordId: string }>
|
||||
| NotificationItem<"FRIEND_REQUEST_RECEIVED", { senderUsername: string }>
|
||||
| NotificationItem<
|
||||
|
|
|
|||
|
|
@ -107,27 +107,3 @@ export const notificationLink = (notification: Notification) => {
|
|||
assertUnreachable(notification);
|
||||
}
|
||||
};
|
||||
|
||||
/** Takes the `meta` object of a notification and transforms it (if needed) to show the translated string to user */
|
||||
export const mapMetaForTranslation = (
|
||||
notification: Notification,
|
||||
language: string,
|
||||
) => {
|
||||
if (
|
||||
notification.type === "SCRIM_SCHEDULED" ||
|
||||
notification.type === "SCRIM_CANCELED" ||
|
||||
notification.type === "SCRIM_STARTING_SOON"
|
||||
) {
|
||||
return {
|
||||
...notification.meta,
|
||||
timeString: new Date(notification.meta.at).toLocaleString(language, {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return notification.meta;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import {
|
|||
parseRequestPayload,
|
||||
} from "~/utils/remix.server";
|
||||
import { idObject } from "~/utils/zod";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
databaseTimestampToJavascriptTimestamp,
|
||||
} from "../../../utils/dates";
|
||||
import { databaseTimestampToDate } from "../../../utils/dates";
|
||||
import { errorToast } from "../../../utils/remix.server";
|
||||
import { requireUser } from "../../auth/core/user.server";
|
||||
import * as Scrim from "../core/Scrim";
|
||||
|
|
@ -42,17 +39,29 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
reason: data.reason,
|
||||
});
|
||||
|
||||
notify({
|
||||
userIds: Scrim.participantIdsListFromAccepted(post),
|
||||
defaultSeenUserIds: [user.id],
|
||||
notification: {
|
||||
type: "SCRIM_CANCELED",
|
||||
meta: {
|
||||
id: post.id,
|
||||
at: databaseTimestampToJavascriptTimestamp(Scrim.getStartTime(post)),
|
||||
const acceptedRequest = post.requests.find((r) => r.isAccepted);
|
||||
if (acceptedRequest) {
|
||||
const postTeamName = Scrim.sideDisplayName(post);
|
||||
const requestTeamName = Scrim.sideDisplayName(acceptedRequest);
|
||||
|
||||
notify({
|
||||
userIds: post.users.map((m) => m.id),
|
||||
defaultSeenUserIds: [user.id],
|
||||
notification: {
|
||||
type: "SCRIM_CANCELED",
|
||||
meta: { id: post.id, opponentTeamName: requestTeamName },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
notify({
|
||||
userIds: acceptedRequest.users.map((m) => m.id),
|
||||
defaultSeenUserIds: [user.id],
|
||||
notification: {
|
||||
type: "SCRIM_CANCELED",
|
||||
meta: { id: post.id, opponentTeamName: postTeamName },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import * as UserRepository from "~/features/user-page/UserRepository.server";
|
|||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
databaseTimestampToJavascriptTimestamp,
|
||||
dateToDatabaseTimestamp,
|
||||
} from "~/utils/dates";
|
||||
import { ConcurrentModificationError } from "~/utils/errors";
|
||||
|
|
@ -157,18 +156,24 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
});
|
||||
}
|
||||
|
||||
const postTeamName = Scrim.sideDisplayName(post);
|
||||
const requestTeamName = Scrim.sideDisplayName(request);
|
||||
|
||||
notify({
|
||||
userIds: [
|
||||
...post.users.map((m) => m.id),
|
||||
...request.users.map((m) => m.id),
|
||||
],
|
||||
userIds: post.users.map((m) => m.id),
|
||||
defaultSeenUserIds: [user.id],
|
||||
notification: {
|
||||
type: "SCRIM_SCHEDULED",
|
||||
meta: {
|
||||
id: post.id,
|
||||
at: databaseTimestampToJavascriptTimestamp(request.at ?? post.at),
|
||||
},
|
||||
meta: { id: post.id, opponentTeamName: requestTeamName },
|
||||
},
|
||||
});
|
||||
|
||||
notify({
|
||||
userIds: request.users.map((m) => m.id),
|
||||
defaultSeenUserIds: [user.id],
|
||||
notification: {
|
||||
type: "SCRIM_SCHEDULED",
|
||||
meta: { id: post.id, opponentTeamName: postTeamName },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import type { ScrimFilters, ScrimPost } from "../scrims-types";
|
||||
import { applyFilters, participantIdsListFromAccepted } from "./Scrim";
|
||||
import {
|
||||
applyFilters,
|
||||
participantIdsListFromAccepted,
|
||||
sideDisplayName,
|
||||
} from "./Scrim";
|
||||
|
||||
type MockUser = { id: number };
|
||||
type MockRequest = { isAccepted: boolean; users: MockUser[] };
|
||||
|
|
@ -78,6 +82,27 @@ describe("participantIdsListFromAccepted", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("sideDisplayName", () => {
|
||||
it("returns the team name when team is set", () => {
|
||||
const result = sideDisplayName({
|
||||
team: { name: "Team Olive" },
|
||||
users: [{ username: "sendou", isOwner: true }],
|
||||
});
|
||||
expect(result).toBe("Team Olive");
|
||||
});
|
||||
|
||||
it("falls back to {owner}'s pickup when team is null", () => {
|
||||
const result = sideDisplayName({
|
||||
team: null,
|
||||
users: [
|
||||
{ username: "alice", isOwner: false },
|
||||
{ username: "sendou", isOwner: true },
|
||||
],
|
||||
});
|
||||
expect(result).toBe("sendou's pickup");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyFilters", () => {
|
||||
function createPostForFilters(
|
||||
at: Date,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,19 @@ export function getStartTime(post: ScrimPost): number {
|
|||
return acceptedRequest?.at ?? post.at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a display name for a scrim side: the team name when set,
|
||||
* otherwise "{ownerUsername}'s pickup".
|
||||
*/
|
||||
export function sideDisplayName(side: {
|
||||
team: { name: string } | null;
|
||||
users: Array<{ username: string; isOwner: boolean }>;
|
||||
}): string {
|
||||
if (side.team) return side.team.name;
|
||||
const owner = side.users.find((u) => u.isOwner) ?? side.users[0];
|
||||
return `${owner.username}'s pickup`;
|
||||
}
|
||||
|
||||
export function applyFilters(post: ScrimPost, filters: ScrimFilters): boolean {
|
||||
const hasMinFilter = filters.divs?.min !== null;
|
||||
const hasMaxFilter = filters.divs?.max !== null;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { add, sub } from "date-fns";
|
|||
import { notify } from "../features/notifications/core/notify.server";
|
||||
import * as Scrim from "../features/scrims/core/Scrim";
|
||||
import * as ScrimPostRepository from "../features/scrims/ScrimPostRepository.server";
|
||||
import { databaseTimestampToJavascriptTimestamp } from "../utils/dates";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Routine } from "./routine.server";
|
||||
|
||||
|
|
@ -19,23 +18,30 @@ export const NotifyScrimStartingSoonRoutine = new Routine({
|
|||
});
|
||||
|
||||
for (const scrim of scrims) {
|
||||
const participantIds = Scrim.participantIdsListFromAccepted(scrim);
|
||||
const acceptedRequest = scrim.requests.find((r) => r.isAccepted);
|
||||
if (!acceptedRequest) continue;
|
||||
|
||||
const postTeamName = Scrim.sideDisplayName(scrim);
|
||||
const requestTeamName = Scrim.sideDisplayName(acceptedRequest);
|
||||
|
||||
logger.info(
|
||||
`Notifying scrim starting soon for scrim ${scrim.id} with ${participantIds.length} participants`,
|
||||
`Notifying scrim starting soon for scrim ${scrim.id} with ${scrim.users.length + acceptedRequest.users.length} participants`,
|
||||
);
|
||||
|
||||
await notify({
|
||||
notification: {
|
||||
type: "SCRIM_STARTING_SOON",
|
||||
meta: {
|
||||
id: scrim.id,
|
||||
at: databaseTimestampToJavascriptTimestamp(
|
||||
Scrim.getStartTime(scrim),
|
||||
),
|
||||
},
|
||||
meta: { id: scrim.id, opponentTeamName: requestTeamName },
|
||||
},
|
||||
userIds: participantIds,
|
||||
userIds: scrim.users.map((u) => u.id),
|
||||
});
|
||||
|
||||
await notify({
|
||||
notification: {
|
||||
type: "SCRIM_STARTING_SOON",
|
||||
meta: { id: scrim.id, opponentTeamName: postTeamName },
|
||||
},
|
||||
userIds: acceptedRequest.users.map((u) => u.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@
|
|||
"notifications.title.SCRIM_NEW_REQUEST": "New Scrim Request",
|
||||
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} requested a scrim",
|
||||
"notifications.title.SCRIM_SCHEDULED": "Scrim Scheduled",
|
||||
"notifications.text.SCRIM_SCHEDULED": "New scrim scheduled at {{timeString}}",
|
||||
"notifications.text.SCRIM_SCHEDULED": "New scrim scheduled vs. {{opponentTeamName}}",
|
||||
"notifications.title.SCRIM_CANCELED": "Scrim Canceled",
|
||||
"notifications.text.SCRIM_CANCELED": "The scrim at {{timeString}} was canceled",
|
||||
"notifications.text.SCRIM_CANCELED": "The scrim vs. {{opponentTeamName}} was canceled",
|
||||
"notifications.title.SCRIM_STARTING_SOON": "Scrim Starting Soon",
|
||||
"notifications.text.SCRIM_STARTING_SOON": "Your scrim at {{timeString}} is starting soon",
|
||||
"notifications.text.SCRIM_STARTING_SOON": "Your scrim vs. {{opponentTeamName}} is starting soon",
|
||||
"notifications.title.COMMISSIONS_CLOSED": "Commissions Closed",
|
||||
"notifications.text.COMMISSIONS_CLOSED": "If your commissions are still open, please re-enable them",
|
||||
"notifications.title.FRIEND_REQUEST_RECEIVED": "Friend Request",
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@
|
|||
"notifications.title.SCRIM_NEW_REQUEST": "Nueva Solicitud de Scrim",
|
||||
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} ha solicitado un scrim",
|
||||
"notifications.title.SCRIM_SCHEDULED": "Scrim Programado",
|
||||
"notifications.text.SCRIM_SCHEDULED": "Nuevo scrim programado para las {{timeString}}",
|
||||
"notifications.text.SCRIM_SCHEDULED": "",
|
||||
"notifications.title.SCRIM_CANCELED": "Scrim Cancelado",
|
||||
"notifications.text.SCRIM_CANCELED": "El scrim de las {{timeString}} fue cancelado",
|
||||
"notifications.text.SCRIM_CANCELED": "",
|
||||
"notifications.title.SCRIM_STARTING_SOON": "El scrim empieza pronto",
|
||||
"notifications.text.SCRIM_STARTING_SOON": "Tu scrim de las {{timeString}} empieza pronto",
|
||||
"notifications.text.SCRIM_STARTING_SOON": "",
|
||||
"notifications.title.COMMISSIONS_CLOSED": "Comisiones Cerradas",
|
||||
"notifications.text.COMMISSIONS_CLOSED": "Si tus comisiones siguen abiertas, por favor vuelve a activarlas",
|
||||
"notifications.title.FRIEND_REQUEST_RECEIVED": "",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"notifications.title.SCRIM_NEW_REQUEST": "Nouvelle Demande De Scrim",
|
||||
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} vous demande de scrim",
|
||||
"notifications.title.SCRIM_SCHEDULED": "Scrim Programmé",
|
||||
"notifications.text.SCRIM_SCHEDULED": "Nouveau scrim programmé à {{timeString}}",
|
||||
"notifications.text.SCRIM_SCHEDULED": "",
|
||||
"notifications.title.SCRIM_CANCELED": "",
|
||||
"notifications.text.SCRIM_CANCELED": "",
|
||||
"notifications.title.SCRIM_STARTING_SOON": "",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"notifications.title.SCRIM_NEW_REQUEST": "Новый Скрим Запрос",
|
||||
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} запросил скрим",
|
||||
"notifications.title.SCRIM_SCHEDULED": "Скрим Запланирован",
|
||||
"notifications.text.SCRIM_SCHEDULED": "Новый скрим запланирован на {{timeString}}",
|
||||
"notifications.text.SCRIM_SCHEDULED": "",
|
||||
"notifications.title.SCRIM_CANCELED": "",
|
||||
"notifications.text.SCRIM_CANCELED": "",
|
||||
"notifications.title.SCRIM_STARTING_SOON": "",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user