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] Strip formatting when pasting into Compose
- [x] Upgraded nostr-tools to 1.4.1
)
## 0.2.11
- [x] Converted threshold to percentage

View File

@ -1,7 +1,5 @@
# 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
- [ ] 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

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/solid.css"
import {find, is, identity, nthArg, pluck} from 'ramda'
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 {onMount} from 'svelte'
import {Router, Route, links, navigate} from "svelte-routing"
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 {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, shuffle, now, sleep} from 'src/util/misc'
import {displayPerson, isLike} from 'src/util/nostr'
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 keys from 'src/agent/keys'
import network from 'src/agent/network'
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 {modal, toast, settings, logUsage, alerts, messages, loadAppData} from "src/app"
import {routes} from "src/app/ui"
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Modal from 'src/partials/Modal.svelte'
import {user} from 'src/agent/user'
import {loadAppData} from "src/app"
import alerts from "src/app/alerts"
import messages from "src/app/messages"
import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui"
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 Person from "src/routes/Person.svelte"
import Alerts from "src/routes/Alerts.svelte"
import Bech32Entity from "src/routes/Bech32Entity.svelte"
import Chat from "src/routes/Chat.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 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})
export let url = ""
const menuIsOpen = writable(false)
const toggleMenu = () => menuIsOpen.update(x => !x)
let scrollY
const searchIsOpen = writable(false)
const toggleSearch = () => searchIsOpen.update(x => !x)
const {ready} = database
const closeModal = async () => {
modal.clear()
menuIsOpen.set(false)
}
const {ready} = database
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(':')))
}
})
onMount(() => {
// Keep scroll position on body, but don't allow scrolling
const unsubModal = modal.subscribe($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 () => {
clearInterval(interval)
unsubHistory()
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>
<Router {url}>
<div use:links class="h-full">
{#if $ready}
<div class="pt-16 text-white h-full lg:ml-56">
<Route path="/alerts" component={Alerts} />
<Route path="/search/:activeTab" component={Search} />
@ -241,107 +199,9 @@
</Route>
<Route path="*" component={NotFound} />
</div>
{/if}
<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>
<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>
<SideNav />
<TopNav />
{#if $modal}
<Modal onEscape={closeModal}>
@ -409,3 +269,4 @@
{/if}
</div>
</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-pub.wellorder.net'},
{url: 'wss://relay.nostr.band'},
{url: 'nostr.pleb.network'},
{url: 'relay.nostrich.de'},
{url: 'relay.damus.io'},
{url: 'wss://nostr.pleb.network'},
{url: 'wss://relay.nostrich.de'},
{url: 'wss://relay.damus.io'},
])
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 {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserNetworkWriteRelays,
getUserNetworkWriteRelays, getUserReadRelays,
} from 'src/agent/relays'
import database from 'src/agent/database'
import pool from 'src/agent/pool'
@ -79,7 +79,7 @@ const loadPeople = (pubkeys, {kinds = personKinds, force = false, ...opts} = {})
}
return load(
getAllPubkeyWriteRelays(pubkeys).slice(0, 10),
getUserReadRelays().concat(getAllPubkeyWriteRelays(pubkeys)).slice(0, 10),
{kinds, authors: pubkeys},
opts
)

View File

@ -1,8 +1,9 @@
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 {first} from 'hurdak/lib/hurdak'
import {Tags} from 'src/util/nostr'
import {synced} from 'src/util/misc'
import {getFollows} from 'src/agent/social'
import database from 'src/agent/database'
import keys from 'src/agent/keys'
@ -19,7 +20,7 @@ import keys from 'src/agent/keys'
// doesn't need to see.
// 5) Advertise relays — write and read back your own relay list
export const relays = writable([])
export const relays = synced('agent/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 {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {log, warn} from 'src/util/logger'
@ -142,7 +142,7 @@ const calculateRoute = (pubkey, url, type, mode, created_at) => {
}
const processRoutes = async events => {
const updates = []
let updates = []
// Sample events so we're not burning too many resources
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)) {
await database.relays.bulkPatch(createMap('url', updates.map(pick(['url']))))
await database.routes.bulkPut(createMap('id', updates.filter(identity)))
}
}
@ -224,8 +227,12 @@ const verifyNip05 = (pubkey, as) =>
database.people.patch({...person, verified_as: as})
if (result.relays?.length > 0) {
const urls = result.relays.filter(isRelay)
database.relays.bulkPatch(createMap('url', urls.map(objOf('url'))))
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', '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 {user} from 'src/agent/user'
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 network from 'src/agent/network'
import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
import alerts from 'src/app/alerts'
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 = pubkey =>
Promise.all([
alerts.load(pubkey),
alerts.listen(pubkey),
messages.listen(pubkey),
network.loadPeople(getNetwork(pubkey)),
])
export const loadAppData = async pubkey => {
if (getUserReadRelays().length > 0) {
await Promise.all([
alerts.load(pubkey),
alerts.listen(pubkey),
messages.listen(pubkey),
network.loadPeople(getNetwork(pubkey)),
])
}
}
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
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})
// Load our user so we can populate network and show profile info
await network.loadPeople([pubkey])
if (getUserReadRelays().length === 0) {
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
loadAppData(pubkey)
// Load network and start listening, but don't wait for it
loadAppData(pubkey)
// 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
// down users' first run experience too much
navigate('/notes/network')
// 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
// down users' first run experience too much
navigate('/notes/network')
}
}
export const addRelay = async url => {
const person = get(user) as Person
const $user = get(user) as Person
relays.update($relays => {
$relays.push({url, write: false, read: true})
if (person) {
if ($user) {
(async () => {
// Publish to the new set of relays
await cmd.setRelays($relays, $relays)
// 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 => {
const person = get(user) as Person
const $user = get(user) as Person
relays.update($relays => {
$relays = reject(whereEq({url}), $relays)
if (person && $relays.length > 0) {
if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}
@ -82,12 +87,12 @@ export const removeRelay = async url => {
}
export const setRelayWriteCondition = async (url, write) => {
const person = get(user) as Person
const $user = get(user) as Person
relays.update($relays => {
$relays = $relays.map(when(whereEq({url}), assoc('write', write)))
if (person && $relays.length > 0) {
if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}

View File

@ -1,5 +1,5 @@
import {pluck, reject} from 'ramda'
import {get} from 'svelte/store'
import {pluck, find, reject} from 'ramda'
import {get, derived} from 'svelte/store'
import {synced, now, timedelta} from 'src/util/misc'
import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
@ -12,6 +12,18 @@ const since = now() - timedelta(30, 'days')
const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {})
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 => {
if (listener) {
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)
}
// Menu
export const menuIsOpen = writable(false)
// Modals
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,
})
import Shell from 'src/Shell.svelte'
import App from 'src/App.svelte'
const app = new Shell({
const app = new App({
target: document.getElementById('app')
})

View File

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

View File

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

View File

@ -12,7 +12,8 @@
import ImageCircle from 'src/partials/ImageCircle.svelte'
import Preview from 'src/partials/Preview.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 Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"

View File

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

View File

@ -50,7 +50,8 @@
on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}}
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-success={quality > 0.7}>
</span>
@ -79,8 +80,8 @@
<div class="flex justify-between gap-2">
<span>Publish to this relay?</span>
<Toggle
value={relay.write !== "!"}
on:change={() => setRelayWriteCondition(relay.url, relay.write === "!" ? "" : "!")} />
value={relay.write}
on:change={() => setRelayWriteCondition(relay.url, !relay.write)} />
</div>
{/if}
</div>

View File

@ -5,7 +5,7 @@
import Anchor from 'src/partials/Anchor.svelte'
import {displayPerson} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
import {messages} from 'src/app'
import messages from 'src/app/messages'
export let joined = false
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>
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 Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte'

View File

@ -8,7 +8,8 @@
import Content from 'src/partials/Content.svelte'
import Like from 'src/partials/Like.svelte'
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 notes = null

View File

@ -8,7 +8,8 @@
import network from 'src/agent/network'
import database from 'src/agent/database'
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 Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"

View File

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

View File

@ -9,7 +9,7 @@
import Content from "src/partials/Content.svelte"
import Heading from 'src/partials/Heading.svelte'
import keys from "src/agent/keys"
import {toast} from "src/app"
import {toast} from "src/app/ui"
const {pubkey, privkey} = keys
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 Content from "src/partials/Content.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"

View File

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

View File

@ -4,13 +4,22 @@
import Content from "src/partials/Content.svelte"
import NewNoteButton from "src/partials/NewNoteButton.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 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
const {petnames} = follows
const setActiveTab = tab => navigate(`/notes/${tab}`)
// If they're not following anyone, skip network tab
if ($petnames.length === 0) {
setActiveTab('popular')
}
</script>
<Content>
@ -33,3 +42,9 @@
</Content>
<NewNoteButton />
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

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

View File

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

View File

@ -2,13 +2,14 @@
import {pluck} from 'ramda'
import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {relays} from "src/agent/relays"
import database from 'src/agent/database'
import {modal} from "src/app"
import {modal} from "src/app/ui"
let q = ""
let search
@ -18,55 +19,60 @@
const joined = new Set(pluck('url', $relays))
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
$knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}
</script>
<Content>
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your relays</h2>
</div>
<Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}>
<i class="fa-solid fa-plus" /> Add Relay
</Anchor>
</div>
<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 in:fly={{y: 20}}>
<Content>
<div class="flex justify-between mt-10">
<div class="flex gap-2 items-center">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your relays</h2>
</div>
<Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}>
<i class="fa-solid fa-plus" /> Add Relay
</Anchor>
</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.
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>
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
{#if $relays.length === 0}
<div class="text-center mt-10 flex gap-2 justify-center items-center">
<i class="fa fa-triangle-exclamation" />
No relays connected
</div>
{/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 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-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 Content from 'src/partials/Content.svelte'
import Tabs from 'src/partials/Tabs.svelte'
import Modal from 'src/partials/Modal.svelte'
import SearchPeople from 'src/views/SearchPeople.svelte'
import RelayList from "src/routes/RelayList.svelte"
import Scan from 'src/views/Scan.svelte'
import {relays} from 'src/agent/relays'
export let activeTab
@ -18,3 +21,9 @@
<Scan />
{/if}
</Content>
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

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

View File

@ -10,7 +10,7 @@
import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
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}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.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 = ''
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 Content from 'src/partials/Content.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 = ''

View File

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

View File

@ -5,7 +5,8 @@
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.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 nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"

View File

@ -1,18 +1,8 @@
<script>
import {uniq} from 'ramda'
import Notes from "src/partials/Notes.svelte"
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
// 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 filter = {kinds: [1, 7]}
const shouldDisplay = note => {
return (
@ -22,4 +12,4 @@
}
</script>
<Notes {relays} {filter} {shouldDisplay} />
<Notes {filter} {shouldDisplay} />

View File

@ -0,0 +1 @@