Add basic profile search

This commit is contained in:
styppo 2023-01-24 01:46:21 +00:00
parent a96a7dbb7f
commit c18c4ed988
No known key found for this signature in database
GPG Key ID: 3AAA685C50724C28
4 changed files with 134 additions and 26 deletions

View File

@ -1,52 +1,68 @@
<template>
<div class="relative-position">
<div class="searchbox" :class="{focused}">
<div class="searchbox-wrapper">
<div class="searchbox-icon">
<BaseIcon icon="search" />
</div>
<div class="searchbox-input">
<form @submit="search">
<q-form @submit.stop="search">
<input
type="text"
placeholder="Search"
placeholder="Search profiles"
v-model="query"
@focus="toggleFocus"
@blur="toggleFocus"
@keyup="search"
>
</form>
</q-form>
</div>
</div>
</div>
<Transition name="fade">
<div v-if="focused" class="searchbox-results">
<div v-if="!results.length" class="query-example">
<b>npub</b> or <b>[user]@domain</b> or <b>name</b>
</div>
<UserCard v-for="pubkey in results" :key="pubkey" :pubkey="pubkey" class="searchbox-results-item" clickable />
</div>
</Transition>
</div>
</template>
<script>
import BaseIcon from 'components/BaseIcon'
import {Notify} from 'quasar'
import SearchProvider from 'src/nostr/SearchProvider'
import UserCard from 'components/User/UserCard.vue'
export default {
name: 'SearchBox',
components: {
UserCard,
BaseIcon,
},
setup() {
return {
provider: new SearchProvider(),
}
},
data() {
return {
focused: false,
query: '',
results: [],
}
},
computed: {
},
methods: {
toggleFocus() {
this.focused = !this.focused
},
async search(e) {
e.preventDefault()
Notify.create({
message: 'Coming soon',
color: 'info',
})
async search() {
if (this.query) {
this.results = (await this.provider.queryProfiles(this.query)).slice(0, 200)
} else {
this.results = []
}
},
},
}
@ -92,6 +108,43 @@ export default {
}
}
}
&-results {
position: absolute;
width: calc(100% + 1rem);
min-height: 48px;
max-height: 70vh;
overflow: hidden;
background-color: $color-bg;
border-radius: .5rem;
z-index: 600;
margin-top: -.75rem;
box-shadow: $shadow-white;
overflow-y: scroll;
scrollbar-color: transparent transparent;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
&::-webkit-scrollbar-thumb { /* Foreground */
background: $color-dark-gray;
}
&::-webkit-scrollbar-track { /* Background */
background: transparent;
}
&-item {
transition: 120ms ease;
margin: 0 !important;
padding: 1rem;
&:hover {
background-color: rgba($color: $color-dark-gray, $alpha: 0.1);
}
}
.query-example {
color: $color-light-gray;
font-size: .95rem;
padding: 1rem;
}
}
&.focused {
border: 1px solid rgba($color: $color-primary, $alpha: 1);
svg {

View File

@ -0,0 +1,34 @@
import {useProfileStore} from 'src/nostr/store/ProfileStore'
import Nip05 from 'src/utils/Nip05'
import {bech32prefix, bech32ToHex, isBech32} from 'src/utils/utils'
export default class SearchProvider {
constructor() {
this.profiles = useProfileStore()
}
async queryProfiles(query) {
const results = new Set()
const [user, domain] = (query?.split('@') || [])
if (domain) {
(await SearchProvider.queryNip05(user, domain)).forEach(pubkey => results.add(pubkey))
this.profiles.findByNip05(query).forEach(pubkey => results.add(pubkey))
} else if (isBech32(query) && bech32prefix(query) === 'npub') {
results.add(bech32ToHex(query))
} else {
this.profiles.findByName(query).forEach(pubkey => results.add(pubkey))
}
return Array.from(results)
}
static async queryNip05(user, domain) {
const names = await Nip05.fetchNames(domain)
if (!names) return []
if (user) {
return Object.entries(names)
.filter(([name, _]) => name?.toLowerCase().startsWith(user.toLowerCase()))
.map(([_, pubkey]) => pubkey)
}
return Object.values(names)
}
}

View File

@ -8,7 +8,17 @@ export const useProfileStore = defineStore('profile', {
getters: {
get(state) {
return pubkey => state.profiles[pubkey]
}
},
findByName(state) {
return query => Object.values(state.profiles)
.filter(profile => profile.name?.toLowerCase().startsWith(query?.toLowerCase()))
.map(profile => profile.pubkey)
},
findByNip05(state) {
return query => Object.values(state.profiles)
.filter(profile => profile.nip05.url?.toLowerCase().endsWith(query?.toLowerCase()))
.map(profile => profile.pubkey)
},
},
actions: {
addEvent(event) {

View File

@ -16,6 +16,17 @@ export default class Nip05 {
}
}
static async fetchNames(domain) {
const url = `https://${domain}/.well-known/nostr.json`
try {
const res = await fetch(url)
const json = await res.json()
return json?.names
} catch (e) {
//console.warn(`Failed to fetch NIP05 data for ${nip05Id}`, e)
}
}
static async verify(pubkey, nip05Id) {
const pk = await Nip05.fetchPubkey(nip05Id)
return pk && pk === pubkey