199 lines
6.7 KiB
Svelte
199 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import type {Snippet} from "svelte"
|
|
import {onMount} from "svelte"
|
|
import {uniqBy} from "@welshman/lib"
|
|
import type {TrustedEvent} from "@welshman/util"
|
|
import {
|
|
Address,
|
|
DELETE,
|
|
getAddress,
|
|
getTagValue,
|
|
getTagValues,
|
|
sortEventsDesc,
|
|
} from "@welshman/util"
|
|
import {load} from "@welshman/net"
|
|
import {pubkey, userProfile} from "@welshman/app"
|
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
import Settings from "@assets/icons/settings.svg?dataurl"
|
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
|
import MenuSettings from "@app/components/MenuSettings.svelte"
|
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
|
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
|
import {
|
|
BOOKMARKS,
|
|
BOOKMARK_LISTS,
|
|
deriveEvents,
|
|
INDEXER_RELAYS,
|
|
loadUserBookmarkCollections,
|
|
loadUserBookmarkList,
|
|
userSpaceUrls,
|
|
PLATFORM_RELAYS,
|
|
} from "@app/core/state"
|
|
import {pushModal} from "@app/util/modal"
|
|
import {notifications} from "@app/util/notifications"
|
|
import {goToChat, makeSpacePath} from "@app/util/routes"
|
|
|
|
type Props = {
|
|
children?: Snippet
|
|
}
|
|
|
|
const {children}: Props = $props()
|
|
|
|
const chatHandler = () => goToChat()
|
|
|
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
|
|
|
const anySpaceNotifications = $derived(
|
|
$userSpaceUrls.some(p => $notifications.has(makeSpacePath(p))),
|
|
)
|
|
|
|
const bookmarkListEvents = deriveEvents([{kinds: [BOOKMARKS, BOOKMARK_LISTS, DELETE]}])
|
|
let loadedBookmarkCountPubkey: string | undefined = $state()
|
|
|
|
const getListKey = (event: TrustedEvent) =>
|
|
event.kind === BOOKMARKS
|
|
? new Address(BOOKMARKS, event.pubkey, "").toString()
|
|
: getAddress(event)
|
|
|
|
const isDeletedList = (event: TrustedEvent, deleteEvents: TrustedEvent[]) => {
|
|
const address =
|
|
event.kind === BOOKMARKS
|
|
? new Address(BOOKMARKS, event.pubkey, "").toString()
|
|
: `${event.kind}:${event.pubkey}:${getTagValue("d", event.tags) || ""}`
|
|
|
|
return deleteEvents.some(deleteEvent => {
|
|
if (deleteEvent.created_at < event.created_at) {
|
|
return false
|
|
}
|
|
|
|
return (
|
|
getTagValues("e", deleteEvent.tags).includes(event.id) ||
|
|
getTagValues("a", deleteEvent.tags).includes(address)
|
|
)
|
|
})
|
|
}
|
|
|
|
const bookmarkListCount = $derived.by(() => {
|
|
if (!$pubkey) {
|
|
return 0
|
|
}
|
|
|
|
const ownEvents = sortEventsDesc($bookmarkListEvents).filter(event => event.pubkey === $pubkey)
|
|
const deleteEvents = ownEvents.filter(event => event.kind === DELETE)
|
|
const listEvents = ownEvents.filter(
|
|
event => event.kind === BOOKMARKS || event.kind === BOOKMARK_LISTS,
|
|
)
|
|
const visibleLists = uniqBy(getListKey, listEvents).filter(
|
|
event => !isDeletedList(event, deleteEvents),
|
|
)
|
|
const hasSavedItems = visibleLists.some(event => event.kind === BOOKMARKS)
|
|
|
|
return visibleLists.length + (hasSavedItems ? 0 : 1)
|
|
})
|
|
|
|
const loadBookmarkCountData = async () => {
|
|
if (!$pubkey || loadedBookmarkCountPubkey === $pubkey) {
|
|
return
|
|
}
|
|
|
|
loadedBookmarkCountPubkey = $pubkey
|
|
|
|
await Promise.all([
|
|
loadUserBookmarkList(),
|
|
loadUserBookmarkCollections(),
|
|
load({
|
|
relays: INDEXER_RELAYS,
|
|
filters: [{kinds: [DELETE], authors: [$pubkey]}],
|
|
}),
|
|
])
|
|
}
|
|
|
|
onMount(() => {
|
|
void loadBookmarkCountData()
|
|
})
|
|
|
|
$effect(() => {
|
|
if ($pubkey && loadedBookmarkCountPubkey !== $pubkey) {
|
|
void loadBookmarkCountData()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<div
|
|
class="ml-sai mt-sai mb-sai relative z-popover isolate hidden w-14 shrink-0 bg-base-200 pt-2 md:block">
|
|
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
|
<PrimaryNavSpaces />
|
|
<div class="flex flex-col">
|
|
<PrimaryNavItem title="Bookmarks" href="/bookmarks" prefix="/bookmarks">
|
|
<div class="relative">
|
|
<ImageIcon alt="Bookmarks" src={Bookmark} size={8} />
|
|
<span class="badge badge-xs badge-neutral absolute -right-2 -top-2"
|
|
>{bookmarkListCount}</span>
|
|
</div>
|
|
</PrimaryNavItem>
|
|
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
|
{#if $userProfile?.picture}
|
|
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
|
{:else}
|
|
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
|
|
{/if}
|
|
</PrimaryNavItem>
|
|
<PrimaryNavItem
|
|
title="Messages"
|
|
onclick={chatHandler}
|
|
notification={$notifications.has("/chat")}>
|
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
|
</PrimaryNavItem>
|
|
<PrimaryNavItem title="Search" href="/people">
|
|
<ImageIcon alt="Search" src={Magnifier} size={8} />
|
|
</PrimaryNavItem>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{@render children?.()}
|
|
|
|
<!-- a little extra something for ios -->
|
|
<div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
|
|
</div>
|
|
<div
|
|
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
|
<div class="flex gap-2 sm:gap-6">
|
|
<PrimaryNavItem title="Search" href="/people">
|
|
<ImageIcon alt="Search" src={Magnifier} size={8} />
|
|
</PrimaryNavItem>
|
|
<PrimaryNavItem
|
|
title="Messages"
|
|
href="/chat"
|
|
onclick={chatHandler}
|
|
notification={$notifications.has("/chat")}>
|
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
|
</PrimaryNavItem>
|
|
<PrimaryNavItem title="Bookmarks" href="/bookmarks" prefix="/bookmarks">
|
|
<div class="relative">
|
|
<ImageIcon alt="Bookmarks" src={Bookmark} size={8} />
|
|
<span class="badge badge-xs badge-neutral absolute -right-2 -top-2"
|
|
>{bookmarkListCount}</span>
|
|
</div>
|
|
</PrimaryNavItem>
|
|
{#if PLATFORM_RELAYS.length !== 1}
|
|
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
|
<ImageIcon alt="Spaces" src={Widget} size={8} />
|
|
</PrimaryNavItem>
|
|
{/if}
|
|
</div>
|
|
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
|
{#if $userProfile?.picture}
|
|
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
|
|
{:else}
|
|
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
|
|
{/if}
|
|
</PrimaryNavItem>
|
|
</div>
|
|
</div>
|