Re-work space navigation #223

This commit is contained in:
Jon Staab
2025-10-06 11:23:19 -07:00
committed by hodlbod
parent b3533c285f
commit f9ac13ba11
68 changed files with 2807 additions and 884 deletions
+1 -96
View File
@@ -1,10 +1,6 @@
import {get, writable} from "svelte/store"
import {
partition,
chunk,
sample,
sleep,
shuffle,
uniq,
int,
YEAR,
@@ -18,12 +14,9 @@ import {
fromPairs,
} from "@welshman/lib"
import {
MESSAGE,
DELETE,
THREAD,
EVENT_TIME,
AUTH_INVITE,
COMMENT,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
@@ -47,24 +40,10 @@ import {
thunkQueue,
makeFeedController,
loadRelay,
loadMutes,
loadFollows,
loadProfile,
loadBlossomServers,
loadRelaySelections,
loadInboxRelaySelections,
} from "@welshman/app"
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
import {
NOTIFIER_RELAY,
INDEXER_RELAYS,
defaultPubkeys,
userRoomsByUrl,
getUrlsForEvent,
loadMembership,
loadSettings,
} from "@app/core/state"
import {NOTIFIER_RELAY, getUrlsForEvent} from "@app/core/state"
// Utils
@@ -359,80 +338,6 @@ export const loadAlertStatuses = (pubkey: string) =>
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
})
// Application requests
export const listenForNotifications = () => {
const controller = new AbortController()
for (const [url, allRooms] of userRoomsByUrl.get()) {
// Limit how many rooms we load at a time, since we have to send a separate filter
// for each one due to relay29 being picky
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
load({
signal: controller.signal,
relays: [url],
filters: [
{kinds: [THREAD], limit: 1},
{kinds: [MESSAGE], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
],
})
request({
signal: controller.signal,
relays: [url],
filters: [
{kinds: [THREAD], since: now()},
{kinds: [MESSAGE], since: now()},
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
],
})
}
return () => controller.abort()
}
export const loadUserData = async (pubkey: string, relays: string[] = []) => {
await Promise.race([sleep(3000), loadRelaySelections(pubkey, relays)])
const promise = Promise.race([
sleep(3000),
Promise.all([
loadInboxRelaySelections(pubkey, relays),
loadBlossomServers(pubkey, relays),
loadMembership(pubkey, relays),
loadSettings(pubkey, relays),
loadProfile(pubkey, relays),
loadFollows(pubkey, relays),
loadMutes(pubkey, relays),
loadAlertStatuses(pubkey),
loadAlerts(pubkey),
]),
])
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
promise.then(async () => {
for (const pubkeys of chunk(50, get(defaultPubkeys))) {
const relays = sample(1, INDEXER_RELAYS)
await sleep(1000)
for (const pubkey of pubkeys) {
loadMembership(pubkey, relays)
loadProfile(pubkey, relays)
loadFollows(pubkey, relays)
loadMutes(pubkey, relays)
}
}
})
return promise
}
export const discoverRelays = (lists: List[]) =>
Promise.all(
uniq(lists.flatMap($l => getRelaysFromList($l)))
+250
View File
@@ -0,0 +1,250 @@
import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store"
import {derived, get} from "svelte/store"
import {call, chunk, sleep, now, identity, WEEK, ago} from "@welshman/lib"
import {
getListTags,
getRelayTagValues,
WRAP,
MESSAGE,
ZAP_GOAL,
THREAD,
EVENT_TIME,
COMMENT,
isSignedEvent,
} from "@welshman/util"
import {request, pull} from "@welshman/net"
import {
pubkey,
loadRelay,
userFollows,
userRelaySelections,
userInboxRelaySelections,
loadRelaySelections,
loadInboxRelaySelections,
loadBlossomServers,
loadFollows,
loadMutes,
loadProfile,
repository,
} from "@welshman/app"
import {
INDEXER_RELAYS,
canDecrypt,
loadSettings,
userMembership,
defaultPubkeys,
decodeRelay,
loadMembership,
} from "@app/core/state"
import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
const syncRelays = () => {
for (const url of INDEXER_RELAYS) {
loadRelay(url)
}
const unsubscribePage = page.subscribe($page => {
if ($page.params.relay) {
loadRelay(decodeRelay($page.params.relay))
}
})
const unsubscribeMembership = userMembership.subscribe($l => {
for (const url of getRelayTagValues(getListTags($l))) {
loadRelay(url)
}
})
return () => {
unsubscribePage()
unsubscribeMembership()
}
}
const syncUserData = () => {
const unsubscribePubkey = pubkey.subscribe($pubkey => {
if ($pubkey) {
loadRelaySelections($pubkey)
}
})
const unsubscribeSelections = userRelaySelections.subscribe($l => {
const $pubkey = pubkey.get()
if ($pubkey) {
loadAlerts($pubkey)
loadAlertStatuses($pubkey)
loadBlossomServers($pubkey)
loadFollows($pubkey)
loadMembership($pubkey)
loadMutes($pubkey)
loadProfile($pubkey)
loadSettings($pubkey)
}
})
const unsubscribeFollows = userFollows.subscribe(async $l => {
for (const pubkeys of chunk(10, get(defaultPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
for (const pk of pubkeys) {
loadRelaySelections(pk).then(() => {
loadMembership(pk)
loadProfile(pk)
loadFollows(pk)
loadMutes(pk)
})
}
}
})
return () => {
unsubscribePubkey()
unsubscribeSelections()
unsubscribeFollows()
}
}
const syncSpace = (url: string) => {
const controller = new AbortController()
// Load historical data
pull({
relays: [url],
signal: controller.signal,
filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}],
events: repository
.query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}])
.filter(isSignedEvent),
})
// Load new events
request({
relays: [url],
signal: controller.signal,
filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT], since: now()}],
})
return () => controller.abort()
}
const syncSpaces = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const unsubscribeMembership = userMembership.subscribe($l => {
const urls = getRelayTagValues(getListTags($l))
// Start syncing newly added spaces
for (const url of urls) {
if (!unsubscribersByUrl.has(url)) {
unsubscribersByUrl.set(url, syncSpace(url))
}
}
// stop syncing removed spaces
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.includes(url)) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
}
})
return () => {
Array.from(unsubscribersByUrl.values()).forEach(call)
unsubscribeMembership()
}
}
const syncDMRelay = (url: string, pubkey: string) => {
const controller = new AbortController()
// Load historical data
pull({
relays: [url],
signal: controller.signal,
filters: [{kinds: [WRAP], "#p": [pubkey], until: ago(WEEK, 2)}],
events: repository
.query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}])
.filter(isSignedEvent),
})
// Load new events
request({
relays: [url],
signal: controller.signal,
filters: [{kinds: [WRAP], "#p": [pubkey], since: ago(WEEK, 2)}],
})
return () => controller.abort()
}
const syncDMs = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
let currentPubkey: string | undefined
const unsubscribeAll = () => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
}
const subscribeAll = (pubkey: string, urls: string[]) => {
// Start syncing newly added relays
for (const url of urls) {
if (!unsubscribersByUrl.has(url)) {
unsubscribersByUrl.set(url, syncDMRelay(url, pubkey))
}
}
// Stop syncing removed spaces
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.includes(url)) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
}
}
// When pubkey changes, re-sync
const unsubscribePubkey = derived([pubkey, canDecrypt], identity).subscribe(
([$pubkey, $canDecrypt]) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
// If we have a pubkey, refresh our user's relay selections then sync our subscriptions
if ($pubkey && $canDecrypt) {
loadRelaySelections($pubkey)
.then(() => loadInboxRelaySelections($pubkey))
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
}
currentPubkey = $pubkey
},
)
// When user inbox relays change, update synchronization
const unsubscribeSelections = userInboxRelaySelections.subscribe($l => {
const $pubkey = pubkey.get()
if ($pubkey && $l) {
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
}
})
return () => {
unsubscribeAll()
unsubscribePubkey()
unsubscribeSelections()
}
}
export const syncApplicationData = () => {
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
return () => unsubscribers.forEach(call)
}