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
+98
-3
No files matched your search
@@ -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,6 +51,7 @@
|
|||||||
{#snippet child({
|
{#snippet child({
|
||||||
props,
|
props,
|
||||||
})}
|
})}
|
||||||
|
<span class="menu-trigger">
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
aria-label={t("menu.open")}
|
aria-label={t("menu.open")}
|
||||||
@@ -59,6 +61,8 @@
|
|||||||
>
|
>
|
||||||
<Icon name="menu" />
|
<Icon name="menu" />
|
||||||
</Button>
|
</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" />
|
||||||
|
{#if entry.panel === "alerts"}
|
||||||
|
<span class="account-name">{t(entry.label)}</span>
|
||||||
|
<AlertBadge inline />
|
||||||
|
{:else}
|
||||||
{t(entry.label)}
|
{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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user