Nest profile data under kind0 to avoid nuking stuff we don't support

This commit is contained in:
Jonathan Staab 2023-02-17 16:39:37 -06:00
parent 545f13e0b5
commit d4aaf98985
25 changed files with 230 additions and 150 deletions

View File

@ -1,18 +1,17 @@
# Current
- [ ] Fix anon/new user experience
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
- [ ] Separate user info and relays so we can still select/figure out relays for anons
- [ ] Separate petnames out as well so anons can follow people
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
- nip07.getRelays, nip05, relay.nostr.band
- [ ] Fix bugs on bugsnag
- [ ] Fix profile merging, put kind0 on its own property so we're not messing other people's profile data up.
- [ ] Fix anon/new user experience
- [ ] When logging in rather than generating a new keypair, ask for a relay to bootstrap from
- [ ] Preload/wait for our big list of relays so we can offer suggestions. Search in the background and let them know if we found their profile.
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
- [ ] Test publishing events with zero relays
- [ ] Try lumping tables into a single key each to reduce load/save contention and time
- [ ] Fix turning off link previews, or make sure it applies to images/videos too
# Snacks
- [ ] open web+nostr links like snort
- [ ] DM/chat read status in encrypted note
- [ ] Relay recommendations based on follows/followers
- [ ] Pinned posts ala snort
@ -76,3 +75,4 @@
- [ ] Add notifications for chat messages
- [ ] Compress events
- https://github.com/nostr-protocol/nips/issues/265#issuecomment-1434250263
- [ ] If you hide something, but the event doesn't get retrived, it gets un-hidden

View File

@ -60,7 +60,7 @@
import Search from "src/routes/Search.svelte"
import Settings from "src/routes/Settings.svelte"
Object.assign(window, {cmd, database, keys, network, pool, sync})
Object.assign(window, {cmd, database, user, keys, network, pool, sync})
export let url = ""
@ -214,7 +214,7 @@
<TopNav />
{#if $modal}
<Modal onEscape={closeModal}>
<Modal onEscape={$modal.noEscape ? null : closeModal}>
{#if $modal.type === 'note/detail'}
{#key $modal.note.id}
<NoteDetail {...$modal} />
@ -256,12 +256,11 @@
{#if $toast}
<div
class="fixed top-0 left-0 right-0 z-10"
transition:fly={{y: -50, duration: 300}}
>
class="fixed top-0 left-0 right-0 z-10 click-events-none"
transition:fly={{y: -50, duration: 300}}>
<div
class="rounded bg-accent shadow-xl mx-24 sm:mx-32 mt-2 p-3 text-white text-center border border-dark"
>
class="rounded bg-accent shadow-xl mx-24 sm:mx-32 mt-2 p-3 text-white text-center
border border-dark click-events-all">
{#if is(String, $toast.message)}
{$toast.message}
{:else}

View File

@ -316,7 +316,13 @@ const watch = (names, f) => {
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
const dropAll = () => Promise.all(Object.values(registry).map(t => t.drop()))
const dropAll = async () => {
for (const table of Object.values(registry)) {
await table.drop()
log(`Successfully dropped table ${table.name}`)
}
}
const ready = derived(pluck('ready', Object.values(registry)), all(identity))

View File

@ -82,7 +82,7 @@ const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any =
}) as Promise<void>
}
const loadPeople = async (pubkeys, {kinds = personKinds, force = false, ...opts} = {}) => {
const loadPeople = async (pubkeys, {relays = null, kinds = personKinds, force = false, ...opts} = {}) => {
pubkeys = uniq(pubkeys)
// If we're not reloading, only get pubkeys we don't already know about
@ -90,19 +90,23 @@ const loadPeople = async (pubkeys, {kinds = personKinds, force = false, ...opts}
pubkeys = getStalePubkeys(pubkeys)
}
// Use the best relays we have, but fall back to user relays
const relays = getAllPubkeyWriteRelays(pubkeys)
.concat(getUserReadRelays())
.slice(0, 3)
await Promise.all(
chunk(256, pubkeys).map(async chunk => {
// Use the best relays we have, but fall back to user relays
const chunkRelays = relays || (
getAllPubkeyWriteRelays(chunk)
.concat(getUserReadRelays())
.slice(0, 3)
)
if (pubkeys.length > 0) {
await load(relays, {kinds, authors: pubkeys}, opts)
}
await load(chunkRelays, {kinds, authors: chunk}, opts)
})
)
}
const loadParents = notes => {
const notesWithParent = notes.filter(findReplyId)
const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent))
const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent)).slice(0, 3)
return load(relays, {kinds: [1], ids: notesWithParent.map(findReplyId)})
}

View File

@ -1,10 +1,9 @@
import {get} from 'svelte/store'
import type {Relay} from 'src/util/types'
import {pick, map, assoc, sortBy, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {Tags, findReplyId} from 'src/util/nostr'
import database from 'src/agent/database'
import user from 'src/agent/user'
import keys from 'src/agent/keys'
// From Mike Dilger:
// 1) Other people's write relays — pull events from people you follow,
@ -49,7 +48,7 @@ export const getUserRelays = () =>
export const getUserReadRelays = () =>
getUserRelays().filter(prop('read')).map(pick(['url', 'score']))
export const getUserWriteRelays = () =>
export const getUserWriteRelays = (): Array<Relay> =>
getUserRelays().filter(prop('write')).map(pick(['url', 'score']))
// Event-related special cases
@ -85,14 +84,9 @@ export const getRelayForPersonHint = (pubkey, event) =>
// our followers, so write to our write relays as well.
export const getEventPublishRelays = event => {
const tags = Tags.from(event)
const pubkeys = tags.type("p").values().all()
const pubkeys = tags.type("p").values().all().concat(event.pubkey)
return uniqByUrl(
pubkeys
.concat(event.pubkey)
.concat(get(keys.pubkey))
.flatMap(getPubkeyReadRelays)
)
return getAllPubkeyReadRelays(pubkeys).concat(getUserWriteRelays())
}

View File

@ -3,7 +3,7 @@ import {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {warn, log} from 'src/util/logger'
import {now, timedelta, shuffle, hash} from 'src/util/misc'
import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr'
import {Tags, personKinds, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database'
const processEvents = async events => {
@ -17,7 +17,7 @@ const processEvents = async events => {
const processProfileEvents = async events => {
const profileEvents = ensurePlural(events)
.filter(e => [0, 3, 12165].includes(e.kind))
.filter(e => personKinds.includes(e.kind))
const updates = {}
for (const e of profileEvents) {
@ -29,19 +29,22 @@ const processProfileEvents = async events => {
...switcherFn(e.kind, {
0: () => {
return tryJson(() => {
const content = JSON.parse(e.content)
const kind0 = JSON.parse(e.content)
// Fire off a nip05 verification
if (content.nip05 && e.created_at > (person.nip05_updated_at || 0)) {
verifyNip05(e.pubkey, content.nip05)
if (e.created_at > (person.kind0_updated_at || 0)) {
if (kind0.nip05) {
verifyNip05(e.pubkey, kind0.nip05)
content.nip05_updated_at = e.created_at
}
kind0.nip05_updated_at = e.created_at
}
if (e.created_at > (person.profile_updated_at || 0)) {
return {
...content,
profile_updated_at: e.created_at,
kind0: {
...person?.kind0,
...updates[e.pubkey]?.kind0,
...kind0,
},
kind0_updated_at: e.created_at,
}
}
})
@ -271,6 +274,10 @@ const processRoutes = async events => {
updates.push(
calculateRoute(pubkey, url, 'tag', 'write', e.created_at)
)
updates.push(
calculateRoute(pubkey, url, 'tag', 'read', e.created_at)
)
})
})
}
@ -279,7 +286,7 @@ const processRoutes = async events => {
if (!isEmpty(updates)) {
await database.relays.bulkPatch(createMap('url', updates.map(pick(['url']))))
await database.routes.bulkPut(createMap('id', updates.filter(identity)))
await database.routes.bulkPut(createMap('id', updates))
}
}

View File

@ -2,7 +2,7 @@ import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {pipe, assoc, whereEq, when, concat, reject, nth, map} from 'ramda'
import {synced} from 'src/util/misc'
import {derived, get} from 'svelte/store'
import {derived} from 'svelte/store'
import database from 'src/agent/database'
import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
@ -28,28 +28,35 @@ const profile = derived(
return null
}
return profileCopy = ($people[pubkey] || {pubkey})
return $people[pubkey] || {pubkey}
}
) as Readable<Person>
const petnames = derived(
[profile, anonPetnames],
([$profile, $anonPetnames]) => {
return petnamesCopy = $profile?.petnames || $anonPetnames
}
([$profile, $anonPetnames]) =>
$profile?.petnames || $anonPetnames
)
const relays = derived(
[profile, anonRelays],
([$profile, $anonRelays]) => {
return relaysCopy = $profile?.relays || $anonRelays
}
([$profile, $anonRelays]) =>
$profile?.relays || $anonRelays
)
// Prime our copies
get(profile)
get(petnames)
get(relays)
// Keep our copies up to date
profile.subscribe($profile => {
profileCopy = $profile
})
petnames.subscribe($petnames => {
petnamesCopy = $petnames
})
relays.subscribe($relays => {
relaysCopy = $relays
})
const user = {
// Profile

View File

@ -1,7 +1,7 @@
import lf from 'localforage'
import memoryStorageDriver from 'localforage-memoryStorageDriver'
import {switcherFn} from 'hurdak/lib/hurdak'
import {error} from 'src/util/logger'
import {error, warn} from 'src/util/logger'
import {where} from 'src/util/misc'
// Firefox private mode doesn't have access to any storage options
@ -15,6 +15,12 @@ const getStore = storeName => {
stores[storeName] = lf.createInstance({name: 'coracle', storeName})
}
if (stores[storeName].dropped) {
warn(`Dropped instance ${storeName} was requested`)
return null
}
return stores[storeName]
}
@ -26,43 +32,52 @@ addEventListener('message', async ({data: {topic, payload, channel}}) => {
const {storeName, method, args} = payload
const instance = getStore(storeName)
const result = await switcherFn(method, {
dump: () => new Promise(resolve => {
const result = {}
if (instance) {
const result = await switcherFn(method, {
dump: () => new Promise(resolve => {
const result = {}
instance.iterate(
(v, k, i) => { result[k] = v },
() => resolve(result),
)
}),
setItems: () => {
for (const [k, v] of Object.entries(args[0])) {
instance.setItem(k, v)
}
},
removeItems: () => {
for (const k of args[0]) {
instance.removeItem(k)
}
},
default: () => instance[method](...args),
})
instance.iterate(
(v, k, i) => { result[k] = v },
() => resolve(result),
)
}),
setItems: async () => {
for (const [k, v] of Object.entries(args[0])) {
await instance.setItem(k, v)
}
},
removeItems: async () => {
for (const k of args[0]) {
await instance.removeItem(k)
}
},
drop: async () => {
instance.dropped = true
await instance.drop()
},
default: () => instance[method](...args),
})
reply('localforage.return', result)
reply('localforage.return', result)
}
},
'localforage.iterate': async () => {
const matchesFilter = where(payload.where)
const instance = getStore(payload.storeName)
getStore(payload.storeName).iterate(
(v, k, i) => {
if (matchesFilter(v)) {
reply('localforage.item', {v, k, i})
}
},
() => {
reply('localforage.iterationComplete')
},
)
if (instance) {
instance.iterate(
(v, k, i) => {
if (matchesFilter(v)) {
reply('localforage.item', {v, k, i})
}
},
() => {
reply('localforage.iterationComplete')
},
)
}
},
default: () => {
throw new Error(`invalid topic: ${topic}`)

View File

@ -1,8 +1,10 @@
import type {DisplayEvent} from 'src/util/types'
import {omit, sortBy, identity} from 'ramda'
import {objOf, omit, sortBy, identity} from 'ramda'
import {get} from 'svelte/store'
import {navigate} from 'svelte-routing'
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {renderContent} from 'src/util/html'
import {shuffle} from 'src/util/misc'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {getNetwork} from 'src/agent/social'
import {getUserReadRelays} from 'src/agent/relays'
@ -11,7 +13,7 @@ import network from 'src/agent/network'
import keys from 'src/agent/keys'
import alerts from 'src/app/alerts'
import messages from 'src/app/messages'
import {routes, modal} from 'src/app/ui'
import {routes, settings, modal} from 'src/app/ui'
export const loadAppData = async pubkey => {
if (getUserReadRelays().length > 0) {
@ -31,14 +33,32 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
keys.setPublicKey(pubkey)
}
modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
modal.set({
type: 'message',
message: "Loading your profile data...",
spinner: true,
noEscape: true,
})
// Get a reasonably sized sample of relays and ask them all for relay information
// for our user so we can bootstrap. This could be improved.
let relays = []
try {
relays = (
await fetch(get(settings).dufflepudUrl + '/relay').then(r => r.json())
).relays.map(objOf('url'))
} catch (e) {
relays = database.relays.all()
}
// Load our user so we can populate network and show profile info
await network.loadPeople([pubkey], {
relays: shuffle(relays).slice(0, 50),
})
if (getUserReadRelays().length === 0) {
navigate('/relays')
} else {
// Load our user so we can populate network and show profile info
await network.loadPeople([pubkey])
// Load network and start listening, but don't wait for it
loadAppData(pubkey)

View File

@ -11,7 +11,7 @@
{#if inert}
<span class="flex gap-2 items-center relative z-10">
<ImageCircle src={person.picture} />
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</span>
{:else}
@ -19,7 +19,7 @@
to={routes.person(person.pubkey)}
class="flex gap-2 items-center relative z-10"
on:click={killEvent}>
<ImageCircle src={person.picture} />
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</Link>
{/if}

View File

@ -17,7 +17,7 @@
let input = null
let prevContent = ''
const search = fuzzy(database.people.all({'name:!nil': null}), {keys: ["name", "pubkey"]})
const search = fuzzy(database.people.all(), {keys: ["kind0.name", "pubkey"]})
const getText = () => {
const selection = document.getSelection()

View File

@ -30,9 +30,12 @@
<slot />
</div>
{#if onEscape}
<div class="absolute top-0 flex w-full justify-end pr-2 -mt-8">
<div class="w-10 h-10 flex justify-center items-center bg-accent rounded-full cursor-pointer border border-solid border-medium border-b-0">
<i class="fa fa-times fa-lg cursor-pointer" on:click={onEscape} />
<div class="absolute top-0 flex w-full justify-end pr-2 -mt-8 pointer-events-none">
<div
class="pointer-events-auto w-10 h-10 flex justify-center items-center bg-accent
rounded-full cursor-pointer border border-solid border-medium border-b-0"
on:click={onEscape}>
<i class="fa fa-times fa-lg" />
</div>
</div>
{/if}

View File

@ -103,7 +103,8 @@
}
const deleteReaction = e => {
cmd.deleteEvent(getEventPublishRelays(note), [e.id])
const relays = getEventPublishRelays(note)
cmd.deleteEvent(relays, [e.id])
if (e.content === '+') {
likes = reject(propEq('pubkey', $profile.pubkey), likes)
@ -208,7 +209,7 @@
<div class="absolute h-px w-3 bg-light z-10" style="left: 0px; top: 27px;" />
{/if}
<Anchor class="text-lg font-bold" href={routes.person($person.pubkey)}>
<ImageCircle size={10} src={$person.picture} />
<ImageCircle size={10} src={$person.kind0?.picture} />
</Anchor>
<div class="flex flex-col gap-2 flex-grow min-w-0">
<div class="flex items-center justify-between">

View File

@ -16,7 +16,7 @@
const addPetname = pubkey => {
const [{url}] = getPubkeyWriteRelays(pubkey)
user.addPetname(pubkey, url, person.name)
user.addPetname(pubkey, url, displayPerson(person))
}
</script>
@ -26,7 +26,7 @@
class="flex gap-4 border-l-2 border-solid border-dark hover:bg-black hover:border-accent transition-all py-3 px-6 overflow-hidden">
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.picture})" />
style="background-image: url({person.kind0?.picture})" />
<div class="flex-grow flex flex-col gap-4 min-w-0">
<div class="flex gap-2 items-start justify-between">
<div class="flex flex-col gap-2">
@ -49,7 +49,7 @@
{/if}
</div>
<p class="overflow-hidden text-ellipsis">
{@html renderContent(ellipsize(person.about || '', 140))}
{@html renderContent(ellipsize(person.kind0?.about || '', 140))}
</p>
</div>
</a>

View File

@ -1,7 +1,6 @@
<script>
import {pluck} from 'ramda'
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import database from 'src/agent/database'
@ -17,7 +16,7 @@
const joined = new Set(pluck('url', $relays))
search = fuzzy(
$knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
$knownRelays.filter(r => !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}

View File

@ -21,7 +21,7 @@
<a href={routes.person($profile.pubkey)} class="flex gap-2 px-4 py-2 pb-6 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$profile.picture})" />
style="background-image: url({$profile.kind0?.picture})" />
<span class="text-lg font-bold">{displayPerson($profile)}</span>
</a>
</li>

View File

@ -24,11 +24,9 @@
[{kinds: [40, 41], ids: [roomId]},
{kinds: [42], '#e': [roomId], since: now()}],
events => {
const newMessages = events.filter(e => e.kind === 42)
network.loadPeople(pluck('pubkey', events))
network.loadPeople(relays, pluck('pubkey', events))
cb(newMessages)
cb(events.filter(e => e.kind === 42))
}
)
}
@ -37,9 +35,7 @@
const relays = getRelaysForEventChildren($room)
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
if (events.length) {
await network.loadPeople(relays, pluck('pubkey', events))
}
network.loadPeople(pluck('pubkey', events))
return events
}

View File

@ -3,13 +3,17 @@
import Anchor from 'src/partials/Anchor.svelte'
import Content from "src/partials/Content.svelte"
import database from 'src/agent/database'
import pool from 'src/agent/pool'
let confirmed = false
const confirm = async () => {
confirmed = true
await database.dropAll()
await Promise.all([
...pool.getConnections().map(c => c.disconnect()),
database.dropAll(),
])
localStorage.clear()

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {sortBy, pluck} from 'ramda'
import {personKinds} from 'src/util/nostr'
import {personKinds, displayPerson} from 'src/util/nostr'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import user from 'src/agent/user'
@ -68,9 +68,9 @@
<Channel
type="dm"
name={$person?.name}
about={$person?.about}
picture={$person?.picture}
name={displayPerson($person)}
about={$person?.kind0?.about}
picture={$person?.kind0?.picture}
link={$person ? routes.person($person.pubkey) : null}
{loadMessages}
{listenForMessages}

View File

@ -92,7 +92,7 @@
}
const follow = async () => {
user.addPetname(pubkey, prop('url', first(relays)), person.name)
user.addPetname(pubkey, prop('url', first(relays)), displayPerson(person))
}
const unfollow = async () => {
@ -114,13 +114,13 @@
background-size: cover;
background-image:
linear-gradient(to bottom, rgba(0, 0, 0, 0.3), #0f0f0e),
url('{person.banner}')" />
url('{person.kind0?.banner}')" />
<Content>
<div class="flex gap-4" in:fly={{y: 20}}>
<div
class="overflow-hidden w-16 h-16 sm:w-32 sm:h-32 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.picture})" />
style="background-image: url({person.kind0?.picture})" />
<div class="flex flex-col gap-4 flex-grow">
<div class="flex justify-between items-center gap-4">
<div class="flex-grow flex flex-col gap-2">
@ -159,7 +159,7 @@
</Anchor>
</div>
</div>
<p>{@html renderContent(person.about || '')}</p>
<p>{@html renderContent(person?.kind0?.about || '')}</p>
{#if person?.petnames}
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
@ -176,7 +176,7 @@
<Tabs tabs={['notes', 'likes', 'relays']} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'}
<Notes {pubkey} {relays} />
<Notes {pubkey} />
{:else if activeTab === 'likes'}
<Likes {pubkey} />
{:else if activeTab === 'relays'}

View File

@ -2,9 +2,7 @@
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import pick from "ramda/src/pick"
import {error} from "src/util/logger"
import {stripExifData} from "src/util/html"
// import {stripExifData} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Anchor from "src/partials/Anchor.svelte"
@ -16,7 +14,7 @@
import cmd from "src/agent/cmd"
import {routes, toast} from "src/app/ui"
let values = {picture: null, about: null, name: null, nip05: null}
let values = user.getProfile().kind0 || {}
const nip05Url = "https://github.com/nostr-protocol/nips/blob/master/05.md"
const pseudUrl = "https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
@ -26,8 +24,7 @@
return navigate("/login")
}
values = pick(Object.keys(values), user.getProfile())
/*
document.querySelector('[name=picture]').addEventListener('change', async e => {
const target = e.target as HTMLInputElement
const [file] = target.files
@ -41,6 +38,7 @@
values.picture = null
}
})
*/
})
const submit = async event => {
@ -93,10 +91,26 @@
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Profile Image</strong>
<input type="file" name="picture" />
<strong>Profile Picture</strong>
<!-- <input type="file" name="picture" />
<p class="text-sm text-light">
Your profile image will have all metadata removed before being published.
</p> -->
<Input type="text" name="picture" wrapperClass="flex-grow" bind:value={values.picture}>
<i slot="before" class="fa fa-image-portrait" />
</Input>
<p class="text-sm text-light">
Enter a url to your profile image - please be mindful of others and
only use small images.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Profile Banner</strong>
<Input type="text" name="banner" wrapperClass="flex-grow" bind:value={values.banner}>
<i slot="before" class="fa fa-panorama" />
</Input>
<p class="text-sm text-light">
Enter a url to your profile's banner image.
</p>
</div>
<Button type="submit" class="text-center">Done</Button>

View File

@ -55,15 +55,15 @@ export const findRoot = e =>
export const findRootId = e => Tags.wrap([findRoot(e)]).values().first()
export const displayPerson = p => {
if (p.display_name) {
return ellipsize(p.display_name, 60)
if (p.kind0?.display_name) {
return ellipsize(p.kind0?.display_name, 60)
}
if (p.name) {
return ellipsize(p.name, 60)
if (p.kind0?.name) {
return ellipsize(p.kind0?.name, 60)
}
return nip19.npubEncode(p.pubkey).slice(4, 12)
return nip19.npubEncode(p.pubkey).slice(-8)
}
export const isLike = content => ['', '+', '🤙', '👍', '❤️'].includes(content)
@ -82,7 +82,17 @@ export const isAlert = (e, pubkey) => {
return true
}
export const isRelay = url => typeof url === 'string' && url.match(/^wss?:\/\/.+/)
export const isRelay = url => (
typeof url === 'string'
// It should have the protocol included
&& url.match(/^wss?:\/\/.+/)
// Don't match stuff with a port number
&& !url.slice(6).match(/:\d+/)
// Don't match raw ip addresses
&& !url.slice(6).match(/\d+\.\d+\.\d+\.\d+/)
// Skip nostr.wine's virtual relays
&& !url.slice(6).match(/\/npub/)
)
export const normalizeRelayUrl = url => url.replace(/\/+$/, '')

View File

@ -9,10 +9,15 @@ export type Relay = {
export type Person = {
pubkey: string
picture?: string
relays?: Array<Relay>
muffle?: Array<Array<string>>
petnames?: Array<Array<string>>
kind0?: {
name?: string
about?: string
nip05?: string
picture?: string
},
}

View File

@ -5,7 +5,7 @@
import {last, reject, pluck, propEq} from 'ramda'
import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import {isRelay, displayPerson} from "src/util/nostr"
import Button from "src/partials/Button.svelte"
import Compose from "src/partials/Compose.svelte"
import Input from "src/partials/Input.svelte"
@ -75,10 +75,8 @@
onMount(() => {
const person = database.people.get(pubkey)
if (person?.name) {
input.type('@' + person.name)
input.trigger({key: 'Enter'})
}
input.type('@' + displayPerson(person))
input.trigger({key: 'Enter'})
})
</script>

View File

@ -1,16 +1,14 @@
<script type="ts">
import Content from 'src/partials/Content.svelte'
import PersonInfo from 'src/partials/PersonInfo.svelte'
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
export let pubkeys
const relays = getAllPubkeyWriteRelays(pubkeys)
const people = database.watch('people', people => people.all({pubkey: pubkeys}))
network.loadPeople(relays, pubkeys)
network.loadPeople(pubkeys)
</script>
<Content gap={2}>