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