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