mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Combine search and scan
This commit is contained in:
parent
f2ea91f178
commit
7062037eee
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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 />
|
||||
|
@ -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,
|
||||
|
63
src/app/shared/Scan.svelte
Normal file
63
src/app/shared/Scan.svelte
Normal 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}
|
@ -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"),
|
||||
|
@ -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>
|
@ -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} />
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user