v3: correctly using SITE_NAME across the app, added notifications on comments
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s

This commit is contained in:
khannurien
2026-04-08 20:12:30 +00:00
parent 856511777c
commit ed7695663e
16 changed files with 185 additions and 59 deletions

View File

@@ -6,10 +6,10 @@
# Defaults to http://localhost:GERBEUR_PORT for local development. # Defaults to http://localhost:GERBEUR_PORT for local development.
# GERBEUR_PUBLIC_URL=https://example.com # GERBEUR_PUBLIC_URL=https://example.com
# Site name used in OG meta tags # Site name shown in the browser tab, app header, OG meta tags, and emails.
GERBEUR_SITE_NAME=gerbeur GERBEUR_SITE_NAME=gerbeur
# Port the API server listens on (the container's internal port) # Port the API server listens on (the container's internal port).
GERBEUR_PORT=8000 GERBEUR_PORT=8000
# Network interface Oak binds to. Default: 0.0.0.0 (all interfaces, required for Docker). # Network interface Oak binds to. Default: 0.0.0.0 (all interfaces, required for Docker).

View File

@@ -12,9 +12,7 @@ COPY public/ ./public/
COPY scripts/ ./scripts/ COPY scripts/ ./scripts/
COPY src/ ./src/ COPY src/ ./src/
# In same-origin deployments (API serves the frontend), no build args are needed # VITE_API_* are only needed when the API lives on a different host than the frontend.
# — the frontend uses relative URLs at runtime. Only set VITE_API_HOSTNAME if
# the API lives on a different host than the frontend (e.g. a separate API server).
ARG VITE_API_PROTOCOL ARG VITE_API_PROTOCOL
ARG VITE_API_HOSTNAME ARG VITE_API_HOSTNAME
ARG VITE_API_PORT ARG VITE_API_PORT

View File

@@ -46,7 +46,7 @@ See [`.env.example`](.env.example) for the full list with descriptions. Key vari
| `GERBEUR_LISTEN_HOST` | Network interface Oak binds to; use `127.0.0.1` to restrict to loopback | `0.0.0.0` | | `GERBEUR_LISTEN_HOST` | Network interface Oak binds to; use `127.0.0.1` to restrict to loopback | `0.0.0.0` |
| `GERBEUR_FRONTEND_URL` | Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host | `GERBEUR_PUBLIC_URL` | | `GERBEUR_FRONTEND_URL` | Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host | `GERBEUR_PUBLIC_URL` |
| `GERBEUR_ALLOWED_ORIGINS` | Comma-separated extra origins for CORS/WebSocket; `PUBLIC_URL` and `FRONTEND_URL` are always included — typically only needed in dev for the Vite server | `""` (empty) | | `GERBEUR_ALLOWED_ORIGINS` | Comma-separated extra origins for CORS/WebSocket; `PUBLIC_URL` and `FRONTEND_URL` are always included — typically only needed in dev for the Vite server | `""` (empty) |
| `GERBEUR_SITE_NAME` | Site name used in OG meta tags and emails | `gerbeur` | | `GERBEUR_SITE_NAME` | Site name used in the browser tab, app header, OG meta tags, and emails | `gerbeur` |
| `GERBEUR_SMTPS_URL` | SMTPS connection URL for outgoing email (`smtps://user:pass@host:465`) | unset | | `GERBEUR_SMTPS_URL` | SMTPS connection URL for outgoing email (`smtps://user:pass@host:465`) | unset |
| `GERBEUR_FROM_EMAIL` | Sender address for outgoing emails — required when `GERBEUR_SMTPS_URL` is set | unset | | `GERBEUR_FROM_EMAIL` | Sender address for outgoing emails — required when `GERBEUR_SMTPS_URL` is set | unset |
| `GERBEUR_WELCOME_EMAIL_BODY` | Markdown body for the account-creation welcome email; supports `{{username}}` and `{{site_name}}` | built-in template | | `GERBEUR_WELCOME_EMAIL_BODY` | Markdown body for the account-creation welcome email; supports `{{username}}` and `{{site_name}}` | built-in template |
@@ -63,12 +63,15 @@ The standard deployment runs API and frontend in a single container. The API ser
```sh ```sh
docker build -t gerbeur . docker build -t gerbeur .
docker build -t gerbeur .
docker run -d \ docker run -d \
-p 8000:8000 \ -p 8000:8000 \
-v gerbeur-db:/app/api/sql \ -v gerbeur-db:/app/api/sql \
-v gerbeur-uploads:/app/api/uploads \ -v gerbeur-uploads:/app/api/uploads \
-e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \ -e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \
-e GERBEUR_PUBLIC_URL=https://example.com \ -e GERBEUR_PUBLIC_URL=https://example.com \
-e GERBEUR_SITE_NAME=mysite \
--name gerbeur \ --name gerbeur \
gerbeur gerbeur
``` ```

View File

@@ -1,13 +1,30 @@
import { Context, Next, send } from "@oak/oak"; import { Context, Next, send } from "@oak/oak";
import { OG_SITE_NAME } from "../config.ts";
async function serveIndexHtml(
context: Context<Record<string, object>>,
root: string,
) {
const filePath = `${root}/index.html`;
const raw = await Deno.readTextFile(filePath);
const html = raw.replaceAll("__SITE_NAME__", OG_SITE_NAME);
context.response.type = "text/html";
context.response.body = html;
}
export function routeStaticFilesFrom(staticPaths: string[]) { export function routeStaticFilesFrom(staticPaths: string[]) {
return async (context: Context<Record<string, object>>, next: Next) => { return async (context: Context<Record<string, object>>, next: Next) => {
const pathname = context.request.url.pathname;
// Serve index.html with runtime placeholder replacement
if (pathname === "/" || pathname === "/index.html") {
await serveIndexHtml(context, staticPaths[0]);
return;
}
for (const path of staticPaths) { for (const path of staticPaths) {
try { try {
await send(context, context.request.url.pathname, { await send(context, pathname, { root: path });
root: path,
index: "index.html",
});
return; return;
} catch { } catch {
continue; continue;
@@ -15,7 +32,7 @@ export function routeStaticFilesFrom(staticPaths: string[]) {
} }
// SPA fallback: serve index.html so client-side routes work on direct navigation // SPA fallback: serve index.html so client-side routes work on direct navigation
await send(context, "/index.html", { root: staticPaths[0] }); await serveIndexHtml(context, staticPaths[0]);
await next(); await next();
}; };
} }

View File

@@ -617,7 +617,8 @@ export type NotificationType =
| "user_dump_posted" | "user_dump_posted"
| "playlist_dump_added" | "playlist_dump_added"
| "dump_upvoted" | "dump_upvoted"
| "user_mentioned"; | "user_mentioned"
| "dump_commented";
export interface PlaylistFollowedData { export interface PlaylistFollowedData {
followerId: string; followerId: string;
@@ -661,13 +662,22 @@ export interface UserMentionedData {
dumpId?: string; dumpId?: string;
} }
export interface DumpCommentedData {
commenterId: string;
commenterUsername: string;
commentId: string;
dumpId: string;
dumpTitle: string;
}
export type NotificationData = export type NotificationData =
| PlaylistFollowedData | PlaylistFollowedData
| UserFollowedData | UserFollowedData
| UserDumpPostedData | UserDumpPostedData
| PlaylistDumpAddedData | PlaylistDumpAddedData
| DumpUpvotedData | DumpUpvotedData
| UserMentionedData; | UserMentionedData
| DumpCommentedData;
export interface Notification { export interface Notification {
id: string; id: string;

View File

@@ -5,7 +5,10 @@ import {
} from "../model/interfaces.ts"; } from "../model/interfaces.ts";
import { type SQLOutputValue } from "node:sqlite"; import { type SQLOutputValue } from "node:sqlite";
import { commentRowToApi, db, isCommentRow } from "../model/db.ts"; import { commentRowToApi, db, isCommentRow } from "../model/db.ts";
import { notifyMentions } from "./notification-service.ts"; import {
notifyDumpOwnerNewComment,
notifyMentions,
} from "./notification-service.ts";
import { linkAttachments } from "./attachment-service.ts"; import { linkAttachments } from "./attachment-service.ts";
const SELECT_COLS = const SELECT_COLS =
@@ -66,6 +69,7 @@ export function createComment(
const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get( const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get(
dumpId, dumpId,
) as { title: string } | undefined; ) as { title: string } | undefined;
notifyDumpOwnerNewComment(userId, id, dumpId);
notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId); notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId);
linkAttachments(body, id); linkAttachments(body, id);
return comment; return comment;

View File

@@ -276,6 +276,36 @@ export function notifyMentions(
} }
} }
export function notifyDumpOwnerNewComment(
commenterId: string,
commentId: string,
dumpId: string,
): void {
const commenterRow = db.prepare(
`SELECT username FROM users WHERE id = ?;`,
).get(commenterId) as { username: string } | undefined;
const dumpRow = db.prepare(
`SELECT title, user_id FROM dumps WHERE id = ?;`,
).get(dumpId) as { title: string; user_id: string } | undefined;
if (!commenterRow || !dumpRow) return;
if (commenterId === dumpRow.user_id) return; // no self-notification
createNotification(
dumpRow.user_id,
"dump_commented",
{
commenterId,
commenterUsername: commenterRow.username,
commentId,
dumpId,
dumpTitle: dumpRow.title,
},
`comment:${commentId}`,
);
}
export function notifyPlaylistFollowersNewDump( export function notifyPlaylistFollowersNewDump(
playlistId: string, playlistId: string,
playlistTitle: string, playlistTitle: string,

View File

@@ -8,9 +8,10 @@
<link href="https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,100..900;1,100..900&family=Space+Grotesk:wght@400;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,100..900;1,100..900&family=Space+Grotesk:wght@400;700;900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#111827" /> <meta name="theme-color" content="#111827" />
<meta name="site-name" content="__SITE_NAME__" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/favicon.svg" /> <link rel="apple-touch-icon" href="/favicon.svg" />
<title>Dumps</title> <title>__SITE_NAME__</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -29,7 +29,8 @@ export function AppHeader(
className={`app-header${centerSlot ? " app-header--has-center" : ""}`} className={`app-header${centerSlot ? " app-header--has-center" : ""}`}
> >
<Link to="/?tab=hot" className="app-header-brand"> <Link to="/?tab=hot" className="app-header-brand">
🚚 gerbeur 🚚 {document.querySelector<HTMLMetaElement>('meta[name="site-name"]')
?.content ?? "gerbeur"}
</Link> </Link>
{centerSlot && <div className="app-header-center">{centerSlot}</div>} {centerSlot && <div className="app-header-center">{centerSlot}</div>}

File diff suppressed because one or more lines are too long

View File

@@ -92,45 +92,51 @@ msgstr "+ New playlist"
msgid "+ Playlist" msgid "+ Playlist"
msgstr "+ Playlist" msgstr "+ Playlist"
#. placeholder {0}: d.commenterUsername
#. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:171
msgid "<0>{0}</0> commented on <1>{1}</1>"
msgstr "<0>{0}</0> commented on <1>{1}</1>"
#. placeholder {0}: d.followerUsername #. placeholder {0}: d.followerUsername
#. placeholder {1}: d.playlistTitle #. placeholder {1}: d.playlistTitle
#: src/pages/Notifications.tsx:124 #: src/pages/Notifications.tsx:131
msgid "<0>{0}</0> followed your playlist <1>{1}</1>" msgid "<0>{0}</0> followed your playlist <1>{1}</1>"
msgstr "<0>{0}</0> followed your playlist <1>{1}</1>" msgstr "<0>{0}</0> followed your playlist <1>{1}</1>"
#. placeholder {0}: d.mentionerUsername #. placeholder {0}: d.mentionerUsername
#: src/pages/Notifications.tsx:166 #: src/pages/Notifications.tsx:183
msgid "<0>{0}</0> mentioned you in <1>{where}</1>" msgid "<0>{0}</0> mentioned you in <1>{where}</1>"
msgstr "<0>{0}</0> mentioned you in <1>{where}</1>" msgstr "<0>{0}</0> mentioned you in <1>{where}</1>"
#. placeholder {0}: d.dumperUsername #. placeholder {0}: d.dumperUsername
#. placeholder {1}: d.dumpTitle #. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:134 #: src/pages/Notifications.tsx:141
msgid "<0>{0}</0> posted <1>{1}</1>" msgid "<0>{0}</0> posted <1>{1}</1>"
msgstr "<0>{0}</0> posted <1>{1}</1>" msgstr "<0>{0}</0> posted <1>{1}</1>"
#. placeholder {0}: d.followerUsername #. placeholder {0}: d.followerUsername
#: src/pages/Notifications.tsx:115 #: src/pages/Notifications.tsx:122
msgid "<0>{0}</0> started following you" msgid "<0>{0}</0> started following you"
msgstr "<0>{0}</0> started following you" msgstr "<0>{0}</0> started following you"
#. placeholder {0}: d.voterUsername #. placeholder {0}: d.voterUsername
#. placeholder {1}: d.dumpTitle #. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:154 #: src/pages/Notifications.tsx:161
msgid "<0>{0}</0> upvoted <1>{1}</1>" msgid "<0>{0}</0> upvoted <1>{1}</1>"
msgstr "<0>{0}</0> upvoted <1>{1}</1>" msgstr "<0>{0}</0> upvoted <1>{1}</1>"
#. placeholder {0}: d.dumpTitle #. placeholder {0}: d.dumpTitle
#. placeholder {1}: d.playlistTitle #. placeholder {1}: d.playlistTitle
#: src/pages/Notifications.tsx:144 #: src/pages/Notifications.tsx:151
msgid "<0>{0}</0> was added to <1>{1}</1>" msgid "<0>{0}</0> was added to <1>{1}</1>"
msgstr "<0>{0}</0> was added to <1>{1}</1>" msgstr "<0>{0}</0> was added to <1>{1}</1>"
#: src/pages/Notifications.tsx:164 #: src/pages/Notifications.tsx:181
msgid "a comment" msgid "a comment"
msgstr "a comment" msgstr "a comment"
#: src/pages/Notifications.tsx:164 #: src/pages/Notifications.tsx:181
msgid "a post" msgid "a post"
msgstr "a post" msgstr "a post"
@@ -373,7 +379,7 @@ msgstr "Dumps"
msgid "Dumps ({0}{1})" msgid "Dumps ({0}{1})"
msgstr "Dumps ({0}{1})" msgstr "Dumps ({0}{1})"
#: src/pages/Notifications.tsx:349 #: src/pages/Notifications.tsx:360
msgid "Earlier" msgid "Earlier"
msgstr "Earlier" msgstr "Earlier"
@@ -432,6 +438,7 @@ msgstr "Failed to generate invite"
#: src/pages/index/HotFeed.tsx:36 #: src/pages/index/HotFeed.tsx:36
#: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/JournalFeed.tsx:48
#: src/pages/index/NewFeed.tsx:36 #: src/pages/index/NewFeed.tsx:36
#: src/pages/Notifications.tsx:334
#: src/pages/UserPublicProfile.tsx:1106 #: src/pages/UserPublicProfile.tsx:1106
#: src/pages/UserPublicProfile.tsx:1148 #: src/pages/UserPublicProfile.tsx:1148
#: src/pages/UserPublicProfile.tsx:1193 #: src/pages/UserPublicProfile.tsx:1193
@@ -623,7 +630,7 @@ msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
msgid "Live updates unavailable." msgid "Live updates unavailable."
msgstr "Live updates unavailable." msgstr "Live updates unavailable."
#: src/pages/Notifications.tsx:390 #: src/pages/Notifications.tsx:407
msgid "Load more" msgid "Load more"
msgstr "Load more" msgstr "Load more"
@@ -658,8 +665,8 @@ msgstr "Loading profile…"
#: src/pages/index/HotFeed.tsx:32 #: src/pages/index/HotFeed.tsx:32
#: src/pages/index/JournalFeed.tsx:44 #: src/pages/index/JournalFeed.tsx:44
#: src/pages/index/NewFeed.tsx:32 #: src/pages/index/NewFeed.tsx:32
#: src/pages/Notifications.tsx:313 #: src/pages/Notifications.tsx:330
#: src/pages/Notifications.tsx:389 #: src/pages/Notifications.tsx:406
#: src/pages/UserDumps.tsx:51 #: src/pages/UserDumps.tsx:51
#: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPlaylists.tsx:342
#: src/pages/UserPublicProfile.tsx:1100 #: src/pages/UserPublicProfile.tsx:1100
@@ -692,7 +699,7 @@ msgstr "Login failed"
msgid "Max 50 MB" msgid "Max 50 MB"
msgstr "Max 50 MB" msgstr "Max 50 MB"
#: src/pages/Notifications.tsx:306 #: src/pages/Notifications.tsx:323
msgid "new" msgid "new"
msgstr "new" msgstr "new"
@@ -762,7 +769,7 @@ msgstr "No users match \"{q}\"."
msgid "Not following anyone yet." msgid "Not following anyone yet."
msgstr "Not following anyone yet." msgstr "Not following anyone yet."
#: src/pages/Notifications.tsx:324 #: src/pages/Notifications.tsx:341
#: src/pages/UserDumps.tsx:123 #: src/pages/UserDumps.tsx:123
#: src/pages/UserPublicProfile.tsx:1340 #: src/pages/UserPublicProfile.tsx:1340
#: src/pages/UserPublicProfile.tsx:1463 #: src/pages/UserPublicProfile.tsx:1463
@@ -771,7 +778,7 @@ msgid "Nothing here yet."
msgstr "Nothing here yet." msgstr "Nothing here yet."
#: src/components/NotificationBell.tsx:42 #: src/components/NotificationBell.tsx:42
#: src/pages/Notifications.tsx:302 #: src/pages/Notifications.tsx:319
msgid "Notifications" msgid "Notifications"
msgstr "Notifications" msgstr "Notifications"
@@ -1021,7 +1028,7 @@ msgstr "This reset link is missing or malformed."
msgid "Title" msgid "Title"
msgstr "Title" msgstr "Title"
#: src/pages/Notifications.tsx:346 #: src/pages/Notifications.tsx:357
msgid "Today" msgid "Today"
msgstr "Today" msgstr "Today"
@@ -1115,11 +1122,11 @@ msgstr "Why are you dumping this?"
msgid "Write a reply…" msgid "Write a reply…"
msgstr "Write a reply…" msgstr "Write a reply…"
#: src/pages/Notifications.tsx:348 #: src/pages/Notifications.tsx:359
msgid "Yesterday" msgid "Yesterday"
msgstr "Yesterday" msgstr "Yesterday"
#: src/pages/Notifications.tsx:327 #: src/pages/Notifications.tsx:344
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."

File diff suppressed because one or more lines are too long

View File

@@ -92,45 +92,51 @@ msgstr "+ Nouvelle collection"
msgid "+ Playlist" msgid "+ Playlist"
msgstr "+ Collection" msgstr "+ Collection"
#. placeholder {0}: d.commenterUsername
#. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:171
msgid "<0>{0}</0> commented on <1>{1}</1>"
msgstr ""
#. placeholder {0}: d.followerUsername #. placeholder {0}: d.followerUsername
#. placeholder {1}: d.playlistTitle #. placeholder {1}: d.playlistTitle
#: src/pages/Notifications.tsx:124 #: src/pages/Notifications.tsx:131
msgid "<0>{0}</0> followed your playlist <1>{1}</1>" msgid "<0>{0}</0> followed your playlist <1>{1}</1>"
msgstr "<0>{0}</0> a suivi votre collection <1>{1}</1>" msgstr "<0>{0}</0> a suivi votre collection <1>{1}</1>"
#. placeholder {0}: d.mentionerUsername #. placeholder {0}: d.mentionerUsername
#: src/pages/Notifications.tsx:166 #: src/pages/Notifications.tsx:183
msgid "<0>{0}</0> mentioned you in <1>{where}</1>" msgid "<0>{0}</0> mentioned you in <1>{where}</1>"
msgstr "<0>{0}</0> vous a mentionné dans <1>{where}</1>" msgstr "<0>{0}</0> vous a mentionné dans <1>{where}</1>"
#. placeholder {0}: d.dumperUsername #. placeholder {0}: d.dumperUsername
#. placeholder {1}: d.dumpTitle #. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:134 #: src/pages/Notifications.tsx:141
msgid "<0>{0}</0> posted <1>{1}</1>" msgid "<0>{0}</0> posted <1>{1}</1>"
msgstr "<0>{0}</0> a publié <1>{1}</1>" msgstr "<0>{0}</0> a publié <1>{1}</1>"
#. placeholder {0}: d.followerUsername #. placeholder {0}: d.followerUsername
#: src/pages/Notifications.tsx:115 #: src/pages/Notifications.tsx:122
msgid "<0>{0}</0> started following you" msgid "<0>{0}</0> started following you"
msgstr "<0>{0}</0> a commencé à vous suivre" msgstr "<0>{0}</0> a commencé à vous suivre"
#. placeholder {0}: d.voterUsername #. placeholder {0}: d.voterUsername
#. placeholder {1}: d.dumpTitle #. placeholder {1}: d.dumpTitle
#: src/pages/Notifications.tsx:154 #: src/pages/Notifications.tsx:161
msgid "<0>{0}</0> upvoted <1>{1}</1>" msgid "<0>{0}</0> upvoted <1>{1}</1>"
msgstr "<0>{0}</0> a voté pour <1>{1}</1>" msgstr "<0>{0}</0> a voté pour <1>{1}</1>"
#. placeholder {0}: d.dumpTitle #. placeholder {0}: d.dumpTitle
#. placeholder {1}: d.playlistTitle #. placeholder {1}: d.playlistTitle
#: src/pages/Notifications.tsx:144 #: src/pages/Notifications.tsx:151
msgid "<0>{0}</0> was added to <1>{1}</1>" msgid "<0>{0}</0> was added to <1>{1}</1>"
msgstr "<0>{0}</0> a été ajouté à <1>{1}</1>" msgstr "<0>{0}</0> a été ajouté à <1>{1}</1>"
#: src/pages/Notifications.tsx:164 #: src/pages/Notifications.tsx:181
msgid "a comment" msgid "a comment"
msgstr "un commentaire" msgstr "un commentaire"
#: src/pages/Notifications.tsx:164 #: src/pages/Notifications.tsx:181
msgid "a post" msgid "a post"
msgstr "une publication" msgstr "une publication"
@@ -357,7 +363,7 @@ msgstr "Recos"
msgid "Dumps ({0}{1})" msgid "Dumps ({0}{1})"
msgstr "Recos ({0}{1})" msgstr "Recos ({0}{1})"
#: src/pages/Notifications.tsx:349 #: src/pages/Notifications.tsx:360
msgid "Earlier" msgid "Earlier"
msgstr "Plus tôt" msgstr "Plus tôt"
@@ -416,6 +422,7 @@ msgstr "Impossible de générer une invitation"
#: src/pages/index/HotFeed.tsx:36 #: src/pages/index/HotFeed.tsx:36
#: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/JournalFeed.tsx:48
#: src/pages/index/NewFeed.tsx:36 #: src/pages/index/NewFeed.tsx:36
#: src/pages/Notifications.tsx:334
#: src/pages/UserPublicProfile.tsx:1106 #: src/pages/UserPublicProfile.tsx:1106
#: src/pages/UserPublicProfile.tsx:1148 #: src/pages/UserPublicProfile.tsx:1148
#: src/pages/UserPublicProfile.tsx:1193 #: src/pages/UserPublicProfile.tsx:1193
@@ -570,7 +577,7 @@ msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative
msgid "Live updates unavailable." msgid "Live updates unavailable."
msgstr "Mises à jour en direct indisponibles." msgstr "Mises à jour en direct indisponibles."
#: src/pages/Notifications.tsx:390 #: src/pages/Notifications.tsx:407
msgid "Load more" msgid "Load more"
msgstr "Charger plus" msgstr "Charger plus"
@@ -605,8 +612,8 @@ msgstr "Chargement du profil…"
#: src/pages/index/HotFeed.tsx:32 #: src/pages/index/HotFeed.tsx:32
#: src/pages/index/JournalFeed.tsx:44 #: src/pages/index/JournalFeed.tsx:44
#: src/pages/index/NewFeed.tsx:32 #: src/pages/index/NewFeed.tsx:32
#: src/pages/Notifications.tsx:313 #: src/pages/Notifications.tsx:330
#: src/pages/Notifications.tsx:389 #: src/pages/Notifications.tsx:406
#: src/pages/UserDumps.tsx:51 #: src/pages/UserDumps.tsx:51
#: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPlaylists.tsx:342
#: src/pages/UserPublicProfile.tsx:1100 #: src/pages/UserPublicProfile.tsx:1100
@@ -639,7 +646,7 @@ msgstr "Connexion échouée"
msgid "Max 50 MB" msgid "Max 50 MB"
msgstr "Max 50 Mo" msgstr "Max 50 Mo"
#: src/pages/Notifications.tsx:306 #: src/pages/Notifications.tsx:323
msgid "new" msgid "new"
msgstr "nouveau" msgstr "nouveau"
@@ -705,7 +712,7 @@ msgstr "Aucun utilisateur ne correspond à « {q} »."
msgid "Not following anyone yet." msgid "Not following anyone yet."
msgstr "Aucun abonnement pour le moment." msgstr "Aucun abonnement pour le moment."
#: src/pages/Notifications.tsx:324 #: src/pages/Notifications.tsx:341
#: src/pages/UserDumps.tsx:123 #: src/pages/UserDumps.tsx:123
#: src/pages/UserPublicProfile.tsx:1340 #: src/pages/UserPublicProfile.tsx:1340
#: src/pages/UserPublicProfile.tsx:1463 #: src/pages/UserPublicProfile.tsx:1463
@@ -714,7 +721,7 @@ msgid "Nothing here yet."
msgstr "Rien ici pour l'instant." msgstr "Rien ici pour l'instant."
#: src/components/NotificationBell.tsx:42 #: src/components/NotificationBell.tsx:42
#: src/pages/Notifications.tsx:302 #: src/pages/Notifications.tsx:319
msgid "Notifications" msgid "Notifications"
msgstr "Notifications" msgstr "Notifications"
@@ -952,7 +959,7 @@ msgstr "Ce lien de réinitialisation est absent ou malformé."
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
#: src/pages/Notifications.tsx:346 #: src/pages/Notifications.tsx:357
msgid "Today" msgid "Today"
msgstr "Aujourd'hui" msgstr "Aujourd'hui"
@@ -1034,11 +1041,11 @@ msgstr "Pourquoi recommandez-vous ça ?"
msgid "Write a reply…" msgid "Write a reply…"
msgstr "Écrire une réponse…" msgstr "Écrire une réponse…"
#: src/pages/Notifications.tsx:348 #: src/pages/Notifications.tsx:359
msgid "Yesterday" msgid "Yesterday"
msgstr "Hier" msgstr "Hier"
#: src/pages/Notifications.tsx:327 #: src/pages/Notifications.tsx:344
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu." msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."

View File

@@ -248,7 +248,8 @@ export type NotificationType =
| "user_dump_posted" | "user_dump_posted"
| "playlist_dump_added" | "playlist_dump_added"
| "dump_upvoted" | "dump_upvoted"
| "user_mentioned"; | "user_mentioned"
| "dump_commented";
export interface PlaylistFollowedData { export interface PlaylistFollowedData {
followerId: string; followerId: string;
@@ -292,13 +293,22 @@ export interface UserMentionedData {
dumpId?: string; dumpId?: string;
} }
export interface DumpCommentedData {
commenterId: string;
commenterUsername: string;
commentId: string;
dumpId: string;
dumpTitle: string;
}
export type NotificationData = export type NotificationData =
| PlaylistFollowedData | PlaylistFollowedData
| UserFollowedData | UserFollowedData
| UserDumpPostedData | UserDumpPostedData
| PlaylistDumpAddedData | PlaylistDumpAddedData
| DumpUpvotedData | DumpUpvotedData
| UserMentionedData; | UserMentionedData
| DumpCommentedData;
export interface Notification { export interface Notification {
id: string; id: string;

View File

@@ -10,6 +10,7 @@ import { ErrorCard } from "../components/ErrorCard.tsx";
import { Tooltip } from "../components/Tooltip.tsx"; import { Tooltip } from "../components/Tooltip.tsx";
import { useWS } from "../hooks/useWS.ts"; import { useWS } from "../hooks/useWS.ts";
import type { import type {
DumpCommentedData,
DumpUpvotedData, DumpUpvotedData,
Notification, Notification,
NotificationData, NotificationData,
@@ -36,7 +37,13 @@ type State =
loadingMore: boolean; loadingMore: boolean;
}; };
type NotifIconKind = "upvote" | "follow" | "dump" | "playlist" | "mention"; type NotifIconKind =
| "upvote"
| "follow"
| "dump"
| "playlist"
| "mention"
| "comment";
function notifIconKind(type: Notification["type"]): NotifIconKind { function notifIconKind(type: Notification["type"]): NotifIconKind {
switch (type) { switch (type) {
@@ -52,6 +59,8 @@ function notifIconKind(type: Notification["type"]): NotifIconKind {
return "playlist"; return "playlist";
case "user_mentioned": case "user_mentioned":
return "mention"; return "mention";
case "dump_commented":
return "comment";
} }
} }
@@ -75,6 +84,7 @@ function NotifIcon({ type }: { type: Notification["type"] }) {
dump: "🚚", dump: "🚚",
playlist: "📜", playlist: "📜",
mention: "@", mention: "@",
comment: "💬",
}; };
return ( return (
<span className={`notif-icon notif-icon--${kind}`}> <span className={`notif-icon notif-icon--${kind}`}>
@@ -96,6 +106,10 @@ function notificationLink(n: Notification): string {
return `/dumps/${(data as PlaylistDumpAddedData).dumpId}`; return `/dumps/${(data as PlaylistDumpAddedData).dumpId}`;
case "dump_upvoted": case "dump_upvoted":
return `/dumps/${(data as DumpUpvotedData).dumpId}`; return `/dumps/${(data as DumpUpvotedData).dumpId}`;
case "dump_commented":
return `/dumps/${(data as DumpCommentedData).dumpId}#comment-${
(data as DumpCommentedData).commentId
}`;
case "user_mentioned": { case "user_mentioned": {
const d = data as UserMentionedData; const d = data as UserMentionedData;
if (d.contextType === "comment") { if (d.contextType === "comment") {
@@ -159,6 +173,16 @@ function notificationContent(n: Notification): React.ReactNode {
</Trans> </Trans>
); );
} }
case "dump_commented": {
const d = data as DumpCommentedData;
return (
<Trans>
<strong>{d.commenterUsername}</strong>
{" commented on "}
<strong>{d.dumpTitle}</strong>
</Trans>
);
}
case "user_mentioned": { case "user_mentioned": {
const d = data as UserMentionedData; const d = data as UserMentionedData;
const where = d.contextTitle || const where = d.contextTitle ||

View File

@@ -4,6 +4,8 @@ import { lingui } from "@lingui/vite-plugin";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
const SITE_NAME = process.env.GERBEUR_SITE_NAME || "gerbeur";
function manifestPlugin(): Plugin { function manifestPlugin(): Plugin {
const cssPath = path.resolve("src/index.css"); const cssPath = path.resolve("src/index.css");
const outPath = path.resolve("public/manifest.webmanifest"); const outPath = path.resolve("public/manifest.webmanifest");
@@ -21,8 +23,8 @@ function manifestPlugin(): Plugin {
const bgColor = cssVar(rootBlock, "--color-bg") ?? "#111827"; const bgColor = cssVar(rootBlock, "--color-bg") ?? "#111827";
const manifest = { const manifest = {
name: "gerbeur", name: SITE_NAME,
short_name: "gerbeur", short_name: SITE_NAME,
start_url: "/", start_url: "/",
display: "standalone", display: "standalone",
background_color: bgColor, background_color: bgColor,
@@ -73,6 +75,18 @@ export default defineConfig({
}, },
plugins: [ plugins: [
manifestPlugin(), manifestPlugin(),
{
// In dev the API server isn't serving index.html, so replace the placeholder here.
// In production the Oak server does the replacement at runtime (see api/lib/static.ts).
name: "inject-site-name",
transformIndexHtml: {
order: "pre",
handler: (html, ctx) =>
ctx.server
? html.replaceAll("__SITE_NAME__", SITE_NAME)
: html,
},
},
lingui(), lingui(),
react({ react({
plugins: [["@lingui/swc-plugin", {}]], plugins: [["@lingui/swc-plugin", {}]],