forked from coracle/flotilla
Add notification badges
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user