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"> <script lang="ts">
import { DropdownMenu } from "bits-ui"; import { DropdownMenu } from "bits-ui";
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
import AccountAvatar from "#lib/components/settings/AccountAvatar.svelte"; import AccountAvatar from "#lib/components/settings/AccountAvatar.svelte";
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte"; import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
import Button from "#lib/components/ui/Button.svelte"; import Button from "#lib/components/ui/Button.svelte";
@@ -50,15 +51,18 @@
{#snippet child({ {#snippet child({
props, props,
})} })}
<Button <span class="menu-trigger">
{...props} <Button
aria-label={t("menu.open")} {...props}
round aria-label={t("menu.open")}
smaller round
variant="translucent" smaller
> variant="translucent"
<Icon name="menu" /> >
</Button> <Icon name="menu" />
</Button>
<AlertBadge />
</span>
{/snippet} {/snippet}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
@@ -105,7 +109,12 @@
onSelect={() => ui.openPanel(entry.panel)} onSelect={() => ui.openPanel(entry.panel)}
> >
<Icon name={entry.icon} size="1.125rem" /> <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> </DropdownMenu.Item>
{/each} {/each}
<DropdownMenu.Separator class="bg-menu-separator" /> <DropdownMenu.Separator class="bg-menu-separator" />
@@ -127,6 +136,12 @@
<AddAccountDialog bind:open={adding} /> <AddAccountDialog bind:open={adding} />
<style lang="scss"> <style lang="scss">
.menu-trigger {
position: relative;
display: flex;
flex-shrink: 0;
}
:global(.main-menu) { :global(.main-menu) {
width: 17rem; width: 17rem;
max-height: calc(100dvh - 5rem); 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"> <script lang="ts">
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
import About from "#lib/components/settings/About.svelte"; import About from "#lib/components/settings/About.svelte";
import Appearance from "#lib/components/settings/Appearance.svelte"; import Appearance from "#lib/components/settings/Appearance.svelte";
import Language from "#lib/components/settings/Language.svelte"; import Language from "#lib/components/settings/Language.svelte";
@@ -77,7 +78,11 @@
icon="unmute" icon="unmute"
label={t("settings.alerts")} label={t("settings.alerts")}
onclick={() => ui.openPanel("alerts")} onclick={() => ui.openPanel("alerts")}
/> >
{#snippet trailing()}
<AlertBadge inline />
{/snippet}
</SettingsItem>
</div> </div>
<About /> <About />
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte";
import { ripple } from "#lib/actions/ripple.js"; import { ripple } from "#lib/actions/ripple.js";
import Icon from "#lib/components/ui/Icon.svelte"; import Icon from "#lib/components/ui/Icon.svelte";
@@ -6,10 +7,11 @@
icon: string; icon: string;
label: string; label: string;
onclick?: () => void; onclick?: () => void;
trailing?: Snippet;
value?: string; value?: string;
} }
let { icon, label, value, onclick }: Props = $props(); let { icon, label, value, onclick, trailing }: Props = $props();
</script> </script>
<button class="settings-item" {onclick} type="button" use:ripple> <button class="settings-item" {onclick} type="button" use:ripple>
@@ -18,6 +20,7 @@
{#if value} {#if value}
<span class="value">{value}</span> <span class="value">{value}</span>
{/if} {/if}
{@render trailing?.()}
{#if onclick} {#if onclick}
<Icon class="chevron" name="arrow-right" size="1rem" /> <Icon class="chevron" name="arrow-right" size="1rem" />
{/if} {/if}
+8 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { untrack } from "svelte"; import { onMount, untrack } from "svelte";
import { cubicOut } from "svelte/easing"; import { cubicOut } from "svelte/easing";
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import ChatList from "#lib/components/ChatList.svelte"; import ChatList from "#lib/components/ChatList.svelte";
@@ -15,6 +15,7 @@
import Icon from "#lib/components/ui/Icon.svelte"; import Icon from "#lib/components/ui/Icon.svelte";
import { t } from "#lib/i18n/locale.svelte.js"; import { t } from "#lib/i18n/locale.svelte.js";
import { accounts } from "#lib/stores/accounts.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 { auth } from "#lib/stores/auth.svelte.js";
import { events } from "#lib/stores/events.svelte.js"; import { events } from "#lib/stores/events.svelte.js";
import { search } from "#lib/stores/search.svelte.js"; import { search } from "#lib/stores/search.svelte.js";
@@ -56,6 +57,12 @@
search.close(); search.close();
}); });
}); });
onMount(() => {
if (page.url.searchParams.has(ALERTS_PARAM)) {
ui.openPanel("alerts");
}
});
</script> </script>
<div <div