Fix some bugs

This commit is contained in:
Jonathan Staab 2023-09-13 10:36:08 -07:00
parent 2869bf23fd
commit 4d5cddbc4e
50 changed files with 425 additions and 254 deletions

View File

@ -119,19 +119,17 @@
const profileOptions = peopleWithName.derived($people =>
$people
.filter(person => person.pubkey !== $session.pubkey)
.filter(person => person.pubkey !== $session?.pubkey)
.map(person => {
const {
pubkey,
profile: {name, display_name},
handle: {address},
} = person
const {pubkey, profile, handle} = person
return {
person,
id: pubkey,
type: "profile",
text: "@" + [name, address, display_name].filter(identity).join(" "),
text:
"@" +
[profile?.name, handle?.address, profile?.display_name].filter(identity).join(" "),
}
})
)

View File

@ -13,8 +13,14 @@
import RelayFeed from "src/app/shared/RelayFeed.svelte"
import Note from "src/app/shared/Note.svelte"
import type {DynamicFilter} from "src/engine2"
import {session, getSetting, searchableRelays, mergeHints, getPubkeyHints} from "src/engine2"
import {compileFilter} from "src/app/state"
import {
session,
compileFilter,
getSetting,
searchableRelays,
mergeHints,
getPubkeyHints,
} from "src/engine2"
export let relays = []
export let filter = {} as DynamicFilter
@ -56,7 +62,7 @@
}
const limit = getSetting("relay_limit")
const authors = (compileFilter(filter).authors || []).concat($session.pubkey)
const authors = (compileFilter(filter).authors || []).concat($session?.pubkey)
const hints = authors.map(pubkey => getPubkeyHints(limit, pubkey, "write"))
return mergeHints(limit, hints)

View File

@ -4,7 +4,7 @@
import {tweened} from "svelte/motion"
import {find, reject, identity, propEq, sum, pluck, sortBy} from "ramda"
import {stringToHue, formatSats, hsl} from "src/util/misc"
import {isLike, toNostrURI} from "src/util/nostr"
import {isLike, fromDisplayEvent, toNostrURI} from "src/util/nostr"
import {quantify} from "hurdak"
import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte"
@ -54,8 +54,10 @@
const muteNote = () => mute("e", note.id)
const react = content => {
like = publishReaction(note, content).event
const react = async content => {
const pub = await publishReaction(note, content)
like = pub.event
}
const deleteReaction = e => {
@ -71,8 +73,9 @@
const broadcast = () => {
const relays = getUserRelayUrls("write")
const event = fromDisplayEvent(note)
Publisher.publish({event: note, relays}).on("progress", toastProgress)
Publisher.publish({event, relays}).on("progress", toastProgress)
}
let like, likes, allLikes, zap, zaps
@ -81,12 +84,12 @@
$: disableActions = !$canSign || muted
$: likes = note.reactions.filter(n => isLike(n.content))
$: like = like || find(propEq("pubkey", $session.pubkey), likes)
$: like = like || find(propEq("pubkey", $session?.pubkey), likes)
$: allLikes = like ? likes.filter(n => n.id !== like?.id).concat(like) : likes
$: $likesCount = allLikes.length
$: zaps = processZaps(note.zaps, note.pubkey)
$: zap = zap || find((z: ZapEvent) => z.request.pubkey === $session.pubkey, zaps)
$: zap = zap || find((z: ZapEvent) => z.request.pubkey === $session?.pubkey, zaps)
$: $zapsTotal =
sum(
@ -97,7 +100,7 @@
)
) / 1000
$: canZap = $person?.zapper && note.pubkey !== $session.pubkey
$: canZap = $person?.zapper && note.pubkey !== $session?.pubkey
$: $repliesCount = note.replies.length
$: {
@ -139,7 +142,7 @@
</button>
<button
class={cx("relative w-16 pt-1 text-left transition-all hover:pb-1 hover:pt-0", {
"pointer-events-none opacity-50": disableActions || note.pubkey === $session.pubkey,
"pointer-events-none opacity-50": disableActions || note.pubkey === $session?.pubkey,
"text-accent": like,
})}
on:click={() => (like ? deleteReaction(like) : react("+"))}>

View File

@ -42,14 +42,16 @@
data.mentions = without([pubkey], data.mentions)
}
const getContent = () => (reply.parse() + "\n" + data.image).trim()
const getContent = () => (reply.parse() + "\n" + (data.image || "")).trim()
const send = async () => {
const content = getContent()
const tags = data.mentions.map(mention)
if (content) {
publishReply(parent, content, tags).on("progress", toastProgress)
const pub = await publishReply(parent, content, tags)
pub.on("progress", toastProgress)
reset()
}

View File

@ -1,15 +1,14 @@
import type {Filter} from "nostr-tools"
import Bugsnag from "@bugsnag/js"
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {writable} from "svelte/store"
import {omit, path, filter, pluck, sortBy, slice} from "ramda"
import {hash, union, sleep, doPipe, shuffle} from "hurdak"
import {path, filter, pluck, sortBy, slice} from "ramda"
import {hash, union, sleep, doPipe} from "hurdak"
import {warn} from "src/util/logger"
import {now} from "src/util/misc"
import {userKinds, noteKinds} from "src/util/nostr"
import {modal, toast} from "src/partials/state"
import type {DynamicFilter, Event} from "src/engine2"
import type {Event} from "src/engine2"
import {
env,
pool,
@ -18,7 +17,6 @@ import {
loadPubkeys,
channels,
follows,
network,
subscribe,
getUserRelayUrls,
getSetting,
@ -154,7 +152,7 @@ export const loadAppData = async () => {
const {pubkey} = session.get()
// Make sure the user and their follows are loaded
await loadPubkeys(pubkey, {force: true, kinds: userKinds})
await loadPubkeys([pubkey], {force: true, kinds: userKinds})
// Load deletes
loadDeletes()
@ -221,20 +219,3 @@ export const toastProgress = progress => {
toast.show("info", payload, pending.size ? null : 8)
}
// Feeds
export const getAuthorsWithDefaults = (pubkeys: string[]) =>
shuffle(pubkeys.length > 0 ? pubkeys : (env.get().DEFAULT_FOLLOWS as string[])).slice(0, 1024)
export const compileFilter = (filter: DynamicFilter): Filter => {
if (filter.authors === "global") {
filter = omit(["authors"], filter)
} else if (filter.authors === "follows") {
filter = {...filter, authors: getAuthorsWithDefaults(follows.get())}
} else if (filter.authors === "network") {
filter = {...filter, authors: getAuthorsWithDefaults(network.get())}
}
return filter as Filter
}

View File

@ -14,8 +14,14 @@
import Heading from "src/partials/Heading.svelte"
import ImageCircle from "src/partials/ImageCircle.svelte"
import type {Person, Event} from "src/engine2"
import {getUserRelayUrls, loadPubkeys, load, displayHandle, derivePerson} from "src/engine2"
import {compileFilter} from "src/app/state"
import {
getUserRelayUrls,
compileFilter,
loadPubkeys,
load,
displayHandle,
derivePerson,
} from "src/engine2"
const getColumns = xs => {
const cols = [[], []]

View File

@ -34,7 +34,11 @@
const edit = () => modal.push({type: "chat/edit", channel: $channel})
const sendMessage = content => publishNip28Message(id, content).result
const sendMessage = async content => {
const pub = await publishNip28Message(id, content)
return pub.result
}
onMount(() => {
const sub = listenForNip28Messages(id)
@ -69,7 +73,7 @@
<div class="flex h-12 flex-col pt-px">
<div class="flex w-full items-center justify-between">
<div class="flex gap-2">
{#if $channel?.nip28?.owner === $session.pubkey}
{#if $channel?.nip28?.owner === $session?.pubkey}
<button class="cursor-pointer text-sm" on:click={edit}>
<i class="fa-solid fa-edit" /> Edit
</button>

View File

@ -20,11 +20,11 @@
const {id, ...content} = channel
if (id) {
const pub = publishNip28ChannelUpdate(id, content)
const pub = await publishNip28ChannelUpdate(id, content)
pub.on("progress", toastProgress)
} else {
const pub = publishNip28ChannelCreate(content)
const pub = await publishNip28ChannelCreate(content)
joinNip28Channel(pub.event.id)
}

View File

@ -22,8 +22,8 @@
sortChannels,
nip28ChannelsWithMeta,
loadPubkeys,
getPubkeysWithDefaults,
} from "src/engine2"
import {getAuthorsWithDefaults} from "src/app/state"
let q = ""
let results = []
@ -54,46 +54,42 @@
document.title = "Chat"
onMount(() => {
const subs = []
const relays = getPubkeyHints(3, $stateKey, "read")
const authors = getAuthorsWithDefaults($follows)
const authors = getPubkeysWithDefaults($follows)
const since = now() - seconds(1, "day")
const filters = [
{kinds: [40, 41], authors, limit: 100},
{limit: 100, kinds: [42], since, authors},
{kinds: [42], since, authors, limit: 100},
] as Filter[]
if ($session.pubkey) {
if ($session) {
filters.push({kinds: [40, 41], authors: [$session.pubkey]})
}
// Pull some relevant channels by grabbing recent messages
subs.push(
load({
relays,
filters,
onEvent: batch(500, (events: Event[]) => {
const channelIds = uniq(
events.filter(e => e.kind === 42).map(e => Tags.from(e).getMeta("e"))
)
load({
relays,
filters,
onEvent: batch(500, (events: Event[]) => {
const channelIds = uniq(
events.filter(e => e.kind === 42).map(e => Tags.from(e).getMeta("e"))
)
loadPubkeys(pluck("pubkey", events))
loadPubkeys(pluck("pubkey", events))
subs.push(
load({
relays,
filters: [
{kinds: [40], ids: channelIds},
{kinds: [41], "#e": channelIds},
],
})
)
}),
})
)
if (channelIds.length > 0) {
load({
relays,
filters: [
{kinds: [40], ids: channelIds},
{kinds: [41], "#e": channelIds},
],
})
}
}),
})
return () => {
subs.map(s => s.close())
scroller.stop()
}
})

View File

@ -4,7 +4,7 @@
import {fly} from "src/util/transition"
import {ellipsize} from "hurdak"
import Anchor from "src/partials/Anchor.svelte"
import {canSign, imgproxy, joinNip28Channel, leaveNip28Channel} from "src/engine2"
import {canSign, hasNewMessages, imgproxy, joinNip28Channel, leaveNip28Channel} from "src/engine2"
export let channel
@ -14,10 +14,11 @@
// Accommodate data urls from legacy
const picture =
channel.picture?.length > 500 ? channel.picture : imgproxy(channel.picture, {w: 112, h: 112})
channel.meta?.picture?.length > 500
? channel.meta.picture
: imgproxy(channel.meta.picture, {w: 112, h: 112})
$: notify =
channel.nip28.joined && (channel.last_checked || channel.last_sent) < channel.last_received
$: showBadge = channel.nip28.joined && hasNewMessages(channel)
</script>
<button
@ -27,13 +28,13 @@
<div
class="h-14 w-14 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style={`background-image: url(${picture})`} />
{#if notify}
{#if showBadge}
<div class="absolute left-2 top-2 h-2 w-2 rounded bg-accent" />
{/if}
<div class="flex min-w-0 flex-grow flex-col justify-start gap-2">
<div class="flex flex-grow items-start justify-between gap-2">
<h2 class="text-lg">
{channel.name || ""}
{channel.meta?.name || ""}
</h2>
{#if channel.nip28.joined}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={leave}>
@ -47,9 +48,9 @@
</Anchor>
{/if}
</div>
{#if channel.about}
{#if channel.meta?.about}
<p class="text-start text-gray-1">
{ellipsize(channel.about, 300)}
{ellipsize(channel.meta.about, 300)}
</p>
{/if}
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {map, sortBy} from "ramda"
import {map, identity, sortBy} from "ramda"
import {quantify} from "hurdak"
import {Tags} from "src/util/nostr"
import {modal} from "src/partials/state"
@ -11,7 +11,15 @@
import Content from "src/partials/Content.svelte"
import NoteById from "src/app/shared/NoteById.svelte"
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
import {session, labels, getUserRelayUrls, follows, subscribe} from "src/engine2"
import {
session,
getSetting,
getPubkeysWithDefaults,
labels,
getPubkeyHints,
follows,
subscribe,
} from "src/engine2"
type LabelGroup = {
label: string
@ -61,12 +69,12 @@
onMount(() => {
const sub = subscribe({
relays: getUserRelayUrls("read"),
relays: getPubkeyHints(getSetting("relay_limit"), $session?.pubkey, "read"),
filters: [
{
kinds: [1985],
"#L": ["#t", "ugc"],
authors: $follows.concat($session.pubkey),
authors: getPubkeysWithDefaults($follows).concat($session?.pubkey).filter(identity),
},
],
})

View File

@ -6,6 +6,7 @@
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {withExtension, loginWithExtension} from "src/engine2"
import {boot} from "src/app/state"
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
@ -13,6 +14,7 @@
withExtension(async ext => {
if (ext) {
loginWithExtension(await ext.getPublicKey())
boot()
} else {
modal.push({type: "login/privkey"})
}

View File

@ -1,26 +1,21 @@
<script lang="ts">
import {filter, whereEq, complement, pluck, prop} from "ramda"
import {toTitle, seconds, batch} from "hurdak"
import {now} from "src/util/misc"
import {filter, whereEq, complement, prop} from "ramda"
import {toTitle} from "hurdak"
import {navigate} from "svelte-routing"
import Tabs from "src/partials/Tabs.svelte"
import Popover from "src/partials/Popover.svelte"
import Content from "src/partials/Content.svelte"
import MessagesListItem from "src/app/views/MessagesListItem.svelte"
import {
session,
channels,
load,
loadPubkeys,
hasNewNip04Messages,
getUserRelayUrls,
sortChannels,
nip04MarkAllRead,
loadAllNip04Messages,
} from "src/engine2"
export let activeTab = "conversations"
const since = now() - seconds(90, "day")
const nip04Channels = channels.derived(filter(whereEq({type: "nip04"})))
const accepted = nip04Channels.derived(filter(prop("last_sent")))
const requests = nip04Channels.derived(filter(complement(prop("last_sent"))))
@ -32,16 +27,7 @@
badge: (tab === "conversations" ? $accepted : $requests).length,
})
load({
relays: getUserRelayUrls("read"),
filters: [
{kinds: [4], authors: [$session.pubkey], since},
{kinds: [4], "#p": [$session.pubkey], since},
],
onEvent: batch(1000, events => {
loadPubkeys(pluck("pubkey", events))
}),
})
loadAllNip04Messages()
document.title = "Direct Messages"
</script>

View File

@ -6,6 +6,7 @@
import {fly} from "src/util/transition"
import {writable} from "svelte/store"
import {annotateMedia} from "src/util/misc"
import {fromDisplayEvent} from "src/util/nostr"
import Anchor from "src/partials/Anchor.svelte"
import Compose from "src/app/shared/Compose.svelte"
import ImageInput from "src/partials/ImageInput.svelte"
@ -45,10 +46,15 @@
tags.push(mention(quote.pubkey))
// Re-broadcast the note we're quoting
Publisher.publish({relays: $relays, event: quote})
Publisher.publish({
relays: $relays,
event: fromDisplayEvent(quote),
})
}
publishNote(content, tags, $relays).on("progress", toastProgress)
const pub = await publishNote(content, tags, $relays)
pub.on("progress", toastProgress)
modal.clear()
}

View File

@ -39,19 +39,18 @@
// If our note came from a feed, we can preload context
context.hydrate([displayNote], depth)
await load({
load({
filters: [{ids: [note.id]}],
relays: selectHints(getSetting("relay_limit"), relays),
onEvent: e => {
context.addContext([e], {depth})
displayNote = first(context.applyContext([e]))
loading = false
},
})
info("NoteDetail", displayNote)
loading = false
})
onDestroy(() => {

View File

@ -16,8 +16,10 @@
const pseudUrl =
"https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
const submit = () => {
publishProfile(values).on("progress", toastProgress)
const submit = async () => {
const pub = await publishProfile(values)
pub.on("progress", toastProgress)
navigate(routes.person($session.pubkey))
}

View File

@ -6,9 +6,9 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {env, settings, publishSettings} from "src/engine2"
import {env, getSettings, publishSettings} from "src/engine2"
let values = {...settings.get()}
let values = getSettings()
const submit = () => {
publishSettings(values)

View File

@ -1,3 +1,4 @@
import {fromDisplayEvent} from "src/util/nostr"
import {getSetting, getPublishHints} from "src/engine2/queries"
import {publishEvent, getReplyTags} from "./util"
import {Publisher} from "./publisher"
@ -11,7 +12,7 @@ export const publishReply = (parent, content, tags = []) => {
const relays = getPublishHints(getSetting("relay_limit"), parent)
// Re-broadcast the note we're replying to
Publisher.publish({relays, event: parent})
Publisher.publish({relays, event: fromDisplayEvent(parent)})
return publishEvent(1, {relays, content, tags: [...tags, ...getReplyTags(parent, true)]})
}

View File

@ -2,6 +2,7 @@ import {assoc, whereEq, when, map} from "ramda"
import {createMapOf} from "hurdak"
import {now} from "src/util/misc"
import {appDataKeys} from "src/util/nostr"
import {EventKind} from "src/engine2/model"
import {channels} from "src/engine2/state"
import {user, nip04, getInboxHints, getSetting} from "src/engine2/queries"
import {setAppData} from "./nip78"
@ -10,7 +11,7 @@ import {publishEvent} from "./util"
export const publishNip04Message = async (recipient, content, tags = [], relays = null) => {
const pubkeys = [recipient, user.get().pubkey]
return publishEvent(4, {
return publishEvent(EventKind.Nip04Message, {
relays: relays || getInboxHints(getSetting("relay_limit"), pubkeys),
content: await nip04.get().encryptAsUser(content, recipient),
tags: [...tags, ["p", recipient]],

View File

@ -1,3 +1,4 @@
import {fromDisplayEvent} from "src/util/nostr"
import {getSetting, getPublishHints} from "src/engine2/queries"
import {publishEvent, getReplyTags} from "./util"
import {Publisher} from "./publisher"
@ -6,7 +7,7 @@ export const publishReaction = (parent, content = "", tags = []) => {
const relays = getPublishHints(getSetting("relay_limit"), parent)
// Re-broadcast the note we're reacting to
Publisher.publish({relays, event: parent})
Publisher.publish({relays, event: fromDisplayEvent(parent)})
return publishEvent(7, {relays, content, tags: [...tags, ...getReplyTags(parent)]})
}

View File

@ -4,8 +4,8 @@ import {buildEvent} from "./util"
import {Publisher} from "./publisher"
// Use an ephemeral private key for user privacy
export const publishReport = (content = "", tags = [], relays = null) =>
export const publishReport = async (content = "", tags = [], relays = null) =>
Publisher.publish({
relays: relays || getUserRelayUrls("write"),
event: signer.get().signWithKey(buildEvent(1984, {content, tags}), generatePrivateKey()),
event: await signer.get().signWithKey(buildEvent(1984, {content, tags}), generatePrivateKey()),
})

View File

@ -1,21 +1,19 @@
import {appDataKeys} from "src/util/nostr"
import {settings} from "src/engine2/state"
import {canSign} from "src/engine2/queries"
import {nip04, user} from "src/engine2/queries"
import {settings, session} from "src/engine2/state"
import {canSign, nip04} from "src/engine2/queries"
import {publishEvent} from "./util"
export const setAppData = async (d: string, data: any) => {
const {pubkey} = user.get()
const json = JSON.stringify(data)
const content = await nip04.get().encryptAsUser(json, pubkey)
if (canSign.get()) {
const {pubkey} = session.get()
const json = JSON.stringify(data)
const content = await nip04.get().encryptAsUser(json, pubkey)
return publishEvent(30078, {content, tags: [["d", d]]})
return publishEvent(30078, {content, tags: [["d", d]]})
}
}
export const publishSettings = async (updates: Record<string, any>) => {
settings.update($settings => ({...$settings, ...updates}))
if (canSign.get()) {
setAppData(appDataKeys.USER_SETTINGS, settings.get())
}
setAppData(appDataKeys.USER_SETTINGS, settings.get())
}

View File

@ -15,7 +15,10 @@ export type EventOpts = {
tags?: string[][]
}
export function buildEvent(kind: number, {content = "", tags = [], created_at = null}: EventOpts) {
export const buildEvent = (
kind: number,
{content = "", tags = [], created_at = null}: EventOpts
) => {
return {kind, content, tags, created_at: created_at || now()}
}
@ -23,10 +26,14 @@ export type PublishOpts = EventOpts & {
relays?: string[]
}
export function publishEvent(kind: number, {relays, content = "", tags = []}: PublishOpts) {
export const publishEvent = async (
kind: number,
{relays, content = "", tags = []}: PublishOpts
) => {
return Publisher.publish({
timeout: 5000,
relays: relays || getUserRelayUrls("write"),
event: signer.get().signAsUser(
event: await signer.get().signAsUser(
buildEvent(kind, {
content,
tags: uniqTags([...tags, tagsFromContent(content)]),

View File

@ -2,6 +2,59 @@ import type {Event as NostrToolsEvent, UnsignedEvent} from "nostr-tools"
// Message types
export enum EventKind {
Profile = 0,
Note = 1,
RecommendRelay = 2,
Petnames = 3,
Nip04Message = 4,
Delete = 5,
Repost = 6,
Reaction = 7,
BadgeAward = 8,
GenericRepost = 16,
ChannelCreation = 40,
ChannelMetadata = 41,
ChannelMessage = 42,
ChannelHideMessage = 43,
ChannelMuteUser = 44,
FileMetadata = 1063,
LiveChatMessage = 1311,
Report = 1984,
Label = 1985,
CommunityPostApproval = 4550,
ZapGoal = 9041,
ZapRequest = 9734,
Zap = 9735,
MuteList = 10000,
PinList = 10001,
RelayList = 10002,
WalletInfo = 13194,
ClientAuth = 22242,
WalletRequest = 23194,
WalletResponse = 23195,
NostrConnect = 24133,
HTTPAuth = 27235,
PeopleList = 30000,
BookmarkList = 30001,
ProfileBadges = 30008,
BadgeDefinition = 30009,
Post = 30023,
PostDraft = 30024,
AppData = 30078,
LiveEvent = 30311,
UserStatus = 30315,
Classified = 30402,
ClassifiedDraft = 30403,
CalendarEventDate = 31922,
CalendarEventTime = 31923,
Calendar = 31924,
CalendarRSVP = 31925,
HandlerRecommendation = 31989,
HandlerInformation = 31990,
CommunityDefinition = 34550,
}
export type NostrEvent = NostrToolsEvent
export type Event = Omit<NostrToolsEvent, "kind"> & {
@ -14,6 +67,18 @@ export type Rumor = UnsignedEvent & {
id: string
}
export type ZapEvent = Event & {
invoiceAmount: number
request: Event
}
export type DisplayEvent = Event & {
zaps: Event[]
replies: DisplayEvent[]
reactions: Event[]
matchesFilter?: boolean
}
export type Filter = {
ids?: string[]
kinds?: number[]
@ -39,18 +104,6 @@ export type Session = {
bunkerToken?: string
}
export type ZapEvent = Event & {
invoiceAmount: number
request: Event
}
export type DisplayEvent = Event & {
zaps: Event[]
replies: DisplayEvent[]
reactions: Event[]
matchesFilter?: boolean
}
export type RelayInfo = {
contact?: string
description?: string
@ -147,6 +200,11 @@ export type Topic = {
last_seen?: number
}
export type Delete = {
value: string
created_at: number
}
export type List = {
name: string
naddr: string

View File

@ -3,22 +3,22 @@ import type {Event} from "src/engine2/model"
import {session, events, alerts} from "src/engine2/state"
import {projections} from "src/engine2/projections/core"
const isMention = (e: Event) => Tags.from(e).pubkeys().includes(session.get().pubkey)
const isMention = (e: Event) => Tags.from(e).pubkeys().includes(session.get()?.pubkey)
const isUserEvent = (id: string) => events.key(id).get()?.pubkey === session.get().pubkey
const isUserEvent = (id: string) => events.key(id).get()?.pubkey === session.get()?.pubkey
const isDescendant = (e: Event) => isUserEvent(findRootId(e))
const isReply = (e: Event) => isUserEvent(findReplyId(e))
const handleNotification = (e: Event) => {
const $pubkey = session.get().pubkey
const $session = session.get()
if (!$pubkey || e.pubkey === $pubkey) {
if (!$session || e.pubkey === $session.pubkey) {
return
}
alerts.key(e.id).set({...e, recipient: $pubkey})
alerts.key(e.id).set({...e, recipient: $session.pubkey})
}
noteKinds.forEach(kind => {

View File

@ -23,5 +23,5 @@ export const updateKey = (key, timestamp, updates, modify = (a: any) => a) => {
}
}
key.set(record)
key.set(modify(record))
}

View File

@ -1,34 +1,44 @@
import {uniq, prop, uniqBy} from "ramda"
import {tryFunc} from "hurdak"
import {tryFunc, sleep} from "hurdak"
import {tryJson} from "src/util/misc"
import {Tags, appDataKeys} from "src/util/nostr"
import type {Channel} from "src/engine2/model"
import {EventKind} from "src/engine2/model"
import {channels} from "src/engine2/state"
import {user, nip04, canSign} from "src/engine2/queries"
import {projections} from "src/engine2/projections/core"
projections.addHandler(30078, async e => {
if (canSign.get() && Tags.from(e).getMeta("d") === appDataKeys.NIP04_LAST_CHECKED) {
await tryJson(async () => {
const payload = await nip04.get().decryptAsUser(e.content, user.get().pubkey)
channels.mapStore.updateAsync(async $channels => {
await tryJson(async () => {
const payload = JSON.parse(await nip04.get().decryptAsUser(e.content, user.get().pubkey))
for (const [id, ts] of Object.entries(payload) as [string, number][]) {
// Ignore weird old stuff
if (id.includes('/')) {
continue
for (const [id, ts] of Object.entries(payload) as [string, number][]) {
// Ignore weird old stuff
if (id === "undefined" || id.includes("/")) {
continue
}
const channel =
$channels.get(id) || ({id, type: "nip04", relays: e.seen_on, messages: []} as Channel)
$channels.set(id, {
...channel,
last_checked: Math.max(ts, channel.last_checked || 0),
})
// No need to lock up the UI decrypting a bunch of stuff
await sleep(16)
}
})
const channel = channels.key(id)
channel.merge({
last_checked: Math.max(ts, channel.get()?.last_checked || 0),
})
}
return $channels
})
}
})
projections.addHandler(4, async e => {
projections.addHandler(EventKind.Nip04Message, async e => {
if (!canSign.get()) {
return
}

View File

@ -6,9 +6,7 @@ import {projections, updateKey} from "src/engine2/projections/core"
projections.addHandler(0, e => {
tryJson(async () => {
const {
kind0: {nip05: address},
} = JSON.parse(e.content)
const {nip05: address} = JSON.parse(e.content)
if (!address) {
return

View File

@ -8,6 +8,6 @@ projections.addHandler(5, e => {
.values()
.all()
.forEach(value => {
deletes.key(value).set({value})
deletes.key(value).set({value, created_at: e.created_at})
})
})

View File

@ -1,4 +1,4 @@
import {prop, map, assocPath, pluck, last, uniqBy, uniq} from "ramda"
import {prop, pipe, assoc, map, assocPath, pluck, last, uniqBy, uniq} from "ramda"
import {Tags, appDataKeys} from "src/util/nostr"
import {tryJson} from "src/util/misc"
import type {Event, Channel} from "src/engine2/model"
@ -15,7 +15,7 @@ projections.addHandler(40, (e: Event) => {
channels.key(e.id),
e.created_at,
{meta, relays},
assocPath(["nip28", "owner"], e.pubkey)
pipe(assoc("type", "nip28"), assocPath(["nip28", "owner"], e.pubkey))
)
}
})
@ -48,7 +48,7 @@ projections.addHandler(30078, async (e: Event) => {
if (Tags.from(e).getMeta("d") === appDataKeys.NIP28_ROOMS_JOINED) {
await tryJson(async () => {
const channelIds = await nip04.get().decryptAsUser(e.content, e.pubkey)
const channelIds = JSON.parse(await nip04.get().decryptAsUser(e.content, e.pubkey))
// Just a bug from when I was building the feature, remove someday
if (!Array.isArray(channelIds)) {
@ -71,7 +71,7 @@ projections.addHandler(30078, async (e: Event) => {
if (Tags.from(e).getMeta("d") === appDataKeys.NIP28_LAST_CHECKED) {
await tryJson(async () => {
const payload = await nip04.get().decryptAsUser(e.content, e.pubkey)
const payload = JSON.parse(await nip04.get().decryptAsUser(e.content, e.pubkey))
for (const key of Object.keys(payload)) {
// Backwards compat from when we used to prefix id/pubkey

View File

@ -6,9 +6,7 @@ import {projections, updateKey} from "src/engine2/projections/core"
projections.addHandler(0, e => {
tryJson(async () => {
const {
kind0: {lud16, lud06},
} = JSON.parse(e.content)
const {lud16, lud06} = JSON.parse(e.content)
const address = (lud16 || lud06 || "").toLowerCase()
if (!address) {

View File

@ -11,7 +11,7 @@ projections.addHandler(30078, e => {
e.created_at > getSetting("updated_at")
) {
tryJson(async () => {
const updates = await nip04.get().decryptAsUser(e.content, user.get().pubkey)
const updates = JSON.parse(await nip04.get().decryptAsUser(e.content, user.get().pubkey))
settings.update($settings => ({
...$settings,

View File

@ -3,7 +3,8 @@ import {Plex, Relays, Executor} from "paravel"
import {error, warn} from "src/util/logger"
import {normalizeRelayUrl} from "src/util/nostr"
import {writable} from "src/engine2/util"
import {env, pool, settings} from "src/engine2/state"
import {env, pool} from "src/engine2/state"
import {getSetting} from "src/engine2/queries"
export const authHandler = writable(null)
@ -30,7 +31,7 @@ export const getUrls = (relays: string[]) => {
export const getExecutor = (urls: string[], {bypassBoot = false} = {}) => {
let target
const {multiplextr_url: muxUrl} = settings.get()
const muxUrl = getSetting("multiplextr_url")
// Try to use our multiplexer, but if it fails to connect fall back to relays. If
// we're only connecting to a single relay, just do it directly, unless we already

View File

@ -6,7 +6,7 @@ import type {Person} from "src/engine2/model"
import {topics, people} from "src/engine2/state"
export const peopleWithName = people.derived($people =>
$people.filter(({profile: p}) => p.name || p.nip05 || p.display_name)
$people.filter(({profile: p}) => p?.name || p?.nip05 || p?.display_name)
)
export const derivePerson = pubkey => people.key(pubkey).derived(defaultTo({pubkey}))

View File

@ -3,7 +3,7 @@ import {difference} from "hurdak"
import {people} from "src/engine2/state"
import {user} from "src/engine2/queries"
export const follows = user.derived($user => ($user.petnames || []).map(nth(1)) as string[])
export const follows = user.derived($user => ($user?.petnames || []).map(nth(1)) as string[])
export const followsSet = follows.derived($follows => new Set($follows))

View File

@ -5,7 +5,7 @@ import {findReplyAndRootIds} from "src/util/nostr"
import {session, lists} from "src/engine2/state"
import {user} from "src/engine2/queries/session"
export const mutes = user.derived($user => ($user.mutes || []).map(nth(1)))
export const mutes = user.derived($user => ($user?.mutes || []).map(nth(1)))
export const mutesSet = mutes.derived($mutes => new Set($mutes))

View File

@ -1,6 +1,19 @@
import {settings} from "src/engine2/state"
import {prop} from "ramda"
import {env, settings} from "src/engine2/state"
export const getSetting = k => settings.get()[k]
export const getDefaultSettings = () => ({
relay_limit: 10,
default_zap: 21,
show_media: true,
report_analytics: true,
imgproxy_url: env.get().IMGPROXY_URL,
dufflepud_url: env.get().DUFFLEPUD_URL,
multiplextr_url: env.get().MULTIPLEXTR_URL,
})
export const getSettings = () => ({...getDefaultSettings(), ...settings.get()})
export const getSetting = k => prop(k, getSettings())
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
const base = getSetting("imgproxy_url")

View File

@ -1,4 +1,3 @@
import {whereEq} from "ramda"
import {nip19} from "nostr-tools"
import {derived} from "src/engine2/util/store"
import {session, people} from "src/engine2/state"
@ -24,11 +23,11 @@ export const stateKey = session.derived($s => $s?.pubkey || "anonymous")
export const user = derived([session, people.mapStore], ([$s, $p]) => $p.get($s?.pubkey))
export const canSign = session.derived(({method}) =>
["bunker", "privkey", "extension"].includes(method)
export const canSign = session.derived($session =>
["bunker", "privkey", "extension"].includes($session?.method)
)
export const canUseGiftWrap = session.derived(whereEq({method: "privkey"}))
export const canUseGiftWrap = session.derived($session => $session?.method === "privkey")
export const ndk = derived([session, ndkInstances], ([$session, $instances]) => {
if (!$session?.bunkerToken) {

View File

@ -7,8 +7,9 @@ import {findReplyAndRootIds, findReplyId, findRootId, Tags, noteKinds} from "src
import {collection} from "src/engine2/util/store"
import type {Collection} from "src/engine2/util/store"
import type {Event, DisplayEvent, Filter} from "src/engine2/model"
import {settings, env} from "src/engine2/state"
import {env} from "src/engine2/state"
import {
getSetting,
mergeHints,
isEventMuted,
getReplyHints,
@ -84,16 +85,12 @@ export class ContextLoader {
return events
}
getRelayLimit() {
return settings.get().relay_limit
}
mergeHints(groups: string[][]) {
if (this.opts.relays) {
return this.opts.relays
}
return mergeHints(this.getRelayLimit(), groups)
return mergeHints(getSetting("relay_limit"), groups)
}
applyContext = (notes: Event[], {substituteParents = false, alreadySeen = new Set()} = {}) => {
@ -180,11 +177,11 @@ export class ContextLoader {
const {root, reply} = findReplyAndRootIds(e)
if (reply && !this.seen.has(reply)) {
info.push({id: reply, hints: getParentHints(this.getRelayLimit(), e)})
info.push({id: reply, hints: getParentHints(getSetting("relay_limit"), e)})
}
if (root && !this.seen.has(root)) {
info.push({id: findRootId(e), hints: getRootHints(this.getRelayLimit(), e)})
info.push({id: findRootId(e), hints: getRootHints(getSetting("relay_limit"), e)})
}
return info
@ -220,7 +217,7 @@ export class ContextLoader {
for (const c of chunk(256, events)) {
load({
relays: this.mergeHints(c.map(e => getReplyHints(this.getRelayLimit(), e))),
relays: this.mergeHints(c.map(e => getReplyHints(getSetting("relay_limit"), e))),
filters: [{kinds: this.getReplyKinds(), "#e": pluck("id", c as Event[])}],
onEvent: batch(100, (context: Event[]) => this.addContext(context, {depth: depth - 1})),
})
@ -228,7 +225,7 @@ export class ContextLoader {
}
})
listenForContext = throttle(5000, () => {
listenForContext = throttle(10_000, () => {
if (this.stopped) {
return
}
@ -248,7 +245,7 @@ export class ContextLoader {
for (const c of chunk(256, findNotes(this.data.get()))) {
this.addSubs([
subscribe({
relays: this.mergeHints(c.map(e => getReplyHints(this.getRelayLimit(), e))),
relays: this.mergeHints(c.map(e => getReplyHints(getSetting("relay_limit"), e))),
filters: [{kinds: this.getReplyKinds(), "#e": pluck("id", c), since: now()}],
onEvent: batch(100, (context: Event[]) => this.addContext(context, {depth: 2})),
}),

View File

@ -0,0 +1,20 @@
import {omit} from "ramda"
import {shuffle} from "hurdak"
import type {DynamicFilter, Filter} from "src/engine2/model"
import {env} from "src/engine2/state"
import {follows, network} from "src/engine2/queries"
export const getPubkeysWithDefaults = (pubkeys: string[]) =>
shuffle(pubkeys.length > 0 ? pubkeys : (env.get().DEFAULT_FOLLOWS as string[])).slice(0, 1024)
export const compileFilter = (filter: DynamicFilter): Filter => {
if (filter.authors === "global") {
filter = omit(["authors"], filter)
} else if (filter.authors === "follows") {
filter = {...filter, authors: getPubkeysWithDefaults(follows.get())}
} else if (filter.authors === "network") {
filter = {...filter, authors: getPubkeysWithDefaults(network.get())}
}
return filter as Filter
}

View File

@ -2,6 +2,7 @@ export * from "./context"
export * from "./cursor"
export * from "./feed"
export * from "./pubkeys"
export * from "./filter"
export * from "./count"
export * from "./load"
export * from "./subscription"

View File

@ -1,6 +1,7 @@
import {matchFilters} from "nostr-tools"
import {prop, groupBy, uniq} from "ramda"
import {batch} from "hurdak"
import {defer} from "hurdak"
import {pushToKey} from "src/util/misc"
import {subscribe} from "./subscription"
import type {Event, Filter} from "src/engine2/model"
@ -12,16 +13,17 @@ export type LoadOpts = {
onClose?: (events: Event[]) => void
}
export const calculateGroup = ({since, until, ...filter}: Filter) => {
export type LoadItem = {
request: LoadOpts
result: ReturnType<typeof defer>
}
export const calculateGroup = ({limit, since, until, ...filter}: Filter) => {
const group = Object.keys(filter)
if (since) {
group.push(`since:${since}`)
}
if (until) {
group.push(`until:${until}`)
}
if (since) group.push(`since:${since}`)
if (limit) group.push(`limit:${limit}`)
if (until) group.push(`until:${until}`)
return group.sort().join("-")
}
@ -33,7 +35,11 @@ export const combineFilters = filters => {
const newFilter = {}
for (const k of Object.keys(group[0])) {
newFilter[k] = uniq(group.flatMap(prop(k)))
if (["since", "until", "limit"].includes(k)) {
newFilter[k] = group[0][k]
} else {
newFilter[k] = uniq(group.flatMap(prop(k)))
}
}
result.push(newFilter)
@ -42,35 +48,59 @@ export const combineFilters = filters => {
return result
}
export const load = batch(500, (requests: LoadOpts[]) => {
const relays = uniq(requests.flatMap(prop("relays")))
const filters = combineFilters(requests.flatMap(prop("filters")))
const queue = []
const sub = subscribe({relays, filters, timeout: 30_000})
export const execute = () => {
const itemsByRelay = {}
for (const item of queue.splice(0)) {
for (const url of item.request.relays) {
pushToKey(itemsByRelay, url, item)
}
}
sub.on("event", (e: Event) => {
for (const req of requests) {
if (!req.onEvent) {
continue
// Group by relay, then by filter
for (const [url, items] of Object.entries(itemsByRelay) as [string, LoadItem[]][]) {
const filters = combineFilters(items.flatMap(item => item.request.filters))
const sub = subscribe({
filters,
relays: [url],
timeout: 15000,
onEvent: e => {
for (const {request} of items) {
if (request.onEvent && matchFilters(request.filters, e)) {
request.onEvent(e)
}
}
},
onEose: url => {
for (const {request} of items) {
request.onEose?.(url)
}
},
onClose: events => {
for (const {request} of items) {
request.onClose?.(events)
}
},
})
sub.result.then(events => {
for (const item of items) {
item.result.resolve(events)
}
})
}
}
if (matchFilters(req.filters, e)) {
req.onEvent(e)
}
}
})
export const load = (request: LoadOpts) => {
const result = defer()
sub.on("eose", url => {
for (const req of requests) {
req.onEose?.(url)
}
})
if (queue.length === 0) {
setTimeout(execute, 500)
}
sub.on("close", events => {
for (const req of requests) {
req.onClose?.(events)
}
})
queue.push({request, result})
return sub.result
})
return result
}

View File

@ -1,14 +1,37 @@
import {user, getInboxHints, getSetting} from "src/engine2/queries"
import {pluck} from "ramda"
import {batch, seconds} from "hurdak"
import {now} from "src/util/misc"
import {EventKind} from "src/engine2/model"
import {session} from "src/engine2/state"
import {getInboxHints, getUserRelayUrls, getSetting} from "src/engine2/queries"
import {load} from "./load"
import {loadPubkeys} from "./pubkeys"
import {subscribe} from "./subscription"
export function loadAllNip04Messages() {
const {pubkey} = session.get()
const since = now() - seconds(90, "day")
load({
relays: getUserRelayUrls("read"),
filters: [
{kinds: [4], authors: [pubkey], since},
{kinds: [4], "#p": [pubkey], since},
],
onEvent: batch(1000, events => {
loadPubkeys(pluck("pubkey", events))
}),
})
}
export function listenForNip04Messages(contactPubkey: string) {
const {pubkey: userPubkey} = user.get()
const {pubkey: userPubkey} = session.get()
return subscribe({
relays: getInboxHints(getSetting("relay_limit"), [contactPubkey, userPubkey]),
filters: [
{kinds: [4], authors: [userPubkey], "#p": [contactPubkey]},
{kinds: [4], authors: [contactPubkey], "#p": [userPubkey]},
{kinds: [EventKind.Nip04Message], authors: [userPubkey], "#p": [contactPubkey]},
{kinds: [EventKind.Nip04Message], authors: [contactPubkey], "#p": [userPubkey]},
],
})
}

View File

@ -5,7 +5,7 @@ import {getUserRelayUrls} from "src/engine2/queries"
import {load} from "./load"
export const loadDeletes = () => {
const since = sumBy(prop("value"), deletes.get())
const since = sumBy(prop("created_at"), deletes.get().filter(prop("created_at"))) || 0
const authors = Object.values(sessions.get()).map(prop("pubkey"))
return load({

View File

@ -1,10 +1,10 @@
import {without, uniq} from "ramda"
import {chunk, seconds, ensurePlural} from "hurdak"
import {chunk, seconds} from "hurdak"
import {personKinds, appDataKeys} from "src/util/nostr"
import {now} from "src/util/misc"
import type {Filter} from "src/engine2/model"
import {people, settings} from "src/engine2/state"
import {mergeHints, getPubkeyHints} from "src/engine2/queries"
import {people} from "src/engine2/state"
import {getSetting, mergeHints, getPubkeyHints} from "src/engine2/queries"
import {load} from "./load"
export type LoadPeopleOpts = {
@ -28,7 +28,7 @@ export const getStalePubkeys = (pubkeys: string[]) => {
const key = people.key(pubkey)
if (key.get()?.last_fetched || 0 > since) {
if ((key.get()?.last_fetched || 0) > since) {
continue
}
@ -41,10 +41,9 @@ export const getStalePubkeys = (pubkeys: string[]) => {
}
export const loadPubkeys = async (
pubkeyGroups: string | string[],
rawPubkeys: string[],
{relays, force, kinds = personKinds}: LoadPeopleOpts = {}
) => {
const rawPubkeys = ensurePlural(pubkeyGroups).reduce((a, b) => a.concat(b), [])
const pubkeys = force ? uniq(rawPubkeys) : getStalePubkeys(rawPubkeys)
const getChunkRelays = (chunk: string[]) => {
@ -52,7 +51,7 @@ export const loadPubkeys = async (
return relays
}
const {relay_limit: limit} = settings.get()
const limit = getSetting("relay_limit")
return mergeHints(
limit,

View File

@ -1,6 +1,6 @@
import {Pool} from "paravel"
import {collection, writable} from "src/engine2/util/store"
import type {Event, Session, Channel, Topic, List, Person, Relay} from "src/engine2/model"
import type {Event, Delete, Session, Channel, Topic, List, Person, Relay} from "src/engine2/model"
// Synchronous stores
@ -14,7 +14,7 @@ export const alertsLastChecked = writable(0)
export const alerts = collection<Event & {recipient: string}>("id")
export const events = collection<Event>("id")
export const deletes = collection<{value: string}>("value")
export const deletes = collection<Delete>("value")
export const labels = collection<Event>("id")
export const topics = collection<Topic>("name")
export const lists = collection<List>("naddr")

View File

@ -225,7 +225,7 @@ export class Storage {
if (window.indexedDB) {
const dbConfig = indexedDBAdapters.map(adapter => adapter.getIndexedDBConfig())
this.db = new IndexedDB("nostr-engine/Storage", 2, dbConfig)
this.db = new IndexedDB("nostr-engine/Storage", 4, dbConfig)
window.addEventListener("beforeunload", () => this.close())

View File

@ -41,6 +41,10 @@ export class Writable<T> implements Readable<T> {
this.set(f(this.value))
}
async updateAsync(f: (v: T) => Promise<T>) {
this.set(await f(this.value))
}
subscribe(f: Subscriber<T>) {
this.subs.push(f)
@ -149,7 +153,11 @@ export class Key<T extends R> implements Readable<T> {
}
// Make sure the pk always get set on the record
m.set(this.key, f({...m.get(this.key), [this.pk]: this.key}))
const {pk, key} = this
const oldValue = {...m.get(key), [pk]: key}
const newValue = {...f(oldValue), [pk]: key}
m.set(this.key, newValue)
return m
})
@ -192,6 +200,11 @@ export class Collection<T extends R> implements Readable<T[]> {
update = (f: (v: T[]) => T[]) =>
this.mapStore.update(m => new Map(f(Array.from(m.values())).map(x => [x[this.pk], x])))
updateAsync = async (f: (v: T[]) => Promise<T[]>) =>
this.mapStore.updateAsync(
async m => new Map((await f(Array.from(m.values()))).map(x => [x[this.pk], x]))
)
reject = (f: (v: T) => boolean) => this.update(reject(f))
filter = (f: (v: T) => boolean) => this.update(filter(f))

View File

@ -73,12 +73,12 @@
// Group messages so we're only showing the person once per chunk
$: {
if (groupedMessages?.length === messages.length) {
scroller.stop()
scroller?.stop()
}
const result = reverse(
sortBy(prop("created_at"), messages).reduce((mx, m) => {
return mx.concat({...m, showProfile: m.pubkey !== last(mx).pubkey})
return mx.concat({...m, showProfile: m.pubkey !== last(mx)?.pubkey})
}, [])
)

View File

@ -1,5 +1,5 @@
import {nip19} from "nostr-tools"
import {is, fromPairs, mergeLeft, last, identity, prop, flatten, uniq} from "ramda"
import {omit, is, fromPairs, mergeLeft, last, identity, prop, flatten, uniq} from "ramda"
import {ensurePlural, between, mapVals, tryFunc, avg, first} from "hurdak"
import type {Filter, Event, DisplayEvent} from "src/engine2/model"
import {tryJson} from "src/util/misc"
@ -162,6 +162,9 @@ export const asDisplayEvent = (event: Event): DisplayEvent => ({
...event,
})
export const fromDisplayEvent = e =>
omit(["replies", "reactions", "zaps", "matchesFilter"], e) as Event
export const toHex = (data: string): string | null => {
if (data.match(/[a-zA-Z0-9]{64}/)) {
return data