mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 00:10:52 +00:00
Add profile search
This commit is contained in:
parent
0d44094e02
commit
606a707e3d
@ -6,13 +6,13 @@
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import ContentEditable from "src/partials/ContentEditable.svelte"
|
||||
import Suggestions from "src/partials/Suggestions.svelte"
|
||||
import {displayProfile} from "src/domain"
|
||||
import {
|
||||
hints,
|
||||
follows,
|
||||
derivePerson,
|
||||
displayPerson,
|
||||
searchPeople,
|
||||
profileSearch,
|
||||
createPeopleLoader,
|
||||
getProfileByPubkey,
|
||||
} from "src/engine"
|
||||
|
||||
export let onSubmit
|
||||
@ -45,8 +45,8 @@
|
||||
let results = []
|
||||
if (word.length > 1 && word.startsWith("@")) {
|
||||
const [followed, notFollowed] = partition(
|
||||
p => $follows.has(p.pubkey),
|
||||
$searchPeople(word.slice(1)),
|
||||
profile => $follows.has(profile.event.pubkey),
|
||||
$profileSearch.searchOptions(word.slice(1)),
|
||||
)
|
||||
|
||||
results = followed.concat(notFollowed)
|
||||
@ -64,7 +64,7 @@
|
||||
return {selection, node, offset, word}
|
||||
}
|
||||
|
||||
const autocomplete = ({person = null, force = false} = {}) => {
|
||||
const autocomplete = ({profile = null, force = false} = {}) => {
|
||||
let completed = false
|
||||
|
||||
const {selection, node, offset, word} = getInfo()
|
||||
@ -98,8 +98,8 @@
|
||||
}
|
||||
|
||||
// Mentions
|
||||
if ((force || word.length > 1) && word.startsWith("@") && person) {
|
||||
annotate("@", displayPerson(person).trim(), pubkeyEncoder.encode(person.pubkey))
|
||||
if ((force || word.length > 1) && word.startsWith("@") && profile) {
|
||||
annotate("@", displayProfile(profile).trim(), pubkeyEncoder.encode(profile.event.pubkey))
|
||||
}
|
||||
|
||||
// Topics
|
||||
@ -130,7 +130,7 @@
|
||||
|
||||
// Enter adds a newline, so do it on key down
|
||||
if (["Enter"].includes(e.code)) {
|
||||
autocomplete({person: suggestions.get()})
|
||||
autocomplete({profile: suggestions.get()})
|
||||
}
|
||||
|
||||
// Only autocomplete topics on space
|
||||
@ -149,7 +149,7 @@
|
||||
applySearch(word)
|
||||
|
||||
if (["Tab"].includes(e.code)) {
|
||||
autocomplete({person: suggestions.get()})
|
||||
autocomplete({profile: suggestions.get()})
|
||||
}
|
||||
|
||||
if (["Escape", "Space"].includes(e.code)) {
|
||||
@ -179,7 +179,7 @@
|
||||
selection.getRangeAt(0).insertNode(spaceNode)
|
||||
selection.collapse(input, 1)
|
||||
|
||||
autocomplete({person: derivePerson(pubkey).get(), force: true})
|
||||
autocomplete({profile: getProfileByPubkey(pubkey), force: true})
|
||||
}
|
||||
|
||||
const createNewLines = (n = 1) => {
|
||||
@ -269,7 +269,7 @@
|
||||
|
||||
<Suggestions
|
||||
bind:this={suggestions}
|
||||
select={person => autocomplete({person})}
|
||||
select={profile => autocomplete({profile})}
|
||||
loading={$loadingPeople}>
|
||||
<div slot="item" let:item>
|
||||
<PersonBadge inert pubkey={item.pubkey} />
|
||||
|
@ -3,7 +3,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import {searchPubkeys, displayPersonByPubkey} from "src/engine"
|
||||
import {profileSearch, displayPersonByPubkey} from "src/engine"
|
||||
import {router} from "src/app/util/router"
|
||||
|
||||
export let feed
|
||||
@ -14,7 +14,7 @@
|
||||
<SearchSelect
|
||||
multiple
|
||||
value={feed.slice(2)}
|
||||
search={$searchPubkeys}
|
||||
search={$profileSearch.search}
|
||||
onChange={pubkeys => onChange([FeedType.Tag, "#p", ...pubkeys])}>
|
||||
<span slot="item" let:item let:context>
|
||||
{#if context === "value"}
|
||||
|
@ -4,7 +4,8 @@
|
||||
import {createScroller} from "src/util/misc"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import PersonSummary from "src/app/shared/PersonSummary.svelte"
|
||||
import {loadPubkeys, derivePerson, personHasName} from "src/engine"
|
||||
import {profileHasName} from "src/domain"
|
||||
import {loadPubkeys, getProfileByPubkey} from "src/engine"
|
||||
|
||||
export let pubkeys
|
||||
|
||||
@ -15,7 +16,7 @@
|
||||
limit += 10
|
||||
}
|
||||
|
||||
const hasName = pubkey => personHasName(derivePerson(pubkey).get())
|
||||
const hasName = pubkey => profileHasName(getProfileByPubkey(pubkey))
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller(loadMore, {
|
||||
|
@ -5,7 +5,7 @@
|
||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import {router} from "src/app/util/router"
|
||||
import {searchPubkeys, createPeopleLoader, displayPersonByPubkey} from "src/engine"
|
||||
import {profileSearch, createPeopleLoader, displayPersonByPubkey} from "src/engine"
|
||||
|
||||
export let value
|
||||
export let multiple = false
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
return $searchPubkeys(term)
|
||||
return $profileSearch.search(term)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
import {parseAnything} from "src/util/nostr"
|
||||
import {router} from "src/app/util/router"
|
||||
import type {Person, Topic} from "src/engine"
|
||||
import {topics, searchPeople, createPeopleLoader} from "src/engine"
|
||||
import {topics, profileSearch, createPeopleLoader} from "src/engine"
|
||||
|
||||
export let term
|
||||
export let replace = false
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
const openTopic = topic => router.at("topics").of(topic).open({replace})
|
||||
|
||||
const openPerson = pubkey => router.at("people").of(pubkey).open({replace})
|
||||
const openProfile = pubkey => router.at("people").of(pubkey).open({replace})
|
||||
|
||||
const onClick = result => {
|
||||
if (result.type === "topic") {
|
||||
@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
if (result.type === "profile") {
|
||||
openPerson(result.id)
|
||||
openProfile(result.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,8 +52,8 @@
|
||||
.derived($topics => fuzzy($topics, {keys: ["name"], threshold: 0.5, shouldSort: true}))
|
||||
|
||||
const results = derived<{type: string; id: string; person?: Person; topic?: Topic}[]>(
|
||||
[term, searchTopics, searchPeople],
|
||||
([$term, $searchTopics, $searchPeople]) => {
|
||||
[term, searchTopics, profileSearch],
|
||||
([$term, $searchTopics, $profileSearch]) => {
|
||||
$term = $term || ""
|
||||
|
||||
if ($term.length > 30) {
|
||||
@ -62,7 +62,9 @@
|
||||
|
||||
return $term.startsWith("#")
|
||||
? $searchTopics($term.slice(1)).map(topic => ({type: "topic", id: topic.name, topic}))
|
||||
: $searchPeople($term).map(person => ({type: "profile", id: person.pubkey, person}))
|
||||
: $profileSearch
|
||||
.searchOptions($term)
|
||||
.map(profile => ({type: "profile", id: profile.event.pubkey, profile}))
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
import PersonSummary from "src/app/shared/PersonSummary.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import type {Relay} from "src/engine"
|
||||
import {urlToRelay, createPeopleLoader, searchPeople, searchRelays} from "src/engine"
|
||||
import {urlToRelay, createPeopleLoader, profileSearch, searchRelays} from "src/engine"
|
||||
|
||||
export let relays
|
||||
export let follows
|
||||
@ -245,14 +245,11 @@
|
||||
<Input bind:value={term}>
|
||||
<i slot="before" class="fa fa-search" />
|
||||
</Input>
|
||||
{#each $searchPeople(term).slice(0, 30) as person (person.pubkey)}
|
||||
<PersonSummary pubkey={person.pubkey}>
|
||||
{#each $profileSearch.search(term).slice(0, 30) as pubkey (pubkey)}
|
||||
<PersonSummary {pubkey}>
|
||||
<div slot="actions" class="flex items-start justify-end">
|
||||
{#if follows.includes(person.pubkey)}
|
||||
<Anchor
|
||||
button
|
||||
class="flex items-center gap-2"
|
||||
on:click={() => removeFollow(person.pubkey)}>
|
||||
{#if follows.includes(pubkey)}
|
||||
<Anchor button class="flex items-center gap-2" on:click={() => removeFollow(pubkey)}>
|
||||
<i class="fa fa-user-slash" /> Unfollow
|
||||
</Anchor>
|
||||
{:else}
|
||||
@ -260,7 +257,7 @@
|
||||
button
|
||||
accent
|
||||
class="flex items-center gap-2"
|
||||
on:click={() => addFollow(person.pubkey)}>
|
||||
on:click={() => addFollow(pubkey)}>
|
||||
<i class="fa fa-user-plus" /> Follow
|
||||
</Anchor>
|
||||
{/if}
|
||||
|
@ -14,6 +14,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import Note from "src/app/shared/Note.svelte"
|
||||
import {profileHasName} from "src/domain"
|
||||
import type {Relay} from "src/engine"
|
||||
import {
|
||||
load,
|
||||
@ -22,8 +23,7 @@
|
||||
pubkey,
|
||||
follows,
|
||||
deriveRelay,
|
||||
derivePerson,
|
||||
personHasName,
|
||||
getProfileByPubkey,
|
||||
displayPersonByPubkey,
|
||||
relayPolicies,
|
||||
relayPolicyUrls,
|
||||
@ -41,7 +41,7 @@
|
||||
const m = new Map<string, string[]>()
|
||||
|
||||
for (const pk of $follows) {
|
||||
if (!personHasName(derivePerson(pk).get())) {
|
||||
if (!profileHasName(getProfileByPubkey(pk))) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {ellipsize} from 'hurdak'
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ellipsize} from "hurdak"
|
||||
import {PROFILE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {SearchHelper} from "src/util/misc"
|
||||
import {tryJson} from "src/util/misc"
|
||||
|
||||
export type Profile = {
|
||||
@ -41,11 +40,16 @@ export const readProfile = (event: TrustedEvent) => {
|
||||
return {...profile, event} as PublishedProfile
|
||||
}
|
||||
|
||||
export const createProfile = ({event, ...profile}: Profile) =>
|
||||
({kind: PROFILE, content: JSON.stringify(profile)})
|
||||
export const createProfile = ({event, ...profile}: Profile) => ({
|
||||
kind: PROFILE,
|
||||
content: JSON.stringify(profile),
|
||||
})
|
||||
|
||||
export const editProfile = ({event, ...profile}: PublishedProfile) =>
|
||||
({kind: PROFILE, content: JSON.stringify(profile), tags: event.tags})
|
||||
export const editProfile = ({event, ...profile}: PublishedProfile) => ({
|
||||
kind: PROFILE,
|
||||
content: JSON.stringify(profile),
|
||||
tags: event.tags,
|
||||
})
|
||||
|
||||
export const displayPubkey = pubkey => {
|
||||
const d = nip19.npubEncode(pubkey)
|
||||
@ -63,16 +67,4 @@ export const displayProfile = (profile?: Profile) => {
|
||||
return "[no name]"
|
||||
}
|
||||
|
||||
export class ProfileSearch extends SearchHelper<PublishedProfile, string> {
|
||||
config = {
|
||||
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
}
|
||||
|
||||
getValue = (option: PublishedProfile) => option.event.pubkey
|
||||
|
||||
display = (address: string) =>
|
||||
displayProfile(this.options.find(profile => this.getValue(profile) === address))
|
||||
}
|
||||
export const profileHasName = (profile?: Profile) => Boolean(profile?.name || profile?.display_name)
|
||||
|
@ -56,6 +56,7 @@ import {
|
||||
inc,
|
||||
sort,
|
||||
groupBy,
|
||||
indexBy,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
READ_RECEIPT,
|
||||
@ -95,7 +96,17 @@ import {
|
||||
subscribe as baseSubscribe,
|
||||
} from "@welshman/net"
|
||||
import type {Publish, PublishRequest, SubscribeRequest} from "@welshman/net"
|
||||
import {fuzzy, synced, withGetter, createBatcher, pushToKey, tryJson, fromCsv} from "src/util/misc"
|
||||
import {
|
||||
fuzzy,
|
||||
synced,
|
||||
withGetter,
|
||||
getter,
|
||||
createBatcher,
|
||||
pushToKey,
|
||||
tryJson,
|
||||
fromCsv,
|
||||
SearchHelper,
|
||||
} from "src/util/misc"
|
||||
import {parseContent} from "src/util/notes"
|
||||
import {
|
||||
appDataKeys,
|
||||
@ -109,11 +120,18 @@ import {
|
||||
reactionKinds,
|
||||
} from "src/util/nostr"
|
||||
import logger from "src/util/logger"
|
||||
import type {PublishedFeed, PublishedListFeed, Collection, PublishedList, Profile} from "src/domain"
|
||||
import type {
|
||||
PublishedFeed,
|
||||
PublishedProfile,
|
||||
PublishedListFeed,
|
||||
Collection,
|
||||
PublishedList,
|
||||
} from "src/domain"
|
||||
import {
|
||||
EDITABLE_LIST_KINDS,
|
||||
ListSearch,
|
||||
FeedSearch,
|
||||
profileHasName,
|
||||
readFeed,
|
||||
readList,
|
||||
readProfile,
|
||||
@ -123,6 +141,7 @@ import {
|
||||
readHandlers,
|
||||
mapListToFeed,
|
||||
getHandlerAddress,
|
||||
displayProfile,
|
||||
} from "src/domain"
|
||||
import type {
|
||||
Channel,
|
||||
@ -254,12 +273,73 @@ export const settings = user.derived(getSettings)
|
||||
|
||||
// Profiles
|
||||
|
||||
export const profiles = deriveEventsMapped<Profile>({
|
||||
export const profiles = deriveEventsMapped<PublishedProfile>({
|
||||
filters: [{kinds: [PROFILE]}],
|
||||
eventToItem: readProfile,
|
||||
itemToEvent: prop("event"),
|
||||
})
|
||||
|
||||
export const profilesByPubkey = derived(profiles, $profiles =>
|
||||
indexBy(p => p.event.pubkey, $profiles),
|
||||
)
|
||||
|
||||
export const getProfilesByPubkey = getter(profilesByPubkey)
|
||||
|
||||
export const getProfileByPubkey = (pubkey: string) => getProfilesByPubkey().get(pubkey)
|
||||
|
||||
export const profilesWithName = derived(profiles, $profiles => $profiles.filter(profileHasName))
|
||||
|
||||
export class ProfileSearch extends SearchHelper<PublishedProfile, string> {
|
||||
config = {
|
||||
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
}
|
||||
|
||||
getValue = (option: PublishedProfile) => option.event.pubkey
|
||||
|
||||
display = (address: string) =>
|
||||
displayProfile(this.options.find(profile => this.getValue(profile) === address))
|
||||
|
||||
getSearch = () => {
|
||||
const $pubkey = pubkey.get()
|
||||
|
||||
primeWotCaches($pubkey)
|
||||
|
||||
const options = this.options.map(profile => ({
|
||||
profile,
|
||||
score: getWotScore($pubkey, profile.event.pubkey),
|
||||
}))
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
keys: [
|
||||
"profile.name",
|
||||
"profile.display_name",
|
||||
{name: "profile.nip05", weight: 0.5},
|
||||
{name: "profile.about", weight: 0.1},
|
||||
],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
})
|
||||
|
||||
return (term: string) => {
|
||||
if (!term) {
|
||||
return sortBy(item => -item.score, options).map(item => item.profile)
|
||||
}
|
||||
|
||||
return doPipe(fuse.search(term), [
|
||||
results =>
|
||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), results),
|
||||
results => results.map((r: any) => r.item.profile),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const profileSearch = derived(profilesWithName, $profiles => new ProfileSearch($profiles))
|
||||
|
||||
// People
|
||||
|
||||
export const fetchHandle = createBatcher(500, async (handles: string[]) => {
|
||||
@ -281,8 +361,6 @@ export const getHandle = cached({
|
||||
getValue: ([handle]) => fetchHandle(handle),
|
||||
})
|
||||
|
||||
export const personHasName = ({profile: p}: Person) => Boolean(p?.name || p?.display_name)
|
||||
|
||||
export const getPersonWithDefault = pubkey => ({pubkey, ...people.key(pubkey).get()})
|
||||
|
||||
export const displayPerson = ({pubkey, profile}: Person) => {
|
||||
@ -451,8 +529,6 @@ export const decodePerson = entity => {
|
||||
})
|
||||
}
|
||||
|
||||
export const peopleWithName = people.derived(filter(personHasName))
|
||||
|
||||
export const derivePerson = pubkey => people.key(pubkey).derived(defaultTo({pubkey}))
|
||||
|
||||
export const mutes = user.derived(getMutes)
|
||||
@ -465,41 +541,6 @@ export const deriveMuted = (value: string) => mutes.derived(s => s.has(value))
|
||||
|
||||
export const deriveFollowing = (pubkey: string) => follows.derived(s => s.has(pubkey))
|
||||
|
||||
export const searchPeople = new Derived(
|
||||
[pubkey, peopleWithName.throttle(300)],
|
||||
([$pubkey, $peopleWithName]: [string, Person[]]): ((term: string) => Person[]) => {
|
||||
primeWotCaches($pubkey)
|
||||
|
||||
const options = $peopleWithName.map(p => ({person: p, score: getWotScore($pubkey, p.pubkey)}))
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
keys: [
|
||||
"person.profile.name",
|
||||
"person.profile.display_name",
|
||||
{name: "person.profile.nip05", weight: 0.5},
|
||||
{name: "person.profile.about", weight: 0.1},
|
||||
],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
})
|
||||
|
||||
return (term: string) => {
|
||||
if (!term) {
|
||||
return sortBy(item => -item.score, options).map(item => item.person)
|
||||
}
|
||||
|
||||
return doPipe(fuse.search(term), [
|
||||
results =>
|
||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), results),
|
||||
results => results.map((r: any) => r.item.person),
|
||||
])
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const searchPubkeys = searchPeople.derived(search => term => pluck("pubkey", search(term)))
|
||||
|
||||
// Events
|
||||
|
||||
export const isEventMuted = new Derived(
|
||||
|
@ -345,7 +345,7 @@ export const getStringWidth = (text: string) => {
|
||||
|
||||
export class SearchHelper<T, V> {
|
||||
config: any
|
||||
fuzzy?: (term: string) => T[]
|
||||
_search?: (term: string) => T[]
|
||||
|
||||
constructor(readonly options: T[]) {}
|
||||
|
||||
@ -353,17 +353,19 @@ export class SearchHelper<T, V> {
|
||||
|
||||
getValue = (option: T): V => option as unknown as V
|
||||
|
||||
getFuzzy = () => fuzzy(this.options, this.config)
|
||||
getSearch = () => fuzzy(this.options, this.config)
|
||||
|
||||
display = (value: V) => String(value)
|
||||
|
||||
search = (term: string) => {
|
||||
if (!this.fuzzy) {
|
||||
this.fuzzy = this.getFuzzy()
|
||||
searchOptions = (term: string) => {
|
||||
if (!this._search) {
|
||||
this._search = this.getSearch()
|
||||
}
|
||||
|
||||
return this.fuzzy(term).map(this.getValue)
|
||||
return this._search(term)
|
||||
}
|
||||
|
||||
search = (term: string) => this.searchOptions(term).map(this.getValue)
|
||||
}
|
||||
|
||||
export const fromCsv = s => (s || "").split(",").filter(identity)
|
||||
|
Loading…
Reference in New Issue
Block a user