Use social petnames

This commit is contained in:
Jonathan Staab 2023-06-28 16:51:53 -07:00
parent 09fa5dd669
commit 82aab16605
14 changed files with 71 additions and 171 deletions

View File

@ -5,9 +5,7 @@ import {partition, uniqBy, sortBy, prop, always, pluck, without, is} from "ramda
import {throttle} from "throttle-debounce"
import {writable} from "svelte/store"
import {ensurePlural, noop, createMap} from "hurdak/lib/hurdak"
import {Tags} from "src/util/nostr"
import {fuzzy} from "src/util/misc"
import user from "src/agent/user"
const Adapter = window.indexedDB ? IncrementalIndexedDBAdapter : Loki.LokiMemoryAdapter
@ -164,11 +162,12 @@ type WatchStore<T> = Writable<T> & {
refresh: () => void
}
export const watch = (names, f) => {
names = ensurePlural(names)
export const watch = (namesOrTables, f) => {
namesOrTables = ensurePlural(namesOrTables)
const store = writable(null) as WatchStore<any>
const tables = names.map(name => (is(Table, name) ? name : registry[name]))
const tables = namesOrTables.map(name => (is(Table, name) ? name : registry[name]))
const names = pluck("name", tables)
// Initialize synchronously if possible
const initialValue = f(...tables)
@ -199,17 +198,7 @@ export const dropAll = () => new Promise(resolve => loki.deleteDatabase(resolve)
const sortByCreatedAt = sortBy(e => -e.created_at)
const sortByScore = sortBy(e => -e.score)
export const people = new Table("people", "pubkey", {
max: 3000,
// Don't delete the user's own profile or those of direct follows
sort: xs => {
const follows = Tags.wrap(user.getPetnames()).values().all()
const whitelist = new Set(follows.concat(user.getPubkey()))
return sortBy(x => (whitelist.has(x.pubkey) ? 0 : x.created_at), xs)
},
})
export const people = new Table("people", "pubkey", {max: 3000})
export const userEvents = new Table("userEvents", "id", {max: 2000, sort: sortByCreatedAt})
export const notifications = new Table("notifications", "id", {sort: sortByCreatedAt})
export const contacts = new Table("contacts", "pubkey")

View File

@ -138,19 +138,6 @@ addHandler(0, e => {
})
})
addHandler(3, e => {
const person = people.get(e.pubkey)
if (e.created_at < person?.petnames_updated_at) {
return
}
updatePerson(e.pubkey, {
petnames_updated_at: e.created_at,
petnames: e.tags.filter(t => t[0] === "p"),
})
})
// User profile, except for events also handled for other users
const userHandler = cb => e => {

View File

@ -1,19 +1,6 @@
import type {Relay, MyEvent} from "src/util/types"
import type {Readable} from "svelte/store"
import {
slice,
uniqBy,
without,
reject,
prop,
pipe,
assoc,
whereEq,
when,
concat,
nth,
map,
} from "ramda"
import {slice, uniqBy, without, reject, prop, pipe, assoc, whereEq, when, concat, map} from "ramda"
import {findReplyId, findRootId} from "src/util/nostr"
import {synced} from "src/util/misc"
import {derived, get} from "svelte/store"
@ -36,7 +23,6 @@ const profile = synced("agent/user/profile", {
},
rooms_joined: [],
last_checked: {},
petnames: [],
relays: pool.defaultRelays,
mutes: [],
lists: [],
@ -45,7 +31,6 @@ const profile = synced("agent/user/profile", {
const settings = derived(profile, prop("settings"))
const roomsJoined = derived(profile, prop("rooms_joined")) as Readable<string>
const lastChecked = derived(profile, prop("last_checked")) as Readable<Record<string, number>>
const petnames = derived(profile, prop("petnames")) as Readable<Array<Array<string>>>
const relays = derived(profile, p =>
pool.forceRelays.length > 0 ? pool.forceRelays : p.relays
) as Readable<Array<Relay>>
@ -110,13 +95,6 @@ export default {
this.setAppData("rooms_joined/v1", without([id], profileCopy.rooms_joined))
},
// Petnames
petnames,
getPetnames: () => profileCopy.petnames,
petnamePubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
getPetnamePubkeys: () => profileCopy.petnames.map(nth(1)),
// Relays
relays,

View File

@ -9,19 +9,20 @@
import {keys, social} from "src/system"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import user from "src/agent/user"
import {watch} from "src/agent/db"
import pool from "src/agent/pool"
import {addToList} from "src/app/state"
export let person
const npub = nip19.npubEncode(person.pubkey)
const {mutes} = user
const {canSign} = keys
const {petnamePubkeys, mutes} = user
const following = watch(social.graph, () => social.isUserFollowing(person.pubkey))
let actions = []
$: muted = find(m => m[1] === person.pubkey, $mutes)
$: following = $petnamePubkeys.includes(person.pubkey)
$: {
actions = []
@ -91,13 +92,13 @@
{#if $canSign}
<Popover triggerType="mouseenter">
<div slot="trigger">
{#if following}
{#if $following}
<i class="fa fa-user-minus cursor-pointer" on:click={unfollow} />
{:else if user.getPubkey() !== person.pubkey}
<i class="fa fa-user-plus cursor-pointer" on:click={follow} />
{/if}
</div>
<div slot="tooltip">{following ? "Unfollow" : "Follow"}</div>
<div slot="tooltip">{$following ? "Unfollow" : "Follow"}</div>
</Popover>
{/if}
<OverflowMenu {actions} />

View File

@ -6,26 +6,31 @@
import Anchor from "src/partials/Anchor.svelte"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
import {keys, social} from "src/system"
import {watch} from "src/agent/db"
import {social} from "src/system"
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
const {canSign} = keys
export let person
export let hasPetname = null
const unfollow = ({pubkey}) => social.unfollow(pubkey)
const unfollow = async ({pubkey}) => {
await social.unfollow(pubkey)
const follow = ({pubkey}) => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
social.follow(pubkey, url, displayPerson(person))
isFollowing = getIsFollowing()
}
const isFollowing = watch(social.graph, () =>
const follow = async ({pubkey}) => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
await social.follow(pubkey, url, displayPerson(person))
isFollowing = getIsFollowing()
}
const getIsFollowing = () =>
hasPetname ? hasPetname(person.pubkey) : social.isUserFollowing(person.pubkey)
)
// Set this manually to avoid a million listeners
let isFollowing = getIsFollowing()
</script>
<div in:fly={{y: 20}}>
@ -46,14 +51,12 @@
</div>
{/if}
</div>
{#if $canSign}
{#if isFollowing}
<Anchor theme="button-accent" stopPropagation on:click={() => unfollow(person)}>
Following
</Anchor>
{:else}
<Anchor theme="button" stopPropagation on:click={() => follow(person)}>Follow</Anchor>
{/if}
{#if isFollowing}
<Anchor theme="button-accent" stopPropagation on:click={() => unfollow(person)}>
Following
</Anchor>
{:else}
<Anchor theme="button" stopPropagation on:click={() => follow(person)}>Follow</Anchor>
{/if}
</div>
<p class="overflow-hidden text-ellipsis">

View File

@ -1,10 +1,10 @@
<script type="ts">
import {onMount} from "svelte"
import {uniq, sortBy, pluck} from "ramda"
import {Tags} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import PersonInfo from "src/app/shared/PersonInfo.svelte"
import {social} from "src/system"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import {getPersonWithFallback} from "src/agent/db"
import {watch} from "src/agent/db"
@ -15,14 +15,13 @@
let pubkeys = []
const person = getPersonWithFallback(pubkey)
const people = watch("people", t => {
return sortBy(p => (p.kind0 ? 0 : 1), pubkeys.map(getPersonWithFallback))
})
onMount(async () => {
if (type === "follows") {
pubkeys = Tags.wrap(person.petnames).values().all()
pubkeys = social.getFollows(pubkey)
people.refresh()
} else {
await network.load({

View File

@ -1,51 +0,0 @@
<script>
import {nth} from "ramda"
import {debounce} from "throttle-debounce"
import Input from "src/partials/Input.svelte"
import Spinner from "src/partials/Spinner.svelte"
import PersonInfo from "src/app/shared/PersonInfo.svelte"
import {sampleRelays} from "src/agent/relays"
import {searchPeople} from "src/agent/db"
import network from "src/agent/network"
import user from "src/agent/user"
export let hideFollows = false
let q
const {petnames} = user
const loadPeople = debounce(500, search => {
if (q.length > 2) {
network.load({
relays: sampleRelays([{url: "wss://relay.nostr.band"}]),
filter: [{kinds: [0], search, limit: 10}],
})
}
})
$: loadPeople(q)
$: results = $searchPeople(q)
.filter(person => {
if (person.pubkey === user.getPubkey()) {
return false
}
if (hideFollows && $petnames.map(nth(1)).includes(person.pubkey)) {
return false
}
return true
})
.slice(0, 50)
</script>
<Input bind:value={q} placeholder="Search for people">
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each results as person (person.pubkey)}
<PersonInfo {person} />
{:else}
<Spinner />
{/each}

View File

@ -4,12 +4,15 @@
import {tweened} from "svelte/motion"
import {numberFmt} from "src/util/misc"
import {modal} from "src/partials/state"
import {social} from "src/system"
import {watch} from "src/agent/db"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import pool from "src/agent/pool"
export let person
const followsCount = watch(social.graph, () => social.getFollowsSet(person.pubkey).size)
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
let followersCount = tweened(0, {interpolate, duration: 1000})
@ -47,13 +50,11 @@
})
</script>
{#if person?.petnames}
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
<strong>{person.petnames.length}</strong> following
</button>
<button on:click={showFollowers}>
<strong>{numberFmt.format($followersCount)}</strong> followers
</button>
</div>
{/if}
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
<strong>{$followsCount}</strong> following
</button>
<button on:click={showFollowers}>
<strong>{numberFmt.format($followersCount)}</strong> followers
</button>
</div>

View File

@ -14,11 +14,11 @@
export let pubkey
const {canSign} = keys
const {petnamePubkeys, mutes} = user
const following = watch(social.graph, () => social.isUserFollowing(pubkey))
const {mutes} = user
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
const person = watch("people", () => getPersonWithFallback(pubkey))
$: following = $petnamePubkeys.includes(pubkey)
$: muted = $mutes.map(nth(1)).includes(pubkey)
const follow = () => {
@ -58,7 +58,7 @@
{:else}
<i title="Mute" class="fa fa-microphone w-6 cursor-pointer text-center" on:click={mute} />
{/if}
{#if following}
{#if $following}
<i
title="Unfollow"
class="fa fa-user-minus w-6 cursor-pointer text-center"

View File

@ -3,7 +3,6 @@
import {generatePrivateKey} from "nostr-tools"
import {fly} from "src/util/transition"
import {navigate} from "svelte-routing"
import {shuffle} from "src/util/misc"
import {displayPerson} from "src/util/nostr"
import OnboardingIntro from "src/app/views/OnboardingIntro.svelte"
import OnboardingProfile from "src/app/views/OnboardingProfile.svelte"
@ -46,7 +45,7 @@
cmd.updateUser(profile).publish(user.getRelays()),
note && cmd.createNote(note).publish(user.getRelays()),
social.updatePetnames(
user.getPetnamePubkeys().map(pubkey => {
social.getUserFollows().map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
const name = displayPerson(getPersonWithFallback(pubkey))
@ -61,16 +60,11 @@
navigate("/notes")
}
// Prime our people cache for hardcoded follows and a sample of people they follow
onMount(async () => {
const relays = sampleRelays(user.getRelays())
const follows = user.getPetnamePubkeys().concat(DEFAULT_FOLLOWS)
await network.loadPeople(follows, {relays})
const others = shuffle(social.getNetwork(follows)).slice(0, 256)
await network.loadPeople(others, {relays})
onMount(() => {
// Prime our database with some defaults
network.loadPeople(DEFAULT_FOLLOWS, {
relays: sampleRelays(user.getRelays()),
})
})
</script>

View File

@ -7,14 +7,13 @@
import Content from "src/partials/Content.svelte"
import PersonInfo from "src/app/shared/PersonInfo.svelte"
import {DEFAULT_FOLLOWS, social} from "src/system"
import {getPersonWithFallback, searchPeople} from "src/agent/db"
import {watch, getPersonWithFallback, searchPeople} from "src/agent/db"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import {modal} from "src/partials/state"
import user from "src/agent/user"
const {petnamePubkeys} = user
const follows = watch(social.graph, social.getUserFollowsSet)
if ($petnamePubkeys.length === 0) {
if ($follows.size === 0) {
social.updatePetnames(
DEFAULT_FOLLOWS.map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
@ -27,7 +26,7 @@
let q = ""
$: results = reject(p => $petnamePubkeys.includes(p.pubkey), $searchPeople(q))
$: results = reject(p => $follows.has(p.pubkey), $searchPeople(q))
</script>
<Content>
@ -47,13 +46,13 @@
<i class="fa fa-user-astronaut fa-lg" />
<h2 class="staatliches text-2xl">Your follows</h2>
</div>
{#if $petnamePubkeys.length === 0}
{#if $follows.size === 0}
<div class="mt-8 flex items-center justify-center gap-2 text-center">
<i class="fa fa-triangle-exclamation" />
<span>No follows selected</span>
</div>
{:else}
{#each $petnamePubkeys as pubkey}
{#each Array.from($follows) as pubkey}
<PersonInfo person={getPersonWithFallback(pubkey)} />
{/each}
{/if}

View File

@ -5,16 +5,14 @@
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import ContentEditable from "src/partials/ContentEditable.svelte"
import Suggestions from "src/partials/Suggestions.svelte"
import {social} from "src/system"
import {searchPeople} from "src/agent/db"
import user from "src/agent/user"
import {getPubkeyWriteRelays} from "src/agent/relays"
export let onSubmit
let contenteditable, suggestions
const {petnamePubkeys} = user
const pubkeyEncoder = {
encode: pubkey => {
const relays = pluck("url", getPubkeyWriteRelays(pubkey))
@ -32,7 +30,7 @@
let results = []
if (word.length > 1 && word.startsWith("@")) {
const [followed, notFollowed] = partition(
p => $petnamePubkeys.includes(p.pubkey),
p => social.isUserFollowing(p.pubkey),
$searchPeople(word.slice(1))
)

View File

@ -37,14 +37,12 @@ export default ({keys, sync, cmd, getUserWriteRelays}) => {
const getPetnames = pubkey => graph.get(pubkey)?.petnames || []
const getPetnamePubkeys = pubkey => getPetnames(pubkey).map(t => t[1])
const getFollowsSet = pubkeys => {
const follows = new Set()
for (const pubkey of ensurePlural(pubkeys)) {
for (const follow of getPetnamePubkeys(pubkey)) {
follows.add(follow)
for (const tag of getPetnames(pubkey)) {
follows.add(tag[1])
}
}
@ -72,7 +70,9 @@ export default ({keys, sync, cmd, getUserWriteRelays}) => {
const getUserKey = () => keys.getPubkey() || "anonymous"
const getUserPetnames = () => getPetnames(getUserKey())
const getUserFollowsSet = () => getFollowsSet(getUserKey())
const getUserFollows = () => getFollows(getUserKey())
const getUserNetworkSet = () => getNetworkSet(getUserKey())
const getUserNetwork = () => getNetwork(getUserKey())
const isUserFollowing = pubkey => isFollowing(getUserKey(), pubkey)
@ -100,12 +100,15 @@ export default ({keys, sync, cmd, getUserWriteRelays}) => {
return {
graph,
getPetnames,
getPetnamePubkeys,
getFollowsSet,
getFollows,
getNetworkSet,
getNetwork,
isFollowing,
getUserPetnames,
getUserFollowsSet,
getUserFollows,
getUserNetworkSet,
getUserNetwork,
isUserFollowing,
updatePetnames,

View File

@ -9,7 +9,6 @@ export type Relay = {
export type Person = {
pubkey: string
petnames?: Array<Array<string>>
relays?: Array<Relay>
mutes?: Array<Array<string>>
kind0?: {