Re-organize app to handle anonymous users

This commit is contained in:
Jonathan Staab 2023-02-16 17:22:51 -06:00
parent 65f184ba48
commit e2baa5c0c9
48 changed files with 488 additions and 407 deletions

View File

@ -13,7 +13,7 @@
- [x] Make chat header overlap main header to save space - [x] Make chat header overlap main header to save space
- [x] Strip formatting when pasting into Compose - [x] Strip formatting when pasting into Compose
- [x] Upgraded nostr-tools to 1.4.1 - [x] Upgraded nostr-tools to 1.4.1
)
## 0.2.11 ## 0.2.11
- [x] Converted threshold to percentage - [x] Converted threshold to percentage

View File

@ -1,7 +1,5 @@
# Current # Current
- [ ] Review 10002 usage https://github.com/nostr-protocol/nips/blob/master/65.md
- [ ] Remove relays from people, pull from routes only
- [ ] Fix anon/new user experience - [ ] Fix anon/new user experience
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead. - [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
- [ ] Separate user info and relays so we can still select/figure out relays for anons - [ ] Separate user info and relays so we can still select/figure out relays for anons

1
public/fonts/Montserrat Normal file
View File

@ -0,0 +1 @@

View File

@ -2,182 +2,78 @@
import "@fortawesome/fontawesome-free/css/fontawesome.css" import "@fortawesome/fontawesome-free/css/fontawesome.css"
import "@fortawesome/fontawesome-free/css/solid.css" import "@fortawesome/fontawesome-free/css/solid.css"
import {find, is, identity, nthArg, pluck} from 'ramda' import {onMount} from 'svelte'
import {createMap, first} from 'hurdak/lib/hurdak'
import {writable, get} from "svelte/store"
import {fly, fade} from "svelte/transition"
import {cubicInOut} from "svelte/easing"
import {Router, Route, links, navigate} from "svelte-routing" import {Router, Route, links, navigate} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history" import {globalHistory} from "svelte-routing/src/history"
import {cubicInOut} from "svelte/easing"
import {writable, get} from "svelte/store"
import {fly, fade} from "svelte/transition"
import {createMap, first} from 'hurdak/lib/hurdak'
import {find, is, identity, nthArg, pluck} from 'ramda'
import {log, warn} from 'src/util/logger' import {log, warn} from 'src/util/logger'
import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, shuffle, now, sleep} from 'src/util/misc' import {timedelta, shuffle, now, sleep} from 'src/util/misc'
import {displayPerson, isLike} from 'src/util/nostr'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import {user} from 'src/agent/user'
import {getUserRelays} from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import network from 'src/agent/network' import network from 'src/agent/network'
import pool from 'src/agent/pool' import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays'
import {relays} from 'src/agent/relays'
import sync from 'src/agent/sync' import sync from 'src/agent/sync'
import {modal, toast, settings, logUsage, alerts, messages, loadAppData} from "src/app" import {user} from 'src/agent/user'
import {routes} from "src/app/ui" import {loadAppData} from "src/app"
import Anchor from 'src/partials/Anchor.svelte' import alerts from "src/app/alerts"
import Content from 'src/partials/Content.svelte' import messages from "src/app/messages"
import Spinner from 'src/partials/Spinner.svelte' import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui"
import Modal from 'src/partials/Modal.svelte'
import RelayCard from "src/partials/RelayCard.svelte" import RelayCard from "src/partials/RelayCard.svelte"
import SignUp from "src/views/SignUp.svelte"
import PersonList from "src/views/PersonList.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import PersonShare from "src/views/PersonShare.svelte"
import NoteCreate from "src/views/NoteCreate.svelte"
import ChatEdit from "src/views/ChatEdit.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Search from "src/routes/Search.svelte"
import Alerts from "src/routes/Alerts.svelte"
import Notes from "src/routes/Notes.svelte"
import Debug from "src/routes/Debug.svelte"
import Login from "src/routes/Login.svelte"
import Logout from "src/routes/Logout.svelte"
import Profile from "src/routes/Profile.svelte"
import Settings from "src/routes/Settings.svelte"
import Keys from "src/routes/Keys.svelte"
import RelayList from "src/routes/RelayList.svelte"
import AddRelay from "src/routes/AddRelay.svelte" import AddRelay from "src/routes/AddRelay.svelte"
import Person from "src/routes/Person.svelte" import Alerts from "src/routes/Alerts.svelte"
import Bech32Entity from "src/routes/Bech32Entity.svelte" import Bech32Entity from "src/routes/Bech32Entity.svelte"
import Chat from "src/routes/Chat.svelte" import Chat from "src/routes/Chat.svelte"
import ChatRoom from "src/routes/ChatRoom.svelte" import ChatRoom from "src/routes/ChatRoom.svelte"
import Debug from "src/routes/Debug.svelte"
import Keys from "src/routes/Keys.svelte"
import Login from "src/routes/Login.svelte"
import Logout from "src/routes/Logout.svelte"
import Messages from "src/routes/Messages.svelte" import Messages from "src/routes/Messages.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Notes from "src/routes/Notes.svelte"
import Person from "src/routes/Person.svelte"
import Profile from "src/routes/Profile.svelte"
import RelayList from "src/routes/RelayList.svelte"
import Search from "src/routes/Search.svelte"
import Settings from "src/routes/Settings.svelte"
import ChatEdit from "src/views/ChatEdit.svelte"
import NoteCreate from "src/views/NoteCreate.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonList from "src/views/PersonList.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import PersonShare from "src/views/PersonShare.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import SignUp from "src/views/SignUp.svelte"
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import Modal from 'src/partials/Modal.svelte'
import SideNav from 'src/partials/SideNav.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import TopNav from 'src/partials/TopNav.svelte'
Object.assign(window, {cmd, database, keys, network, pool, sync}) Object.assign(window, {cmd, database, keys, network, pool, sync})
export let url = "" export let url = ""
const menuIsOpen = writable(false) let scrollY
const toggleMenu = () => menuIsOpen.update(x => !x)
const searchIsOpen = writable(false) const {ready} = database
const toggleSearch = () => searchIsOpen.update(x => !x)
const closeModal = async () => { const closeModal = async () => {
modal.clear() modal.clear()
menuIsOpen.set(false) menuIsOpen.set(false)
} }
const {ready} = database onMount(() => {
const {lastCheckedAlerts, mostRecentAlert} = alerts
const {lastCheckedByPubkey, mostRecentByPubkey} = messages
let menuIcon
let scrollY
let suspendedSubs = []
let slowConnections = []
let hasNewMessages = false
$: {
hasNewMessages = Boolean(find(
([k, t]) => {
return t > now() - timedelta(7, 'days') && ($lastCheckedByPubkey[k] || 0) < t
},
Object.entries($mostRecentByPubkey)
))
}
database.onReady(() => {
if ($user) {
loadAppData($user.pubkey)
}
// Background work
const interval = setInterval(() => {
alertSlowConnections()
retrieveRelayMeta()
}, 30_000)
const alertSlowConnections = () => {
// Only notify about relays the user is actually subscribed to
const relayUrls = pluck('url', getUserRelays())
// Prune connections we haven't used in a while
pool.getConnections()
.filter(conn => conn.lastRequest < Date.now() - 60_000)
.forEach(conn => conn.disconnect())
// Log stats for debugging purposes
log(
'Connection stats',
pool.getConnections()
.map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`)
)
// Alert the user to any heinously slow connections
slowConnections = pool.getConnections()
.filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3)
}
const retrieveRelayMeta = async () => {
const {dufflepudUrl} = $settings
if (!dufflepudUrl) {
return
}
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not sending too many concurrent http requests
const staleRelays = shuffle(
await database.relays.all({
'refreshed_at:lt': now() - timedelta(7, 'days'),
})
).slice(0, 10)
const freshRelays = await Promise.all(
staleRelays.map(async ({url}) => {
try {
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()}
} catch (e) {
if (!e.toString().includes('Failed to fetch')) {
warn(e)
}
return {url, refreshed_at: now()}
}
})
)
database.relays.bulkPatch(createMap('url', freshRelays.filter(identity)))
}
// Close menu on click outside
document.querySelector("html").addEventListener("click", e => {
if (e.target !== menuIcon) {
menuIsOpen.set(false)
}
})
// Log usage on navigate
const unsubHistory = globalHistory.listen(({location}) => {
if (!location.hash) {
// Remove identifying information, e.g. pubkeys, event ids, etc
const name = location.pathname.slice(1)
.replace(/(npub|nprofile|note|nevent)[^\/]+/g, (_, m) => `<${m}>`)
logUsage(btoa(['page', name].join(':')))
}
})
// Keep scroll position on body, but don't allow scrolling // Keep scroll position on body, but don't allow scrolling
const unsubModal = modal.subscribe($modal => { const unsubModal = modal.subscribe($modal => {
if ($modal) { if ($modal) {
@ -196,17 +92,79 @@
} }
}) })
// Log usage on navigate
const unsubHistory = globalHistory.listen(({location}) => {
if (!location.hash) {
// Remove identifying information, e.g. pubkeys, event ids, etc
const name = location.pathname.slice(1)
.replace(/(npub|nprofile|note|nevent)[^\/]+/g, (_, m) => `<${m}>`)
logUsage(btoa(['page', name].join(':')))
}
})
return () => { return () => {
clearInterval(interval)
unsubHistory() unsubHistory()
unsubModal() unsubModal()
} }
}) })
database.onReady(() => {
if ($user) {
loadAppData($user.pubkey)
}
const interval = setInterval(
async () => {
const {dufflepudUrl} = $settings
if (!dufflepudUrl) {
return
}
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not sending too many concurrent http requests
const staleRelays = shuffle(
await database.relays.all({
'refreshed_at:lt': now() - timedelta(7, 'days'),
})
).slice(0, 10)
const freshRelays = await Promise.all(
staleRelays.map(async ({url}) => {
try {
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()}
} catch (e) {
if (!e.toString().includes('Failed to fetch')) {
warn(e)
}
return {url, refreshed_at: now()}
}
})
)
database.relays.bulkPatch(createMap('url', freshRelays.filter(identity)))
},
30_000
)
return () => {
clearInterval(interval)
}
})
</script> </script>
<Router {url}> <Router {url}>
<div use:links class="h-full"> <div use:links class="h-full">
{#if $ready}
<div class="pt-16 text-white h-full lg:ml-56"> <div class="pt-16 text-white h-full lg:ml-56">
<Route path="/alerts" component={Alerts} /> <Route path="/alerts" component={Alerts} />
<Route path="/search/:activeTab" component={Search} /> <Route path="/search/:activeTab" component={Search} />
@ -241,107 +199,9 @@
</Route> </Route>
<Route path="*" component={NotFound} /> <Route path="*" component={NotFound} />
</div> </div>
{/if}
<ul <SideNav />
class="py-20 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl <TopNav />
border-r border-medium text-white overflow-hidden z-10 lg:ml-0"
class:-ml-56={!$menuIsOpen}
>
{#if $user}
<li>
<a href={routes.person($user.pubkey)} class="flex gap-2 px-4 py-2 pb-6 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$user.picture})" />
<span class="text-lg font-bold">{displayPerson($user)}</span>
</a>
</li>
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/alerts">
<i class="fa-solid fa-bell mr-2" /> Alerts
{#if $mostRecentAlert > $lastCheckedAlerts}
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
{/if}
</a>
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/search/people">
<i class="fa-solid fa-search mr-2" /> Search
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/network">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>
{#if $user}
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
<i class="fa-solid fa-message mr-2" /> Chat
{#if hasNewMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-7" />
{/if}
</a>
</li>
{/if}
<li class="h-px mx-3 my-4 bg-medium" />
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/relays">
<i class="fa-solid fa-server mr-2" /> Relays
{#if slowConnections.length > 0}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-8" />
{/if}
</a>
</li>
{#if $user}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/keys">
<i class="fa-solid fa-key mr-2" /> Keys
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/settings">
<i class="fa-solid fa-gear mr-2" /> Settings
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/logout">
<i class="fa-solid fa-right-from-bracket mr-2" /> Logout
</a>
</li>
{:else}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/login">
<i class="fa-solid fa-right-to-bracket mr-2" /> Login
</a>
</li>
{/if}
{#if import.meta.env.VITE_SHOW_DEBUG_ROUTE === 'true'}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/debug">
<i class="fa-solid fa-bug mr-2" /> Debug
</a>
</li>
{/if}
</ul>
<div
class="fixed top-0 bg-dark flex justify-between items-center text-white w-full p-4
border-b border-medium z-10"
>
<div class="lg:hidden">
<button class="fa-solid fa-bars fa-2xl cursor-pointer" bind:this={menuIcon} on:click={toggleMenu} />
</div>
<Anchor external type="unstyled" href="https://github.com/staab/coracle" class="flex items-center gap-2">
<img alt="Coracle Logo" src="/images/favicon.png" class="w-8" />
<h1 class="staatliches text-3xl">Coracle</h1>
</Anchor>
{#if $mostRecentAlert > $lastCheckedAlerts || hasNewMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12 lg:hidden" />
{/if}
</div>
{#if $modal} {#if $modal}
<Modal onEscape={closeModal}> <Modal onEscape={closeModal}>
@ -409,3 +269,4 @@
{/if} {/if}
</div> </div>
</Router> </Router>

View File

@ -1,2 +0,0 @@
onboarding

View File

@ -1,18 +0,0 @@
<script lang="ts">
import Onboarding from 'src/Onboarding.svelte'
import App from 'src/App.svelte'
import {relays} from 'src/agent/relays'
import {showOnboarding} from 'src/app/ui'
$: {
if ($relays.length === 0) {
showOnboarding.set(true)
}
}
</script>
{#if $showOnboarding}
<Onboarding />
{:else}
<App />
{/if}

View File

@ -242,9 +242,9 @@ const relays = new Table('relays', 'url', {
{url: 'wss://nostr.zebedee.cloud'}, {url: 'wss://nostr.zebedee.cloud'},
{url: 'wss://nostr-pub.wellorder.net'}, {url: 'wss://nostr-pub.wellorder.net'},
{url: 'wss://relay.nostr.band'}, {url: 'wss://relay.nostr.band'},
{url: 'nostr.pleb.network'}, {url: 'wss://nostr.pleb.network'},
{url: 'relay.nostrich.de'}, {url: 'wss://relay.nostrich.de'},
{url: 'relay.damus.io'}, {url: 'wss://relay.damus.io'},
]) ])
return Object.assign(data, defaults) return Object.assign(data, defaults)

View File

@ -4,7 +4,7 @@ import {chunk} from 'hurdak/lib/hurdak'
import {batch, timedelta, now} from 'src/util/misc' import {batch, timedelta, now} from 'src/util/misc'
import { import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores, getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserNetworkWriteRelays, getUserNetworkWriteRelays, getUserReadRelays,
} from 'src/agent/relays' } from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import pool from 'src/agent/pool' import pool from 'src/agent/pool'
@ -79,7 +79,7 @@ const loadPeople = (pubkeys, {kinds = personKinds, force = false, ...opts} = {})
} }
return load( return load(
getAllPubkeyWriteRelays(pubkeys).slice(0, 10), getUserReadRelays().concat(getAllPubkeyWriteRelays(pubkeys)).slice(0, 10),
{kinds, authors: pubkeys}, {kinds, authors: pubkeys},
opts opts
) )

View File

@ -1,8 +1,9 @@
import type {Relay} from 'src/util/types' import type {Relay} from 'src/util/types'
import {writable, get} from 'svelte/store' import {get} from 'svelte/store'
import {pick, map, assoc, sortBy, uniq, uniqBy, prop} from 'ramda' import {pick, map, assoc, sortBy, uniq, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak' import {first} from 'hurdak/lib/hurdak'
import {Tags} from 'src/util/nostr' import {Tags} from 'src/util/nostr'
import {synced} from 'src/util/misc'
import {getFollows} from 'src/agent/social' import {getFollows} from 'src/agent/social'
import database from 'src/agent/database' import database from 'src/agent/database'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -19,7 +20,7 @@ import keys from 'src/agent/keys'
// doesn't need to see. // doesn't need to see.
// 5) Advertise relays — write and read back your own relay list // 5) Advertise relays — write and read back your own relay list
export const relays = writable([]) export const relays = synced('agent/relays', [])
// Pubkey relays // Pubkey relays

View File

@ -1,4 +1,4 @@
import {pick, identity, isEmpty} from 'ramda' import {pick, objOf, identity, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools' import {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak' import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {log, warn} from 'src/util/logger' import {log, warn} from 'src/util/logger'
@ -142,7 +142,7 @@ const calculateRoute = (pubkey, url, type, mode, created_at) => {
} }
const processRoutes = async events => { const processRoutes = async events => {
const updates = [] let updates = []
// Sample events so we're not burning too many resources // Sample events so we're not burning too many resources
for (const e of ensurePlural(shuffle(events)).slice(0, 10)) { for (const e of ensurePlural(shuffle(events)).slice(0, 10)) {
@ -199,7 +199,10 @@ const processRoutes = async events => {
}) })
} }
updates = updates.filter(identity)
if (!isEmpty(updates)) { if (!isEmpty(updates)) {
await database.relays.bulkPatch(createMap('url', updates.map(pick(['url']))))
await database.routes.bulkPut(createMap('id', updates.filter(identity))) await database.routes.bulkPut(createMap('id', updates.filter(identity)))
} }
} }
@ -224,8 +227,12 @@ const verifyNip05 = (pubkey, as) =>
database.people.patch({...person, verified_as: as}) database.people.patch({...person, verified_as: as})
if (result.relays?.length > 0) { if (result.relays?.length > 0) {
const urls = result.relays.filter(isRelay)
database.relays.bulkPatch(createMap('url', urls.map(objOf('url'))))
database.routes.bulkPut( database.routes.bulkPut(
createMap('id', result.relays.filter(isRelay).flatMap(url =>[ createMap('id', urls.flatMap(url =>[
calculateRoute(pubkey, url, 'nip05', 'write', now()), calculateRoute(pubkey, url, 'nip05', 'write', now()),
calculateRoute(pubkey, url, 'nip05', 'read', now()), calculateRoute(pubkey, url, 'nip05', 'read', now()),
])) ]))

31
src/app/connection.js Normal file
View File

@ -0,0 +1,31 @@
import {pluck} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {writable} from 'svelte/store'
import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays'
export const slowConnections = writable([])
setInterval(() => {
// Only notify about relays the user is actually subscribed to
const relayUrls = pluck('url', getUserRelays())
// Prune connections we haven't used in a while
pool.getConnections()
.filter(conn => conn.lastRequest < Date.now() - 60_000)
.forEach(conn => conn.disconnect())
// Log stats for debugging purposes
log(
'Connection stats',
pool.getConnections()
.map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`)
)
// Alert the user to any heinously slow connections
slowConnections.set(
pool.getConnections()
.filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3)
)
}, 30_000)

View File

@ -7,24 +7,25 @@ import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr' import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {getNetwork} from 'src/agent/social' import {getNetwork} from 'src/agent/social'
import {relays} from 'src/agent/relays' import {relays, getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import alerts from 'src/app/alerts' import alerts from 'src/app/alerts'
import messages from 'src/app/messages' import messages from 'src/app/messages'
import {toast, routes, modal, settings, logUsage} from 'src/app/ui' import {routes, modal} from 'src/app/ui'
export {toast, modal, settings, alerts, messages, logUsage} export const loadAppData = async pubkey => {
if (getUserReadRelays().length > 0) {
export const loadAppData = pubkey => await Promise.all([
Promise.all([ alerts.load(pubkey),
alerts.load(pubkey), alerts.listen(pubkey),
alerts.listen(pubkey), messages.listen(pubkey),
messages.listen(pubkey), network.loadPeople(getNetwork(pubkey)),
network.loadPeople(getNetwork(pubkey)), ])
]) }
}
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => { export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
if (privkey) { if (privkey) {
@ -35,31 +36,35 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
modal.set({type: 'message', message: "Loading your profile data...", spinner: true}) modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
// Load our user so we can populate network and show profile info if (getUserReadRelays().length === 0) {
await network.loadPeople([pubkey]) navigate('/relays')
} else {
// Load our user so we can populate network and show profile info
await network.loadPeople([pubkey])
// Load network and start listening, but don't wait for it // Load network and start listening, but don't wait for it
loadAppData(pubkey) loadAppData(pubkey)
// Not ideal, but the network tab depends on the user's social network being // Not ideal, but the network tab depends on the user's social network being
// loaded, so put them on global when they first log in so we're not slowing // loaded, so put them on global when they first log in so we're not slowing
// down users' first run experience too much // down users' first run experience too much
navigate('/notes/network') navigate('/notes/network')
}
} }
export const addRelay = async url => { export const addRelay = async url => {
const person = get(user) as Person const $user = get(user) as Person
relays.update($relays => { relays.update($relays => {
$relays.push({url, write: false, read: true}) $relays.push({url, write: false, read: true})
if (person) { if ($user) {
(async () => { (async () => {
// Publish to the new set of relays // Publish to the new set of relays
await cmd.setRelays($relays, $relays) await cmd.setRelays($relays, $relays)
// Reload alerts, messages, etc // Reload alerts, messages, etc
await loadAppData(person.pubkey) await loadAppData($user.pubkey)
})() })()
} }
@ -68,12 +73,12 @@ export const addRelay = async url => {
} }
export const removeRelay = async url => { export const removeRelay = async url => {
const person = get(user) as Person const $user = get(user) as Person
relays.update($relays => { relays.update($relays => {
$relays = reject(whereEq({url}), $relays) $relays = reject(whereEq({url}), $relays)
if (person && $relays.length > 0) { if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays) cmd.setRelays($relays, $relays)
} }
@ -82,12 +87,12 @@ export const removeRelay = async url => {
} }
export const setRelayWriteCondition = async (url, write) => { export const setRelayWriteCondition = async (url, write) => {
const person = get(user) as Person const $user = get(user) as Person
relays.update($relays => { relays.update($relays => {
$relays = $relays.map(when(whereEq({url}), assoc('write', write))) $relays = $relays.map(when(whereEq({url}), assoc('write', write)))
if (person && $relays.length > 0) { if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays) cmd.setRelays($relays, $relays)
} }

View File

@ -1,5 +1,5 @@
import {pluck, reject} from 'ramda' import {pluck, find, reject} from 'ramda'
import {get} from 'svelte/store' import {get, derived} from 'svelte/store'
import {synced, now, timedelta} from 'src/util/misc' import {synced, now, timedelta} from 'src/util/misc'
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays' import {getUserReadRelays} from 'src/agent/relays'
@ -12,6 +12,18 @@ const since = now() - timedelta(30, 'days')
const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {}) const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {})
const lastCheckedByPubkey = synced('app/messages/lastCheckedByPubkey', {}) const lastCheckedByPubkey = synced('app/messages/lastCheckedByPubkey', {})
const hasNewMessages = derived(
[lastCheckedByPubkey, mostRecentByPubkey],
([$lastCheckedByPubkey, $mostRecentByPubkey]) => {
return Boolean(find(
([k, t]) => {
return t > now() - timedelta(7, 'days') && ($lastCheckedByPubkey[k] || 0) < t
},
Object.entries($mostRecentByPubkey)
))
}
)
const listen = async pubkey => { const listen = async pubkey => {
if (listener) { if (listener) {
listener.unsub() listener.unsub()
@ -44,4 +56,4 @@ const listen = async pubkey => {
) )
} }
export default {listen, mostRecentByPubkey, lastCheckedByPubkey} export default {listen, mostRecentByPubkey, lastCheckedByPubkey, hasNewMessages}

View File

@ -35,6 +35,10 @@ toast.show = (type, message, timeout = 5) => {
}, timeout * 1000) }, timeout * 1000)
} }
// Menu
export const menuIsOpen = writable(false)
// Modals // Modals
export const modal = { export const modal = {
@ -110,5 +114,3 @@ export const logUsage = async name => {
} }
} }
} }
export const showOnboarding = writable(false)

View File

@ -7,9 +7,9 @@ Bugsnag.start({
collectUserIp: false, collectUserIp: false,
}) })
import Shell from 'src/Shell.svelte' import App from 'src/App.svelte'
const app = new Shell({ const app = new App({
target: document.getElementById('app') target: document.getElementById('app')
}) })

View File

@ -6,7 +6,7 @@
import {formatTimestamp} from 'src/util/misc' import {formatTimestamp} from 'src/util/misc'
import {killEvent} from 'src/util/html' import {killEvent} from 'src/util/html'
import database from 'src/agent/database' import database from 'src/agent/database'
import {modal} from 'src/app' import {modal} from 'src/app/ui'
export let note export let note

View File

@ -1,7 +1,7 @@
<script> <script>
import {fly, fade} from "svelte/transition" import {fly, fade} from "svelte/transition"
export let onEscape export let onEscape = null
export let nested = false export let nested = false
let root let root
@ -14,9 +14,10 @@
} }
}} /> }} />
<div class="fixed inset-0 z-10 modal" bind:this={root}> <div class="fixed inset-0 z-20 modal" bind:this={root}>
<button <div
class="absolute inset-0 bg-black cursor-pointer" class="absolute inset-0 bg-black"
class:cursor-pointer={onEscape}
class:opacity-75={!nested} class:opacity-75={!nested}
class:opacity-25={nested} class:opacity-25={nested}
transition:fade transition:fade

View File

@ -0,0 +1,14 @@
<script lang="ts">
import cx from 'classnames'
export let href
export let icon
export let inert = false
</script>
<li class={cx($$props.class, {'transition-all hover:bg-accent': !inert}, "cursor-pointer")}>
<a {href} class="px-4 py-2 flex gap-2 items-center">
<i class={`fa fa-${icon}`} />
<slot />
</a>
</li>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import {modal} from "src/app" import {modal} from "src/app/ui"
export let pubkey = null export let pubkey = null
</script> </script>

View File

@ -12,7 +12,8 @@
import ImageCircle from 'src/partials/ImageCircle.svelte' import ImageCircle from 'src/partials/ImageCircle.svelte'
import Preview from 'src/partials/Preview.svelte' import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import {toast, settings, modal, renderNote} from "src/app" import {toast, settings, modal} from "src/app/ui"
import {renderNote} from "src/app"
import {formatTimestamp, stringToColor} from 'src/util/misc' import {formatTimestamp, stringToColor} from 'src/util/misc'
import Compose from "src/partials/Compose.svelte" import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte" import Card from "src/partials/Card.svelte"

View File

@ -9,11 +9,13 @@
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Note from "src/partials/Note.svelte" import Note from "src/partials/Note.svelte"
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import network from 'src/agent/network' import network from 'src/agent/network'
import {modal, mergeParents} from "src/app" import {modal} from "src/app/ui"
import {mergeParents} from "src/app"
export let relays
export let filter export let filter
export let relays = []
export let shouldDisplay = always(true) export let shouldDisplay = always(true)
let notes = [] let notes = []
@ -74,6 +76,10 @@
} }
onMount(() => { onMount(() => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
const sub = network.listen(relays, {...filter, since}, onChunk) const sub = network.listen(relays, {...filter, since}, onChunk)
const scroller = createScroller(() => { const scroller = createScroller(() => {

View File

@ -50,7 +50,8 @@
on:mouseout={() => {showStatus = false}} on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}} on:mouseover={() => {showStatus = true}}
class="w-2 h-2 rounded-full bg-medium cursor-pointer" class="w-2 h-2 rounded-full bg-medium cursor-pointer"
class:bg-danger={quality <= 0.3} class:bg-medium={message === 'Not connected'}
class:bg-danger={quality <= 0.3 && message !== 'Not connected'}
class:bg-warning={between(0.3, 0.7, quality)} class:bg-warning={between(0.3, 0.7, quality)}
class:bg-success={quality > 0.7}> class:bg-success={quality > 0.7}>
</span> </span>
@ -79,8 +80,8 @@
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<span>Publish to this relay?</span> <span>Publish to this relay?</span>
<Toggle <Toggle
value={relay.write !== "!"} value={relay.write}
on:change={() => setRelayWriteCondition(relay.url, relay.write === "!" ? "" : "!")} /> on:change={() => setRelayWriteCondition(relay.url, !relay.write)} />
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -5,7 +5,7 @@
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import {displayPerson} from 'src/util/nostr' import {displayPerson} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc' import {now, timedelta} from 'src/util/misc'
import {messages} from 'src/app' import messages from 'src/app/messages'
export let joined = false export let joined = false
export let room export let room

View File

@ -0,0 +1,96 @@
<script lang="ts">
import {displayPerson} from 'src/util/nostr'
import {user} from 'src/agent/user'
import {menuIsOpen, routes} from 'src/app/ui'
import alerts from 'src/app/alerts'
import messages from 'src/app/messages'
import {slowConnections} from 'src/app/connection'
const {mostRecentAlert, lastCheckedAlerts} = alerts
const {hasNewMessages} = messages
</script>
<ul
class="py-20 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
border-r border-medium text-white overflow-hidden z-10 lg:ml-0"
class:-ml-56={!$menuIsOpen}
>
{#if $user}
<li>
<a href={routes.person($user.pubkey)} class="flex gap-2 px-4 py-2 pb-6 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$user.picture})" />
<span class="text-lg font-bold">{displayPerson($user)}</span>
</a>
</li>
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/alerts">
<i class="fa-solid fa-bell mr-2" /> Alerts
{#if $mostRecentAlert > $lastCheckedAlerts}
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
{/if}
</a>
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/search/people">
<i class="fa-solid fa-search mr-2" /> Search
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/network">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>
{#if $user}
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
<i class="fa-solid fa-message mr-2" /> Chat
{#if $hasNewMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-7" />
{/if}
</a>
</li>
{/if}
<li class="h-px mx-3 my-4 bg-medium" />
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/relays">
<i class="fa-solid fa-server mr-2" /> Relays
{#if $slowConnections.length > 0}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-8" />
{/if}
</a>
</li>
{#if $user}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/keys">
<i class="fa-solid fa-key mr-2" /> Keys
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/settings">
<i class="fa-solid fa-gear mr-2" /> Settings
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/logout">
<i class="fa-solid fa-right-from-bracket mr-2" /> Logout
</a>
</li>
{:else}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/login">
<i class="fa-solid fa-right-to-bracket mr-2" /> Login
</a>
</li>
{/if}
{#if import.meta.env.VITE_SHOW_DEBUG_ROUTE === 'true'}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/debug">
<i class="fa-solid fa-bug mr-2" /> Debug
</a>
</li>
{/if}
</ul>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import {onMount} from 'svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {menuIsOpen} from 'src/app/ui'
import alerts from "src/app/alerts"
import messages from "src/app/messages"
const {lastCheckedAlerts, mostRecentAlert} = alerts
const {hasNewMessages} = messages
const toggleMenu = () => menuIsOpen.update(x => !x)
onMount(() => {
document.querySelector("html").addEventListener("click", e => {
if (!(e.target as any).matches('.fa-bars')) {
menuIsOpen.set(false)
}
})
})
</script>
<div
class="fixed top-0 bg-dark flex justify-between items-center text-white w-full p-4
border-b border-medium z-10"
>
<button class="lg:hidden fa fa-bars fa-2xl cursor-pointer" on:click={toggleMenu} />
<Anchor external type="unstyled" href="https://github.com/staab/coracle" class="flex items-center gap-2">
<img alt="Coracle Logo" src="/images/favicon.png" class="w-8" />
<h1 class="staatliches text-3xl">Coracle</h1>
</Anchor>
{#if $mostRecentAlert > $lastCheckedAlerts || $hasNewMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12 lg:hidden" />
{/if}
</div>

View File

@ -1,6 +1,7 @@
<script> <script>
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {toast, modal, addRelay} from "src/app" import {toast, modal} from "src/app/ui"
import {addRelay} from 'src/app'
import Input from 'src/partials/Input.svelte' import Input from 'src/partials/Input.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'

View File

@ -8,7 +8,8 @@
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Like from 'src/partials/Like.svelte' import Like from 'src/partials/Like.svelte'
import database from 'src/agent/database' import database from 'src/agent/database'
import {alerts, asDisplayEvent} from 'src/app' import alerts from 'src/app/alerts'
import {asDisplayEvent} from 'src/app'
let limit = 0 let limit = 0
let notes = null let notes = null

View File

@ -8,7 +8,8 @@
import network from 'src/agent/network' import network from 'src/agent/network'
import database from 'src/agent/database' import database from 'src/agent/database'
import {getUserReadRelays} from 'src/agent/relays' import {getUserReadRelays} from 'src/agent/relays'
import {modal, messages} from 'src/app' import {modal} from 'src/app/ui'
import messages from 'src/app/messages'
import Room from "src/partials/Room.svelte" import Room from "src/partials/Room.svelte"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"

View File

@ -7,7 +7,7 @@
import {getRelaysForEventChildren} from 'src/agent/relays' import {getRelaysForEventChildren} from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import {modal} from 'src/app' import {modal} from 'src/app/ui'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
export let entity export let entity

View File

@ -9,7 +9,7 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import keys from "src/agent/keys" import keys from "src/agent/keys"
import {toast} from "src/app" import {toast} from "src/app/ui"
const {pubkey, privkey} = keys const {pubkey, privkey} = keys
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md" const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"

View File

@ -3,7 +3,8 @@
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import {modal, login} from "src/app" import {modal} from "src/app/ui"
import {login} from "src/app"
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md" const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"

View File

@ -11,7 +11,7 @@
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import {routes} from 'src/app/ui' import {routes} from 'src/app/ui'
import {messages} from 'src/app' import messages from 'src/app/messages'
export let entity export let entity

View File

@ -4,13 +4,22 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import NewNoteButton from "src/partials/NewNoteButton.svelte" import NewNoteButton from "src/partials/NewNoteButton.svelte"
import Tabs from "src/partials/Tabs.svelte" import Tabs from "src/partials/Tabs.svelte"
import Modal from 'src/partials/Modal.svelte'
import RelayList from "src/routes/RelayList.svelte"
import Network from "src/views/notes/Network.svelte" import Network from "src/views/notes/Network.svelte"
import Popular from "src/views/notes/Popular.svelte" import Popular from "src/views/notes/Popular.svelte"
import {user} from 'src/agent/user' import {relays} from 'src/agent/relays'
import {user, follows} from 'src/agent/user'
export let activeTab export let activeTab
const {petnames} = follows
const setActiveTab = tab => navigate(`/notes/${tab}`) const setActiveTab = tab => navigate(`/notes/${tab}`)
// If they're not following anyone, skip network tab
if ($petnames.length === 0) {
setActiveTab('popular')
}
</script> </script>
<Content> <Content>
@ -33,3 +42,9 @@
</Content> </Content>
<NewNoteButton /> <NewNoteButton />
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

@ -21,8 +21,7 @@
import network from "src/agent/network" import network from "src/agent/network"
import keys from "src/agent/keys" import keys from "src/agent/keys"
import database from "src/agent/database" import database from "src/agent/database"
import {routes} from "src/app/ui" import {routes, modal} from "src/app/ui"
import {modal} from "src/app"
export let npub export let npub
export let activeTab export let activeTab

View File

@ -14,8 +14,7 @@
import {user} from "src/agent/user" import {user} from "src/agent/user"
import {getUserWriteRelays} from 'src/agent/relays' import {getUserWriteRelays} from 'src/agent/relays'
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast} from "src/app" import {routes, toast} from "src/app/ui"
import {routes} from "src/app/ui"
let values = {picture: null, about: null, name: null, nip05: null} let values = {picture: null, about: null, name: null, nip05: null}

View File

@ -2,13 +2,14 @@
import {pluck} from 'ramda' import {pluck} from 'ramda'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte" import RelayCard from "src/partials/RelayCard.svelte"
import {relays} from "src/agent/relays" import {relays} from "src/agent/relays"
import database from 'src/agent/database' import database from 'src/agent/database'
import {modal} from "src/app" import {modal} from "src/app/ui"
let q = "" let q = ""
let search let search
@ -18,55 +19,60 @@
const joined = new Set(pluck('url', $relays)) const joined = new Set(pluck('url', $relays))
search = fuzzy( search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)), $knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
{keys: ["name", "description", "url"]} {keys: ["name", "description", "url"]}
) )
} }
</script> </script>
<Content> <div in:fly={{y: 20}}>
<div class="flex justify-between"> <Content>
<div class="flex gap-2 items-center"> <div class="flex justify-between mt-10">
<i class="fa fa-server fa-lg" /> <div class="flex gap-2 items-center">
<h2 class="staatliches text-2xl">Your relays</h2> <i class="fa fa-server fa-lg" />
</div> <h2 class="staatliches text-2xl">Your relays</h2>
<Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}> </div>
<i class="fa-solid fa-plus" /> Add Relay <Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}>
</Anchor> <i class="fa-solid fa-plus" /> Add Relay
</div> </Anchor>
<p>
Relays are hubs for your content and connections. At least one is required to
interact with the network, but you can join as many as you like.
</p>
{#if $relays.length === 0}
<div class="text-center">No relays connected</div>
{/if}
<div class="grid grid-cols-1 gap-4">
{#each $relays as relay (relay.url)}
<RelayCard showControls {relay} />
{/each}
</div>
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
{#if ($knownRelays || []).length > 0}
<div class="pt-2 mb-2 border-b border-solid border-medium" />
<div class="flex gap-2 items-center">
<i class="fa fa-globe fa-lg" />
<h2 class="staatliches text-2xl">Other relays</h2>
</div> </div>
<p> <p>
Coracle automatically discovers relays as you browse the network. Adding more relays Relays are hubs for your content and connections. At least one is required to
will generally make things quicker to load, at the expense of higher data usage. interact with the network, but you can join as many as you like.
</p> </p>
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search"> {#if $relays.length === 0}
<i slot="before" class="fa-solid fa-search" /> <div class="text-center mt-10 flex gap-2 justify-center items-center">
</Input> <i class="fa fa-triangle-exclamation" />
No relays connected
</div>
{/if} {/if}
{#each (search(q) || []).slice(0, 50) as relay (relay.url)} <div class="grid grid-cols-1 gap-4">
<RelayCard {relay} /> {#each $relays as relay (relay.url)}
{/each} <RelayCard showControls {relay} />
<small class="text-center"> {/each}
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)} </div>
of {($knownRelays || []).length - $relays.length} known relays <div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
</small> {#if ($knownRelays || []).length > 0}
</div> <div class="pt-2 mb-2 border-b border-solid border-medium" />
</Content> <div class="flex gap-2 items-center">
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other relays</h2>
</div>
<p>
Coracle automatically discovers relays as you browse the network. Adding more relays
will generally make things quicker to load, at the expense of higher data usage.
</p>
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
{/if}
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)}
of {($knownRelays || []).length - $relays.length} known relays
</small>
</div>
</Content>
</div>

View File

@ -2,8 +2,11 @@
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Tabs from 'src/partials/Tabs.svelte' import Tabs from 'src/partials/Tabs.svelte'
import Modal from 'src/partials/Modal.svelte'
import SearchPeople from 'src/views/SearchPeople.svelte' import SearchPeople from 'src/views/SearchPeople.svelte'
import RelayList from "src/routes/RelayList.svelte"
import Scan from 'src/views/Scan.svelte' import Scan from 'src/views/Scan.svelte'
import {relays} from 'src/agent/relays'
export let activeTab export let activeTab
@ -18,3 +21,9 @@
<Scan /> <Scan />
{/if} {/if}
</Content> </Content>
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

@ -8,7 +8,7 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte" import Heading from "src/partials/Heading.svelte"
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {toast, settings} from "src/app" import {toast, settings} from "src/app/ui"
let values = {...$settings} let values = {...$settings}

View File

@ -10,7 +10,7 @@
import {getUserWriteRelays} from 'src/agent/relays' import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast, modal} from "src/app" import {toast, modal} from "src/app/ui"
export let room = {name: null, id: null, about: null, picture: null} export let room = {name: null, id: null, about: null, picture: null}

View File

@ -6,6 +6,7 @@
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing" import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import Compose from "src/partials/Compose.svelte" import Compose from "src/partials/Compose.svelte"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
@ -17,7 +18,7 @@
import {getUserWriteRelays} from 'src/agent/relays' import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database' import database from 'src/agent/database'
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast, modal} from "src/app" import {toast, modal} from "src/app/ui"
export let pubkey = null export let pubkey = null
@ -33,7 +34,7 @@
const joined = new Set(pluck('url', relays)) const joined = new Set(pluck('url', relays))
search = fuzzy( search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)), $knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
{keys: ["name", "description", "url"]} {keys: ["name", "description", "url"]}
) )
} }

View File

@ -8,7 +8,7 @@
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {getUserWriteRelays} from 'src/agent/relays' import {getUserWriteRelays} from 'src/agent/relays'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import {modal} from 'src/app' import {modal} from 'src/app/ui'
const muffle = $user.muffle || [] const muffle = $user.muffle || []
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always'] const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']

View File

@ -7,7 +7,7 @@
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Input from 'src/partials/Input.svelte' import Input from 'src/partials/Input.svelte'
import {getPubkeyWriteRelays} from 'src/agent/relays' import {getPubkeyWriteRelays} from 'src/agent/relays'
import {modal, toast} from 'src/app' import {modal, toast} from 'src/app/ui'
const {pubkey} = $modal.person const {pubkey} = $modal.person
const relays = [prop('url', getPubkeyWriteRelays(pubkey))] const relays = [prop('url', getPubkeyWriteRelays(pubkey))]

View File

@ -4,7 +4,8 @@
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import {toast, login} from "src/app" import {toast} from "src/app/ui"
import {login} from "src/app"
let nsec = '' let nsec = ''
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md" const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"

View File

@ -4,7 +4,8 @@
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import {toast, login} from "src/app" import {toast} from "src/app/ui"
import {login} from "src/app"
let npub = '' let npub = ''

View File

@ -1,4 +1,5 @@
<script> <script>
import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import {personKinds} from "src/util/nostr" import {personKinds} from "src/util/nostr"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
@ -28,6 +29,8 @@
{#each (search ? search(q) : []).slice(0, 30) as person (person.pubkey)} {#each (search ? search(q) : []).slice(0, 30) as person (person.pubkey)}
{#if person.pubkey !== $user?.pubkey} {#if person.pubkey !== $user?.pubkey}
<PersonInfo {person} /> <div in:fly={{y: 20}}>
<PersonInfo {person} />
</div>
{/if} {/if}
{/each} {/each}

View File

@ -5,7 +5,8 @@
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import {toast, login} from "src/app" import {toast} from "src/app/ui"
import {login} from "src/app"
const nsec = nip19.nsecEncode(generatePrivateKey()) const nsec = nip19.nsecEncode(generatePrivateKey())
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md" const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"

View File

@ -1,18 +1,8 @@
<script> <script>
import {uniq} from 'ramda'
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {isLike} from 'src/util/nostr' import {isLike} from 'src/util/nostr'
import {user} from 'src/agent/user'
import {getFollows, getNetwork} from 'src/agent/social'
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
// Get first- and second-order follows. shuffle and slice network so we're not const filter = {kinds: [1, 7]}
// sending too many pubkeys. This will also result in some variety.
const follows = getFollows($user?.pubkey)
const network = getNetwork($user?.pubkey)
const authors = uniq(follows.concat(network)).slice(0, 100)
const relays = getAllPubkeyWriteRelays(authors)
const filter = {kinds: [1, 7], authors}
const shouldDisplay = note => { const shouldDisplay = note => {
return ( return (
@ -22,4 +12,4 @@
} }
</script> </script>
<Notes {relays} {filter} {shouldDisplay} /> <Notes {filter} {shouldDisplay} />

View File

@ -0,0 +1 @@