Migrate to new welshman stores

This commit is contained in:
Jon Staab
2025-11-20 15:54:06 -08:00
parent 3a63894562
commit 64c77cfd13
22 changed files with 503 additions and 541 deletions
+5 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {preventDefault} from "@lib/html"
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
import {randomInt, map, displayList, TIMEZONE, identity} from "@welshman/lib"
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
import type {Filter} from "@welshman/util"
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
@@ -13,7 +13,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {alerts, userSpaceUrls} from "@app/core/state"
import {alertsById, userSpaceUrls} from "@app/core/state"
import {requestRelayClaim} from "@app/core/requests"
import {createAlert} from "@app/core/commands"
import {canSendPushNotifications} from "@app/util/push"
@@ -45,7 +45,9 @@
let loading = $state(false)
let cron = $state(WEEKLY)
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
let email = $state(
map(a => getTagValue("email", a.tags), $alertsById.values()).filter(identity)[0] || "",
)
const back = () => history.back()
+4 -4
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {sleep} from "@welshman/lib"
import {sleep, filter} from "@welshman/lib"
import {getTagValue, getAddress, RelayMode} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds"
import {getPubkeyRelays, pubkey} from "@welshman/app"
@@ -13,8 +13,8 @@
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {
alerts,
dmAlert,
alertsById,
deriveAlertStatus,
getAlertFeed,
userSettingsValues,
@@ -33,7 +33,7 @@
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
const filteredAlerts = $derived(
$alerts.filter(alert => {
filter(alert => {
const feed = getAlertFeed(alert)
// Skip non-feeds and DM alerts
@@ -43,7 +43,7 @@
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
return true
}),
}, $alertsById.values()),
)
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
+2 -2
View File
@@ -51,9 +51,9 @@
import {
INDEXER_RELAYS,
userSettingsValues,
deriveChat,
splitChatId,
PLATFORM_NAME,
deriveChat,
} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {prependParent} from "@app/core/commands"
@@ -151,7 +151,7 @@
let compose: ChatCompose | undefined = $state()
let parent: TrustedEvent | undefined = $state()
let chatCompose: HTMLElement | undefined = $state()
let dlists: HTMLElelist | undefined = $state()
let dynamicPadding: HTMLElement | undefined = $state()
const elements = $derived.by(() => {
const elements = []
+2 -2
View File
@@ -3,7 +3,7 @@
import {max, formatTimestampRelative} from "@welshman/lib"
import {COMMENT} from "@welshman/util"
import {load} from "@welshman/net"
import {deriveEvents} from "@welshman/store"
import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util"
import {repository} from "@welshman/app"
import {notifications} from "@app/util/notifications"
@@ -13,7 +13,7 @@
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
const replies = deriveEvents(repository, {filters})
const replies = deriveArray(deriveEventsById({repository, filters}))
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
onMount(() => {
+2 -2
View File
@@ -4,6 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
@@ -11,7 +12,6 @@
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import {trackerStore} from "@app/core/state"
import {clip} from "@app/util/toast"
type Props = {
@@ -23,7 +23,7 @@
const relays = url ? [url] : Router.get().Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays})
const seenOn = $trackerStore.getRelays(event.id)
const seenOn = tracker.getRelays(event.id)
const npub1 = nip19.npubEncode(event.pubkey)
const json = JSON.stringify(event, null, 2)
const copyLink = () => clip(nevent1)
+9 -6
View File
@@ -2,7 +2,7 @@
import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveEventsMapped} from "@welshman/store"
import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -16,11 +16,14 @@
const {url, event, ...props}: Props = $props()
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
)
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
+2 -2
View File
@@ -4,7 +4,7 @@
import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {userMutes} from "@welshman/app"
import {userMuteList} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -32,7 +32,7 @@
muted = false
}
let muted = $state(getPubkeyTagValues(getListTags($userMutes)).includes(event.pubkey))
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
</script>
<div class="flex flex-col gap-2 {restProps.class}">
@@ -3,7 +3,7 @@
import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveEventsMapped} from "@welshman/store"
import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -14,11 +14,14 @@
const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
}),
)
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
+3 -3
View File
@@ -3,13 +3,13 @@
import {load} from "@welshman/net"
import {Router} from "@welshman/router"
import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {deriveArray, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT} from "@welshman/util"
import {repository, loadRelayList} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupLists, MESSAGE_KINDS} from "@app/core/state"
import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state"
import {goToEvent} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
@@ -20,7 +20,7 @@
const {pubkey, url}: Props = $props()
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
const events = deriveEvents(repository, {filters})
const events = deriveArray(deriveEventsById({repository, filters}))
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
+19 -16
View File
@@ -2,7 +2,7 @@
import cx from "classnames"
import {onMount} from "svelte"
import type {Snippet} from "svelte"
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
import {groupBy, map, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
import {
REPORT,
REACTION,
@@ -15,7 +15,7 @@
DELETE,
} from "@welshman/util"
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveEvents, deriveEventsMapped} from "@welshman/store"
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net"
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
@@ -46,19 +46,22 @@
children,
}: Props = $props()
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const reports = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
)
const reactions = deriveEvents(repository, {
filters: [{kinds: [REACTION], "#e": [event.id]}],
})
const reactions = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
)
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
)
const onReactionClick = (events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
@@ -77,18 +80,18 @@
const onReportClick = () => pushModal(ReportDetails, {url, event})
const reportReasons = $derived(uniq($reports.map(e => getTag("e", e.tags)?.[2])))
const reportReasons = $derived(uniq(map(e => getTag("e", e.tags)?.[2], $reports.values())))
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
const groupedReactions = $derived(
groupBy(
getReactionKey,
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions),
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions.values()),
),
)
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps))
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps.values()))
onMount(() => {
const controller = new AbortController()
+5 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {deriveEventsById} from "@welshman/store"
import {repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Button from "@lib/components/Button.svelte"
@@ -14,14 +14,15 @@
const {url, event}: Props = $props()
const reports = deriveEvents(repository, {
const reports = deriveEventsById({
repository,
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const back = () => history.back()
const onDelete = () => {
if ($reports.length === 0) {
if ($reports.size === 0) {
back()
}
}
@@ -36,7 +37,7 @@
<div>All reports for this event are shown below.</div>
{/snippet}
</ModalHeader>
{#each $reports as report (report.id)}
{#each $reports.values() as report (report.id)}
<div class="card2 card2-sm bg-alt">
<ReportItem {url} event={report} {onDelete} />
</div>
+4 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import {uniqBy, prop, ifLet} from "@welshman/lib"
import {ifLet} from "@welshman/lib"
import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {manageRelay, relays, fetchRelayDirectly} from "@welshman/app"
import {manageRelay, relaysByUrl, notifyRelay, fetchRelayDirectly} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -78,6 +78,8 @@
return new Map($relaysByUrl)
})
notifyRelay(relay)
})
pushToast({message: "Your changes have been saved!"})
+7 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {some} from "@welshman/lib"
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
import {deriveRelay, pubkey} from "@welshman/app"
import {fly} from "@lib/transition"
@@ -40,16 +41,15 @@
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import {
ENABLE_ZAPS,
CONTENT_KINDS,
deriveSpaceMembers,
deriveEventsForUrl,
deriveUserRooms,
deriveOtherRooms,
userSpaceUrls,
hasNip29,
alerts,
alertsById,
deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
deriveEventsForUrl,
} from "@app/core/state"
import {notifications} from "@app/util/notifications"
import {pushModal} from "@app/util/modal"
@@ -67,10 +67,12 @@
const members = deriveSpaceMembers(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
const hasAlerts = $derived(
some(a => getTagValue("feed", a.tags)?.includes(url), $alertsById.values()),
)
const spaceKinds = derived(
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
deriveEventsForUrl(url, [{kinds: [REPORT]}]),
$events => new Set($events.map(e => e.kind)),
)
+6 -14
View File
@@ -46,7 +46,6 @@ import {
APP_DATA,
isSignedEvent,
makeEvent,
displayProfile,
normalizeRelayUrl,
makeList,
addToListPublicly,
@@ -79,7 +78,6 @@ import {
session,
repository,
publishThunk,
profilesByPubkey,
tagEvent,
tagEventForReaction,
userRelayList,
@@ -90,7 +88,7 @@ import {
tagEventForQuote,
waitForThunkError,
getPubkeyRelays,
userBlossomServers,
userBlossomServerList,
shouldUnwrap,
} from "@welshman/app"
import {compressFile} from "@lib/html"
@@ -106,7 +104,6 @@ import {
userSpaceUrls,
userSettingsValues,
getSetting,
userMessagingRelays,
userGroupList,
shouldIgnoreError,
} from "@app/core/state"
@@ -122,13 +119,6 @@ export const getPubkeyHints = (pubkey: string) => {
return hints
}
export const getPubkeyPetname = (pubkey: string) => {
const profile = profilesByPubkey.get().get(pubkey)
const display = displayProfile(profile)
return display
}
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
if (parent) {
const nevent = nip19.neventEncode({
@@ -538,11 +528,13 @@ export const createDmAlert = async () => {
shouldUnwrap.set(true)
}
const $pubkey = pubkey.get()!
return createAlert({
description: `for direct messages.`,
feed: makeIntersectionFeed(
feedFromFilters([{kinds: [WRAP], "#p": [pubkey.get()!]}]),
makeRelayFeed(...get(userMessagingRelays)),
feedFromFilters([{kinds: [WRAP], "#p": [$pubkey]}]),
makeRelayFeed(...getPubkeyRelays($pubkey, RelayMode.Messaging)),
),
})
}
@@ -651,7 +643,7 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
}
}
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
const userUrls = getTagValues("server", getListTags(userBlossomServerList.get()))
for (const url of userUrls) {
return normalizeBlossomUrl(url)
+8 -3
View File
@@ -4,6 +4,7 @@ import {
int,
YEAR,
DAY,
assoc,
insertAt,
sortBy,
now,
@@ -32,7 +33,7 @@ import {load, request} from "@welshman/net"
import {repository, makeFeedController, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
import {NOTIFIER_RELAY, getEventsForUrl} from "@app/core/state"
import {NOTIFIER_RELAY} from "@app/core/state"
// Utils
@@ -47,7 +48,9 @@ export const makeFeed = ({
element: HTMLElement
onExhausted?: () => void
}) => {
const initialEvents = getEventsForUrl(url, filters)
const initialIds = Array.from(tracker.getIds(url))
const initialFilters = filters.map(assoc("ids", initialIds))
const initialEvents = repository.query(initialFilters)
const seen = new Set(initialEvents.map(e => e.id))
const controller = new AbortController()
const buffer = writable(initialEvents)
@@ -144,7 +147,9 @@ export const makeCalendarFeed = ({
}) => {
const interval = int(5, DAY)
const controller = new AbortController()
const initialEvents = getEventsForUrl(url, filters)
const initialIds = Array.from(tracker.getIds(url))
const initialFilters = filters.map(assoc("ids", initialIds))
const initialEvents = repository.query(initialFilters)
let exhaustedScrollers = 0
let backwardWindow = [now() - interval, now()]
+224 -276
View File
@@ -1,33 +1,35 @@
import twColors from "tailwindcss/colors"
import {Capacitor} from "@capacitor/core"
import {get, derived, writable} from "svelte/store"
import {get, derived, readable, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {
on,
gt,
max,
find,
spec,
call,
first,
assoc,
remove,
uniqBy,
sortBy,
append,
sort,
prop,
uniq,
indexBy,
partition,
pushToMapKey,
shuffle,
parseJson,
memoize,
addToMapKey,
identity,
groupBy,
always,
tryCatch,
fromPairs,
} from "@welshman/lib"
import type {Socket} from "@welshman/net"
import type {Override} from "@welshman/lib"
import type {RepositoryUpdate} from "@welshman/net"
import {
Pool,
load,
@@ -37,7 +39,17 @@ import {
SocketEvent,
netContext,
} from "@welshman/net"
import {collection, custom, throttled, deriveEvents, deriveEventsMapped} from "@welshman/store"
import {
getter,
throttled,
deriveArray,
makeDeriveEvent,
makeLoadItem,
makeDeriveItem,
deriveItemsByKey,
deriveEventsByIdByUrl,
deriveEventsByIdForUrl,
} from "@welshman/store"
import {isKindFeed, findFeed} from "@welshman/feeds"
import {
ALERT_ANDROID,
@@ -72,16 +84,14 @@ import {
ROOMS,
THREAD,
WRAP,
PROFILE,
ZAP_GOAL,
ZAP_REQUEST,
ZAP_RESPONSE,
asDecryptedEvent,
displayProfile,
getGroupTags,
getIdFilters,
getListTags,
getPubkeyTagValues,
getRelaysFromList,
getRelayTagValues,
getTagValue,
getTagValues,
@@ -89,47 +99,33 @@ import {
makeEvent,
normalizeRelayUrl,
readList,
RelayMode,
verifyEvent,
readRoomMeta,
makeRoomMeta,
ManagementMethod,
} from "@welshman/util"
import type {
TrustedEvent,
RelayProfile,
PublishedList,
PublishedRoomMeta,
List,
Filter,
} from "@welshman/util"
import type {TrustedEvent, RelayProfile, PublishedRoomMeta, List, Filter} from "@welshman/util"
import {decrypt} from "@welshman/signer"
import {routerContext, Router} from "@welshman/router"
import {
pubkey,
repository,
profilesByPubkey,
tracker,
makeTrackerStore,
makeRepositoryStore,
createSearch,
userFollows,
userFollowList,
ensurePlaintext,
thunks,
sign,
signer,
makeOutboxLoader,
appContext,
getThunkError,
publishThunk,
userRelayList,
userMessagingRelayList,
deriveRelay,
makeUserData,
makeUserLoader,
manageRelay,
displayProfileByPubkey,
} from "@welshman/app"
import type {Thunk} from "@welshman/app"
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
@@ -208,87 +204,28 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}`
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
entityLink(nip19.nprofileEncode({pubkey, relays}))
export const bootstrapPubkeys = derived(userFollows, $userFollows => {
export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
const appPubkeys = DEFAULT_PUBKEYS.split(",")
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollows)))
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList)))
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
})
export const trackerStore = makeTrackerStore()
export const repositoryStore = makeRepositoryStore()
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
let attempted = false
const filters = getIdFilters([idOrAddress])
const relays = [...hints, ...INDEXER_RELAYS]
return derived(
deriveEvents(repository, {filters, includeDeleted: true}),
(events: TrustedEvent[]) => {
if (!attempted && events.length === 0) {
load({relays, filters})
attempted = true
}
return events[0]
},
)
}
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
const getThunksByEventId = memoize(() => {
const thunksByEventId = new Map<string, Thunk[]>()
for (const thunk of $thunks) {
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
}
return thunksByEventId
})
return (id: string) => {
const urls = $tracker.getRelays(id)
for (const thunk of getThunksByEventId().get(id) || []) {
for (const url of thunk.options.relays) {
urls.add(url)
}
}
return Array.from(urls)
}
export const deriveEvent = makeDeriveEvent({
repository,
includeDeleted: true,
onDerive: (filters: Filter[], relays: string[]) => load({filters, relays}),
})
export const getEventsForUrl = (url: string, filters: Filter[]) => {
const ids = uniq([
...tracker.getIds(url),
...get(thunks)
.filter(t => t.options.relays.includes(url))
.map(t => t.event.id),
])
return repository.query(filters.map(assoc("ids", ids)))
}
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived([trackerStore, thunks], ([$tracker, $thunks]) => {
const ids = uniq([
...$tracker.getIds(url),
...$thunks.filter(t => t.options.relays.includes(url)).map(t => t.event.id),
])
deriveArray(deriveEventsByIdForUrl({url, tracker, repository, filters}))
return repository.query(filters.map(assoc("ids", ids)))
})
export const deriveSignedEventsForUrl = (url: string, filters: Filter[]) =>
export const deriveRelaySignedEvents = (url: string, filters: Filter[]) =>
derived(
[deriveEventsForUrl(url, filters), deriveRelay(url)],
([$events, $relay]) => $events,
// Disable this check for now since khatru doesn't support self
// $relay?.self ? $events.filter(spec({pubkey: $relay.self})) : [],
[deriveRelay(url), deriveEventsForUrl(url, filters)],
([relay, events]) => events,
// khatru doesn't support relay.self, uncomment when it's ready
// filter(spec({pubkey: relay.self}), events)
)
// Context
@@ -351,25 +288,25 @@ export const defaultSettings = {
show_notifications_badge: true,
}
export const settings = deriveEventsMapped<Settings>(repository, {
export const settingsByPubkey = deriveItemsByKey({
repository,
getKey: settings => settings.event.pubkey,
filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
itemToEvent: item => item.event,
eventToItem: async (event: TrustedEvent) => ({
event,
values: {...defaultSettings, ...parseJson(await ensurePlaintext(event))},
}),
eventToItem: async (event: TrustedEvent) => {
const values = {...defaultSettings, ...parseJson(await ensurePlaintext(event))}
return {event, values}
},
})
export const {
indexStore: settingsByPubkey,
deriveItem: deriveSettings,
loadItem: loadSettings,
} = collection({
name: "settings",
store: settings,
getKey: settings => settings.event.pubkey,
load: makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
})
export const getSettingsByPubkey = getter(settingsByPubkey)
export const getSettings = (pubkey: string) => getSettingsByPubkey().get(pubkey)
export const loadSettings = makeLoadItem(
makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
getSettings,
)
export const userSettings = makeUserData({
mapStore: settingsByPubkey,
@@ -397,9 +334,10 @@ export type Alert = {
tags: string[][]
}
export const alerts = deriveEventsMapped<Alert>(repository, {
export const alertsById = deriveItemsByKey<Alert>({
repository,
getKey: alert => alert.event.id,
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const $signer = signer.get()
@@ -414,13 +352,13 @@ export const alerts = deriveEventsMapped<Alert>(repository, {
export const getAlertFeed = (alert: Alert) =>
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
export const dmAlert = derived(alerts, $alerts =>
$alerts.find(alert => {
const feed = getAlertFeed(alert)
return findFeed(feed, f => isKindFeed(f) && f.includes(WRAP))
}),
)
export const dmAlert = derived(alertsById, $alertsById => {
for (const alert of $alertsById.values()) {
if (findFeed(getAlertFeed(alert), f => isKindFeed(f) && f.includes(WRAP))) {
return alert
}
}
})
// Alert Statuses
@@ -429,9 +367,10 @@ export type AlertStatus = {
tags: string[][]
}
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
export const alertStatusesByAddress = deriveItemsByKey<AlertStatus>({
repository,
filters: [{kinds: [ALERT_STATUS]}],
itemToEvent: item => item.event,
getKey: alertStatus => getTagValue("d", alertStatus.event.tags)!,
eventToItem: async event => {
const $signer = signer.get()
@@ -443,15 +382,10 @@ export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
},
})
export const deriveAlertStatus = (address: string) =>
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
export const deriveAlertStatus = makeDeriveItem(alertStatusesByAddress)
// Chats
export const chatMessages = deriveEvents(repository, {
filters: [{kinds: [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]}],
})
export type Chat = {
id: string
pubkeys: string[]
@@ -460,66 +394,77 @@ export type Chat = {
search_text: string
}
export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys.concat(pubkey.get()!))).join(",")
export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys)).join(",")
export const splitChatId = (id: string) => id.split(",")
export const chats = derived(
[pubkey, chatMessages, profilesByPubkey],
([$pubkey, $messages, $profilesByPubkey]) => {
const messagesByChatId = new Map<string, TrustedEvent[]>()
export const chatsById = call(() => {
const chatsById = new Map<string, Chat>()
const chatsByPubkey = new Map<string, Chat[]>()
for (const message of $messages) {
const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
chat.search_text =
chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
: chat.pubkeys.map(displayProfileByPubkey).join(" ")
pushToMapKey(messagesByChatId, chatId, message)
}
return chat as Chat
}
const displayPubkey = (pubkey: string) => {
const profile = $profilesByPubkey.get(pubkey)
return readable(chatsById, set => {
const unsubscribers = [
on(repository, "update", ({added}: RepositoryUpdate) => {
let dirty = false
for (const event of added) {
if ([DIRECT_MESSAGE, DIRECT_MESSAGE_FILE].includes(event.kind)) {
const pubkeys = getPubkeyTagValues(event.tags).concat(event.pubkey)
const id = makeChatId(pubkeys)
const chat = chatsById.get(id)
const messages = append(event, chat?.messages || [])
const last_activity = Math.max(chat?.last_activity || 0, event.created_at)
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
return profile ? displayProfile(profile) : ""
}
chatsById.set(id, updatedChat)
return sortBy(
c => -c.last_activity,
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
const pubkeys = remove($pubkey!, splitChatId(id))
const messages = sortBy(e => -e.created_at, uniqBy(prop("id"), events))
const last_activity = messages[0].created_at
const search_text =
pubkeys.length === 0
? displayPubkey($pubkey!) + " note to self"
: pubkeys.map(displayPubkey).join(" ")
for (const pubkey of pubkeys) {
const pubkeyChats = chatsByPubkey.get(pubkey) || []
const uniqueChats = uniqBy(chat => chat.id, append(updatedChat, pubkeyChats))
return {id, pubkeys, messages, last_activity, search_text}
chatsByPubkey.set(pubkey, uniqueChats)
}
dirty = true
}
if (event.kind === PROFILE) {
for (const chat of chatsByPubkey.get(event.pubkey) || []) {
addSearchText(chat)
dirty = true
}
}
}
if (dirty) {
set(chatsById)
}
}),
)
},
)
]
export const {
indexStore: chatsById,
deriveItem: deriveChat,
loadItem: loadChat,
} = collection({
name: "chats",
store: chats,
getKey: chat => chat.id,
load: always(Promise.resolve()),
return () => unsubscribers.forEach(call)
})
})
export const chatSearch = derived(chats, $chats =>
createSearch($chats, {
export const deriveChat = makeDeriveItem(chatsById)
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
return createSearch(Array.from($chatsByPubkey.values()), {
getValue: (chat: Chat) => chat.id,
fuseOptions: {keys: ["search_text"]},
}),
)
})
})
// Rooms
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
export type Room = PublishedRoomMeta & {
id: string
url: string
@@ -532,95 +477,94 @@ export const splitRoomId = (id: string) => id.split("'")
export const hasNip29 = (relay?: RelayProfile) =>
relay?.supported_nips?.map?.(String)?.includes?.("29")
export const roomMetas = deriveEventsMapped<PublishedRoomMeta>(repository, {
filters: [{kinds: [ROOM_META]}],
itemToEvent: item => item.event,
eventToItem: readRoomMeta,
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
tracker,
repository,
filters: [{kinds: [ROOM_META, ROOM_DELETE]}],
})
export const roomDeletes = deriveEvents(repository, {
filters: [{kinds: [ROOM_DELETE]}],
})
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
const result = new Map<string, Room[]>()
export const rooms = derived(
[roomMetas, roomDeletes, getUrlsForEvent],
([$roomMetas, $roomDeletes, $getUrlsForEvent]) => {
const result = new Map<string, Room>()
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
const deletedByH = new Map<string, number>()
for (const event of $roomDeletes) {
for (const event of deleteEvents) {
for (const h of getTagValues("h", event.tags)) {
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
}
}
for (const meta of $roomMetas) {
for (const event of metaEvents) {
const meta = readRoomMeta(event)
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
continue
}
for (const url of $getUrlsForEvent(meta.event.id)) {
const id = makeRoomId(url, meta.h)
result.set(id, {...meta, url, id})
}
pushToMapKey(result, url, {...meta, url, id: makeRoomId(url, meta.h)})
}
}
return Array.from(result.values())
},
return result
})
export const roomsById = derived(roomsByUrl, roomsByUrl =>
indexBy(room => room.id, Array.from(roomsByUrl.values()).flatMap(identity)),
)
export const roomsByUrl = derived(rooms, $rooms => groupBy(c => c.url, $rooms))
export const getRoomsById = getter(roomsById)
export const {
indexStore: roomsById,
deriveItem: _deriveRoom,
loadItem: _loadRoom,
} = collection({
name: "rooms",
store: rooms,
getKey: room => room.id,
load: async (id: string) => {
export const getRoom = (id: string) => getRoomsById().get(id)
export const loadRoom = call(() => {
const _fetchRoom = async (id: string) => {
const [url, h] = splitRoomId(id)
await load({
relays: [url],
filters: [{kinds: [ROOM_META], "#d": [h]}],
})
},
}
const _loadRoom = makeLoadItem(_fetchRoom, getRoom)
return (url: string, h: string) => _loadRoom(makeRoomId(url, h))
})
export const deriveRoom = (url: string, h: string) =>
derived(_deriveRoom(makeRoomId(url, h)), $meta => $meta || makeRoomMeta({h}))
export const deriveRoom = call(() => {
const _deriveRoom = makeDeriveItem(roomsById, loadRoom)
export const displayRoom = (url: string, h: string) =>
roomsById.get().get(makeRoomId(url, h))?.name || h
return (url: string, h: string) =>
derived(_deriveRoom(makeRoomId(url, h)), room => room || makeRoomMeta({h}))
})
export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h))?.name || h
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
// User space/room lists
export const groupLists = deriveEventsMapped<PublishedList>(repository, {
export const groupListsByPubkey = deriveItemsByKey({
repository,
filters: [{kinds: [ROOMS]}],
itemToEvent: item => item.event,
getKey: list => list.event.pubkey,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
})
export const {
indexStore: groupListsByPubkey,
deriveItem: deriveGroupList,
loadItem: loadGroupList,
} = collection({
name: "groupLists",
store: groupLists,
getKey: list => list.event.pubkey,
load: makeOutboxLoader(ROOMS),
})
export const getGroupListsByPubkey = getter(groupListsByPubkey)
export const groupListsPubkeysByUrl = derived(groupLists, $groupLists => {
export const getGroupList = (pubkey: string) => getGroupListsByPubkey().get(pubkey)
export const loadGroupList = makeLoadItem(makeOutboxLoader(ROOMS), getGroupList)
export const deriveGroupList = makeDeriveItem(groupListsByPubkey, loadGroupList)
export const groupListsPubkeysByUrl = derived(groupListsByPubkey, $groupListsByPubkey => {
const result = new Map<string, Set<string>>()
for (const list of $groupLists) {
for (const list of $groupListsByPubkey.values()) {
const tags = getListTags(list)
for (const url of getRelayTagValues(tags)) {
@@ -639,8 +583,8 @@ export const groupListsPubkeysByUrl = derived(groupLists, $groupLists => {
return result
})
export const getSpaceUrlsFromGroupList = ($groupLists: List | undefined) => {
const tags = getListTags($groupLists)
export const getSpaceUrlsFromGroupList = (groupList: List | undefined) => {
const tags = getListTags(groupList)
const urls = getRelayTagValues(tags)
for (const tag of getGroupTags(tags)) {
@@ -654,10 +598,10 @@ export const getSpaceUrlsFromGroupList = ($groupLists: List | undefined) => {
return uniq(urls.map(normalizeRelayUrl))
}
export const getSpaceRoomsFromGroupList = (url: string, $groupList: List | undefined) => {
export const getSpaceRoomsFromGroupList = (url: string, groupList: List | undefined) => {
const rooms: string[] = []
for (const [_, h, relay] of getGroupTags(getListTags($groupList))) {
for (const [_, h, relay] of getGroupTags(getListTags(groupList))) {
if (url === relay) {
rooms.push(h)
}
@@ -673,7 +617,7 @@ export const userGroupList = makeUserData({
export const loadUserGroupList = makeUserLoader(loadGroupList)
export const userSpaceUrls = derived(userGroupList, getSpaceUrlsFromGroupLists)
export const userSpaceUrls = derived(userGroupList, getSpaceUrlsFromGroupList)
export const deriveUserRooms = (url: string) =>
derived([userGroupList, roomsById], ([$userGroupList, $roomsById]) => {
@@ -705,9 +649,7 @@ export const deriveOtherRooms = (url: string) =>
export const deriveSpaceMembers = (url: string) =>
derived(
deriveSignedEventsForUrl(url, [
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]},
]),
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
$events => {
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
@@ -755,43 +697,45 @@ export const deriveSpaceBannedPubkeyItems = (url: string) => {
return store
}
export const deriveRoomMembers = (url: string, h: string) =>
derived(
deriveEventsForUrl(url, [
{kinds: [ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]),
$events => {
const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
export const deriveRoomMembers = (url: string, h: string) => {
const filters: Filter[] = [
{kinds: [ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]
if (membersEvent) {
return uniq(getPubkeyTagValues(membersEvent.tags))
}
return derived(deriveEventsForUrl(url, filters), $events => {
const membersEvent = find(spec({kind: ROOM_MEMBERS}), $events)
const members = new Set<string>()
if (membersEvent) {
return uniq(getPubkeyTagValues(membersEvent.tags))
}
for (const event of sortBy(e => -e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
const members = new Set<string>()
if (event.kind === ROOM_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
for (const event of sortBy(e => -e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === ROOM_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
if (event.kind === ROOM_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
return Array.from(members)
},
)
if (event.kind === ROOM_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
export const deriveRoomAdmins = (url: string, h: string) =>
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), $events => {
return Array.from(members)
})
}
export const deriveRoomAdmins = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
return derived(deriveEventsForUrl(url, filters), $events => {
const adminsEvent = first($events)
if (adminsEvent) {
@@ -800,6 +744,7 @@ export const deriveRoomAdmins = (url: string, h: string) =>
return []
})
}
// User membership status
@@ -821,12 +766,14 @@ export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
return store
})
export const deriveUserSpaceMembershipStatus = (url: string) =>
derived(
export const deriveUserSpaceMembershipStatus = (url: string) => {
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
return derived(
[
pubkey,
deriveSpaceMembers(url),
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
deriveEventsForUrl(url, filters),
deriveUserIsSpaceAdmin(url),
],
([$pubkey, $members, $events, $isAdmin]) => {
@@ -849,6 +796,7 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
},
)
}
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived(
@@ -856,12 +804,14 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
)
export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
derived(
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
return derived(
[
pubkey,
deriveRoomMembers(url, h),
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
deriveEventsForUrl(url, filters),
deriveUserIsRoomAdmin(url, h),
],
([$pubkey, $members, $events, $isAdmin]) => {
@@ -884,14 +834,13 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
},
)
}
export const deriveUserCanCreateRoom = (url: string) =>
derived(
[
pubkey,
deriveEventsForUrl(url, [{kinds: [ROOM_CREATE_PERMISSION]}]),
deriveUserIsSpaceAdmin(url),
],
export const deriveUserCanCreateRoom = (url: string) => {
const filters: Filter[] = [{kinds: [ROOM_CREATE_PERMISSION]}]
return derived(
[pubkey, deriveEventsForUrl(url, filters), deriveUserIsSpaceAdmin(url)],
([$pubkey, $events, $isAdmin]) => {
for (const event of $events) {
if (getPubkeyTagValues(event.tags).includes($pubkey!)) {
@@ -902,6 +851,7 @@ export const deriveUserCanCreateRoom = (url: string) =>
return $isAdmin
},
)
}
// Other utils
@@ -920,13 +870,10 @@ export const displayReaction = (content: string) => {
return content
}
export const deriveSocket = (url: string) =>
custom<Socket>(set => {
const pool = Pool.get()
const socket = pool.get(url)
set(socket)
export const deriveSocket = (url: string) => {
const socket = Pool.get().get(url)
return readable(socket, set => {
const subs = [
on(socket, SocketEvent.Error, () => set(socket)),
on(socket, SocketEvent.Status, () => set(socket)),
@@ -935,6 +882,7 @@ export const deriveSocket = (url: string) =>
return () => subs.forEach(call)
})
}
export const deriveSocketStatus = (url: string) =>
throttled(
+12 -13
View File
@@ -24,15 +24,16 @@ import {request, load, pull} from "@welshman/net"
import {
pubkey,
loadRelay,
userFollows,
userFollowList,
userRelayList,
userMessagingRelayList,
loadRelayList,
loadMessagingRelayList,
loadBlossomServers,
loadFollows,
loadMutes,
loadBlossomServerList,
loadFollowList,
loadMuteList,
loadProfile,
tracker,
repository,
shouldUnwrap,
hasNegentropy,
@@ -48,7 +49,6 @@ import {
userGroupList,
bootstrapPubkeys,
decodeRelay,
getUrlsForEvent,
getSpaceUrlsFromGroupList,
getSpaceRoomsFromGroupList,
makeCommentFilter,
@@ -65,7 +65,6 @@ type PullOpts = {
}
const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
const $getUrlsForEvent = get(getUrlsForEvent)
const [smart, dumb] = partition(hasNegentropy, relays)
const events = repository.query(filters, {shouldSort: false}).filter(isSignedEvent)
const promises: Promise<TrustedEvent[]>[] = [pull({relays: smart, filters, signal, events})]
@@ -73,7 +72,7 @@ const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
// Since pulling from relays without negentropy is expensive, limit how many
// duplicates we repeatedly download
for (const url of dumb) {
const urlEvents = events.filter(e => $getUrlsForEvent(e.id).includes(url))
const urlEvents = events.filter(e => tracker.getRelays(e.id).has(url))
if (urlEvents.length >= 100) {
filters = filters.map(assoc("since", sortBy(e => -e.created_at, urlEvents)[10]!.created_at))
@@ -212,16 +211,16 @@ const syncUserData = () => {
if ($pubkey) {
loadAlerts($pubkey)
loadAlertStatuses($pubkey)
loadBlossomServers($pubkey)
loadFollows($pubkey)
loadBlossomServerList($pubkey)
loadFollowList($pubkey)
loadGroupList($pubkey)
loadMutes($pubkey)
loadMuteList($pubkey)
loadProfile($pubkey)
loadSettings($pubkey)
}
})
const unsubscribeFollows = userFollows.subscribe(async $l => {
const unsubscribeFollows = userFollowList.subscribe(async $l => {
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
@@ -231,8 +230,8 @@ const syncUserData = () => {
await loadRelayList(pk)
await loadGroupList(pk)
await loadProfile(pk)
await loadFollows(pk)
await loadMutes(pk)
await loadFollowList(pk)
await loadMuteList(pk)
}),
)
}
+158 -140
View File
@@ -1,9 +1,10 @@
import {derived, get} from "svelte/store"
import {Badge} from "@capawesome/capacitor-badge"
import {synced, throttled} from "@welshman/store"
import {pubkey, relaysByUrl} from "@welshman/app"
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
import {prop, find, call, spec, first, identity, now, groupBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsByIdByUrl} from "@welshman/store"
import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
import {
makeSpacePath,
@@ -15,10 +16,8 @@ import {
makeRoomPath,
} from "@app/util/routes"
import {
chats,
chatsById,
hasNip29,
getUrlsForEvent,
repositoryStore,
userSettingsValues,
userGroupList,
getSpaceUrlsFromGroupList,
@@ -40,151 +39,170 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [
// Derived notifications state
export const notifications = derived(
throttled(
1000,
derived(
[pubkey, checked, chats, userGroupList, repositoryStore, getUrlsForEvent, relaysByUrl],
identity,
export const notifications = call(() => {
const goalCommentFilters = [{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}]
const threadCommentFilters = [{kinds: [COMMENT], "#K": [String(THREAD)]}]
const calendarCommentFilters = [{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}]
const messageFilters = [{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}]
return derived(
throttled(
1000,
derived(
[
pubkey,
checked,
chatsById,
userGroupList,
relaysByUrl,
deriveEventsByIdByUrl({tracker, repository, filters: goalCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: threadCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: calendarCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: messageFilters}),
],
identity,
),
),
),
([$pubkey, $checked, $chats, $userGroupList, $repository, $getUrlsForEvent, $relaysByUrl]) => {
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
if (!latestEvent || latestEvent.pubkey === $pubkey) {
return false
}
for (const [entryPath, ts] of Object.entries($checked)) {
const isMatch =
entryPath === "*" ||
entryPath.startsWith(path) ||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
if (isMatch && ts > latestEvent.created_at) {
([
$pubkey,
$checked,
$chatsById,
$userGroupList,
$relaysByUrl,
goalCommentsByUrl,
threadCommentsByUrl,
calendarCommentsByUrl,
messagesByUrl,
]) => {
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
if (!latestEvent || latestEvent.pubkey === $pubkey) {
return false
}
}
return true
}
for (const [entryPath, ts] of Object.entries($checked)) {
const isMatch =
entryPath === "*" ||
entryPath.startsWith(path) ||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
const paths = new Set<string>()
for (const {pubkeys, messages} of $chats) {
const chatPath = makeChatPath(pubkeys)
if (hasNotification(chatPath, messages[0])) {
paths.add("/chat")
paths.add(chatPath)
}
}
const allGoalComments = $repository.query([{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}])
const allThreadComments = $repository.query([{kinds: [COMMENT], "#K": [String(THREAD)]}])
const allCalendarComments = $repository.query([{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}])
const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}])
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
const spacePath = makeSpacePath(url)
const spacePathMobile = spacePath + ":mobile"
const goalPath = makeGoalPath(url)
const threadPath = makeThreadPath(url)
const calendarPath = makeCalendarPath(url)
const messagesPath = makeSpaceChatPath(url)
const goalComments = allGoalComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const threadComments = allThreadComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const calendarComments = allCalendarComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const messages = allMessages.filter(e => $getUrlsForEvent(e.id).includes(url))
const commentsByGoalId = groupBy(
e => getTagValue("E", e.tags),
goalComments.filter(spec({kind: COMMENT})),
)
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
const goalItemPath = makeGoalPath(url, goalId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(goalPath, comment)) {
paths.add(goalPath)
}
if (hasNotification(goalItemPath, comment)) {
paths.add(goalItemPath)
}
}
const commentsByThreadId = groupBy(
e => getTagValue("E", e.tags),
threadComments.filter(spec({kind: COMMENT})),
)
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
const threadItemPath = makeThreadPath(url, threadId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(threadPath, comment)) {
paths.add(threadPath)
}
if (hasNotification(threadItemPath, comment)) {
paths.add(threadItemPath)
}
}
const commentsByEventId = groupBy(
e => getTagValue("E", e.tags),
calendarComments.filter(spec({kind: COMMENT})),
)
for (const [eventId, [comment]] of commentsByEventId.entries()) {
const calendarItemPath = makeCalendarPath(url, eventId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(calendarPath, comment)) {
paths.add(calendarPath)
}
if (hasNotification(calendarItemPath, comment)) {
paths.add(calendarItemPath)
}
}
if (hasNip29($relaysByUrl.get(url))) {
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
const roomPath = makeRoomPath(url, h)
const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
if (hasNotification(roomPath, latestEvent)) {
paths.add(spacePathMobile)
paths.add(spacePath)
paths.add(roomPath)
if (isMatch && ts > latestEvent.created_at) {
return false
}
}
} else {
if (hasNotification(messagesPath, messages[0])) {
paths.add(spacePathMobile)
paths.add(spacePath)
paths.add(messagesPath)
return true
}
const paths = new Set<string>()
for (const {pubkeys, messages} of $chatsById.values()) {
const chatPath = makeChatPath(pubkeys)
if (hasNotification(chatPath, messages[0])) {
paths.add("/chat")
paths.add(chatPath)
}
}
}
return paths
},
)
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
const spacePath = makeSpacePath(url)
const spacePathMobile = spacePath + ":mobile"
const goalPath = makeGoalPath(url)
const threadPath = makeThreadPath(url)
const calendarPath = makeCalendarPath(url)
const messagesPath = makeSpaceChatPath(url)
const goalComments = goalCommentsByUrl.get(url)?.values() || []
const threadComments = threadCommentsByUrl.get(url)?.values() || []
const calendarComments = calendarCommentsByUrl.get(url)?.values() || []
const messages = messagesByUrl.get(url)?.values() || []
const commentsByGoalId = groupBy(
e => getTagValue("E", e.tags),
goalComments.filter(spec({kind: COMMENT})),
)
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
const goalItemPath = makeGoalPath(url, goalId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(goalPath, comment)) {
paths.add(goalPath)
}
if (hasNotification(goalItemPath, comment)) {
paths.add(goalItemPath)
}
}
const commentsByThreadId = groupBy(
e => getTagValue("E", e.tags),
threadComments.filter(spec({kind: COMMENT})),
)
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
const threadItemPath = makeThreadPath(url, threadId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(threadPath, comment)) {
paths.add(threadPath)
}
if (hasNotification(threadItemPath, comment)) {
paths.add(threadItemPath)
}
}
const commentsByEventId = groupBy(
e => getTagValue("E", e.tags),
calendarComments.filter(spec({kind: COMMENT})),
)
for (const [eventId, [comment]] of commentsByEventId.entries()) {
const calendarItemPath = makeCalendarPath(url, eventId)
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
if (hasNotification(calendarPath, comment)) {
paths.add(calendarPath)
}
if (hasNotification(calendarItemPath, comment)) {
paths.add(calendarItemPath)
}
}
if (hasNip29($relaysByUrl.get(url))) {
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
const roomPath = makeRoomPath(url, h)
const latestEvent = find(e => e.tags.some(spec(["h", h])), messages)
if (hasNotification(roomPath, latestEvent)) {
paths.add(spacePathMobile)
paths.add(spacePath)
paths.add(roomPath)
}
}
} else {
if (hasNotification(messagesPath, first(messages))) {
paths.add(spacePathMobile)
paths.add(spacePath)
paths.add(messagesPath)
}
}
}
return paths
},
)
})
export const badgeCount = derived(notifications, notifications => {
return notifications.size
+15 -31
View File
@@ -1,5 +1,5 @@
import {on, throttle, fromPairs, batch} from "@welshman/lib"
import {throttled, freshness} from "@welshman/store"
import {on, throttle, indexBy, fromPairs, batch} from "@welshman/lib"
import {throttled} from "@welshman/store"
import {
ALERT_ANDROID,
ALERT_EMAIL,
@@ -38,13 +38,14 @@ import type {Zapper, TrustedEvent, RelayProfile} from "@welshman/util"
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
import type {Handle, RelayStats} from "@welshman/app"
import {
plaintext,
tracker,
relays,
relayStats,
plaintext,
repository,
handles,
zappers,
relaysByUrl,
relayStatsByUrl,
onRelayStats,
handlesByNip05,
zappersByLnurl,
onZapper,
onHandle,
wrapManager,
@@ -185,9 +186,9 @@ const relaysAdapter = {
name: "relays",
keyPath: "url",
init: async (table: IDBTable<RelayProfile>) => {
relays.set(await table.getAll())
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelay(batch(3000, table.bulkPut))
return onRelay(batch(1000, table.bulkPut))
},
}
@@ -195,9 +196,9 @@ const relayStatsAdapter = {
name: "relayStats",
keyPath: "url",
init: async (table: IDBTable<RelayStats>) => {
relayStats.set(await table.getAll())
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return throttled(3000, relayStats).subscribe(table.bulkPut)
return onRelayStats(batch(1000, table.bulkPut))
},
}
@@ -205,9 +206,9 @@ const handlesAdapter = {
name: "handles",
keyPath: "nip05",
init: async (table: IDBTable<Handle>) => {
handles.set(await table.getAll())
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(3000, table.bulkPut))
return onHandle(batch(1000, table.bulkPut))
},
}
@@ -215,28 +216,12 @@ const zappersAdapter = {
name: "zappers",
keyPath: "lnurl",
init: async (table: IDBTable<Zapper>) => {
zappers.set(await table.getAll())
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(3000, table.bulkPut))
},
}
type FreshnessItem = {key: string; value: number}
const freshnessAdapter = {
name: "freshness",
keyPath: "key",
init: async (table: IDBTable<FreshnessItem>) => {
const initialRecords = await table.getAll()
freshness.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
return throttled(3000, freshness).subscribe($freshness => {
table.bulkPut(Object.entries($freshness).map(([key, value]) => ({key, value})))
})
},
}
type PlaintextItem = {key: string; value: string}
const plaintextAdapter = {
@@ -280,7 +265,6 @@ export const adapters = [
relayStatsAdapter,
handlesAdapter,
zappersAdapter,
freshnessAdapter,
plaintextAdapter,
wrapManagerAdapter,
]
@@ -28,7 +28,7 @@
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const event = deriveEvent(id, [url])
const filters = [{kinds: [COMMENT], "#E": [id]}]
const replies = deriveEventsDesc(deriveEventsById({filters, repository}))
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import {sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
@@ -27,10 +27,10 @@
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const event = deriveEvent(id, [url])
const filters = [{kinds: [COMMENT], "#E": [id]}]
const replies = deriveEventsDesc(deriveEventsById({repository, filters}))
const summary = getTagValue("summary", $event.tags)
const summary = getTagValue("summary", $event?.tags || [])
const back = () => history.back()
@@ -71,7 +71,7 @@
</div>
{/snippet}
{#snippet title()}
<h1 class="text-xl">{$event.content}</h1>
<h1 class="text-xl">{$event?.content}</h1>
{/snippet}
{#snippet action()}
<div>
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import {sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
@@ -26,7 +26,7 @@
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const event = deriveEvent(id, [url])
const filters = [{kinds: [COMMENT], "#E": [id]}]
const replies = deriveEventsDesc(deriveEventsById({filters, repository}))