mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 08:21:20 +00:00
Make the app store a real thing
This commit is contained in:
parent
f1bccacff9
commit
1d5a50cb25
@ -1,7 +1,8 @@
|
|||||||
# Current
|
# Current
|
||||||
|
|
||||||
- [ ] Refactor
|
- [ ] 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?
|
- [ ] Remove external dependencies from engine, open source it?
|
||||||
- [ ] Show nip 5's in search and other places
|
- [ ] Show nip 5's in search and other places
|
||||||
- [ ] Normalize all relay urls, see comment by daniele
|
- [ ] Normalize all relay urls, see comment by daniele
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="cursor-pointer">
|
<li class="cursor-pointer">
|
||||||
<a class="block px-4 py-2 transition-all hover:bg-accent hover:text-white" href="/apps">
|
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-3 my-4 h-px bg-gray-6" />
|
<li class="mx-3 my-4 h-px bg-gray-6" />
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
const mute = () => User.mute("e", note.id)
|
const mute = () => User.mute("e", note.id)
|
||||||
|
|
||||||
const react = async content => {
|
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))
|
like = first(await Outbox.publish(Builder.createReaction(note, content), relays))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {annotateMedia} from "src/util/misc"
|
import {annotateMedia, displayUrl} from "src/util/misc"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Media from "src/partials/Media.svelte"
|
import Media from "src/partials/Media.svelte"
|
||||||
|
|
||||||
@ -19,6 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Anchor class="underline" external href={value.url}>
|
<Anchor class="underline" external href={value.url}>
|
||||||
{value.url.replace(/https?:\/\/(www\.)?/, "")}
|
{displayUrl(value.url)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,25 +1,87 @@
|
|||||||
<script>
|
<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 {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 Card from "src/partials/Card.svelte"
|
||||||
import Heading from "src/partials/Heading.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 = [
|
const getColumns = xs => {
|
||||||
["https://zapstr.live", "Zapstr", "Stream music, zap artists"],
|
const cols = [[], []]
|
||||||
["https://highlighter.com", "Highlighter", "Highlight anything and share it with friends."],
|
|
||||||
["https://www.wavman.app", "Wavman", "A nostalgic music player built on nostr."],
|
xs.forEach((x, i) => {
|
||||||
["https://feeds.nostr.band", "Feeds", "Find custom curated feeds - and create your own."],
|
cols[i % 2].push(x)
|
||||||
["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."],
|
return cols.flatMap(x => x)
|
||||||
["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."],
|
const copy = (label, value) => {
|
||||||
["https://getalby.com", "Alby", "Sign in to nostr apps without sharing your private key."],
|
copyToClipboard(value)
|
||||||
["https://nostrplebs.com", "NostrPlebs", "Get verified at nostrplebs.com."],
|
toast.show("info", `${label} copied to clipboard!`)
|
||||||
["https://nadar.tigerville.no", "Nadar", "Find out what relays know about your post."],
|
}
|
||||||
["https://pinstr.app", "Pinstr", "Create and manage collections of notes."],
|
|
||||||
]
|
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"
|
document.title = "Apps"
|
||||||
</script>
|
</script>
|
||||||
@ -30,12 +92,51 @@
|
|||||||
<Heading>Recommended micro-apps</Heading>
|
<Heading>Recommended micro-apps</Heading>
|
||||||
<p>Hand-picked recommendations to enhance your nostr experience.</p>
|
<p>Hand-picked recommendations to enhance your nostr experience.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
<div class="columns-sm gap-4">
|
||||||
{#each apps as [url, title, description]}
|
{#each getColumns(apps) as app}
|
||||||
<Card class="flex flex-col gap-2">
|
<Card class="mb-4 flex break-inside-avoid flex-col gap-4">
|
||||||
<h1 class="text-2xl">{title}</h1>
|
<div class="flex gap-4">
|
||||||
<p class="h-16">{description}</p>
|
<ImageCircle size={14} src={app.info.picture} />
|
||||||
<Media link={{url}} />
|
<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>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
39
src/partials/Image.svelte
Normal file
39
src/partials/Image.svelte
Normal 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}
|
@ -1,7 +1,7 @@
|
|||||||
import {bech32, utf8} from "@scure/base"
|
import {bech32, utf8} from "@scure/base"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {pluck} from "ramda"
|
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 Fuse from "fuse.js/dist/fuse.min.js"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {warn} from "src/util/logger"
|
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) => {
|
export const pushToKey = <T>(m: Record<string, T[]>, k: string, v: T) => {
|
||||||
m[k] = m[k] || []
|
m[k] = m[k] || []
|
||||||
m[k].push(v)
|
m[k].push(v)
|
||||||
|
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
export const race = (p, promises) => {
|
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(/[\/\?]/))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user