feat(frontend): live unseen alerts badge on the menu button and in settings
This commit is contained in:
1 parent
ea3a7c0a61
commit
2c56934133
5 files changed
+108
-13
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user