feat(frontend): global notices
This commit is contained in:
parent
b07f4b75c0
commit
b0431ff962
7 changed files with 81 additions and 5 deletions
|
@ -68,6 +68,7 @@ builder
|
||||||
{
|
{
|
||||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||||
};
|
};
|
||||||
|
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||||
})
|
})
|
||||||
.ConfigureApiBehaviorOptions(options =>
|
.ConfigureApiBehaviorOptions(options =>
|
||||||
{
|
{
|
||||||
|
|
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
49
Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fastRequest } from "$api";
|
||||||
|
import type { UserSettings } from "$api/models";
|
||||||
|
import { idTimestamp } from "$lib";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
import log from "$lib/log";
|
||||||
|
import { renderUnsafeMarkdown } from "$lib/markdown";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
type Props = { id: string; message: string; settings?: UserSettings; token: string | null };
|
||||||
|
let { id, message, settings, token }: Props = $props();
|
||||||
|
|
||||||
|
let lastReadNotice = $state(settings?.last_read_notice || null);
|
||||||
|
|
||||||
|
// Render the notice if:
|
||||||
|
// - user is not logged in (no settings object)
|
||||||
|
// - last read notice is null (never marked any notice as read)
|
||||||
|
// - last read notice ID is smaller than the current one (has not marked the current notice as read)
|
||||||
|
let renderNotice = $derived(!lastReadNotice || lastReadNotice < id);
|
||||||
|
let canDismiss = $derived(!!token);
|
||||||
|
let renderedMessage = $derived(renderUnsafeMarkdown(message));
|
||||||
|
|
||||||
|
let dismiss = async () => {
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
await fastRequest("PATCH", "/users/@me/settings", { token, body: { last_read_notice: id } });
|
||||||
|
lastReadNotice = id;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("error updating last read notice ID:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if renderNotice}
|
||||||
|
<div class="alert alert-light" role="alert">
|
||||||
|
<div>
|
||||||
|
{@html renderedMessage}
|
||||||
|
</div>
|
||||||
|
{#if canDismiss}
|
||||||
|
<div>
|
||||||
|
<!-- svelte-ignore a11y_invalid_attribute -->
|
||||||
|
<a href="#" tabindex="0" role="button" onclick={() => dismiss()} onkeyup={() => dismiss()}>
|
||||||
|
{$t("notification.mark-as-read")}
|
||||||
|
</a>
|
||||||
|
• {idTimestamp(id).toLocaleString(DateTime.DATETIME_MED)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -38,6 +38,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-body-secondary">
|
<div class="card-footer text-body-secondary">
|
||||||
{idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)}
|
{idTimestamp(notification.id).toLocaleString(DateTime.DATETIME_MED)}
|
||||||
• <a href="/settings/notifications/ack/{notification.id}">Mark as read</a>
|
• <a href="/settings/notifications/ack/{notification.id}">{$t("notification.mark-as-read")}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -350,6 +350,8 @@
|
||||||
"notification": {
|
"notification": {
|
||||||
"suspension": "Your account has been suspended for the following reason: {{reason}}",
|
"suspension": "Your account has been suspended for the following reason: {{reason}}",
|
||||||
"warning": "You have been warned 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}}"
|
"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}}",
|
||||||
|
"mark-as-read": "Mark as read",
|
||||||
|
"no-notifications": "You have no notifications."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
|
@ -10,6 +11,15 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{#if data.meta.notice}
|
||||||
|
<GlobalNotice
|
||||||
|
id={data.meta.notice.id}
|
||||||
|
message={data.meta.notice.message}
|
||||||
|
settings={data.meUser?.settings}
|
||||||
|
token={data.token}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<h1>pronouns.cc</h1>
|
<h1>pronouns.cc</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -3,15 +3,28 @@
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
|
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
|
||||||
import { isActive } from "$lib/pageUtils.svelte";
|
import { isActive } from "$lib/pageUtils.svelte";
|
||||||
|
import type { LayoutData } from "./$types";
|
||||||
|
import GlobalNotice from "$components/GlobalNotice.svelte";
|
||||||
|
|
||||||
type Props = { children: Snippet };
|
type Props = { data: LayoutData; children: Snippet };
|
||||||
let { children }: Props = $props();
|
let { data, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$t("title.settings")} • pronouns.cc</title>
|
<title>{$t("title.settings")} • pronouns.cc</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if data.meta.notice}
|
||||||
|
<div class="container">
|
||||||
|
<GlobalNotice
|
||||||
|
id={data.meta.notice.id}
|
||||||
|
message={data.meta.notice.message}
|
||||||
|
settings={data.meUser?.settings}
|
||||||
|
token={data.token}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Nav pills justified fill class="flex-column flex-md-row mb-2">
|
<Nav pills justified fill class="flex-column flex-md-row mb-2">
|
||||||
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
|
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import Notification from "$components/settings/Notification.svelte";
|
import Notification from "$components/settings/Notification.svelte";
|
||||||
import UrlAlert from "$components/URLAlert.svelte";
|
import UrlAlert from "$components/URLAlert.svelte";
|
||||||
|
import { t } from "$lib/i18n";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
@ -12,5 +13,5 @@
|
||||||
{#each data.notifications as notification (notification.id)}
|
{#each data.notifications as notification (notification.id)}
|
||||||
<Notification {notification} />
|
<Notification {notification} />
|
||||||
{:else}
|
{:else}
|
||||||
You have no notifications.
|
{$t("notification.no-notifications")}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
Loading…
Add table
Reference in a new issue