Change scrim notification text not to include timestamp
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

Closes #2998
This commit is contained in:
Kalle 2026-04-26 13:05:48 +03:00
parent ae6401f429
commit fb86b9f24d
14 changed files with 121 additions and 90 deletions

View File

@ -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(

View File

@ -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");
});
});

View File

@ -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) },
};

View File

@ -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<

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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 },
},
});

View File

@ -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,

View File

@ -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;

View File

@ -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),
});
}
},

View File

@ -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",

View File

@ -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": "",

View File

@ -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": "",

View File

@ -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": "",