Make the app store a real thing

This commit is contained in:
Jonathan Staab 2023-07-25 09:36:07 -07:00
parent f1bccacff9
commit 1d5a50cb25
7 changed files with 180 additions and 29 deletions

View File

@ -1,7 +1,8 @@
# Current
- [ ] Refactor
- [ ] Improve publishing dialog, make it openable with relay deets
- [ ] Upgrade app store to use real events
- Show supported kinds
- [ ] Remove external dependencies from engine, open source it?
- [ ] Show nip 5's in search and other places
- [ ] Normalize all relay urls, see comment by daniele

View File

@ -85,7 +85,7 @@
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent hover:text-white" href="/apps">
<i class="fa fa-motorcycle mr-2" /> Apps
<i class="fa fa-box mr-2" /> Apps
</a>
</li>
<li class="mx-3 my-4 h-px bg-gray-6" />

View File

@ -39,7 +39,7 @@
const mute = () => User.mute("e", note.id)
const react = async content => {
const relays = Nip65.getPublishHints(3, note, User.getRelayUrls("write"))
const relays = Nip65.getPublishHints(5, note, User.getRelayUrls("write"))
like = first(await Outbox.publish(Builder.createReaction(note, content), relays))
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {annotateMedia} from "src/util/misc"
import {annotateMedia, displayUrl} from "src/util/misc"
import Anchor from "src/partials/Anchor.svelte"
import Media from "src/partials/Media.svelte"
@ -19,6 +19,6 @@
</div>
{:else}
<Anchor class="underline" external href={value.url}>
{value.url.replace(/https?:\/\/(www\.)?/, "")}
{displayUrl(value.url)}
</Anchor>
{/if}

View File

@ -1,25 +1,87 @@
<script>
import {nip05} from "nostr-tools"
import {uniqBy, sortBy} from "ramda"
import {batch, quantify} from "hurdak"
import {tryJson, displayDomain, pushToKey} from "src/util/misc"
import {copyToClipboard} from "src/util/html"
import {Tags} from "src/util/nostr"
import {fly} from "src/util/transition"
import Media from "src/partials/Media.svelte"
import {toast, modal} from "src/partials/state"
import Image from "src/partials/Image.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Chip from "src/partials/Chip.svelte"
import Card from "src/partials/Card.svelte"
import Heading from "src/partials/Heading.svelte"
import ImageCircle from "src/partials/ImageCircle.svelte"
import {Network, Directory, Nip05, User, pubkeyLoader} from "src/app/engine"
import {compileFilter} from "src/app/state"
const apps = [
["https://zapstr.live", "Zapstr", "Stream music, zap artists"],
["https://highlighter.com", "Highlighter", "Highlight anything and share it with friends."],
["https://www.wavman.app", "Wavman", "A nostalgic music player built on nostr."],
["https://feeds.nostr.band", "Feeds", "Find custom curated feeds - and create your own."],
["https://blowater.deno.dev", "Blowater", "The best nostr micro-client for managing DMs."],
["https://listr.lol", "Listr", "Build, share, and browse custom lists."],
["https://nosbin.com", "Nosbin", "Copy/paste/share."],
["https://nostrnests.com/", "Nostr Nests", "Live stream your thoughts, as the happen."],
["https://nostr.watch", "Nostr Watch", "A directory of nostr relays."],
["https://nostr.directory", "Nostr Directory", "Validate your nostr pubkey with Twitter."],
["https://getalby.com", "Alby", "Sign in to nostr apps without sharing your private key."],
["https://nostrplebs.com", "NostrPlebs", "Get verified at nostrplebs.com."],
["https://nadar.tigerville.no", "Nadar", "Find out what relays know about your post."],
["https://pinstr.app", "Pinstr", "Create and manage collections of notes."],
]
const getColumns = xs => {
const cols = [[], []]
xs.forEach((x, i) => {
cols[i % 2].push(x)
})
return cols.flatMap(x => x)
}
const copy = (label, value) => {
copyToClipboard(value)
toast.show("info", `${label} copied to clipboard!`)
}
const goToNip05 = async entity => {
const profile = await nip05.queryProfile(entity)
if (profile) {
modal.push({type: "person/feed", pubkey: profile.pubkey})
} else {
copy("Address", entity)
}
}
Network.subscribe({
timeout: 5000,
filter: [{kinds: [31990]}, compileFilter({kinds: [31989], authors: "follows"})],
relays: User.getRelayUrls("read"),
onEvent: batch(500, events => {
const pubkeys = []
for (const e of events) {
pubkeys.push(e.pubkey)
if (e.kind === 31990) {
e.address = [e.kind, e.pubkey, Tags.from(e).getMeta("d")].join(":")
handlers = handlers.concat(e)
} else {
for (const a of Tags.from(e).type("a").values().all()) {
recsByNaddr = pushToKey(recsByNaddr, a, e)
}
}
}
pubkeyLoader.load(pubkeys)
}),
})
let handlers = []
let recsByNaddr = {}
$: apps = uniqBy(
e => e.info.name,
sortBy(
e => -e.recs.length,
handlers.map(e => {
e.recs = recsByNaddr[e.address] || []
e.info = tryJson(() => JSON.parse(e.content)) || Directory.getProfile(e.pubkey)
e.handle = Nip05.getHandle(e.pubkey)
return e
})
)
)
document.title = "Apps"
</script>
@ -30,12 +92,51 @@
<Heading>Recommended micro-apps</Heading>
<p>Hand-picked recommendations to enhance your nostr experience.</p>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{#each apps as [url, title, description]}
<Card class="flex flex-col gap-2">
<h1 class="text-2xl">{title}</h1>
<p class="h-16">{description}</p>
<Media link={{url}} />
<div class="columns-sm gap-4">
{#each getColumns(apps) as app}
<Card class="mb-4 flex break-inside-avoid flex-col gap-4">
<div class="flex gap-4">
<ImageCircle size={14} src={app.info.picture} />
<div class="flex min-w-0 flex-grow flex-col gap-2">
<h1 class="text-2xl">{app.info.display_name || app.info.name}</h1>
{#if app.handle}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-gray-1">{Nip05.displayHandle(app.handle)}</span>
</div>
{/if}
</div>
</div>
<p>{app.info.about}</p>
<div>
{#if app.info.website}
<Anchor external href={app.info.website} class="mb-2 mr-2 inline-block">
<Chip><i class="fa fa-link" />{displayDomain(app.info.website)}</Chip>
</Anchor>
{/if}
{#if app.info.lud16}
<div class="mb-2 mr-2 inline-block cursor-pointer">
<Chip on:click={() => copy("Address", app.info.lud16)}>
<i class="fa fa-bolt" />{app.info.lud16}
</Chip>
</div>
{/if}
{#if app.info.nip05}
<div class="mb-2 mr-2 inline-block cursor-pointer">
<Chip on:click={() => goToNip05(app.info.nip05)}>
<i class="fa fa-at" />{app.info.nip05}
</Chip>
</div>
{/if}
</div>
{#if app.recs.length > 0}
<i class="text-sm">
Recommended by {quantify(app.recs.length, "person", "people")} you follow.
</i>
{/if}
{#if app.info.banner}
<Image class="rounded" src={app.info.banner} />
{/if}
</Card>
{/each}
</div>

39
src/partials/Image.svelte Normal file
View File

@ -0,0 +1,39 @@
<style>
@keyframes placeholder {
0% {
opacity: 0.1;
}
100% {
opacity: 0.2;
}
}
.placeholder {
animation-name: placeholder;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
</style>
<script lang="ts">
import {onMount} from "svelte"
let element
let loading = true
onMount(() => {
element.addEventListener("load", () => {
loading = false
})
})
</script>
<img class:hidden={loading} bind:this={element} {...$$props} />
{#if loading}
<slot name="placeholder">
<div class="placeholder h-48 rounded bg-gray-5" />
</slot>
{/if}

View File

@ -1,7 +1,7 @@
import {bech32, utf8} from "@scure/base"
import {debounce} from "throttle-debounce"
import {pluck} from "ramda"
import {Storage, seconds, tryFunc, sleep, round} from "hurdak"
import {Storage, first, seconds, tryFunc, sleep, round} from "hurdak"
import Fuse from "fuse.js/dist/fuse.min.js"
import {writable} from "svelte/store"
import {warn} from "src/util/logger"
@ -230,6 +230,8 @@ export const webSocketURLToPlainOrBase64 = (url: string): string => {
export const pushToKey = <T>(m: Record<string, T[]>, k: string, v: T) => {
m[k] = m[k] || []
m[k].push(v)
return m
}
export const race = (p, promises) => {
@ -248,3 +250,11 @@ export const race = (p, promises) => {
})
})
}
export const displayUrl = url => {
return url.replace(/https?:\/\/(www\.)?/, "").replace(/\/$/, "")
}
export const displayDomain = url => {
return first(displayUrl(url).split(/[\/\?]/))
}