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(),
 | 
			
		||||
        };
 | 
			
		||||
        options.SerializerSettings.DateParseHandling = DateParseHandling.None;
 | 
			
		||||
    })
 | 
			
		||||
    .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 class="card-footer text-body-secondary">
 | 
			
		||||
		{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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -350,6 +350,8 @@
 | 
			
		|||
	"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}}"
 | 
			
		||||
		"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">
 | 
			
		||||
	import GlobalNotice from "$components/GlobalNotice.svelte";
 | 
			
		||||
	import type { PageData } from "./$types";
 | 
			
		||||
 | 
			
		||||
	type Props = { data: PageData };
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +11,15 @@
 | 
			
		|||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
	<p>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,15 +3,28 @@
 | 
			
		|||
	import { t } from "$lib/i18n";
 | 
			
		||||
	import { Nav, NavLink } from "@sveltestrap/sveltestrap";
 | 
			
		||||
	import { isActive } from "$lib/pageUtils.svelte";
 | 
			
		||||
	import type { LayoutData } from "./$types";
 | 
			
		||||
	import GlobalNotice from "$components/GlobalNotice.svelte";
 | 
			
		||||
 | 
			
		||||
	type Props = { children: Snippet };
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
	type Props = { data: LayoutData; children: Snippet };
 | 
			
		||||
	let { data, children }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>{$t("title.settings")} • pronouns.cc</title>
 | 
			
		||||
</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">
 | 
			
		||||
	<Nav pills justified fill class="flex-column flex-md-row mb-2">
 | 
			
		||||
		<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
	import type { PageData } from "./$types";
 | 
			
		||||
	import Notification from "$components/settings/Notification.svelte";
 | 
			
		||||
	import UrlAlert from "$components/URLAlert.svelte";
 | 
			
		||||
	import { t } from "$lib/i18n";
 | 
			
		||||
 | 
			
		||||
	type Props = { data: PageData };
 | 
			
		||||
	let { data }: Props = $props();
 | 
			
		||||
| 
						 | 
				
			
			@ -12,5 +13,5 @@
 | 
			
		|||
{#each data.notifications as notification (notification.id)}
 | 
			
		||||
	<Notification {notification} />
 | 
			
		||||
{:else}
 | 
			
		||||
	You have no notifications.
 | 
			
		||||
	{$t("notification.no-notifications")}
 | 
			
		||||
{/each}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue