Re-design relays page and person relays list with metadata

This commit is contained in:
Jonathan Staab 2023-02-07 14:24:13 -06:00
parent f44515d0df
commit a8efa9ffc2
11 changed files with 181 additions and 103 deletions

View File

@ -25,8 +25,10 @@ module.exports = {
},
rules: {
"a11y-click-events-have-key-events": "off",
"a11y-autofocus": "off",
"no-unused-vars": ["error", {args: "none"}],
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
},
ignorePatterns: ["*.svg"]
ignorePatterns: ["*.svg"],
}

View File

@ -1,6 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run qa:lint
npm run qa:check
npm run check

View File

@ -34,9 +34,12 @@ If you like Coracle and want to support its development, you can donate sats via
# Missions
- [ ] Image uploads
- Use dufflepud. Default will charge via lightning and have a tos, others can self-host and skip that.
- Default will charge via lightning and have a tos, others can self-host and skip that.
- Add banner field to profile
- [ ] Allow users to select which relay they're reading from, allow more advanced custom feeds ala tweetdeck
- Linode/Digital Ocean
- https://github.com/brandonsavage/Upload
- https://github.com/seaweedfs/seaweedfs
- https://github.com/cubefs/cubefs
- [ ] Add relay selector when publishing a note
- [ ] Support invoices, tips, zaps https://twitter.com/jb55/status/1604131336247476224
- [ ] Separate settings for read, write, and broadcast relays based on NIP 65
@ -53,9 +56,21 @@ If you like Coracle and want to support its development, you can donate sats via
- https://github.com/nostr-protocol/nips/pull/205#issuecomment-1419234230
- [ ] Change network tab to list relays the user is connected to
- [ ] Sync mentions box and in-reply mentions
- [ ] Add petnames for channels
- [ ] Channels
- [ ] Ability to leave/mute DM conversation
- [ ] Add petnames for channels
- [ ] Add notifications for chat messages
# Current
- [x] Re-design relays page and person relays list with metadata
- [ ] Add relay selection to new note screen
- [ ] Add relay selection to reply widget
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
- [ ] Show notification at top of feeds: "Showing notes from 3 relays". Click to customize.
- [ ] Click through on relays page to view a feed for only that relay.
- [ ] Custom views: slider between fast/complete with a warning at either extreme
# Changelog
## 0.2.10

View File

@ -7,9 +7,9 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"qa:lint": "eslint src/*/** --quiet",
"qa:check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"qa:all": "run-p qa:lint qa:check",
"check:es": "eslint src/*/** --quiet",
"check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"check": "run-p check:*",
"watch": "find src -type f | entr -r"
},
"devDependencies": {

View File

@ -10,8 +10,8 @@
import {Router, Route, links, navigate} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history"
import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, now, sleep} from 'src/util/misc'
import {keys, user, pool, getRelays} from 'src/agent'
import {timedelta, shuffle, now, sleep} from 'src/util/misc'
import {db, keys, user, pool, getRelays} from 'src/agent'
import {modal, toast, settings, logUsage, alerts, messages, loadAppData} from "src/app"
import {routes} from "src/app/ui"
import Anchor from 'src/partials/Anchor.svelte'
@ -82,7 +82,14 @@
loadAppData($user.pubkey)
}
// Background work
const interval = setInterval(() => {
alertSlowConnections()
retrieveRelayMeta()
}, 2_000)
const alertSlowConnections = () => {
// Only notify about relays the user is actually subscribed to
const relayUrls = pluck('url', getRelays())
@ -101,7 +108,34 @@
// Alert the user to any heinously slow connections
slowConnections = pool.getConnections()
.filter(({url, stats: s}) => relayUrls.includes(url) && s.timer / s.count > 3000)
}, 10_000)
}
const retrieveRelayMeta = async () => {
const {dufflepudUrl} = $settings
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not sending too many concurrent http requests
const allRelays = await db.table('relays').toArray()
const staleRelays = allRelays
.filter(r => (r.refreshed_at || 0) < now() - timedelta(7, 'days'))
const staleRelaysSample = shuffle(staleRelays).slice(0, 10)
const freshRelays = await Promise.all(
staleRelaysSample.map(async ({url}) => {
const res = await fetch(dufflepudUrl + '/relay/info', {
method: 'POST',
body: JSON.stringify({url}),
headers: {
'Content-Type': 'application/json',
},
})
return {...await res.json(), url, refreshed_at: now()}
})
)
db.table('relays').bulkPut(freshRelays)
}
// Close menu on click outside
document.querySelector("html").addEventListener("click", e => {

View File

@ -23,7 +23,7 @@ export const loadAppData = pubkey => {
])
}
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}, usingExtension = false) => {
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
if (privkey) {
keys.setPrivateKey(privkey)
} else {

View File

@ -0,0 +1,87 @@
<script lang="ts">
import {last, find, propEq} from 'ramda'
import {onMount} from 'svelte'
import {poll} from "src/util/misc"
import {switcher} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import Toggle from "src/partials/Toggle.svelte"
import {user, pool} from "src/agent"
import {addRelay, removeRelay, setRelayWriteCondition} from "src/app"
export let relay
export let i = 0
export let showControls = false
let status = null
let showStatus = false
let joined = false
$: joined = find(propEq('url', relay.url), $user?.relays || [])
onMount(() => {
return poll(300, async () => {
const conn = find(propEq('url', relay.url), pool.getConnections())
if (conn) {
const slow = conn.status === 'ready' && conn.stats.timer / conn.stats.count > 1000
// Be more strict here than with alerts
status = slow ? 'slow' : conn.status
}
})
})
</script>
<div
class="rounded border border-solid border-medium bg-dark shadow flex flex-col justify-between gap-3 py-3 px-6"
in:fly={{y: 20, delay: i * 100}}>
<div class="flex gap-2 items-center justify-between">
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
<span>{last(relay.url.split('://'))}</span>
{#if joined}
<span
on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}}
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
class:bg-danger={status === 'error'}
class:bg-warning={['pending', 'slow'].includes(status)}
class:bg-success={status === 'ready'}>
</span>
<p
class="text-light text-sm transition-all"
class:opacity-0={!showStatus}
class:opacity-1={showStatus}>
{switcher(status, {
error: 'Not connected',
pending: 'Trying to connect',
slow: 'Slow connection',
ready: 'Connected',
default: 'Waiting to reconnect',
})}
</p>
{/if}
</div>
{#if joined}
<button class="flex gap-3 items-center text-light" on:click={() => removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
{:else}
<button class="flex gap-3 items-center text-light" on:click={() => addRelay(relay.url)}>
<i class="fa fa-right-to-bracket" /> Join
</button>
{/if}
</div>
{#if relay.description}
<p>{relay.description}</p>
{/if}
{#if joined && showControls}
<div class="border-b border-solid border-medium -mx-6" />
<div class="flex justify-between gap-2">
<span>Publish to this relay?</span>
<Toggle
value={relay.write !== "!"}
on:change={() => setRelayWriteCondition(relay.url, relay.write === "!" ? "" : "!")} />
</div>
{/if}
</div>

View File

@ -11,7 +11,7 @@
const {nostr} = window as any
if (nostr) {
await login({pubkey: await nostr.getPublicKey()}, true)
await login({pubkey: await nostr.getPublicKey()})
} else {
modal.set({type: 'login/privkey'})
}

View File

@ -1,17 +1,17 @@
<script>
import {liveQuery} from 'dexie'
import {whereEq, find, last} from 'ramda'
import {noop} from 'hurdak/lib/hurdak'
import {pluck} from 'ramda'
import {noop, createMap} from 'hurdak/lib/hurdak'
import {onMount} from 'svelte'
import {get} from 'svelte/store'
import {fly} from 'svelte/transition'
import {fuzzy, poll} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Toggle from "src/partials/Toggle.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {pool, db, user, ready} from "src/agent"
import {modal, addRelay, removeRelay, setRelayWriteCondition, settings} from "src/app"
import {modal, settings} from "src/app"
import defaults from "src/agent/defaults"
let q = ""
@ -21,9 +21,9 @@
fetch(get(settings).dufflepudUrl + '/relay')
.then(async res => {
const {relays} = await res.json()
const json = await res.json()
for (const url of relays) {
for (const url of json.relays) {
db.table('relays').put({url})
}
}).catch(noop)
@ -34,23 +34,27 @@
const knownRelays = liveQuery(() => db.table('relays').toArray())
$: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
$: {
const joined = pluck('url', $user?.relays || [])
const data = ($knownRelays || []).filter(r => !joined.includes(r.url))
const join = async url => {
await addRelay({url})
}
const leave = async url => {
await removeRelay(url)
search = fuzzy(data, {keys: ["name", "description", "url"]})
}
onMount(() => {
// Attempt to connect so we can show status
relays.forEach(relay => pool.connect(relay.url))
return poll(300, () => {
return poll(300, async () => {
if ($ready) {
relays = $user?.relays || []
const userRelays = $user?.relays || []
const urls = pluck('url', userRelays)
const relaysByUrl = createMap(
'url',
await db.table('relays').where('url').anyOf(urls).toArray()
)
relays = userRelays.map(relay => ({...relaysByUrl[relay.url], ...relay}))
}
status = Object.fromEntries(
@ -84,49 +88,9 @@
{#if relays.length === 0}
<div class="text-center">No relays connected</div>
{/if}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{#each relays as {url, write}, i (url)}
<div class="rounded border border-solid border-medium bg-dark shadow" in:fly={{y: 20, delay: i * 100}}>
<div class="flex flex-col gap-2 py-3 px-6">
<div class="flex gap-2 items-center justify-between">
<strong class="flex gap-2 items-center">
<i class={url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
{last(url.split('://'))}
</strong>
<button class="fa fa-times cursor-pointer" on:click={() => leave(url)} />
</div>
<p class="text-light">
{#if status[url] === 'error'}
<div class="flex gap-2 items-center">
<span class="inline-block w-2 h-2 rounded bg-danger" /> Not connected
</div>
{:else if status[url] === 'pending'}
<div class="flex gap-2 items-center">
<span class="inline-block w-2 h-2 rounded bg-warning" /> Trying to connect
</div>
{:else if status[url] === 'slow'}
<div class="flex gap-2 items-center">
<span class="inline-block w-2 h-2 rounded bg-warning" /> Slow connection
</div>
{:else if status[url] === 'ready'}
<div class="flex gap-2 items-center">
<span class="inline-block w-2 h-2 rounded bg-success" /> Connected
</div>
{:else}
<div class="flex gap-2 items-center">
<span class="inline-block w-2 h-2 rounded bg-medium" /> Waiting to reconnect
</div>
{/if}
</p>
</div>
<div class="border-b border-solid border-medium" />
<div class="flex justify-between gap-2 py-3 px-6">
<span>Publish to this relay?</span>
<Toggle
value={write !== "!"}
on:change={() => setRelayWriteCondition(url, write === "!" ? "" : "!")} />
</div>
</div>
<div class="grid grid-cols-1 gap-4">
{#each relays as relay, i (relay.url)}
<RelayCard showControls {relay} {i} />
{/each}
</div>
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
@ -144,18 +108,8 @@
<i slot="before" class="fa-solid fa-search" />
</Input>
{/if}
{#each (search(q) || []).slice(0, 50) as {url, name, description} (url)}
{#if !find(whereEq({url}), relays)}
<div class="flex gap-2 justify-between">
<div>
<strong>{name || url}</strong>
<p class="text-light">{description || ''}</p>
</div>
<button class="underline cursor-pointer" on:click={() => join(url)}>
Join
</button>
</div>
{/if}
{#each (search(q) || []).slice(0, 50) as relay, i (relay.url)}
<RelayCard {relay} {i} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - relays.length, 50)}

View File

@ -12,7 +12,7 @@
let input = null
const onSubmit = async e => {
const onSubmit = async () => {
const {content, mentions, topics} = input.parse()
if (content) {

View File

@ -1,10 +1,7 @@
<script>
import {last, find, whereEq} from 'ramda'
import {fly} from 'svelte/transition'
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {user} from 'src/agent'
import {addRelay, removeRelay} from "src/app"
import RelayCard from "src/partials/RelayCard.svelte"
export let person
</script>
@ -18,20 +15,10 @@
{#if (person.relays || []).length === 0}
<div class="pt-8 text-center">No relays found</div>
{:else}
{#each person.relays as {url, write}, i (url)}
<div class="rounded border border-solid border-medium bg-dark shadow p-2 px-3">
<div class="flex gap-2 items-center justify-between">
<strong class="flex gap-2 items-center">
<i class={url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
{last(url.split('://'))}
</strong>
{#if find(whereEq({url}), $user.relays)}
<Anchor type="button" on:click={() => removeRelay({url})}>Leave</Anchor>
{:else}
<Anchor type="button" on:click={() => addRelay({url})}>Join</Anchor>
{/if}
</div>
</div>
{#each person.relays as relay, i (relay.url)}
{#if relay.write !== '!'}
<RelayCard {relay} {i} />
{/if}
{/each}
{/if}
</Content>