Add notification badges

This commit is contained in:
Jon Staab
2024-11-14 10:53:13 -08:00
parent b296067e55
commit 14ad4ec785
21 changed files with 256 additions and 126 deletions
+9 -2
View File
@@ -1,14 +1,16 @@
<script lang="ts">
import {onMount} from "svelte"
import {page} from "$app/stores"
import {remove} from "@welshman/lib"
import {remove, assoc} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
import {fade} from "@lib/transition"
import Link from "@lib/components/Link.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {makeChatPath} from "@app/routes"
import {CHAT_FILTERS, deriveNotification} from "@app/notifications"
export let id: string
export let pubkeys: string[]
@@ -17,6 +19,8 @@
const message = messages[0]
const others = remove($pubkey!, pubkeys)
const active = $page.params.chat === id
const path = makeChatPath(pubkeys)
const notification = deriveNotification(path, CHAT_FILTERS.map(assoc("authors", pubkeys)))
onMount(() => {
for (const pk of others) {
@@ -30,7 +34,7 @@
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {$$props.class}"
class:bg-base-100={active}>
<div class="flex flex-col justify-start gap-1">
<div class="flex justify-between gap-2">
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
{#if others.length === 1}
<ProfileCircle pubkey={others[0]} size={5} />
@@ -44,6 +48,9 @@
</p>
{/if}
</div>
{#if !active && $notification}
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
{/if}
</div>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
{message.content}
+2
View File
@@ -13,6 +13,7 @@
import {PLATFORM_NAME} from "@app/state"
import {pushToast} from "@app/toast"
import {loadUserData} from "@app/commands"
import {setChecked} from "@app/notifications"
const signUp = () => pushModal(SignUp)
@@ -32,6 +33,7 @@
await loadUserData(session.pubkey, {relays})
pushToast({message: "Successfully logged in!"})
setChecked("*")
clearModals()
}
+2
View File
@@ -13,6 +13,7 @@
import InfoBunker from "@app/components/InfoBunker.svelte"
import {loginWithNip46, loadUserData} from "@app/commands"
import {pushModal, clearModals} from "@app/modal"
import {setChecked} from "@app/notifications"
import {pushToast} from "@app/toast"
import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state"
@@ -79,6 +80,7 @@
await loadUserData(pubkey)
setChecked("*")
clearModals()
}
})
+8 -9
View File
@@ -13,6 +13,7 @@
import SpaceJoin from "@app/components/SpaceJoin.svelte"
import ProfileList from "@app/components/ProfileList.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
import {
getMembershipRoomsByUrl,
getMembershipUrls,
@@ -22,11 +23,15 @@
roomsByUrl,
GENERAL,
} from "@app/state"
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
export let url
const threadsPath = makeSpacePath(url, "threads")
const threadsNotification = deriveNotification(threadsPath, THREAD_FILTERS, url)
const openMenu = () => {
showMenu = true
}
@@ -121,7 +126,7 @@
</SecondaryNavItem>
</div>
<div in:fly={{delay: getDelay()}}>
<SecondaryNavItem href={makeSpacePath(url, "threads")}>
<SecondaryNavItem href={threadsPath} notification={$threadsNotification}>
<Icon icon="notes-minimalistic" /> Threads
</SecondaryNavItem>
</div>
@@ -130,17 +135,11 @@
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
</div>
<div transition:slide={{delay: getDelay()}}>
<SecondaryNavItem href={makeSpacePath(url, GENERAL)}>
<Icon icon="hashtag" />
{GENERAL}
</SecondaryNavItem>
<MenuSpaceRoomItem {url} room={GENERAL} />
</div>
{#each rooms as room, i (room)}
<div transition:slide={{delay: getDelay()}}>
<SecondaryNavItem href={makeSpacePath(url, room)}>
<Icon icon="hashtag" />
{room}
</SecondaryNavItem>
<MenuSpaceRoomItem {url} {room} />
</div>
{/each}
{#if otherRooms.length > 0}
@@ -0,0 +1,17 @@
<script lang="ts">
import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import {makeSpacePath} from "@app/routes"
import {deriveNotification, getRoomFilters} from "@app/notifications"
export let url
export let room
const path = makeSpacePath(url, room)
const notification = deriveNotification(path, getRoomFilters(room), url)
</script>
<SecondaryNavItem href={path} notification={$notification}>
<Icon icon="hashtag" />
{room}
</SecondaryNavItem>
+11 -16
View File
@@ -1,16 +1,17 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import {userProfile} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte"
import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import MenuSpaces from "@app/components/MenuSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userMembership, getMembershipUrls, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
import {deriveNotification, CHAT_FILTERS} from "@app/notifications"
const chatNotification = deriveNotification("/chat", CHAT_FILTERS)
const addSpace = () => pushModal(SpaceAdd)
@@ -24,24 +25,14 @@
<div class="flex h-full flex-col justify-between">
<div>
{#if PLATFORM_RELAY}
<PrimaryNavItem
title={displayRelayUrl(PLATFORM_RELAY)}
href={makeSpacePath(PLATFORM_RELAY)}
class="tooltip-right">
<SpaceAvatar url={PLATFORM_RELAY} />
</PrimaryNavItem>
<PrimaryNavItemSpace url={PLATFORM_RELAY} />
{:else}
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
</PrimaryNavItem>
<Divider />
{#each getMembershipUrls($userMembership) as url (url)}
<PrimaryNavItem
title={displayRelayUrl(url)}
href={makeSpacePath(url)}
class="tooltip-right">
<SpaceAvatar {url} />
</PrimaryNavItem>
<PrimaryNavItemSpace {url} />
{/each}
<PrimaryNavItem title="Add Space" on:click={addSpace} class="tooltip-right">
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
@@ -59,7 +50,11 @@
<PrimaryNavItem title="Notes" href="/notes" class="tooltip-right">
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem title="Messages" href="/chat" class="tooltip-right">
<PrimaryNavItem
title="Messages"
href="/chat"
class="tooltip-right"
notification={$chatNotification}>
<Avatar icon="letter" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
@@ -0,0 +1,20 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.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>
<PrimaryNavItem
title={displayRelayUrl(url)}
href={path}
class="tooltip-right"
notification={$notification}>
<SpaceAvatar {url} />
</PrimaryNavItem>
+2
View File
@@ -10,6 +10,7 @@
import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import {pushModal, clearModals} from "@app/modal"
import {setChecked} from "@app/notifications"
import {PLATFORM_NAME} from "@app/state"
import {pushToast} from "@app/toast"
@@ -42,6 +43,7 @@
if (await loginBroker.connect("", nip46Perms)) {
addSession({method: "nip46", pubkey, secret, handler: {...handler, pubkey}})
pushToast({message: "Successfully logged in!"})
setChecked("*")
clearModals()
} else {
pushToast({
+10 -2
View File
@@ -13,6 +13,8 @@
import ThunkStatus from "@app/components/ThunkStatus.svelte"
import ThreadMenu from "@app/components/ThreadMenu.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {deriveNotification} from "@app/notifications"
import {makeSpacePath} from "@app/routes"
import {COMMENT} from "@app/state"
export let url
@@ -21,7 +23,10 @@
const thunk = $thunks[event.id]
const deleted = deriveIsDeleted(repository, event)
const replies = deriveEvents(repository, {filters: [{kinds: [COMMENT], "#E": [event.id]}]})
const path = makeSpacePath(url, "threads", event.id)
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
const notification = deriveNotification(path, filters, url)
const replies = deriveEvents(repository, {filters})
const showPopover = () => popover.show()
@@ -58,7 +63,10 @@
<Icon icon="reply" />
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
</div>
<div class="btn btn-neutral btn-xs hidden rounded-full sm:flex">
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
{#if $notification}
<div class="h-2 w-2 rounded-full bg-primary" />
{/if}
Active {formatTimestampRelative(lastActive)}
</div>
{/if}
+51
View File
@@ -0,0 +1,51 @@
import {writable, derived} from "svelte/store"
import {deriveEvents} from "@welshman/store"
import {repository, pubkey} from "@welshman/app"
import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib"
import type {Filter} from "@welshman/util"
import {DIRECT_MESSAGE} from "@welshman/util"
import {MESSAGE, THREAD, COMMENT, deriveEventsForUrl} from "@app/state"
// Checked state
export const checked = writable<Record<string, number>>({})
export const deriveChecked = (key: string) => derived(checked, prop(key))
export const setChecked = (key: string, ts = now()) =>
checked.update(state => ({...state, [key]: ts}))
// Filters for various routes
export const CHAT_FILTERS: Filter[] = [{kinds: [DIRECT_MESSAGE]}]
export const SPACE_FILTERS: Filter[] = [{kinds: [THREAD, MESSAGE, COMMENT]}]
export const ROOM_FILTERS: Filter[] = [{kinds: [MESSAGE]}]
export const THREAD_FILTERS: Filter[] = [
{kinds: [THREAD]},
{kinds: [COMMENT], "#K": [String(THREAD)]},
]
export const getNotificationFilters = (since: number): Filter[] =>
[...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since))
export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#~", [room]))
// Notification derivation
export const deriveNotification = (path: string, filters: Filter[], url?: string) => {
const events = url ? deriveEventsForUrl(url, filters) : deriveEvents(repository, {filters})
return derived(
[pubkey, deriveChecked("*"), deriveChecked(path), events],
([$pubkey, $allChecked, $checked, $events]) => {
const [latestEvent] = sortBy($e => -$e.created_at, $events)
return (
latestEvent?.pubkey !== $pubkey && lt(max([$allChecked, $checked]), latestEvent?.created_at)
)
},
)
}
+7 -11
View File
@@ -1,10 +1,9 @@
import twColors from "tailwindcss/colors"
import {get, derived, writable} from "svelte/store"
import {get, derived} from "svelte/store"
import {nip19} from "nostr-tools"
import type {Maybe} from "@welshman/lib"
import {
ctx,
now,
setContext,
remove,
assoc,
@@ -17,7 +16,6 @@ import {
nthEq,
shuffle,
parseJson,
prop,
} from "@welshman/lib"
import {
getIdFilters,
@@ -253,6 +251,12 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
)
}
export const getEventsForUrl = (url: string, filters: Filter[]) =>
sortBy(
e => -e.created_at,
repository.query(filters).filter(e => e.tags.find(nthEq(0, "~"))?.[2] === url),
)
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived(deriveEvents(repository, {filters}), $events =>
sortBy(
@@ -261,14 +265,6 @@ export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
),
)
// Last checked timestamps, notifications
export const checked = writable<Record<string, number>>({})
export const deriveChecked = (key: string) => derived(checked, prop(key))
export const setChecked = (key: string, ts = now()) => checked.update(assoc(key, ts))
// Settings
export const SETTINGS = 38489
+7
View File
@@ -5,6 +5,7 @@
export let title = ""
export let href = ""
export let prefix = ""
export let notification = false
$: active = $page.url?.pathname?.startsWith(prefix || href || "bogus")
</script>
@@ -16,6 +17,9 @@
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
{#if !active && notification}
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" />
{/if}
<slot />
</div>
</a>
@@ -26,6 +30,9 @@
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
{#if !active && notification}
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" />
{/if}
<slot />
</div>
</Button>
+10 -2
View File
@@ -22,9 +22,11 @@
<script lang="ts">
import cx from "classnames"
import {fade} from "@lib/transition"
import {page} from "$app/stores"
export let href: string = ""
export let notification = false
$: active = $page.url.pathname === href
</script>
@@ -36,11 +38,14 @@
on:click
class={cx(
$$props.class,
"flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
"relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
)}
class:text-base-content={active}
class:bg-base-100={active}>
<slot />
{#if !active && notification}
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade />
{/if}
</a>
{:else}
<button
@@ -48,10 +53,13 @@
on:click
class={cx(
$$props.class,
"flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
"relative flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
)}
class:text-base-content={active}
class:bg-base-100={active}>
{#if !active && notification}
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade />
{/if}
<slot />
</button>
{/if}
+25 -15
View File
@@ -55,8 +55,11 @@
MESSAGE,
COMMENT,
THREAD,
GENERAL,
} from "@app/state"
import {loadUserData, subscribePersistent} from "@app/commands"
import {checked} from "@app/notifications"
import * as notifications from "@app/notifications"
import * as state from "@app/state"
// Migration: old nostrtalk instance used different sessions
@@ -67,7 +70,7 @@
let ready: Promise<unknown> = Promise.resolve()
onMount(async () => {
Object.assign(window, {get, ...lib, ...util, ...net, ...app, ...state})
Object.assign(window, {get, ...lib, ...util, ...net, ...app, ...state, ...notifications})
const getScoreEvent = () => {
const ALWAYS_KEEP = Infinity
@@ -133,6 +136,7 @@
events: storageAdapters.fromRepository(repository, {throttle: 300, migrate: migrateEvents}),
relays: {keyPath: "url", store: throttled(1000, relays)},
handles: {keyPath: "nip05", store: throttled(1000, handles)},
checked: storageAdapters.fromObjectStore(checked, {throttle: 1000}),
freshness: storageAdapters.fromObjectStore(freshness, {
throttle: 1000,
migrate: migrateFreshness,
@@ -167,29 +171,32 @@
await loadUserData($pubkey)
}
// Listen for space data, populate space-based notifications
let unsubRooms: any
userMembership.subscribe($membership => {
unsubRooms?.()
const since = ago(30)
const rooms = uniq(getMembershipRooms($membership).map(m => m.room))
const rooms = uniq(getMembershipRooms($membership).map(m => m.room)).concat(GENERAL)
const relays = uniq(getMembershipUrls($membership))
if (relays.length > 0) {
subscribePersistent({
relays,
filters: [
{kinds: [THREAD], since},
{kinds: [MESSAGE], "#~": rooms, since},
{kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), since},
{kinds: [DELETE], "#k": [THREAD, COMMENT, MESSAGE].map(String), since},
{kinds: [MEMBERSHIPS], "#r": relays, since},
],
})
}
subscribePersistent({
relays,
filters: [
{kinds: [THREAD], since},
{kinds: [THREAD], limit: 1},
{kinds: [MESSAGE], "#~": rooms, since},
{kinds: [MESSAGE], "#~": rooms, limit: 1},
{kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), since},
{kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), limit: 1},
{kinds: [DELETE], "#k": [THREAD, COMMENT, MESSAGE].map(String), since},
{kinds: [MEMBERSHIPS], "#r": relays, since: ago(WEEK, 2)},
],
})
})
// Listen for chats, populate chat-based notifications
let unsubChats: any
derived([pubkey, userInboxRelaySelections], identity).subscribe(
@@ -198,7 +205,10 @@
if ($pubkey) {
unsubChats = subscribePersistent({
filters: [{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)}],
filters: [
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
{kinds: [WRAP], "#p": [$pubkey], limit: 100},
],
relays: getRelayUrls($userInboxRelaySelections),
})
}
+3 -3
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy} from "svelte"
import {page} from "$app/stores"
import Chat from "@app/components/Chat.svelte"
import {setChecked} from "@app/state"
import {setChecked} from "@app/notifications"
onMount(() => {
onDestroy(() => {
setChecked($page.url.pathname)
})
</script>
+6 -1
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import {onMount} from "svelte"
import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores"
import Page from "@lib/components/Page.svelte"
import Delay from "@lib/components/Delay.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte"
import {pushToast} from "@app/toast"
import {setChecked} from "@app/notifications"
import {checkRelayConnection, checkRelayAuth} from "@app/commands"
import {decodeRelay} from "@app/state"
@@ -26,6 +27,10 @@
onMount(() => {
checkConnection()
})
onDestroy(() => {
setChecked($page.url.pathname)
})
</script>
{#key url}
+1 -6
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import {onMount} from "svelte"
import {page} from "$app/stores"
import {deriveRelay} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
@@ -11,7 +10,7 @@
import ProfileFeed from "@app/components/ProfileFeed.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import {decodeRelay, setChecked} from "@app/state"
import {decodeRelay} from "@app/state"
import {pushDrawer} from "@app/modal"
import {makeChatPath} from "@app/routes"
@@ -21,10 +20,6 @@
const openMenu = () => pushDrawer(MenuSpace, {url})
$: pubkey = $relay?.profile?.pubkey
onMount(() => {
setChecked($page.url.pathname)
})
</script>
<div class="relative flex flex-col">
@@ -8,7 +8,7 @@
</script>
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy} from "svelte"
import {page} from "$app/stores"
import {sortBy, append} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
@@ -34,8 +34,8 @@
MESSAGE,
COMMENT,
getMembershipRoomsByUrl,
setChecked,
} from "@app/state"
import {setChecked} from "@app/notifications"
import {addRoomMembership, removeRoomMembership} from "@app/commands"
import {pushDrawer} from "@app/modal"
import {popKey} from "@app/implicit"
@@ -91,7 +91,7 @@
elements.reverse()
}
onMount(() => {
onDestroy(() => {
setChecked($page.url.pathname)
})
@@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores"
import {sortBy, last, ago} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
@@ -14,7 +14,8 @@
import EventItem from "@app/components/EventItem.svelte"
import EventCreate from "@app/components/EventCreate.svelte"
import {pushModal, pushDrawer} from "@app/modal"
import {deriveEventsForUrl, pullConservatively, decodeRelay, setChecked} from "@app/state"
import {deriveEventsForUrl, pullConservatively, decodeRelay} from "@app/state"
import {setChecked} from "@app/notifications"
const url = decodeRelay($page.params.relay)
const kinds = [EVENT_DATE, EVENT_TIME]
@@ -54,8 +55,6 @@
.slice(0, limit)
onMount(() => {
setChecked($page.url.pathname)
const sub = subscribe({filters: [{kinds, since: ago(30)}]})
pullConservatively({filters: [{kinds}], relays: [url]})
@@ -63,6 +62,10 @@
return () => sub.close()
})
onDestroy(() => {
setChecked($page.url.pathname)
})
setTimeout(() => {
loading = false
}, 5000)
+49 -51
View File
@@ -1,13 +1,14 @@
<script lang="ts">
import {onMount} from "svelte"
import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores"
import {sortBy, uniqBy} from "@welshman/lib"
import {sortBy, sleep, uniqBy} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {nthEq} from "@welshman/lib"
import {createFeedController, userMutes} from "@welshman/app"
import {createScroller, type Scroller} from "@lib/html"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import PageBar from "@lib/components/PageBar.svelte"
@@ -15,72 +16,67 @@
import MenuSpace from "@app/components/MenuSpace.svelte"
import ThreadItem from "@app/components/ThreadItem.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {THREAD, COMMENT, decodeRelay, setChecked} from "@app/state"
import {THREAD, decodeRelay, getEventsForUrl} from "@app/state"
import {pushModal, pushDrawer} from "@app/modal"
import {THREAD_FILTERS, setChecked} from "@app/notifications"
const url = decodeRelay($page.params.relay)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const filters: Filter[] = [{kinds: [THREAD]}, {kinds: [COMMENT], "#K": [String(THREAD)]}]
const openMenu = () => pushDrawer(MenuSpace, {url})
const createThread = () => pushModal(ThreadCreate, {url})
const ctrl = createFeedController({
useWindowing: true,
feed: makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(THREAD_FILTERS)),
onEvent: (event: TrustedEvent) => {
if (
event.kind === THREAD &&
!event.tags.some(nthEq(0, "e")) &&
!mutedPubkeys.includes(event.pubkey)
) {
buffer.push(event)
}
},
onExhausted: () => {
loading = false
},
})
let loading = true
let unmounted = false
let element: Element
let scroller: Scroller
let buffer: TrustedEvent[] = []
let events: TrustedEvent[] = []
onMount(() => {
setChecked($page.url.pathname)
let unmounted = false
const ctrl = createFeedController({
useWindowing: true,
feed: makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(filters)),
onEvent: (event: TrustedEvent) => {
if (
event.kind === THREAD &&
!event.tags.some(nthEq(0, "e")) &&
!mutedPubkeys.includes(event.pubkey)
) {
buffer.push(event)
}
},
onExhausted: () => {
loading = false
},
})
let events: TrustedEvent[] = sortBy(e => -e.created_at, getEventsForUrl(url, [{kinds: [THREAD]}]))
onMount(async () => {
// Element is frequently not defined. I don't know why
setTimeout(() => {
if (!unmounted) {
scroller = createScroller({
element,
delay: 300,
threshold: 3000,
onScroll: () => {
buffer = uniqBy(
e => e.id,
sortBy(e => -e.created_at, buffer),
)
events = [...events, ...buffer.splice(0, 5)]
await sleep(1000)
if (buffer.length < 50) {
ctrl.load(50)
}
},
})
}
}, 1000)
if (!unmounted) {
scroller = createScroller({
element,
delay: 300,
threshold: 3000,
onScroll: () => {
buffer = sortBy(e => -e.created_at, buffer)
events = uniqBy(e => e.id, [...events, ...buffer.splice(0, 5)])
return () => {
unmounted = true
scroller?.stop()
if (buffer.length < 50) {
ctrl.load(50)
}
},
})
}
})
onDestroy(() => {
unmounted = true
scroller?.stop()
setChecked($page.url.pathname)
})
</script>
<div class="relative flex h-screen flex-col" bind:this={element}>
@@ -101,7 +97,9 @@
</PageBar>
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
{#each events as event (event.id)}
<ThreadItem {url} {event} />
<div in:fly>
<ThreadItem {url} {event} />
</div>
{/each}
<p class="flex h-10 items-center justify-center py-20">
<Spinner {loading}>
@@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onMount, onDestroy} from "svelte"
import {sortBy, nthEq, sleep} from "@welshman/lib"
import {page} from "$app/stores"
import {repository, subscribe} from "@welshman/app"
@@ -14,6 +14,7 @@
import ThreadActions from "@app/components/ThreadActions.svelte"
import ThreadReply from "@app/components/ThreadReply.svelte"
import {COMMENT, deriveEvent, decodeRelay} from "@app/state"
import {setChecked} from "@app/notifications"
import {pushDrawer} from "@app/modal"
const {relay, id} = $page.params
@@ -43,6 +44,10 @@
return () => sub.close()
})
onDestroy(() => {
setChecked($page.url.pathname)
})
</script>
<div class="relative flex flex-col-reverse gap-3 px-2">