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
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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."
|
||||||
|
|
||||||
|
|||||||
14
src/model.ts
14
src/model.ts
@@ -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;
|
||||||
|
|||||||
@@ -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 ||
|
||||||
|
|||||||
@@ -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", {}]],
|
||||||
|
|||||||
Reference in New Issue
Block a user