mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
Add basic profile search
This commit is contained in:
parent
a96a7dbb7f
commit
c18c4ed988
@ -1,52 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="searchbox" :class="{focused}">
|
<div class="relative-position">
|
||||||
<div class="searchbox-wrapper">
|
<div class="searchbox" :class="{focused}">
|
||||||
<div class="searchbox-icon">
|
<div class="searchbox-wrapper">
|
||||||
<BaseIcon icon="search" />
|
<div class="searchbox-icon">
|
||||||
</div>
|
<BaseIcon icon="search" />
|
||||||
<div class="searchbox-input">
|
</div>
|
||||||
<form @submit="search">
|
<div class="searchbox-input">
|
||||||
<input
|
<q-form @submit.stop="search">
|
||||||
type="text"
|
<input
|
||||||
placeholder="Search"
|
type="text"
|
||||||
v-model="query"
|
placeholder="Search profiles"
|
||||||
@focus="toggleFocus"
|
v-model="query"
|
||||||
@blur="toggleFocus"
|
@focus="toggleFocus"
|
||||||
>
|
@blur="toggleFocus"
|
||||||
</form>
|
@keyup="search"
|
||||||
|
>
|
||||||
|
</q-form>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseIcon from 'components/BaseIcon'
|
import BaseIcon from 'components/BaseIcon'
|
||||||
import {Notify} from 'quasar'
|
import SearchProvider from 'src/nostr/SearchProvider'
|
||||||
|
import UserCard from 'components/User/UserCard.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchBox',
|
name: 'SearchBox',
|
||||||
components: {
|
components: {
|
||||||
|
UserCard,
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
provider: new SearchProvider(),
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
focused: false,
|
focused: false,
|
||||||
query: '',
|
query: '',
|
||||||
|
results: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleFocus() {
|
toggleFocus() {
|
||||||
this.focused = !this.focused
|
this.focused = !this.focused
|
||||||
},
|
},
|
||||||
async search(e) {
|
async search() {
|
||||||
e.preventDefault()
|
if (this.query) {
|
||||||
|
this.results = (await this.provider.queryProfiles(this.query)).slice(0, 200)
|
||||||
Notify.create({
|
} else {
|
||||||
message: 'Coming soon',
|
this.results = []
|
||||||
color: 'info',
|
}
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -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 {
|
&.focused {
|
||||||
border: 1px solid rgba($color: $color-primary, $alpha: 1);
|
border: 1px solid rgba($color: $color-primary, $alpha: 1);
|
||||||
svg {
|
svg {
|
||||||
|
34
src/nostr/SearchProvider.js
Normal file
34
src/nostr/SearchProvider.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,17 @@ export const useProfileStore = defineStore('profile', {
|
|||||||
getters: {
|
getters: {
|
||||||
get(state) {
|
get(state) {
|
||||||
return pubkey => state.profiles[pubkey]
|
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: {
|
actions: {
|
||||||
addEvent(event) {
|
addEvent(event) {
|
||||||
|
@ -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) {
|
static async verify(pubkey, nip05Id) {
|
||||||
const pk = await Nip05.fetchPubkey(nip05Id)
|
const pk = await Nip05.fetchPubkey(nip05Id)
|
||||||
return pk && pk === pubkey
|
return pk && pk === pubkey
|
||||||
|
Loading…
Reference in New Issue
Block a user