Add mobile nav notification badges

This commit is contained in:
Jon Staab
2024-11-14 12:16:44 -08:00
parent 14ad4ec785
commit 2978d91977
11 changed files with 128 additions and 131 deletions
+27 -54
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {fly, slide} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte" import Popover from "@lib/components/Popover.svelte"
@@ -55,17 +55,6 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
const getDelay = (reset = false) => {
if (reset) {
delay = 0
} else {
delay += 50
}
return delay
}
let delay = 0
let showMenu = false let showMenu = false
let replaceState = false let replaceState = false
let element: Element let element: Element
@@ -120,53 +109,37 @@
</Popover> </Popover>
{/if} {/if}
</div> </div>
<div in:fly={{delay: getDelay(true)}}> <SecondaryNavItem href={makeSpacePath(url)}>
<SecondaryNavItem href={makeSpacePath(url)}> <Icon icon="home-smile" /> Home
<Icon icon="home-smile" /> Home </SecondaryNavItem>
</SecondaryNavItem> <SecondaryNavItem href={threadsPath} notification={$threadsNotification}>
</div> <Icon icon="notes-minimalistic" /> Threads
<div in:fly={{delay: getDelay()}}> </SecondaryNavItem>
<SecondaryNavItem href={threadsPath} notification={$threadsNotification}> <div class="h-2" />
<Icon icon="notes-minimalistic" /> Threads <SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
</SecondaryNavItem> <MenuSpaceRoomItem {url} room={GENERAL} />
</div>
<div transition:slide={{delay: getDelay()}}>
<div class="h-2" />
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
</div>
<div transition:slide={{delay: getDelay()}}>
<MenuSpaceRoomItem {url} room={GENERAL} />
</div>
{#each rooms as room, i (room)} {#each rooms as room, i (room)}
<div transition:slide={{delay: getDelay()}}> <MenuSpaceRoomItem {url} {room} />
<MenuSpaceRoomItem {url} {room} />
</div>
{/each} {/each}
{#if otherRooms.length > 0} {#if otherRooms.length > 0}
<div transition:slide={{delay: getDelay()}}> <div class="h-2" />
<div class="h-2" /> <SecondaryNavHeader>
<SecondaryNavHeader> {#if rooms.length > 0}
{#if rooms.length > 0} Other Rooms
Other Rooms {:else}
{:else} Rooms
Rooms {/if}
{/if} </SecondaryNavHeader>
</SecondaryNavHeader>
</div>
{/if} {/if}
{#each otherRooms as room, i (room)} {#each otherRooms as room, i (room)}
<div transition:slide={{delay: getDelay()}}> <SecondaryNavItem href={makeSpacePath(url, room)}>
<SecondaryNavItem href={makeSpacePath(url, room)}> <Icon icon="hashtag" />
<Icon icon="hashtag" /> {room}
{room}
</SecondaryNavItem>
</div>
{/each}
<div in:fly={{delay: getDelay()}}>
<SecondaryNavItem on:click={addRoom}>
<Icon icon="add-circle" />
Create room
</SecondaryNavItem> </SecondaryNavItem>
</div> {/each}
<SecondaryNavItem on:click={addRoom}>
<Icon icon="add-circle" />
Create room
</SecondaryNavItem>
</SecondaryNavSection> </SecondaryNavSection>
</div> </div>
+3 -19
View File
@@ -1,15 +1,11 @@
<script lang="ts"> <script lang="ts">
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte" import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userMembership, getMembershipUrls, PLATFORM_RELAY} from "@app/state" import {userMembership, getMembershipUrls, PLATFORM_RELAY} from "@app/state"
import {makeSpacePath} from "@app/routes"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const addSpace = () => pushModal(SpaceAdd) const addSpace = () => pushModal(SpaceAdd)
@@ -17,23 +13,11 @@
<div class="column menu gap-2"> <div class="column menu gap-2">
{#if PLATFORM_RELAY} {#if PLATFORM_RELAY}
<Link replaceState href={makeSpacePath(PLATFORM_RELAY)}> <MenuSpacesItem url={PLATFORM_RELAY} />
<CardButton>
<div slot="icon"><SpaceAvatar url={PLATFORM_RELAY} /></div>
<div slot="title"><RelayName url={PLATFORM_RELAY} /></div>
<div slot="info"><RelayDescription url={PLATFORM_RELAY} /></div>
</CardButton>
</Link>
<Divider /> <Divider />
{:else if getMembershipUrls($userMembership).length > 0} {:else if getMembershipUrls($userMembership).length > 0}
{#each getMembershipUrls($userMembership) as url (url)} {#each getMembershipUrls($userMembership) as url (url)}
<Link replaceState href={makeSpacePath(url)}> <MenuSpacesItem {url} />
<CardButton>
<div slot="icon"><SpaceAvatar {url} /></div>
<div slot="title"><RelayName {url} /></div>
<div slot="info"><RelayDescription {url} /></div>
</CardButton>
</Link>
{/each} {/each}
<Divider /> <Divider />
{/if} {/if}
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import Link from "@lib/components/Link.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import {makeSpacePath} from "@app/routes"
import {deriveNotification, SPACE_FILTERS} from "@app/notifications"
export let url
const path = makeSpacePath(url)
const notification = deriveNotification(path, SPACE_FILTERS, url)
</script>
<Link replaceState href={path}>
<CardButton>
<div slot="icon"><SpaceAvatar {url} /></div>
<div slot="title" class="flex gap-1">
<RelayName {url} />
{#if $notification}
<div class="relative top-1 h-2 w-2 rounded-full bg-primary" />
{/if}
</div>
<div slot="info"><RelayDescription {url} /></div>
</CardButton>
</Link>
+3 -3
View File
@@ -9,7 +9,7 @@
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userMembership, getMembershipUrls, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state" import {userMembership, getMembershipUrls, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {deriveNotification, CHAT_FILTERS} from "@app/notifications" import {deriveNotification, spacesNotification, CHAT_FILTERS} from "@app/notifications"
const chatNotification = deriveNotification("/chat", CHAT_FILTERS) const chatNotification = deriveNotification("/chat", CHAT_FILTERS)
@@ -76,10 +76,10 @@
<PrimaryNavItem title="Notes" href="/notes"> <PrimaryNavItem title="Notes" href="/notes">
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" /> <Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem title="Messages" href="/chat"> <PrimaryNavItem title="Messages" href="/chat" notification={$chatNotification}>
<Avatar icon="letter" class="!h-10 !w-10" /> <Avatar icon="letter" class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem title="Spaces" on:click={showSpacesMenu}> <PrimaryNavItem title="Spaces" on:click={showSpacesMenu} notification={$spacesNotification}>
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" /> <Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
</div> </div>
+22 -1
View File
@@ -4,7 +4,15 @@ import {repository, pubkey} from "@welshman/app"
import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib" import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib"
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {DIRECT_MESSAGE} from "@welshman/util" import {DIRECT_MESSAGE} from "@welshman/util"
import {MESSAGE, THREAD, COMMENT, deriveEventsForUrl} from "@app/state" import {makeSpacePath} from "@app/routes"
import {
MESSAGE,
THREAD,
COMMENT,
deriveEventsForUrl,
getMembershipUrls,
userMembership,
} from "@app/state"
// Checked state // Checked state
@@ -49,3 +57,16 @@ export const deriveNotification = (path: string, filters: Filter[], url?: string
}, },
) )
} }
export const spacesNotification = derived(
[pubkey, checked, userMembership, deriveEvents(repository, {filters: SPACE_FILTERS})],
([$pubkey, $checked, $userMembership, $events]) => {
return getMembershipUrls($userMembership).some(url => {
const path = makeSpacePath(url)
const lastChecked = max([$checked["*"], $checked[path]])
const [latestEvent] = sortBy($e => -$e.created_at, $events)
return latestEvent?.pubkey !== $pubkey && lt(lastChecked, latestEvent?.created_at)
})
},
)
-11
View File
@@ -1,11 +0,0 @@
<script lang="ts">
import {sleep} from "@welshman/lib"
export let delay = 1
</script>
{#await sleep(delay)}
<!-- pass -->
{:then}
<slot />
{/await}
+6 -6
View File
@@ -17,10 +17,10 @@
class:bg-base-300={active} class:bg-base-300={active}
class:tooltip={title} class:tooltip={title}
data-tip={title}> data-tip={title}>
{#if !active && notification}
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" />
{/if}
<slot /> <slot />
{#if !active && notification}
<div class="absolute right-2 top-2 h-2 w-2 rounded-full bg-primary" />
{/if}
</div> </div>
</a> </a>
{:else} {:else}
@@ -30,10 +30,10 @@
class:bg-base-300={active} class:bg-base-300={active}
class:tooltip={title} class:tooltip={title}
data-tip={title}> data-tip={title}>
{#if !active && notification}
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" />
{/if}
<slot /> <slot />
{#if !active && notification}
<div class="absolute right-2 top-2 h-2 w-2 rounded-full bg-primary" />
{/if}
</div> </div>
</Button> </Button>
{/if} {/if}
+4 -1
View File
@@ -2,6 +2,7 @@
import "@src/app.css" import "@src/app.css"
import {onMount} from "svelte" import {onMount} from "svelte"
import {get, derived} from "svelte/store" import {get, derived} from "svelte/store"
import {page} from "$app/stores"
import {dev} from "$app/environment" import {dev} from "$app/environment"
import {identity, uniq, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib" import {identity, uniq, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
@@ -229,7 +230,9 @@
{:then} {:then}
<div data-theme={$theme}> <div data-theme={$theme}>
<AppContainer> <AppContainer>
<slot /> {#key $page.url.pathname}
<slot />
{/key}
</AppContainer> </AppContainer>
<ModalContainer /> <ModalContainer />
<div class="tippy-target" /> <div class="tippy-target" />
+11 -17
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores"
import {ctx} from "@welshman/lib" import {ctx} from "@welshman/lib"
import {WRAP} from "@welshman/util" import {WRAP} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
@@ -56,22 +55,17 @@
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" /> <input bind:value={term} class="grow" type="text" />
</label> </label>
{#key $page.params.chat} <div class="overflow-auto">
<div class="overflow-auto"> {#each chats as { id, pubkeys, messages } (id)}
{#each chats as { id, pubkeys, messages } (id)} <ChatItem {id} {pubkeys} {messages} />
<ChatItem {id} {pubkeys} {messages} /> {/each}
{/each} {#await promise}
{#await promise} <div class="border-t border-solid border-base-100 px-6 py-4 text-xs">
<div class="border-t border-solid border-base-100 px-6 py-4 text-xs"> <Spinner loading>Loading conversations...</Spinner>
<Spinner loading>Loading conversations...</Spinner> </div>
</div> {/await}
{/await} </div>
</div>
{/key}
</SecondaryNav> </SecondaryNav>
<Page> <Page>
{#key JSON.stringify($page.params)} <slot />
<slot />
{/key}
</Page> </Page>
+7
View File
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import {page} from "$app/stores"
import {onDestroy} from "svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ContentSearch from "@lib/components/ContentSearch.svelte" import ContentSearch from "@lib/components/ContentSearch.svelte"
@@ -6,12 +8,17 @@
import ChatStart from "@app/components/ChatStart.svelte" import ChatStart from "@app/components/ChatStart.svelte"
import {chatSearch} from "@app/state" import {chatSearch} from "@app/state"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {setChecked} from "@app/notifications"
let term = "" let term = ""
const startChat = () => pushModal(ChatStart) const startChat = () => pushModal(ChatStart)
$: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1) $: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1)
onDestroy(() => {
setChecked($page.url.pathname)
})
</script> </script>
<div class="hidden min-h-screen md:hero"> <div class="hidden min-h-screen md:hero">
+18 -19
View File
@@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import Page from "@lib/components/Page.svelte" import Page from "@lib/components/Page.svelte"
import Delay from "@lib/components/Delay.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte" import MenuSpace from "@app/components/MenuSpace.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {checkRelayConnection, checkRelayAuth} from "@app/commands" import {checkRelayConnection, checkRelayAuth} from "@app/commands"
import {decodeRelay} from "@app/state" import {decodeRelay} from "@app/state"
import {deriveNotification, SPACE_FILTERS} from "@app/notifications"
$: url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const notification = deriveNotification($page.url.pathname, SPACE_FILTERS, url)
const ifLet = <T,>(x: T | undefined, f: (x: T) => void) => (x === undefined ? undefined : f(x)) const ifLet = <T,>(x: T | undefined, f: (x: T) => void) => (x === undefined ? undefined : f(x))
@@ -24,24 +26,21 @@
}) })
} }
// We have to watch this one, since on mobile the badge wil be visible when active
$: {
if ($notification) {
setChecked($page.url.pathname)
}
}
onMount(() => { onMount(() => {
checkConnection() checkConnection()
}) })
onDestroy(() => {
setChecked($page.url.pathname)
})
</script> </script>
{#key url} <SecondaryNav>
<Delay> <MenuSpace {url} />
<SecondaryNav> </SecondaryNav>
<MenuSpace {url} /> <Page>
</SecondaryNav> <slot />
<Page> </Page>
{#key $page.params.room}
<slot />
{/key}
</Page>
</Delay>
{/key}