diff --git a/Foxnouns.Frontend/src/lib/api/models/moderation.ts b/Foxnouns.Frontend/src/lib/api/models/moderation.ts index eee9382..689e9b8 100644 --- a/Foxnouns.Frontend/src/lib/api/models/moderation.ts +++ b/Foxnouns.Frontend/src/lib/api/models/moderation.ts @@ -112,3 +112,12 @@ export enum ClearableField { Flags = "FLAGS", CustomPreferences = "CUSTOM_PREFERENCES", } + +export type Notification = { + id: string; + type: "NOTICE" | "WARNING" | "SUSPENSION"; + message?: string; + localization_key?: string; + localization_params: Record; + acknowledged: boolean; +}; diff --git a/Foxnouns.Frontend/src/lib/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte index edfbd1a..2074347 100644 --- a/Foxnouns.Frontend/src/lib/components/Navbar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Navbar.svelte @@ -13,13 +13,21 @@ import Logo from "$components/Logo.svelte"; import { t } from "$lib/i18n"; - type Props = { user: MeUser | null; meta: Meta }; - let { user, meta }: Props = $props(); + type Props = { user: MeUser | null; meta: Meta; unreadNotifications?: boolean }; + let { user, meta, unreadNotifications }: Props = $props(); let isOpen = $state(true); const toggleMenu = () => (isOpen = !isOpen); +{#if user && unreadNotifications} +
+ {$t("nav.unread-notification-text")} +
+ {$t("nav.unread-notification-link")} +
+{/if} + {#if user && user.deleted}
{#if user.suspended} @@ -87,6 +95,11 @@ background-color: var(--bs-danger-bg-subtle); } + .notification-alert { + color: var(--bs-warning-text-emphasis); + background-color: var(--bs-warning-bg-subtle); + } + /* These exact values make it look almost identical to the SVG version, which is what we want */ #beta-text { font-size: 0.7em; diff --git a/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte b/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte new file mode 100644 index 0000000..c452f4c --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte @@ -0,0 +1,43 @@ + + +
+
+
+ +
+

+ {#if notification.localization_key} + {$t(notification.localization_key, notification.localization_params)} + {:else} + {notification.message} + {/if} +

+
+
+
+ +
diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index fe10b04..9f54943 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -9,7 +9,9 @@ "reactivate-account-link": "Reactivate account", "delete-permanently-link": "I want my account deleted permanently", "reactivate-or-delete-link": "I want to reactivate my account or delete all my data", - "export-link": "I want to export a copy of my data" + "export-link": "I want to export a copy of my data", + "unread-notification-text": "You have an unread notification.", + "unread-notification-link": "Go to your notifications" }, "avatar-tooltip": "Avatar for {{name}}", "profile": { @@ -329,7 +331,9 @@ }, "alert": { "auth-method-remove-success": "Successfully unlinked account!", - "auth-required": "You must log in to access this page." + "auth-required": "You must log in to access this page.", + "notif-ack-successful": "Successfully marked notification as read!", + "notif-ack-fail": "Could not mark notification as read." }, "footer": { "version": "Version", @@ -342,5 +346,10 @@ "changelog": "Changelog", "donate": "Donate", "about-contact": "About and contact" + }, + "notification": { + "suspension": "Your account has been suspended for the following reason: {{reason}}", + "warning": "You have been warned for the following reason: {{reason}}", + "warning-cleared-fields": "You have been warned for the following reason: {{reason}}\n\nAdditionally, the following fields have been cleared from your profile:\n{{clearedFields}}" } } diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index 82f3cb2..2debd7c 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -2,16 +2,24 @@ import { clearToken, TOKEN_COOKIE_NAME } from "$lib"; import { apiRequest } from "$api"; import ApiError, { ErrorCode } from "$api/error"; import type { Meta, MeUser } from "$api/models"; +import type { Notification } from "$api/models/moderation"; import log from "$lib/log"; import type { LayoutServerLoad } from "./$types"; export const load = (async ({ fetch, cookies }) => { let token: string | null = null; let meUser: MeUser | null = null; + let unreadNotifications: boolean = false; if (cookies.get(TOKEN_COOKIE_NAME)) { try { meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); token = cookies.get(TOKEN_COOKIE_NAME) || null; + + const notifications = await apiRequest("GET", "/notifications", { + fetch, + cookies, + }); + unreadNotifications = notifications.filter((n) => !n.acknowledged).length > 0; } catch (e) { if (e instanceof ApiError && e.code === ErrorCode.AuthenticationRequired) clearToken(cookies); else log.error("Could not fetch /users/@me and token has not expired:", e); @@ -19,5 +27,5 @@ export const load = (async ({ fetch, cookies }) => { } const meta = await apiRequest("GET", "/meta", { fetch, cookies }); - return { meta, meUser, token }; + return { meta, meUser, token, unreadNotifications }; }) satisfies LayoutServerLoad; diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte index e5be130..b991f8a 100644 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -11,7 +11,7 @@
- + {@render children?.()}