feat(frontend): live unseen alerts badge on the menu button and in settings

This commit is contained in:
hh committed 2026-10-19 02:57:27 +00:00
1 parent ea3a7c0a61
commit 2c56934133
5 files changed
+108 -13

No files matched your search

+25 -10
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { DropdownMenu } from "bits-ui";
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
import AccountAvatar from "#lib/components/settings/AccountAvatar.svelte";
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
import Button from "#lib/components/ui/Button.svelte";
@@ -50,15 +51,18 @@
{#snippet child({
props,
})}
<Button
{...props}
aria-label={t("menu.open")}
round
smaller
variant="translucent"
>
<Icon name="menu" />
</Button>
<span class="menu-trigger">
<Button
{...props}
aria-label={t("menu.open")}
round
smaller
variant="translucent"
>
<Icon name="menu" />
</Button>
<AlertBadge />
</span>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
@@ -105,7 +109,12 @@
onSelect={() => ui.openPanel(entry.panel)}
>
<Icon name={entry.icon} size="1.125rem" />
{t(entry.label)}
{#if entry.panel === "alerts"}
<span class="account-name">{t(entry.label)}</span>
<AlertBadge inline />
{:else}
{t(entry.label)}
{/if}
</DropdownMenu.Item>
{/each}
<DropdownMenu.Separator class="bg-menu-separator" />
@@ -127,6 +136,12 @@
<AddAccountDialog bind:open={adding} />
<style lang="scss">
.menu-trigger {
position: relative;
display: flex;
flex-shrink: 0;
}
:global(.main-menu) {
width: 17rem;
max-height: calc(100dvh - 5rem);
@@ -0,0 +1,65 @@
<script lang="ts">
import { t } from "#lib/i18n/locale.svelte.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { alerts } from "#lib/stores/alerts.svelte.js";
interface Props {
inline?: boolean;
}
const { inline = false }: Props = $props();
const MAX_SHOWN = 99;
const count = $derived(alerts.unseen);
$effect(() => {
alerts.track(accounts.selectedId);
});
</script>
{#if count > 0}
<span class="alert-badge" title={t("alerts.badge", { count })} class:inline>
{count > MAX_SHOWN ? `${MAX_SHOWN}+` : count}
</span>
{/if}
<style lang="scss">
.alert-badge {
pointer-events: none;
position: absolute;
top: 0;
right: -0.125rem;
min-width: 1.125rem;
height: 1.125rem;
padding: 0 0.3125rem;
border-radius: 0.5625rem;
font-size: 0.6875rem;
font-weight: var(--font-weight-medium);
font-variant-numeric: tabular-nums;
line-height: 1.125rem;
color: var(--color-white);
text-align: center;
background-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-background);
&.inline {
position: static;
flex-shrink: 0;
min-width: 1.375rem;
height: 1.375rem;
padding: 0 0.4375rem;
border-radius: 0.6875rem;
font-size: 0.8125rem;
line-height: 1.375rem;
box-shadow: none;
}
}
</style>
@@ -1,4 +1,5 @@
<script lang="ts">
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
import About from "#lib/components/settings/About.svelte";
import Appearance from "#lib/components/settings/Appearance.svelte";
import Language from "#lib/components/settings/Language.svelte";
@@ -77,7 +78,11 @@
icon="unmute"
label={t("settings.alerts")}
onclick={() => ui.openPanel("alerts")}
/>
>
{#snippet trailing()}
<AlertBadge inline />
{/snippet}
</SettingsItem>
</div>
<About />
@@ -1,4 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { ripple } from "#lib/actions/ripple.js";
import Icon from "#lib/components/ui/Icon.svelte";
@@ -6,10 +7,11 @@
icon: string;
label: string;
onclick?: () => void;
trailing?: Snippet;
value?: string;
}
let { icon, label, value, onclick }: Props = $props();
let { icon, label, value, onclick, trailing }: Props = $props();
</script>
<button class="settings-item" {onclick} type="button" use:ripple>
@@ -18,6 +20,7 @@
{#if value}
<span class="value">{value}</span>
{/if}
{@render trailing?.()}
{#if onclick}
<Icon class="chevron" name="arrow-right" size="1rem" />
{/if}
+8 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { untrack } from "svelte";
import { onMount, untrack } from "svelte";
import { cubicOut } from "svelte/easing";
import { fly } from "svelte/transition";
import ChatList from "#lib/components/ChatList.svelte";
@@ -15,6 +15,7 @@
import Icon from "#lib/components/ui/Icon.svelte";
import { t } from "#lib/i18n/locale.svelte.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { ALERTS_PARAM } from "#lib/stores/alerts.svelte.js";
import { auth } from "#lib/stores/auth.svelte.js";
import { events } from "#lib/stores/events.svelte.js";
import { search } from "#lib/stores/search.svelte.js";
@@ -56,6 +57,12 @@
search.close();
});
});
onMount(() => {
if (page.url.searchParams.has(ALERTS_PARAM)) {
ui.openPanel("alerts");
}
});
</script>
<div