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,125 +2,120 @@
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
onMount(() => {
// Keep scroll position on body, but don't allow scrolling
const unsubModal = modal.subscribe($modal => {
if ($modal) {
logUsage(btoa(['modal', $modal.type].join(':')))
let menuIcon
let scrollY
let suspendedSubs = []
let slowConnections = []
let hasNewMessages = false
// This is not idempotent, so don't duplicate it
if (document.body.style.position !== 'fixed') {
scrollY = window.scrollY
$: {
hasNewMessages = Boolean(find(
([k, t]) => {
return t > now() - timedelta(7, 'days') && ($lastCheckedByPubkey[k] || 0) < t
},
Object.entries($mostRecentByPubkey)
))
document.body.style.top = `-${scrollY}px`
document.body.style.position = `fixed`
}
} else {
document.body.setAttribute('style', '')
window.scrollTo(0, scrollY)
}
})
// 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 () => {
unsubHistory()
unsubModal()
}
})
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 interval = setInterval(
async () => {
const {dufflepudUrl} = $settings
if (!dufflepudUrl) {
@ -158,55 +153,18 @@
)
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
const unsubModal = modal.subscribe($modal => {
if ($modal) {
logUsage(btoa(['modal', $modal.type].join(':')))
// This is not idempotent, so don't duplicate it
if (document.body.style.position !== 'fixed') {
scrollY = window.scrollY
document.body.style.top = `-${scrollY}px`
document.body.style.position = `fixed`
}
} else {
document.body.setAttribute('style', '')
window.scrollTo(0, scrollY)
}
})
},
30_000
)
return () => {
clearInterval(interval)
unsubHistory()
unsubModal()
}
})
</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([
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,6 +36,9 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
if (getUserReadRelays().length === 0) {
navigate('/relays')
} else {
// Load our user so we can populate network and show profile info
await network.loadPeople([pubkey])
@ -46,20 +50,21 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
// 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,14 +19,15 @@
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>
<div in:fly={{y: 20}}>
<Content>
<div class="flex justify-between">
<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>
@ -39,7 +41,10 @@
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>
<div class="text-center mt-10 flex gap-2 justify-center items-center">
<i class="fa fa-triangle-exclamation" />
No relays connected
</div>
{/if}
<div class="grid grid-cols-1 gap-4">
{#each $relays as relay (relay.url)}
@ -50,7 +55,7 @@
{#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" />
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other relays</h2>
</div>
<p>
@ -70,3 +75,4 @@
</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}
<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 @@