Combine search and scan

This commit is contained in:
Jonathan Staab 2023-05-21 11:23:48 -07:00
parent f2ea91f178
commit 7062037eee
10 changed files with 164 additions and 123 deletions

View File

@ -3,6 +3,7 @@
# 0.2.29
- [x] Register url handler for web+nostr and use that for sharing
- [x] Combine search and scan pages
# 0.2.28

View File

@ -200,7 +200,7 @@ const sortByCreatedAt = sortBy(e => -e.created_at)
const sortByLastSeen = sortBy(e => -e.last_seen)
export const people = new Table("people", "pubkey", {
max: 5000,
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()

View File

@ -28,9 +28,18 @@ import {people} from "src/agent/db"
import pool from "src/agent/pool"
import sync from "src/agent/sync"
// If we ask for a pubkey and get nothing back, don't ask again this page load
const attemptedPubkeys = new Set()
const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about
return uniq(pubkeys).filter(pubkey => {
if (attemptedPubkeys.has(pubkey)) {
return false
}
attemptedPubkeys.add(pubkey)
const p = people.get(pubkey)
return !p || p.updated_at < now() - timedelta(1, "days")

View File

@ -16,7 +16,6 @@
import NotFound from "src/app/views/NotFound.svelte"
import PersonDetail from "src/app/views/PersonDetail.svelte"
import Search from "src/app/views/Search.svelte"
import Scan from "src/app/views/Scan.svelte"
import RelayDetail from "src/app/views/RelayDetail.svelte"
import RelayList from "src/app/views/RelayList.svelte"
import UserProfile from "src/app/views/UserProfile.svelte"
@ -38,11 +37,6 @@
<Search />
</EnsureData>
</Route>
<Route path="/scan">
<EnsureData enforcePeople={false}>
<Scan />
</EnsureData>
</Route>
<Route path="/notes" let:params>
<EnsureData>
<Feeds />

View File

@ -58,11 +58,6 @@
<i class="fa fa-search mr-2" /> Search
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent hover:text-white" href="/scan">
<i class="fa fa-qrcode mr-2" /> Scan
</a>
</li>
<li
class={cx("relative", {
"cursor-pointer": $canPublish,

View File

@ -0,0 +1,63 @@
<script lang="ts">
import QrScanner from "qr-scanner"
import {onDestroy} from "svelte"
import Modal from "src/partials/Modal.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Content from "src/partials/Content.svelte"
export let onScan
let video
let scanner
let status = "closed"
const onDecode = result => {
onScan(result.data)
}
export const start = () => {
status = "loading"
scanner = new Promise(resolve => {
setTimeout(async () => {
const scanner = new QrScanner(video, onDecode, {
returnDetailedScanResult: true,
})
await scanner.start()
resolve(scanner)
status = "ready"
}, 1000)
})
}
export const stop = async () => {
status = "closed"
if (scanner) {
const s = await scanner
await s.stop()
await s.destroy()
}
}
onDestroy(stop)
</script>
{#if status !== "closed"}
<Modal onEscape={stop}>
<Content>
{#if status === "loading"}
<Spinner>Loading your camera...</Spinner>
{/if}
<div
class="m-auto rounded border border-solid border-gray-6 bg-gray-8 p-4"
class:hidden={status !== "ready"}>
<video class="m-auto rounded" bind:this={video} />
</div>
</Content>
</Modal>
{/if}

View File

@ -7,7 +7,7 @@ import {writable} from "svelte/store"
import {max, omit, pluck, sortBy, find, slice, propEq} from "ramda"
import {createMap, doPipe, first} from "hurdak/lib/hurdak"
import {warn} from "src/util/logger"
import {hash, sleep} from "src/util/misc"
import {hash, sleep, clamp} from "src/util/misc"
import {now, timedelta} from "src/util/misc"
import {Tags, isNotification, userKinds} from "src/util/nostr"
import {findReplyId} from "src/util/nostr"
@ -160,9 +160,15 @@ const processChats = async (pubkey, events) => {
export const listen = async () => {
const pubkey = user.getPubkey()
const {roomsJoined} = user.getProfile()
const since = now() - timedelta(30, "days")
const kinds = enableZaps ? [1, 4, 7, 9735] : [1, 4, 7]
const eventIds = doPipe(userEvents.all({kind: 1, created_at: {$gt: since}}), [
// Only grab notifications since we last checked, with some wiggle room
const since =
clamp([now() - timedelta(30, "days"), now()], notifications._coll.max("created_at")) -
timedelta(1, "hours")
const eventIds = doPipe(userEvents, [
t => t.all({kind: 1, created_at: {$gt: now() - timedelta(30, "days")}}),
sortBy(e => -e.created_at),
slice(0, 256),
pluck("id"),

View File

@ -1,86 +0,0 @@
<script lang="ts">
import QrScanner from "qr-scanner"
import {onDestroy} from "svelte"
import {navigate} from "svelte-routing"
import {find} from "ramda"
import {nip05, nip19} from "nostr-tools"
import {toast} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Content from "src/partials/Content.svelte"
let video,
value,
scanner,
status = ""
const onDecode = result => {
handleInput(result.data)
}
const handleInput = async input => {
input = input.replace("nostr:", "")
if (find(s => input.startsWith(s), ["note1", "npub1", "nevent1", "nprofile1"])) {
navigate("/" + input)
return
}
if (input.match(/^[a-f0-9]{64}$/)) {
navigate("/" + nip19.npubEncode(input))
return
}
let profile = await nip05.queryProfile(input)
if (profile) {
navigate("/" + nip19.nprofileEncode(profile))
return
}
toast.show("warning", "That isn't a valid nostr identifier")
}
const showVideo = async () => {
status = "loading"
scanner = new QrScanner(video, onDecode, {
returnDetailedScanResult: true,
})
await scanner.start()
status = "ready"
}
onDestroy(async () => {
if (scanner) {
await scanner.stop()
await scanner.destroy()
}
})
</script>
<Content>
<div class="flex flex-col items-center justify-center">
<Heading>Find Something</Heading>
<p>
Enter any nostr identifier (npub, nevent, nprofile, note or user@domain.tld), or click on the
camera icon to scan with your device's camera instead.
</p>
</div>
<form class="flex gap-2" on:submit|preventDefault={() => handleInput(value)}>
<Input placeholder="nprofile1..." bind:value wrapperClass="flex-grow" />
<Anchor type="button" on:click={() => handleInput(value)}>
<i class="fa fa-arrow-right" />
</Anchor>
<Anchor type="button" on:click={showVideo}>
<i class="fa fa-camera" />
</Anchor>
</form>
{#if status === "loading"}
<Spinner>Loading your camera...</Spinner>
{/if}
<video bind:this={video} />
</Content>

View File

@ -1,19 +1,24 @@
<script>
import {debounce} from "throttle-debounce"
import {identity, sortBy, prop} from "ramda"
import {fuzzy} from "src/util/misc"
import {navigate} from "svelte-routing"
import {nip05, nip19} from "nostr-tools"
import {identity} from "ramda"
import {fuzzy, tryFunc} from "src/util/misc"
import {modal} from "src/partials/state"
import Input from "src/partials/Input.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import BorderLeft from "src/partials/BorderLeft.svelte"
import Scan from "src/app/shared/Scan.svelte"
import PersonInfo from "src/app/shared/PersonInfo.svelte"
import {sampleRelays, getUserReadRelays} from "src/agent/relays"
import network from "src/agent/network"
import {watch} from "src/agent/db"
import {watch, people} from "src/agent/db"
import user from "src/agent/user"
let q
let q = ""
let options = []
let scanner
const openTopic = topic => {
modal.push({type: "topic/feed", topic})
@ -27,7 +32,7 @@
relays: sampleRelays([{url: "wss://relay.nostr.band"}]),
filter: [{kinds: [0], search, limit: 10}],
})
} else {
} else if (people._coll.count() < 50) {
network.load({
relays: getUserReadRelays(),
filter: [{kinds: [0], limit: 50}],
@ -35,25 +40,70 @@
}
})
$: loadPeople(q)
const tryParseEntity = debounce(500, async entity => {
entity = entity.replace("nostr:", "")
$: search = watch(["people", "topics"], (p, t) => {
const topics = t
.all()
.map(topic => ({type: "topic", id: topic.name, topic, text: "#" + topic.name}))
if (entity.length < 5) {
return
}
const people = p
.all({"kind0.name": {$type: "string"}, pubkey: {$ne: user.getPubkey()}})
.map(person => ({
person,
type: "person",
id: person.pubkey,
text: "@" + [person.kind0.name, person.kind0.about].filter(identity).join(" "),
}))
if (entity.match(/^[a-f0-9]{64}$/)) {
navigate("/" + nip19.npubEncode(entity))
} else if (entity.includes("@")) {
let profile = await nip05.queryProfile(entity)
return fuzzy(sortBy(prop("id"), topics.concat(people)), {keys: ["text"]})
if (profile) {
navigate("/" + nip19.nprofileEncode(profile))
}
} else {
tryFunc(() => {
nip19.decode(entity)
navigate("/" + entity)
}, "TypeError")
}
})
const topicOptions = watch(["topics"], t =>
t.all().map(topic => ({type: "topic", id: topic.name, topic, text: "#" + topic.name}))
)
const personOptions = watch(["people"], t =>
t
.all({
pubkey: {$ne: user.getPubkey()},
$or: [{"kind0.name": {$type: "string"}}, {"kind0.display_name": {$type: "string"}}],
})
.map(person => {
const {name, about, display_name} = person.kind0
return {
person,
type: "person",
id: person.pubkey,
text: "@" + [name, about, display_name].filter(identity).join(" "),
}
})
)
$: {
loadPeople(q)
tryParseEntity(q)
}
$: firstChar = q.slice(0, 1)
$: {
if (firstChar === "@") {
options = $personOptions
} else if (firstChar === "#") {
options = $topicOptions
} else {
options = $personOptions.concat($topicOptions)
}
}
$: search = fuzzy(options, {keys: ["text"]})
document.title = "Search"
</script>
@ -61,14 +111,19 @@
<div class="flex flex-col items-center justify-center">
<Heading>Search</Heading>
<p>
Search for people and topics on Nostr. For now, only results that have already been loaded
will appear in search results.
Enter any nostr identifier or search term to find people and topics. You can also click on the
camera icon to scan with your device's camera instead.
</p>
</div>
<Input bind:value={q} placeholder="Search for people or topics">
<i slot="before" class="fa-solid fa-search" />
<i
slot="after"
class="fa-solid fa-camera cursor-pointer text-accent"
on:click={() => scanner.start()} />
</Input>
{#each $search(q).slice(0, 50) as result (result.type + result.id)}
{#each search(q).slice(0, 50) as result (result.type + result.id)}
{#if result.type === "topic"}
<BorderLeft on:click={() => openTopic(result.topic.name)}>
#{result.topic.name}
@ -78,3 +133,5 @@
{/if}
{/each}
</Content>
<Scan bind:this={scanner} onScan={tryParseEntity} />

View File

@ -418,3 +418,5 @@ export const webSocketURLToPlainOrBase64 = (url: string): string => {
}
return url
}
export const clamp = ([min, max], n) => Math.min(max, Math.max(min, n))