Use welshman dvms

This commit is contained in:
Jon Staab 2024-07-19 09:08:11 -07:00
parent ab7a28ae34
commit b318cc6808
11 changed files with 81 additions and 124 deletions

View File

@ -19,7 +19,9 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] NIP 87 closed groups - [x] NIP 87 closed groups
- [x] NIP 72 communities - [x] NIP 72 communities
- [x] NIP 89 client tag support - [x] NIP 89 client tag support
- [x] NIP 89 handler integration
- [x] NIP 32 labeling and collections - [x] NIP 32 labeling and collections
- [x] NIP 17 DMs
- [x] Private group calendars and listings - [x] Private group calendars and listings
- [x] Cross-posting between communities and main feed - [x] Cross-posting between communities and main feed
- [x] Bech32 entity search and scan - [x] Bech32 entity search and scan
@ -46,6 +48,12 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] Onboarding workflow - [x] Onboarding workflow
- [x] Multi-account support - [x] Multi-account support
- [x] Notifications view - [x] Notifications view
- [x] Web of trust scores for less spam and better group/feed suggestions
- [x] Customizable and shareable feeds and lists
- [x] Customizable invite links
- [x] Reporting via tagr-bot
- [x] Nostr Wallet Connect support
- [x] Date/time localization
You can find a more complete changelog [here](./CHANGELOG.md). You can find a more complete changelog [here](./CHANGELOG.md).
@ -53,9 +61,9 @@ You can find a more complete changelog [here](./CHANGELOG.md).
- Clone the project repository: `git clone https://github.com/coracle-social/coracle.git` - Clone the project repository: `git clone https://github.com/coracle-social/coracle.git`
- Navigate to the project directory: `cd coracle` - Navigate to the project directory: `cd coracle`
- Install dependencies: `yarn` - Install dependencies: `npm i`
- Customize configuration in `.env` (optional, see below) - Customize configuration in `.env` (optional, see below)
- Start the development server: `yarn dev` - Start the development server: `npm run dev`
# Customization # Customization

BIN
package-lock.json generated

Binary file not shown.

View File

@ -9,7 +9,7 @@
"check:es": "eslint 'src/**/*.{js,ts,svelte}' --quiet", "check:es": "eslint 'src/**/*.{js,ts,svelte}' --quiet",
"check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error", "check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"check:fmt": "prettier --check $(git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs)", "check:fmt": "prettier --check $(git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs)",
"check:errors": "run-p check:es check:ts check:cycles", "check:errors": "run-p check:es check:ts",
"check": "run-p check:errors check:fmt", "check": "run-p check:errors check:fmt",
"format": "prettier --write $(git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs)", "format": "prettier --write $(git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs)",
"watch": "find src -type f | entr -r" "watch": "find src -type f | entr -r"
@ -57,7 +57,8 @@
"@welshman/feeds": "^0.0.12", "@welshman/feeds": "^0.0.12",
"@welshman/lib": "^0.0.11", "@welshman/lib": "^0.0.11",
"@welshman/net": "^0.0.14", "@welshman/net": "^0.0.14",
"@welshman/util": "^0.0.17", "@welshman/util": "^0.0.18",
"@welshman/dvm": "^0.0.2",
"bowser": "^2.11.0", "bowser": "^2.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {append} from "ramda" import {append} from "@welshman/lib"
import {updateIn} from "src/util/misc" import {updateIn} from "src/util/misc"
import {slide} from "src/util/transition" import {slide} from "src/util/transition"
import Card from "src/partials/Card.svelte" import Card from "src/partials/Card.svelte"
@ -9,7 +9,9 @@
export let task export let task
const hideTask = () => const hideTask = () =>
updateCurrentSession(updateIn("onboarding_tasks_completed", append(task))) updateCurrentSession(
updateIn("onboarding_tasks_completed", (tasks: string[]) => append(task, tasks)),
)
</script> </script>
{#if !$session.onboarding_tasks_completed.includes(task)} {#if !$session.onboarding_tasks_completed.includes(task)}
@ -19,9 +21,7 @@
<FlexColumn> <FlexColumn>
<slot /> <slot />
</FlexColumn> </FlexColumn>
<i <i class="fa fa-times absolute right-0 top-0 cursor-pointer p-2" on:click={hideTask} />
class="fa fa-times absolute right-0 top-0 cursor-pointer p-2"
on:click={hideTask} />
</Card> </Card>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import {router} from "src/app/util/router" import {router} from "src/app/util/router"
import {deriveProfileDisplay, loadPubkeys} from "src/engine" import {deriveProfileDisplay, loadPubkeys} from "src/engine"
@ -11,6 +12,6 @@
loadPubkeys([pubkey]) loadPubkeys([pubkey])
</script> </script>
<Anchor modal stopPropagation class={$$props.class} href={path}> <Anchor modal stopPropagation class={cx("!no-underline", $$props.class)} href={path}>
@<span class="underline">{$display}</span> @<span class="underline">{$display}</span>
</Anchor> </Anchor>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {makeRelayFeed} from "@welshman/feeds" import {makeRelayFeed, makeScopeFeed, Scope} from "@welshman/feeds"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/app/shared/Feed.svelte" import Feed from "src/app/shared/Feed.svelte"
import {router} from "src/app/util/router" import {router} from "src/app/util/router"
@ -13,6 +13,8 @@
if (isPlatformFeed) { if (isPlatformFeed) {
globalFeed.set(makeFeed({definition: makeRelayFeed(...$env.PLATFORM_RELAYS)})) globalFeed.set(makeFeed({definition: makeRelayFeed(...$env.PLATFORM_RELAYS)}))
} else {
globalFeed.set(makeFeed({definition: makeScopeFeed(Scope.Follows)}))
} }
document.title = "Feeds" document.title = "Feeds"

View File

@ -18,7 +18,7 @@
userIsGroupMember, userIsGroupMember,
updateCurrentSession, updateCurrentSession,
communityListsByAddress, communityListsByAddress,
searchGroupMeta, groupMetaSearch,
groupMeta, groupMeta,
} from "src/engine" } from "src/engine"
@ -34,7 +34,7 @@
let limit = 20 let limit = 20
let element = null let element = null
$: otherGroupMeta = reject(userIsMember, $searchGroupMeta(q)).slice(0, limit) $: otherGroupMeta = reject(userIsMember, $groupMetaSearch.searchOptions(q)).slice(0, limit)
document.title = "Groups" document.title = "Groups"

View File

@ -15,8 +15,8 @@
import GroupCircle from "src/app/shared/GroupCircle.svelte" import GroupCircle from "src/app/shared/GroupCircle.svelte"
import PersonSelect from "src/app/shared/PersonSelect.svelte" import PersonSelect from "src/app/shared/PersonSelect.svelte"
import {router} from "src/app/util/router" import {router} from "src/app/util/router"
import {displayRelayUrl, displayGroupMeta} from "src/domain" import {displayRelayUrl} from "src/domain"
import {hints, relaySearch, searchGroupMeta, groupMetaByAddress} from "src/engine" import {hints, relaySearch, groupMetaSearch, displayGroupByAddress} from "src/engine"
export let initialPubkey = null export let initialPubkey = null
export let initialGroupAddress = null export let initialGroupAddress = null
@ -70,8 +70,6 @@
groups = toSpliced(groups, i, 1) groups = toSpliced(groups, i, 1)
} }
const displayGroupFromAddress = a => displayGroupMeta($groupMetaByAddress.get(a))
let relayInput, groupInput let relayInput, groupInput
let sections = [] let sections = []
let pubkeys = [] let pubkeys = []
@ -185,7 +183,7 @@
</p> </p>
{#each groups as group, i (group.address + i)} {#each groups as group, i (group.address + i)}
<ListItem on:remove={() => removeGroup(i)}> <ListItem on:remove={() => removeGroup(i)}>
<span slot="label">{displayGroupFromAddress(group.address)}</span> <span slot="label">{displayGroupByAddress(group.address)}</span>
<span slot="data"> <span slot="data">
<Input bind:value={group.claim} placeholder="Invite code (optional)" /> <Input bind:value={group.claim} placeholder="Invite code (optional)" />
</span> </span>
@ -194,8 +192,8 @@
<SearchSelect <SearchSelect
value={null} value={null}
bind:this={groupInput} bind:this={groupInput}
search={$searchGroupMeta} search={$groupMetaSearch.searchOptions}
displayItem={displayGroupMeta} displayItem={$groupMetaSearch.displayOption}
getKey={groupMeta => getAddress(groupMeta.event)} getKey={groupMeta => getAddress(groupMeta.event)}
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}> onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
<i slot="before" class="fa fa-search" /> <i slot="before" class="fa fa-search" />

View File

@ -17,6 +17,7 @@ import {
getIdFilters, getIdFilters,
isGroupAddress, isGroupAddress,
isSignedEvent, isSignedEvent,
createEvent,
WRAP, WRAP,
WRAP_NIP04, WRAP_NIP04,
EPOCH, EPOCH,
@ -28,6 +29,7 @@ import {
HANDLER_INFORMATION, HANDLER_INFORMATION,
HANDLER_RECOMMENDATION, HANDLER_RECOMMENDATION,
} from "@welshman/util" } from "@welshman/util"
import {makeDvmRequest} from "@welshman/dvm"
import {updateIn, createBatcher} from "src/util/misc" import {updateIn, createBatcher} from "src/util/misc"
import {giftWrapKinds, noteKinds, reactionKinds, repostKinds} from "src/util/nostr" import {giftWrapKinds, noteKinds, reactionKinds, repostKinds} from "src/util/nostr"
import {always, partition, pluck, uniq, without} from "ramda" import {always, partition, pluck, uniq, without} from "ramda"
@ -38,7 +40,6 @@ import {
getUserCircles, getUserCircles,
getGroupReqInfo, getGroupReqInfo,
getCommunityReqInfo, getCommunityReqInfo,
dvmRequest,
env, env,
getFollows, getFollows,
getFilterSelections, getFilterSelections,
@ -59,9 +60,9 @@ import {
subscribe, subscribe,
subscribePersistent, subscribePersistent,
dufflepud, dufflepud,
signer,
} from "src/engine/state" } from "src/engine/state"
import {updateCurrentSession, updateSession} from "src/engine/commands" import {updateCurrentSession, updateSession} from "src/engine/commands"
import {loadPubkeyRelays} from "src/engine/requests/pubkeys"
export * from "src/engine/requests/pubkeys" export * from "src/engine/requests/pubkeys"
@ -275,22 +276,22 @@ export const feedLoader = new FeedLoader<TrustedEvent>({
} }
}, },
requestDVM: async ({kind, onEvent, tags = [], ...request}) => { requestDVM: async ({kind, onEvent, tags = [], ...request}) => {
let relays tags = [...tags, ["expiration", String(now() + 5)]]
if (request.relays?.length > 0) {
relays = hints.fromRelays(request.relays).getUrls()
} else {
const pubkeys = tags.filter(nthEq(0, "p")).map(nth(1))
await loadPubkeyRelays(pubkeys) const req = makeDvmRequest({
event: await signer.get().signAsUser(createEvent(kind, {tags})),
relays:
request.relays?.length > 0
? hints.fromRelays(request.relays).getUrls()
: hints.Messages(tags.filter(nthEq(0, "p")).map(nth(1))).getUrls(),
})
relays = hints.Messages(pubkeys).getUrls() await new Promise<void>(resolve => {
} req.emitter.on("result", (url, event) => {
onEvent(event)
const event = await dvmRequest({kind, tags, relays}) resolve()
})
if (event) { })
onEvent(event)
}
}, },
getPubkeysForScope: (scope: string) => { getPubkeysForScope: (scope: string) => {
const $pubkey = pubkey.get() const $pubkey = pubkey.get()

View File

@ -54,7 +54,7 @@ const getFiltersForKey = (key: string, authors: string[]) => {
case "pubkey/relays": case "pubkey/relays":
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}] return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
case "pubkey/profile": case "pubkey/profile":
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}] return [{authors, kinds: [PROFILE, FOLLOWS, MUTES, HANDLER_INFORMATION, COMMUNITIES]}]
case "pubkey/user": case "pubkey/user":
return [ return [
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]}, {authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]},

View File

@ -92,7 +92,7 @@ import {
publish as basePublish, publish as basePublish,
subscribe as baseSubscribe, subscribe as baseSubscribe,
} from "@welshman/net" } from "@welshman/net"
import type {Publish, PublishRequest, SubscribeRequest} from "@welshman/net" import type {PublishRequest, SubscribeRequest} from "@welshman/net"
import * as Content from "@welshman/content" import * as Content from "@welshman/content"
import {fuzzy, synced, withGetter, pushToKey, tryJson, fromCsv, SearchHelper} from "src/util/misc" import {fuzzy, synced, withGetter, pushToKey, tryJson, fromCsv, SearchHelper} from "src/util/misc"
import { import {
@ -108,6 +108,7 @@ import {
} from "src/util/nostr" } from "src/util/nostr"
import logger from "src/util/logger" import logger from "src/util/logger"
import type { import type {
GroupMeta,
PublishedFeed, PublishedFeed,
PublishedProfile, PublishedProfile,
PublishedListFeed, PublishedListFeed,
@ -142,6 +143,7 @@ import {
filterRelaysByNip, filterRelaysByNip,
displayRelayUrl, displayRelayUrl,
readGroupMeta, readGroupMeta,
displayGroupMeta,
} from "src/domain" } from "src/domain"
import type { import type {
Channel, Channel,
@ -693,7 +695,32 @@ export const groupMetaByAddress = withGetter(
export const deriveGroupMeta = (address: string) => export const deriveGroupMeta = (address: string) =>
derived(groupMetaByAddress, $m => $m.get(address)) derived(groupMetaByAddress, $m => $m.get(address))
export const searchGroupMeta = derived( export const displayGroupByAddress = a => displayGroupMeta(groupMetaByAddress.get().get(a))
export class GroupSearch extends SearchHelper<GroupMeta & {score: number}, string> {
config = {
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
threshold: 0.3,
shouldSort: false,
includeScore: true,
}
getSearch = () => {
const fuse = new Fuse(this.options, this.config)
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
return (term: string) =>
term
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item)
: sortBy(meta => -meta.score, this.options)
}
getValue = (option: GroupMeta) => getAddress(option.event)
displayValue = displayGroupByAddress
}
export const groupMetaSearch = derived(
[groupMeta, communityListsByAddress, userFollows], [groupMeta, communityListsByAddress, userFollows],
([$groupMeta, $communityListsByAddress, $userFollows]) => { ([$groupMeta, $communityListsByAddress, $userFollows]) => {
const options = $groupMeta.map(meta => { const options = $groupMeta.map(meta => {
@ -704,19 +731,7 @@ export const searchGroupMeta = derived(
return {...meta, score: followedMembers.length} return {...meta, score: followedMembers.length}
}) })
const fuse = new Fuse(options, { return new GroupSearch(options)
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
threshold: 0.3,
shouldSort: false,
includeScore: true,
})
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
return (term: string) =>
term
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item)
: sortBy(meta => -meta.score, options)
}, },
) )
@ -1975,75 +1990,6 @@ export const getClientTags = () => {
return [tag] return [tag]
} }
// DVMs
export type DVMRequestOpts = {
kind: number
input?: any
inputOpts?: string[]
tags?: string[][]
relays?: string[]
timeout?: number
onPublish?: (pub: Publish) => void
onProgress?: (e: TrustedEvent) => void
sk?: string
}
export const dvmRequest = async ({
kind,
tags = [],
timeout = 30_000,
relays = [],
onPublish = null,
onProgress = null,
sk = null,
}: DVMRequestOpts): Promise<TrustedEvent> => {
if (!sk && !signer.get().isEnabled()) {
sk = generatePrivateKey()
}
if (relays.length === 0) {
relays = hints.merge([hints.WriteRelays(), hints.fromRelays(env.get().DVM_RELAYS)]).getUrls()
}
tags = tags.concat([["expiration", String(now() + seconds(1, "hour"))]])
const pub = await createAndPublish({kind, relays, sk, tags, forcePlatform: false})
onPublish?.(pub)
return new Promise(resolve => {
const kinds = [kind + 1000]
if (onProgress) {
kinds.push(7000)
}
const sub = subscribe({
relays,
timeout,
filters: [
{
kinds,
since: now() - seconds(1, "minute"),
"#e": [pub.request.event.id],
},
],
onEvent: (e: TrustedEvent) => {
if (e.kind === 7000) {
onProgress?.(e)
} else {
resolve(e)
sub.close()
}
},
onComplete: () => {
resolve(null)
},
})
})
}
// Thread // Thread
const getAncestorIds = e => { const getAncestorIds = e => {
@ -2162,19 +2108,19 @@ class IndexedDBAdapter {
const removedRecords = prev.filter(r => !currentIds.has(r[key])) const removedRecords = prev.filter(r => !currentIds.has(r[key]))
if (newRecords.length > 0) { if (newRecords.length > 0) {
console.log("putting", name, newRecords.length, current.length)
await storage.bulkPut(name, newRecords) await storage.bulkPut(name, newRecords)
} }
if (removedRecords.length > 0) { if (removedRecords.length > 0) {
console.trace("deleting", name, removedRecords.length, current.length) if (name === "repository") {
console.trace("deleting", removedRecords.length, current.length)
}
await storage.bulkDelete(name, removedRecords.map(prop(key))) await storage.bulkDelete(name, removedRecords.map(prop(key)))
} }
// If we have much more than our limit, prune our store. This will get persisted // If we have much more than our limit, prune our store. This will get persisted
// the next time around. // the next time around.
if (current.length > limit * 1.5) { if (current.length > limit * 1.5) {
console.log("pruning", name, current.length)
set((sort ? sort(current) : current).slice(0, limit)) set((sort ? sort(current) : current).slice(0, limit))
} }