Add prettier

This commit is contained in:
Jonathan Staab 2023-03-08 08:02:18 -06:00
parent 1dd9c6f791
commit 24715a52be
95 changed files with 2123 additions and 2007 deletions

View File

@ -29,6 +29,7 @@ module.exports = {
"no-unused-vars": ["error", {args: "none"}],
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-semi": "off",
"no-useless-escape": "off",
},
ignorePatterns: ["*.svg"],

View File

@ -2,4 +2,5 @@
. "$(dirname -- "$0")/_/husky.sh"
npm run check
npm run format

13
.prettierrc Normal file
View File

@ -0,0 +1,13 @@
{
"semi": false,
"printWidth": 100,
"bracketSameLine": true,
"svelteSortOrder" : "options-styles-scripts-markup",
"arrowParens": "avoid",
"bracketSpacing": false,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"pluginSearchDirs": false
}

View File

@ -5,7 +5,7 @@
- [ ] Create my own version of nostr.how and extension explanation
- [ ] Move extension suggestion to later in the app, maybe a notification if they don't have one installed
- [ ] Reassure user that if they don't copy the key now they can get it later in settings
- [ ] Design empty state for messages
- [ ] Design empty state for messages page
- [ ] Add copy to explain that chat is public, dms are encrypted
- [ ] Add QR code that pre-fills follows and relays for a new user

View File

@ -2,31 +2,90 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="57x57" href="/images/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/images/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/images/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/images/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/images/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/images/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/images/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/images/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/images/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon/favicon-16x16.png">
<link rel="mask-icon" href="/images/logo.svg" color="#FFFFFF">
<meta name="msapplication-TileColor" content="#EB5E28">
<meta name="msapplication-TileImage" content="/images/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#EB5E28">
<link
rel="apple-touch-icon"
sizes="57x57"
href="/images/favicon/apple-icon-57x57.png"
/>
<link
rel="apple-touch-icon"
sizes="60x60"
href="/images/favicon/apple-icon-60x60.png"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="/images/favicon/apple-icon-72x72.png"
/>
<link
rel="apple-touch-icon"
sizes="76x76"
href="/images/favicon/apple-icon-76x76.png"
/>
<link
rel="apple-touch-icon"
sizes="114x114"
href="/images/favicon/apple-icon-114x114.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/images/favicon/apple-icon-120x120.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/images/favicon/apple-icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/images/favicon/apple-icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/favicon/apple-icon-180x180.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon/android-icon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/images/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="/images/favicon/favicon-96x96.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/images/favicon/favicon-16x16.png"
/>
<link rel="mask-icon" href="/images/logo.svg" color="#FFFFFF" />
<meta name="msapplication-TileColor" content="#EB5E28" />
<meta
name="msapplication-TileImage"
content="/images/favicon/ms-icon-144x144.png"
/>
<meta name="theme-color" content="#EB5E28" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="description" content="Nostr, your way.">
<meta property="og:title" content="Coracle">
<meta property="description" content="Nostr, your way." />
<meta property="og:title" content="Coracle" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Nostr, your way.">
<meta property="og:image" content="/images/banner.png">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:url" content="https://coracle.social">
<meta property="og:description" content="Nostr, your way." />
<meta property="og:image" content="/images/banner.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:url" content="https://coracle.social" />
<title>Coracle</title>
</head>
<body class="w-full">

BIN
package-lock.json generated

Binary file not shown.

View File

@ -10,6 +10,7 @@
"check:es": "eslint src/*/** --quiet",
"check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"check": "run-p check:*",
"format": "prettier --write '{public,src}/**/*.{css,html,js,svelte}'",
"watch": "find src -type f | entr -r"
},
"devDependencies": {
@ -20,6 +21,9 @@
"eslint": "^8.33.0",
"eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.19",
"prettier": "^2.8.4",
"prettier-plugin-svelte": "^2.9.0",
"prettier-plugin-tailwindcss": "^0.2.4",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.5",
"vite": "^3.2.3",

View File

@ -2,25 +2,25 @@
import "@fortawesome/fontawesome-free/css/fontawesome.css"
import "@fortawesome/fontawesome-free/css/solid.css"
import {onMount} from 'svelte'
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 {timedelta, shuffle, now, sleep} from 'src/util/misc'
import {displayPerson, isLike} from 'src/util/nostr'
import cmd from 'src/agent/cmd'
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, initializeRelayList} from 'src/agent/relays'
import sync from 'src/agent/sync'
import user from 'src/agent/user'
import {createMap, first} from "hurdak/lib/hurdak"
import {find, is, identity, nthArg, pluck} from "ramda"
import {log, warn} from "src/util/logger"
import {timedelta, shuffle, now, sleep} from "src/util/misc"
import {displayPerson, isLike} from "src/util/nostr"
import cmd from "src/agent/cmd"
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, initializeRelayList} from "src/agent/relays"
import sync from "src/agent/sync"
import user from "src/agent/user"
import {loadAppData} from "src/app"
import alerts from "src/app/alerts"
import {modal, routes, menuIsOpen, logUsage} from "src/app/ui"
@ -82,34 +82,33 @@
// Keep scroll position on body, but don't allow scrolling
const unsubModal = modal.subscribe($modal => {
if ($modal) {
logUsage(btoa(['modal', $modal.type].join(':')))
logUsage(btoa(["modal", $modal.type].join(":")))
// This is not idempotent, so don't duplicate it
if (document.body.style.position !== 'fixed') {
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', '')
document.body.setAttribute("style", "")
window.scrollTo(0, scrollY)
}
})
// Remove identifying information, e.g. pubkeys, event ids, etc
const getPageName = () =>
location.pathname.slice(1)
.replace(/(npub|nprofile|note|nevent)[^\/]+/g, (_, m) => `<${m}>`)
location.pathname.slice(1).replace(/(npub|nprofile|note|nevent)[^\/]+/g, (_, m) => `<${m}>`)
// Log usage on navigate
const unsubHistory = globalHistory.listen(({location}) => {
if (!location.hash) {
logUsage(btoa(['page', getPageName()].join(':')))
logUsage(btoa(["page", getPageName()].join(":")))
}
})
logUsage(btoa(['page', getPageName()].join(':')))
logUsage(btoa(["page", getPageName()].join(":")))
return () => {
unsubHistory()
@ -124,48 +123,45 @@
loadAppData(user.getPubkey())
}
const interval = setInterval(
async () => {
const {dufflepudUrl} = user.getSettings()
const interval = setInterval(async () => {
const {dufflepudUrl} = user.getSettings()
if (!dufflepudUrl) {
return
}
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)
// 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',
},
})
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()}
return {...(await res.json()), url, refreshed_at: now()}
} catch (e) {
if (!e.toString().includes("Failed to fetch")) {
warn(e)
}
})
)
database.relays.bulkPatch(createMap('url', freshRelays.filter(identity)))
},
30_000
)
return {url, refreshed_at: now()}
}
})
)
database.relays.bulkPatch(createMap("url", freshRelays.filter(identity)))
}, 30_000)
return () => {
clearInterval(interval)
@ -176,104 +172,103 @@
<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">
<EnsureData enforcePeople={false}>
<Search />
</EnsureData>
</Route>
<Route path="/scan">
<EnsureData enforcePeople={false}>
<Scan />
</EnsureData>
</Route>
<Route path="/notes/:activeTab" let:params>
<EnsureData>
<Feeds activeTab={params.activeTab} />
</EnsureData>
</Route>
<Route path="/people/:npub/:activeTab" let:params>
{#key params.npub}
<PersonDetail npub={params.npub} activeTab={params.activeTab} />
{/key}
</Route>
<Route path="/chat" component={ChatList} />
<Route path="/chat/:entity" let:params>
{#key params.entity}
<ChatDetail entity={params.entity} />
{/key}
</Route>
<Route path="/messages" component={MessagesList} />
<Route path="/messages/:entity" let:params>
{#key params.entity}
<MessagesDetail entity={params.entity} />
{/key}
</Route>
<Route path="/keys" component={Keys} />
<Route path="/relays" component={RelayList} />
<Route path="/relays/:b64url" let:params>
{#key params.b64url}
<RelayDetail url={atob(params.b64url)} />
{/key}
</Route>
<Route path="/profile" component={Profile} />
<Route path="/settings" component={Settings} />
<Route path="/login" component={Login} />
<Route path="/logout" component={Logout} />
<Route path="/debug" component={Debug} />
<Route path="/:entity" let:params>
{#key params.entity}
<Bech32Entity entity={params.entity} />
{/key}
</Route>
<Route path="*" component={NotFound} />
</div>
<div class="h-full pt-16 text-white lg:ml-56">
<Route path="/alerts" component={Alerts} />
<Route path="/search">
<EnsureData enforcePeople={false}>
<Search />
</EnsureData>
</Route>
<Route path="/scan">
<EnsureData enforcePeople={false}>
<Scan />
</EnsureData>
</Route>
<Route path="/notes/:activeTab" let:params>
<EnsureData>
<Feeds activeTab={params.activeTab} />
</EnsureData>
</Route>
<Route path="/people/:npub/:activeTab" let:params>
{#key params.npub}
<PersonDetail npub={params.npub} activeTab={params.activeTab} />
{/key}
</Route>
<Route path="/chat" component={ChatList} />
<Route path="/chat/:entity" let:params>
{#key params.entity}
<ChatDetail entity={params.entity} />
{/key}
</Route>
<Route path="/messages" component={MessagesList} />
<Route path="/messages/:entity" let:params>
{#key params.entity}
<MessagesDetail entity={params.entity} />
{/key}
</Route>
<Route path="/keys" component={Keys} />
<Route path="/relays" component={RelayList} />
<Route path="/relays/:b64url" let:params>
{#key params.b64url}
<RelayDetail url={atob(params.b64url)} />
{/key}
</Route>
<Route path="/profile" component={Profile} />
<Route path="/settings" component={Settings} />
<Route path="/login" component={Login} />
<Route path="/logout" component={Logout} />
<Route path="/debug" component={Debug} />
<Route path="/:entity" let:params>
{#key params.entity}
<Bech32Entity entity={params.entity} />
{/key}
</Route>
<Route path="*" component={NotFound} />
</div>
{/if}
<SideNav />
<TopNav />
{#if $modal}
<Modal onEscape={$modal.noEscape ? null : closeModal}>
{#if $modal.type === 'note/detail'}
{#key $modal.note.id}
<NoteDetail {...$modal} invertColors />
{/key}
{:else if $modal.type === 'note/create'}
<NoteCreate pubkey={$modal.pubkey} />
{:else if $modal.type === 'relay/add'}
<AddRelay />
{:else if $modal.type === 'onboarding'}
<Onboarding stage={$modal.stage} />
{:else if $modal.type === 'room/edit'}
<ChatEdit {...$modal} />
{:else if $modal.type === 'login/privkey'}
<PrivKeyLogin />
{:else if $modal.type === 'login/pubkey'}
<PubKeyLogin />
{:else if $modal.type === 'login/connect'}
<ConnectUser />
{:else if $modal.type === 'person/settings'}
<PersonSettings person={$modal.person} />
{:else if $modal.type === 'person/info'}
<PersonProfileInfo person={$modal.person} />
{:else if $modal.type === 'person/share'}
<PersonShare person={$modal.person} />
{:else if $modal.type === 'person/list'}
<PersonList pubkeys={$modal.pubkeys} />
{:else if $modal.type === 'message'}
<Content size="lg">
<div class="text-center">{$modal.message}</div>
{#if $modal.spinner}
<Spinner delay={0} />
{/if}
</Content>
{/if}
</Modal>
<Modal onEscape={$modal.noEscape ? null : closeModal}>
{#if $modal.type === "note/detail"}
{#key $modal.note.id}
<NoteDetail {...$modal} invertColors />
{/key}
{:else if $modal.type === "note/create"}
<NoteCreate pubkey={$modal.pubkey} />
{:else if $modal.type === "relay/add"}
<AddRelay />
{:else if $modal.type === "onboarding"}
<Onboarding stage={$modal.stage} />
{:else if $modal.type === "room/edit"}
<ChatEdit {...$modal} />
{:else if $modal.type === "login/privkey"}
<PrivKeyLogin />
{:else if $modal.type === "login/pubkey"}
<PubKeyLogin />
{:else if $modal.type === "login/connect"}
<ConnectUser />
{:else if $modal.type === "person/settings"}
<PersonSettings person={$modal.person} />
{:else if $modal.type === "person/info"}
<PersonProfileInfo person={$modal.person} />
{:else if $modal.type === "person/share"}
<PersonShare person={$modal.person} />
{:else if $modal.type === "person/list"}
<PersonList pubkeys={$modal.pubkeys} />
{:else if $modal.type === "message"}
<Content size="lg">
<div class="text-center">{$modal.message}</div>
{#if $modal.spinner}
<Spinner delay={0} />
{/if}
</Content>
{/if}
</Modal>
{/if}
<Toast />
</div>
</Router>

View File

@ -1,22 +1,22 @@
import lf from 'localforage'
import memoryStorageDriver from 'localforage-memoryStorageDriver'
import {switcherFn} from 'hurdak/lib/hurdak'
import {error} from 'src/util/logger'
import lf from "localforage"
import memoryStorageDriver from "localforage-memoryStorageDriver"
import {switcherFn} from "hurdak/lib/hurdak"
import {error} from "src/util/logger"
// Firefox private mode doesn't have access to any storage options
lf.defineDriver(memoryStorageDriver)
lf.setDriver([lf.INDEXEDDB, lf.WEBSQL, lf.LOCALSTORAGE, 'memoryStorageDriver'])
lf.setDriver([lf.INDEXEDDB, lf.WEBSQL, lf.LOCALSTORAGE, "memoryStorageDriver"])
addEventListener('message', async ({data: {topic, payload, channel}}) => {
addEventListener("message", async ({data: {topic, payload, channel}}) => {
const reply = (topic, payload) => postMessage({channel, topic, payload})
switcherFn(topic, {
'localforage.call': async () => {
"localforage.call": async () => {
const {method, args} = payload
const result = await lf[method](...args)
reply('localforage.return', result)
reply("localforage.return", result)
},
default: () => {
throw new Error(`invalid topic: ${topic}`)
@ -24,5 +24,5 @@ addEventListener('message', async ({data: {topic, payload, channel}}) => {
})
})
addEventListener('error', error)
addEventListener('unhandledrejection', error)
addEventListener("error", error)
addEventListener("unhandledrejection", error)

View File

@ -54,7 +54,10 @@
color: white;
}
html, body, #app, #app > div {
html,
body,
#app,
#app > div {
height: 100%;
}
@ -62,17 +65,15 @@ html, body, #app, #app > div {
.tippy-box {
background-color: #0f0f0e !important;
border: 1px solid #403D39;
box-shadow: 3px 3px 20px #0f0f0e,
3px -3px 20px #0f0f0e,
-3px 3px 20px #0f0f0e,
-3px -3px 20px #0f0f0e;
border: 1px solid #403d39;
box-shadow: 3px 3px 20px #0f0f0e, 3px -3px 20px #0f0f0e, -3px 3px 20px #0f0f0e,
-3px -3px 20px #0f0f0e;
}
.tippy-box[data-placement^=top]>.tippy-arrow:before {
border-top-color: #403D39 !important;
.tippy-box[data-placement^="top"] > .tippy-arrow:before {
border-top-color: #403d39 !important;
}
.tippy-box[data-placement^=top]>.tippy-arrow {
bottom: -1px !important;
.tippy-box[data-placement^="top"] > .tippy-arrow {
bottom: -1px !important;
}

View File

@ -1,14 +1,14 @@
import {pluck} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {writable} from 'svelte/store'
import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays'
import {pluck} from "ramda"
import {first} from "hurdak/lib/hurdak"
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 = new Set(pluck('url', getUserRelays()))
const relayUrls = new Set(pluck("url", getUserRelays()))
// Prune connections we haven't used in a while
Object.values(pool.getConnections())
@ -17,7 +17,8 @@ setInterval(() => {
// Alert the user to any heinously slow connections
slowConnections.set(
Object.values(pool.getConnections())
.filter(c => relayUrls.has(c.nostr.url) && first(c.getQuality()) < 0.3)
Object.values(pool.getConnections()).filter(
c => relayUrls.has(c.nostr.url) && first(c.getQuality()) < 0.3
)
)
}, 30_000)

View File

@ -1,8 +1,8 @@
import 'src/app.css'
import "src/app.css"
import Bugsnag from "@bugsnag/js"
import App from 'src/App.svelte'
import {installPrompt} from 'src/app/ui'
import App from "src/App.svelte"
import {installPrompt} from "src/app/ui"
Bugsnag.start({
apiKey: "2ea412feabfe14dc9849c6f0b4fa7003",
@ -18,5 +18,5 @@ window.addEventListener("beforeinstallprompt", e => {
})
export default new App({
target: document.getElementById('app')
target: document.getElementById("app"),
})

View File

@ -16,12 +16,13 @@
switcher(type, {
anchor: "underline",
button: "py-2 px-4 rounded bg-white text-accent whitespace-nowrap",
'button-circle': "w-10 h-10 flex justify-center items-center rounded-full bg-white text-accent whitespace-nowrap",
'button-accent': "py-2 px-4 rounded bg-accent text-white whitespace-nowrap",
}),
"button-circle":
"w-10 h-10 flex justify-center items-center rounded-full bg-white text-accent whitespace-nowrap",
"button-accent": "py-2 px-4 rounded bg-accent text-white whitespace-nowrap",
})
)
</script>
<a on:click {...$$props} {href} class={className} target={external ? '_blank' : null}>
<a on:click {...$$props} {href} class={className} target={external ? "_blank" : null}>
<slot />
</a>

View File

@ -1,25 +1,25 @@
<script>
import {Link} from 'svelte-routing'
import ImageCircle from 'src/partials/ImageCircle.svelte'
import {killEvent} from 'src/util/html'
import {displayPerson} from 'src/util/nostr'
import {routes} from 'src/app/ui'
import {Link} from "svelte-routing"
import ImageCircle from "src/partials/ImageCircle.svelte"
import {killEvent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import {routes} from "src/app/ui"
export let person
export let inert = false
</script>
{#if inert}
<span class="flex gap-2 items-center relative z-10">
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</span>
<span class="relative z-10 flex items-center gap-2">
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</span>
{:else}
<Link
to={routes.person(person.pubkey)}
class="flex gap-2 items-center relative z-10"
on:click={killEvent}>
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</Link>
<Link
to={routes.person(person.pubkey)}
class="relative z-10 flex items-center gap-2"
on:click={killEvent}>
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</Link>
{/if}

View File

@ -8,11 +8,11 @@
const className = cx(
$$props.class,
"py-2 px-4 rounded cursor-pointer",
{'text-light': disabled},
{"text-light": disabled},
switcher(theme, {
default: "bg-white text-accent",
accent: "text-white bg-accent",
}),
})
)
</script>

View File

@ -1,6 +1,6 @@
<script>
import cx from 'classnames'
import {fly} from 'svelte/transition'
import cx from "classnames"
import {fly} from "svelte/transition"
export let interactive = false
export let invertColors = false
@ -9,9 +9,9 @@
<div
on:click
in:fly={{y: 20}}
class={cx($$props.class, "card p-3 text-white rounded-2xl bg-dark", {
'bg-dark border border-solid border-medium': !invertColors,
'bg-medium border border-solid border-shimmer': invertColors,
class={cx($$props.class, "card rounded-2xl bg-dark p-3 text-white", {
"border border-solid border-medium bg-dark": !invertColors,
"border border-solid border-shimmer bg-medium": invertColors,
"cursor-pointer transition-all": interactive,
"hover:bg-medium": interactive && !invertColors,
"hover:bg-dark": interactive && invertColors,

View File

@ -1,13 +1,13 @@
<script>
import {onMount} from 'svelte'
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {prop, path as getPath, reverse, pluck, uniqBy, sortBy, last} from 'ramda'
import {sleep, createScroller, Cursor} from 'src/util/misc'
import Spinner from 'src/partials/Spinner.svelte'
import user from 'src/agent/user'
import database from 'src/agent/database'
import network from 'src/agent/network'
import {onMount} from "svelte"
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import {prop, path as getPath, reverse, pluck, uniqBy, sortBy, last} from "ramda"
import {sleep, createScroller, Cursor} from "src/util/misc"
import Spinner from "src/partials/Spinner.svelte"
import user from "src/agent/user"
import database from "src/agent/database"
import network from "src/agent/network"
export let loadMessages
export let listenForMessages
@ -25,20 +25,17 @@
$: {
// Group messages so we're only showing the person once per chunk
annotatedMessages = reverse(
sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
(mx, m) => {
const person = database.getPersonWithFallback(m.pubkey)
const showPerson = person.pubkey !== getPath(['person', 'pubkey'], last(mx))
sortBy(prop("created_at"), uniqBy(prop("id"), messages)).reduce((mx, m) => {
const person = database.getPersonWithFallback(m.pubkey)
const showPerson = person.pubkey !== getPath(["person", "pubkey"], last(mx))
return mx.concat({...m, person, showPerson})
},
[]
)
return mx.concat({...m, person, showPerson})
}, [])
)
}
// flex-col means the first is the last
const getLastListItem = () => document.querySelector('ul[class=channel-messages] li')
const getLastListItem = () => document.querySelector("ul[class=channel-messages] li")
const stickToBottom = async (behavior, cb) => {
const shouldStick = window.scrollY + window.innerHeight > document.body.scrollHeight - 200
@ -58,24 +55,24 @@
onMount(() => {
if (!$profile) {
return navigate('/login')
return navigate("/login")
}
const sub = listenForMessages(
newMessages => stickToBottom('smooth', () => {
const sub = listenForMessages(newMessages =>
stickToBottom("smooth", () => {
loading = sleep(30_000)
messages = messages.concat(newMessages)
network.loadPeople(pluck('pubkey', newMessages))
network.loadPeople(pluck("pubkey", newMessages))
})
)
const scroller = createScroller(
async () => {
await loadMessages(cursor, newMessages => {
stickToBottom('auto', () => {
stickToBottom("auto", () => {
loading = sleep(30_000)
messages = sortBy(e => -e.created_at, newMessages.concat(messages))
network.loadPeople(pluck('pubkey', newMessages))
network.loadPeople(pluck("pubkey", newMessages))
cursor.update(messages)
})
})
@ -93,69 +90,72 @@
const content = textarea.value.trim()
if (content) {
textarea.value = ''
textarea.value = ""
const event = await sendMessage(content)
stickToBottom('smooth', () => {
stickToBottom("smooth", () => {
messages = sortBy(e => -e.created_at, [event].concat(messages))
})
}
}
const onKeyPress = e => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
send()
}
}
</script>
<svelte:window on:scroll={() => { showNewMessages = false}} />
<svelte:window
on:scroll={() => {
showNewMessages = false
}} />
<div class="flex gap-4 h-full">
<div class="flex h-full gap-4">
<div class="relative w-full">
<div class="flex flex-col py-18 pb-20 h-full">
<ul class="pb-6 p-4 overflow-auto flex-grow flex flex-col-reverse justify-start channel-messages">
<div class="py-18 flex h-full flex-col pb-20">
<ul
class="channel-messages flex flex-grow flex-col-reverse justify-start overflow-auto p-4 pb-6">
{#each annotatedMessages as m (m.id)}
<li in:fly={{y: 20}} class="py-1 flex flex-col gap-2">
<li in:fly={{y: 20}} class="flex flex-col gap-2 py-1">
<slot name="message" message={m} />
</li>
{/each}
{#await loading}
<Spinner>Looking for messages...</Spinner>
<Spinner>Looking for messages...</Spinner>
{:then}
<div in:fly={{y: 20}} class="text-center py-20">End of message history</div>
<div in:fly={{y: 20}} class="py-20 text-center">End of message history</div>
{/await}
</ul>
</div>
<div class="fixed z-20 top-0 w-full border-b border-solid
<div
class="fixed top-0 z-20 w-full border-b border-solid
border-medium bg-dark p-4">
<slot name="header" />
</div>
<div class="fixed z-10 bottom-0 w-full flex bg-medium border-medium border-t border-solid border-dark lg:-ml-56 lg:pl-56">
<div
class="fixed bottom-0 z-10 flex w-full border-t border-solid border-medium border-dark bg-medium lg:-ml-56 lg:pl-56">
<textarea
rows="3"
autofocus
placeholder="Type something..."
bind:this={textarea}
on:keypress={onKeyPress}
class="w-full p-2 text-white bg-medium
placeholder:text-light outline-0 resize-none" />
class="w-full resize-none bg-medium p-2
text-white outline-0 placeholder:text-light" />
<button
on:click={send}
class="flex flex-col py-8 p-4 justify-center gap-2 border-l border-solid border-dark
hover:bg-accent transition-all cursor-pointer text-white ">
class="flex cursor-pointer flex-col justify-center gap-2 border-l border-solid border-dark p-4
py-8 text-white transition-all hover:bg-accent ">
<i class="fa-solid fa-paper-plane fa-xl" />
</button>
</div>
</div>
{#if showNewMessages}
<div class="fixed w-full flex justify-center bottom-32" transition:fly|local={{y: 20}}>
<div class="rounded-full bg-accent text-white py-2 px-4">
New messages found
<div class="fixed bottom-32 flex w-full justify-center" transition:fly|local={{y: 20}}>
<div class="rounded-full bg-accent py-2 px-4 text-white">New messages found</div>
</div>
</div>
{/if}
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import {prop, repeat, reject, sortBy, last} from 'ramda'
import {onMount} from 'svelte'
import {ensurePlural} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import {prop, repeat, reject, sortBy, last} from "ramda"
import {onMount} from "svelte"
import {ensurePlural} from "hurdak/lib/hurdak"
import {fly} from "svelte/transition"
import {fuzzy} from "src/util/misc"
import {displayPerson} from "src/util/nostr"
import {fromParentOffset} from "src/util/html"
@ -15,12 +15,11 @@
let mentions = []
let suggestions = []
let input = null
let prevContent = ''
let prevContent = ""
const search = fuzzy(
database.people.all({'kind0.name:!nil': null}),
{keys: ["kind0.name", "pubkey"]}
)
const search = fuzzy(database.people.all({"kind0.name:!nil": null}), {
keys: ["kind0.name", "pubkey"],
})
const getText = () => {
const selection = document.getSelection()
@ -44,17 +43,21 @@
const selection = document.getSelection()
const {focusNode, focusOffset} = selection
const prefixElement = document.createTextNode(prefix)
const span = document.createElement('span')
const span = document.createElement("span")
// Space includes a zero-width space to avoid having the cursor end up inside
// mention span on backspace, and a space for convenience in composition.
const space = document.createTextNode("\u200B\u00a0")
span.classList.add('underline')
span.classList.add("underline")
span.innerText = content
// Remove our partial mention text
selection.setBaseAndExtent(...fromParentOffset(input, text.length - chars), focusNode, focusOffset)
selection.setBaseAndExtent(
...fromParentOffset(input, text.length - chars),
focusNode,
focusOffset
)
selection.deleteFromDocument()
// Add the prefix, decorated text, and a trailing space
@ -69,7 +72,7 @@
const pickSuggestion = person => {
const display = displayPerson(person)
highlightWord('@', getWord().length, display)
highlightWord("@", getWord().length, display)
mentions.push({
pubkey: person.pubkey,
@ -82,31 +85,31 @@
}
const onKeyDown = e => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
return onSubmit()
}
if (e.key === 'Escape' && suggestions[index]) {
if (e.key === "Escape" && suggestions[index]) {
index = 0
suggestions = []
e.stopPropagation()
}
if (['Enter', 'Tab', 'ArrowUp', 'ArrowDown', ' '].includes(e.key) && suggestions[index]) {
if (["Enter", "Tab", "ArrowUp", "ArrowDown", " "].includes(e.key) && suggestions[index]) {
e.preventDefault()
}
}
const onKeyUp = e => {
if (['Enter', 'Tab', ' '].includes(e.key) && suggestions[index]) {
if (["Enter", "Tab", " "].includes(e.key) && suggestions[index]) {
pickSuggestion(suggestions[index])
}
if (e.key === 'ArrowUp' && suggestions[index - 1]) {
if (e.key === "ArrowUp" && suggestions[index - 1]) {
index -= 1
}
if (e.key === 'ArrowDown' && suggestions[index + 1]) {
if (e.key === "ArrowDown" && suggestions[index + 1]) {
index += 1
}
@ -114,7 +117,7 @@
const text = getText()
const word = getWord()
if (!text.match(/\s$/) && word.startsWith('@')) {
if (!text.match(/\s$/) && word.startsWith("@")) {
suggestions = search(word.slice(1)).slice(0, 5)
} else {
index = 0
@ -139,7 +142,7 @@
const topic = getText().match(/#([-\w]+\s)$/)
if (topic) {
highlightWord('#', topic[0].length, topic[1].trim())
highlightWord("#", topic[0].length, topic[1].trim())
}
}
@ -168,10 +171,11 @@
let offset = 0
// For whatever reason the textarea gives us 2x - 1 line breaks
let content = input.innerText
.replace(/(\n+)/g, x => repeat('\n', Math.ceil(x.length / 2)).join(''))
let content = input.innerText.replace(/(\n+)/g, x =>
repeat("\n", Math.ceil(x.length / 2)).join("")
)
const validMentions = sortBy(prop('end'), reject(prop('invalid'), mentions))
const validMentions = sortBy(prop("end"), reject(prop("invalid"), mentions))
for (const [i, {end, length}] of validMentions.entries()) {
const offsetEnd = end - offset
const start = offsetEnd - length
@ -182,17 +186,17 @@
}
// Remove our zero-length spaces
content = content.replace(/\u200B/g, '').trim()
content = content.replace(/\u200B/g, "").trim()
return {
content,
topics: content.match(/#[-\w]+/g) || [],
mentions: validMentions.map(prop('pubkey')),
mentions: validMentions.map(prop("pubkey")),
}
}
onMount(() => {
input.addEventListener('paste', e => {
input.addEventListener("paste", e => {
e.preventDefault()
const selection = window.getSelection()
@ -201,15 +205,14 @@
selection.deleteFromDocument()
}
type((e.clipboardData || (window as any).clipboardData).getData('text'))
type((e.clipboardData || (window as any).clipboardData).getData("text"))
})
})
</script>
<div class="flex">
<div
class="text-white w-full outline-0 p-2 min-w-0"
class="w-full min-w-0 p-2 text-white outline-0"
autofocus
contenteditable
bind:this={input}
@ -219,16 +222,16 @@
</div>
{#if suggestions.length > 0}
<div class="rounded border border-solid border-medium mt-2 flex flex-col" in:fly={{y: 20}}>
{#each suggestions as person, i (person.pubkey)}
<button
class="py-2 px-4 cursor-pointer border-l-2 border-solid border-black"
class:bg-black={index !== i}
class:bg-dark={index === i}
class:border-accent={index === i}
on:click={() => pickSuggestion(person)}>
<Badge inert {person} />
</button>
{/each}
</div>
<div class="mt-2 flex flex-col rounded border border-solid border-medium" in:fly={{y: 20}}>
{#each suggestions as person, i (person.pubkey)}
<button
class="cursor-pointer border-l-2 border-solid border-black py-2 px-4"
class:bg-black={index !== i}
class:bg-dark={index === i}
class:border-accent={index === i}
on:click={() => pickSuggestion(person)}>
<Badge inert {person} />
</button>
{/each}
</div>
{/if}

View File

@ -1,30 +1,30 @@
<script>
import cx from 'classnames'
import cx from "classnames"
export let gap = 6
export let size = "2xl"
const className = `flex flex-col m-auto text-white gap-${gap}`
if (!['inherit', 'lg', '2xl'].includes(size)) {
if (!["inherit", "lg", "2xl"].includes(size)) {
throw new Error(`Invalid size: ${size}`)
}
</script>
{#if size === 'inherit'}
<div {...$$props} class={cx($$props.class, className, "w-full")}>
<slot />
</div>
{#if size === "inherit"}
<div {...$$props} class={cx($$props.class, className, "w-full")}>
<slot />
</div>
{/if}
{#if size === 'lg'}
<div {...$$props} class={cx($$props.class, className, "p-2 py-16 max-w-lg")}>
<slot />
</div>
{#if size === "lg"}
<div {...$$props} class={cx($$props.class, className, "max-w-lg p-2 py-16")}>
<slot />
</div>
{/if}
{#if size === '2xl'}
<div {...$$props} class={cx($$props.class, className, "p-4 max-w-2xl")}>
<slot />
</div>
{#if size === "2xl"}
<div {...$$props} class={cx($$props.class, className, "max-w-2xl p-4")}>
<slot />
</div>
{/if}

View File

@ -1,5 +1,7 @@
<script>
import cx from 'classnames'
import cx from "classnames"
</script>
<h1 {...$$props} class={cx($$props.class, "staatliches text-6xl my-4")}><slot /></h1>
<h1 {...$$props} class={cx($$props.class, "staatliches my-4 text-6xl")}>
<slot />
</h1>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import cx from 'classnames'
import cx from "classnames"
export let src
export let size = 4
@ -8,7 +8,7 @@
<div
class={cx(
$$props.class,
`overflow-hidden w-${size} h-${size} rounded-full bg-cover bg-center shrink-0 border
border-solid border-white inline-block`
)}
`overflow-hidden w-${size} h-${size} inline-block shrink-0 rounded-full border border-solid
border-white bg-cover bg-center`
)}
style="background-image: url({src})" />

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {filter, identity} from 'ramda'
import {filter, identity} from "ramda"
import Input from "src/partials/Input.svelte"
import Modal from "src/partials/Modal.svelte"
import Content from "src/partials/Content.svelte"
@ -24,7 +24,7 @@
const opts = filter(identity, {maxWidth, maxHeight})
file = blobToFile(await stripExifData(inputFile, opts))
quote = await postJson(user.dufflepud('/upload/quote'), {
quote = await postJson(user.dufflepud("/upload/quote"), {
uploads: [{size: file.size}],
})
})
@ -57,11 +57,14 @@
<div class="flex gap-2">
{#if !hideInput}
<Input type="text" wrapperClass="flex-grow" bind:value={value} placeholder="https://">
<i slot="before" class={`fa fa-${icon}`} />
</Input>
<Input type="text" wrapperClass="flex-grow" bind:value placeholder="https://">
<i slot="before" class={`fa fa-${icon}`} />
</Input>
{/if}
<div on:click={() => { isOpen = true }}>
<div
on:click={() => {
isOpen = true
}}>
<slot name="button">
<Anchor type="button">
<i class="fa fa-upload" />
@ -71,23 +74,23 @@
</div>
{#if quote}
<Modal onEscape={decline}>
<Content>
<h1 class="staatliches text-2xl">Confirm File Upload</h1>
<p>Please accept the following terms:</p>
<p>{quote.terms}</p>
<div class="flex gap-2">
<Anchor type="button" on:click={decline} {loading}>Decline</Anchor>
<Anchor type="button-accent" on:click={accept} {loading}>Accept</Anchor>
</div>
</Content>
</Modal>
<Modal onEscape={decline}>
<Content>
<h1 class="staatliches text-2xl">Confirm File Upload</h1>
<p>Please accept the following terms:</p>
<p>{quote.terms}</p>
<div class="flex gap-2">
<Anchor type="button" on:click={decline} {loading}>Decline</Anchor>
<Anchor type="button-accent" on:click={accept} {loading}>Accept</Anchor>
</div>
</Content>
</Modal>
{:else if isOpen}
<Modal onEscape={decline}>
<Content>
<h1 class="staatliches text-2xl">Upload a File</h1>
<p>Click below to select a file to upload.</p>
<input type="file" bind:this={input} />
</Content>
</Modal>
<Modal onEscape={decline}>
<Content>
<h1 class="staatliches text-2xl">Upload a File</h1>
<p>Click below to select a file to upload.</p>
<input type="file" bind:this={input} />
</Content>
</Modal>
{/if}

View File

@ -8,20 +8,20 @@
const className = cx(
$$props.class,
"rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full placeholder:text-placeholder",
{"pl-10": $$slots.before, "pr-10": $$slots.after},
{"pl-10": $$slots.before, "pr-10": $$slots.after}
)
</script>
<div class={cx(wrapperClass, "relative")}>
<input {...$$props} class={className} bind:value on:change on:input />
{#if $$slots.before}
<div class="absolute top-0 left-0 pt-3 px-3 text-dark flex gap-2">
<slot name="before" />
</div>
<div class="absolute top-0 left-0 flex gap-2 px-3 pt-3 text-dark">
<slot name="before" />
</div>
{/if}
{#if $$slots.after}
<div class="absolute top-0 right-0 pt-3 px-3 text-dark flex gap-2 bg-light rounded m-px">
<slot name="after" />
</div>
<div class="absolute top-0 right-0 m-px flex gap-2 rounded bg-light px-3 pt-3 text-dark">
<slot name="after" />
</div>
{/if}
</div>

View File

@ -8,31 +8,31 @@
<svelte:body
on:keydown={e => {
if (e.key === 'Escape' && !root.querySelector('.modal')) {
if (e.key === "Escape" && !root.querySelector(".modal")) {
onEscape?.()
}
}} />
<div class="fixed inset-0 z-30 modal bg-black/75" bind:this={root} transition:fade>
<div class="modal fixed inset-0 z-30 bg-black/75" bind:this={root} transition:fade>
<div
class="modal-content overflow-auto h-full"
class="modal-content h-full overflow-auto"
bind:this={content}
transition:fly={{y: 1000}}
class:cursor-pointer={onEscape}
on:click={onEscape}>
<div class="mt-12 min-h-full">
{#if onEscape}
<div class="flex w-full justify-end p-2 sticky top-0 z-10 pointer-events-none">
<div
class="w-10 h-10 flex justify-center items-center bg-accent pointer-events-auto
rounded-full cursor-pointer border border-solid border-medium">
<i class="fa fa-times fa-lg" />
<div class="pointer-events-none sticky top-0 z-10 flex w-full justify-end p-2">
<div
class="pointer-events-auto flex h-10 w-10 cursor-pointer items-center justify-center
rounded-full border border-solid border-medium bg-accent">
<i class="fa fa-times fa-lg" />
</div>
</div>
</div>
{/if}
<div class="absolute w-full h-full bg-dark mt-12" />
<div class="absolute mt-12 h-full w-full bg-dark" />
<div
class="relative bg-dark border-t border-solid border-medium h-full w-full pt-2 pb-10 cursor-auto"
class="relative h-full w-full cursor-auto border-t border-solid border-medium bg-dark pt-2 pb-10"
on:click|stopPropagation>
<slot />
</div>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import {last} from 'ramda'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import {last} from "ramda"
import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak"
import {renderContent, noEvent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import Anchor from 'src/partials/Anchor.svelte'
import Anchor from "src/partials/Anchor.svelte"
import {routes} from "src/app/ui"
export let person
@ -15,34 +15,32 @@
<a
in:fly={{y: 20}}
href={routes.person(person.pubkey)}
class="flex gap-4 border-l-2 border-solid border-dark hover:bg-black hover:border-accent transition-all py-3 px-4 overflow-hidden">
class="flex gap-4 overflow-hidden border-l-2 border-solid border-dark py-3 px-4 transition-all hover:border-accent hover:bg-black">
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-12 w-12 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({person.kind0?.picture})" />
<div class="flex-grow flex flex-col gap-4 min-w-0">
<div class="flex gap-2 items-start justify-between">
<div class="flex min-w-0 flex-grow flex-col gap-4">
<div class="flex items-start justify-between gap-2">
<div class="flex flex-col gap-2">
<h1 class="text-xl">{displayPerson(person)}</h1>
{#if person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last(person.verified_as.split('@'))}</span>
</div>
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last(person.verified_as.split("@"))}</span>
</div>
{/if}
</div>
{#if removePetname}
<Anchor type="button-accent" on:click={noEvent(() => removePetname(person))}>
Following
</Anchor>
<Anchor type="button-accent" on:click={noEvent(() => removePetname(person))}>
Following
</Anchor>
{/if}
{#if addPetname}
<Anchor type="button" on:click={noEvent(() => addPetname(person))}>
Follow
</Anchor>
<Anchor type="button" on:click={noEvent(() => addPetname(person))}>Follow</Anchor>
{/if}
</div>
<p class="overflow-hidden text-ellipsis">
{@html renderContent(ellipsize(person.kind0?.about || '', 140))}
{@html renderContent(ellipsize(person.kind0?.about || "", 140))}
</p>
</div>
</a>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import 'tippy.js/dist/tippy.css'
import 'tippy.js/animations/shift-away.css'
import tippy from 'tippy.js'
import {onMount} from 'svelte'
import "tippy.js/dist/tippy.css"
import "tippy.js/animations/shift-away.css"
import tippy from "tippy.js"
import {onMount} from "svelte"
export let triggerType = 'click'
export let triggerType = "click"
let trigger
let tooltip
@ -16,23 +16,23 @@
allowHTML: true,
interactive: true,
trigger: triggerType,
animation: 'shift-away',
animation: "shift-away",
onShow: () => {
const [tooltipContents] = tooltip.children
// If we've already triggered it, tooltipContents will be empty
if (tooltipContents) {
instance.popper.querySelector('.tippy-content').appendChild(tooltipContents)
instance.popper.addEventListener('mouseleave', e => instance.hide())
instance.popper.addEventListener('click', e => {
if (e.target.closest('.tippy-close')) {
instance.popper.querySelector(".tippy-content").appendChild(tooltipContents)
instance.popper.addEventListener("mouseleave", e => instance.hide())
instance.popper.addEventListener("click", e => {
if (e.target.closest(".tippy-close")) {
instance.hide()
}
})
}
},
onHidden: () => {
const [tooltipContents] = instance.popper.querySelector('.tippy-content').children
const [tooltipContents] = instance.popper.querySelector(".tippy-content").children
tooltip.appendChild(tooltipContents)
},
@ -51,7 +51,7 @@
<svelte:body
on:keydown={e => {
if (e.key === 'Escape') {
if (e.key === "Escape") {
instance.hide()
}
}} />

View File

@ -1,8 +1,8 @@
<script>
import {onMount} from 'svelte'
import {slide} from 'svelte/transition'
import Anchor from 'src/partials/Anchor.svelte'
import user from 'src/agent/user'
import {onMount} from "svelte"
import {slide} from "svelte/transition"
import Anchor from "src/partials/Anchor.svelte"
import user from "src/agent/user"
export let url
export let onClose = null
@ -15,17 +15,17 @@
}
onMount(async () => {
if (url.match('\.(jpg|jpeg|png|gif)')) {
if (url.match(".(jpg|jpeg|png|gif)")) {
preview = {image: url}
} else if (url.match('\.(mov|mp4)')) {
} else if (url.match(".(mov|mp4)")) {
preview = {video: url}
} else {
try {
const res = await fetch(user.dufflepud('/link/preview'), {
method: 'POST',
const res = await fetch(user.dufflepud("/link/preview"), {
method: "POST",
body: JSON.stringify({url}),
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
})
@ -42,31 +42,31 @@
</script>
{#if preview}
<div in:slide>
<Anchor
external
href={url}
style="background-color: rgba(15, 15, 14, 0.5)"
class="relative rounded border border-solid border-medium flex flex-col overflow-hidden">
{#if preview.image}
<img alt="Link preview" src={preview.image} class="object-center max-h-96 object-contain" />
{/if}
{#if preview.video}
<video controls src={preview.video} class="object-center max-h-96 object-contain" />
{/if}
<div class="h-px bg-medium" />
{#if preview.title}
<div class="px-4 py-2 text-black flex flex-col bg-white">
<strong class="whitespace-nowrap text-ellipsis overflow-hidden">{preview.title}</strong>
<small>{preview.description}</small>
</div>
{/if}
<div
on:click|preventDefault={close}
class="w-6 h-6 rounded-full bg-white border border-solid border-medium shadow absolute
top-0 right-0 m-1 text-black flex justify-center items-center opacity-50">
<i class="fa fa-times" />
</div>
</Anchor>
</div>
<div in:slide>
<Anchor
external
href={url}
style="background-color: rgba(15, 15, 14, 0.5)"
class="relative flex flex-col overflow-hidden rounded border border-solid border-medium">
{#if preview.image}
<img alt="Link preview" src={preview.image} class="max-h-96 object-contain object-center" />
{/if}
{#if preview.video}
<video controls src={preview.video} class="max-h-96 object-contain object-center" />
{/if}
<div class="h-px bg-medium" />
{#if preview.title}
<div class="flex flex-col bg-white px-4 py-2 text-black">
<strong class="overflow-hidden text-ellipsis whitespace-nowrap">{preview.title}</strong>
<small>{preview.description}</small>
</div>
{/if}
<div
on:click|preventDefault={close}
class="absolute top-0 right-0 m-1 flex h-6 w-6 items-center justify-center
rounded-full border border-solid border-medium bg-white text-black opacity-50 shadow">
<i class="fa fa-times" />
</div>
</Anchor>
</div>
{/if}

View File

@ -1,9 +1,9 @@
<script lang="ts">
import QRCode from 'qrcode'
import {onMount} from 'svelte'
import Input from 'src/partials/Input.svelte'
import QRCode from "qrcode"
import {onMount} from "svelte"
import Input from "src/partials/Input.svelte"
import {copyToClipboard} from "src/util/html"
import {toast} from 'src/app/ui'
import {toast} from "src/app/ui"
export let code
@ -19,7 +19,8 @@
})
</script>
<div class="rounded bg-black border border-solid border-medium p-4 flex flex-col gap-4 max-w-sm m-auto">
<div
class="m-auto flex max-w-sm flex-col gap-4 rounded border border-solid border-medium bg-black p-4">
<canvas class="m-auto rounded" bind:this={canvas} />
<Input value={code}>
<button slot="after" class="fa fa-copy" on:click={copy} />

View File

@ -1,15 +1,15 @@
<script lang="ts">
import cx from 'classnames'
import {last} from 'ramda'
import {onMount} from 'svelte'
import cx from "classnames"
import {last} from "ramda"
import {onMount} from "svelte"
import {poll, stringToColor} from "src/util/misc"
import {between} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import {between} from "hurdak/lib/hurdak"
import {fly} from "svelte/transition"
import Anchor from "src/partials/Anchor.svelte"
import pool from 'src/agent/pool'
import pool from "src/agent/pool"
export let relay
export let theme = 'dark'
export let theme = "dark"
export let removeRelay = null
export let addRelay = null
@ -22,7 +22,7 @@
const conn = await pool.getConnection(relay.url)
if (conn) {
[quality, message] = conn.getQuality()
;[quality, message] = conn.getQuality()
} else {
quality = null
message = "Not connected"
@ -34,49 +34,48 @@
<div
class={cx(
`bg-${theme}`,
"rounded border border-l-2 border-solid border-medium shadow flex flex-col justify-between gap-3 py-3 px-6"
"flex flex-col justify-between gap-3 rounded border border-l-2 border-solid border-medium py-3 px-6 shadow"
)}
style={`border-left-color: ${stringToColor(relay.url)}`}
in:fly={{y: 20}}>
<div class="flex gap-2 items-center justify-between">
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("wss") ? "fa fa-lock" : "fa fa-unlock"} />
<Anchor type="unstyled" href={`/relays/${btoa(relay.url)}`}>
{last(relay.url.split('://'))}
{last(relay.url.split("://"))}
</Anchor>
<span
on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}}
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
class:bg-medium={message === 'Not connected'}
class:bg-danger={quality <= 0.3 && message !== 'Not connected'}
on:mouseout={() => {
showStatus = false
}}
on:mouseover={() => {
showStatus = true
}}
class="h-2 w-2 cursor-pointer rounded-full bg-medium"
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>
class:bg-success={quality > 0.7} />
<p
class="text-light text-sm transition-all hidden sm:block"
class="hidden text-sm text-light transition-all sm:block"
class:opacity-0={!showStatus}
class:opacity-1={showStatus}>
{message}
</p>
</div>
{#if removeRelay}
<button
class="flex gap-3 items-center text-light"
on:click={() => removeRelay(relay)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
<button class="flex items-center gap-3 text-light" on:click={() => removeRelay(relay)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
{/if}
{#if addRelay}
<button
class="flex gap-3 items-center text-light"
on:click={() => addRelay(relay)}>
<i class="fa fa-right-to-bracket" /> Join
</button>
<button class="flex items-center gap-3 text-light" on:click={() => addRelay(relay)}>
<i class="fa fa-right-to-bracket" /> Join
</button>
{/if}
</div>
{#if relay.description}
<p>{relay.description}</p>
<p>{relay.description}</p>
{/if}
<slot name="controls" />
</div>

View File

@ -1,24 +1,24 @@
<script lang="ts">
import {last} from 'ramda'
import {fly} from 'svelte/transition'
import {last} from "ramda"
import {fly} from "svelte/transition"
import {stringToColor} from "src/util/misc"
export let relay
</script>
<div
class="rounded border border-l-2 border-solid border-medium shadow flex flex-col
justify-between gap-3 py-3 px-6"
class="flex flex-col justify-between gap-3 rounded border border-l-2 border-solid
border-medium py-3 px-6 shadow"
style={`border-left-color: ${stringToColor(relay.url)}`}
in:fly={{y: 20}}>
<div class="flex gap-2 items-center justify-between">
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
<span>{last(relay.url.split('://'))}</span>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("wss") ? "fa fa-lock" : "fa fa-unlock"} />
<span>{last(relay.url.split("://"))}</span>
</div>
<slot name="actions" />
</div>
{#if relay.description}
<p>{relay.description}</p>
<p>{relay.description}</p>
{/if}
</div>

View File

@ -7,7 +7,7 @@
const className = cx(
$$props.class,
"rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full text-dark",
{"pl-10": $$slots.before, "pr-10": $$slots.after},
{"pl-10": $$slots.before, "pr-10": $$slots.after}
)
</script>
@ -15,10 +15,10 @@
<select {...$$props} class={className} bind:value>
<slot />
</select>
<div class="absolute top-0 left-0 pt-3 px-3 text-dark flex gap-2">
<div class="absolute top-0 left-0 flex gap-2 px-3 pt-3 text-dark">
<slot name="before" />
</div>
<div class="absolute top-0 right-0 pt-3 px-3 text-dark flex gap-2">
<div class="absolute top-0 right-0 flex gap-2 px-3 pt-3 text-dark">
<slot name="after" />
</div>
</div>

View File

@ -7,16 +7,18 @@
<div>
<div class="inline-block">
<div class="rounded flex border border-solid border-light cursor-pointer">
<div class="flex cursor-pointer rounded border border-solid border-light">
{#each options as option, i}
<div
class={cx("px-4 py-2 transition-all", {
"border-l border-solid border-light": i > 0,
"bg-accent": value === option,
})}
on:click={() => { value = option }}>
{option}
</div>
<div
class={cx("px-4 py-2 transition-all", {
"border-l border-solid border-light": i > 0,
"bg-accent": value === option,
})}
on:click={() => {
value = option
}}>
{option}
</div>
{/each}
</div>
</div>

View File

@ -1,11 +1,11 @@
<script>
import {fade} from 'svelte/transition'
import {Circle2} from 'svelte-loading-spinners'
import {fade} from "svelte/transition"
import {Circle2} from "svelte-loading-spinners"
export let delay = 1000
</script>
<div class="py-20 flex flex-col gap-4 items-center justify-center" in:fade={{delay}}>
<div class="flex flex-col items-center justify-center gap-4 py-20" in:fade={{delay}}>
<slot />
<Circle2 colorOuter="#CCC5B9" colorInner="#403D39" colorCenter="#EB5E28" />
</div>

View File

@ -1,6 +1,6 @@
<script>
import {fly} from 'svelte/transition'
import {toTitle} from 'hurdak/lib/hurdak'
import {fly} from "svelte/transition"
import {toTitle} from "hurdak/lib/hurdak"
export let tabs
export let activeTab
@ -8,17 +8,17 @@
export let getDisplay = tab => ({title: toTitle(tab), badge: null})
</script>
<div class="border-b border-solid border-dark flex pt-2 overflow-auto" in:fly={{y: 20}}>
<div class="flex overflow-auto border-b border-solid border-dark pt-2" in:fly={{y: 20}}>
{#each tabs as tab}
{@const {title, badge} = getDisplay(tab)}
<button
class="cursor-pointer hover:border-b border-solid border-medium px-8 py-4 flex gap-2"
class:border-b={activeTab === tab}
on:click={() => setActiveTab(tab)}>
<div>{title}</div>
{#if badge}
<div class="rounded-full bg-medium px-2 h-6">{badge}</div>
{/if}
</button>
{@const {title, badge} = getDisplay(tab)}
<button
class="flex cursor-pointer gap-2 border-solid border-medium px-8 py-4 hover:border-b"
class:border-b={activeTab === tab}
on:click={() => setActiveTab(tab)}>
<div>{title}</div>
{#if badge}
<div class="h-6 rounded-full bg-medium px-2">{badge}</div>
{/if}
</button>
{/each}
</div>

View File

@ -1,3 +1,5 @@
<svelte:options accessors />
<script>
import cx from "classnames"
@ -7,10 +9,8 @@
const className = cx(
$$props.class,
"rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full text-dark",
"placeholder:text-dark placeholder:opacity-75",
"placeholder:text-dark placeholder:opacity-75"
)
</script>
<svelte:options accessors />
<textarea {...$$props} class={className} bind:this={element} bind:value on:keydown on:keypress />

View File

@ -1,14 +1,14 @@
<script>
import Switch from "svelte-switch"
import {createEventDispatcher} from 'svelte'
import {createEventDispatcher} from "svelte"
export let value
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail.checked
dispatch('change', value)
dispatch("change", value)
}
</script>

View File

@ -1,15 +1,15 @@
<script>
import {sortBy, assoc} from 'ramda'
import {onMount} from 'svelte'
import {fly} from 'svelte/transition'
import {now, createScroller} from 'src/util/misc'
import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte'
import Alert from 'src/views/alerts/Alert.svelte'
import Mention from 'src/views/alerts/Mention.svelte'
import database from 'src/agent/database'
import user from 'src/agent/user'
import {lastChecked} from 'src/app/alerts'
import {sortBy, assoc} from "ramda"
import {onMount} from "svelte"
import {fly} from "svelte/transition"
import {now, createScroller} from "src/util/misc"
import Spinner from "src/partials/Spinner.svelte"
import Content from "src/partials/Content.svelte"
import Alert from "src/views/alerts/Alert.svelte"
import Mention from "src/views/alerts/Mention.svelte"
import database from "src/agent/database"
import user from "src/agent/user"
import {lastChecked} from "src/app/alerts"
let limit = 0
let notes = null
@ -17,20 +17,18 @@
onMount(() => {
document.title = "Notifications"
lastChecked.update(assoc('alerts', now()))
lastChecked.update(assoc("alerts", now()))
return createScroller(async () => {
limit += 10
// Filter out alerts for which we failed to find the required context. The bug
// is really upstream of this, but it's an easy fix
const events = user.mute(database.alerts.all())
.filter(e => (
e.replies.length > 0
|| e.likedBy.length > 0
|| e.zappedBy?.length > 0
|| e.isMention
))
const events = user
.mute(database.alerts.all())
.filter(
e => e.replies.length > 0 || e.likedBy.length > 0 || e.zappedBy?.length > 0 || e.isMention
)
notes = sortBy(e => -e.created_at, events).slice(0, limit)
})
@ -38,25 +36,23 @@
</script>
{#if notes}
<Content>
{#each notes as note (note.id)}
<div in:fly={{y: 20}}>
{#if note.replies.length > 0}
<Alert type="replies" {note} />
{:else if note.zappedBy?.length > 0}
<Alert type="zaps" {note} />
{:else if note.likedBy.length > 0}
<Alert type="likes" {note} />
<Content>
{#each notes as note (note.id)}
<div in:fly={{y: 20}}>
{#if note.replies.length > 0}
<Alert type="replies" {note} />
{:else if note.zappedBy?.length > 0}
<Alert type="zaps" {note} />
{:else if note.likedBy.length > 0}
<Alert type="likes" {note} />
{:else}
<Mention {note} />
{/if}
</div>
{:else}
<Mention {note} />
{/if}
</div>
{:else}
<Content size="lg" class="text-center">
No alerts found - check back later!
<Content size="lg" class="text-center">No alerts found - check back later!</Content>
{/each}
</Content>
{/each}
</Content>
{:else}
<Spinner />
<Spinner />
{/if}

View File

@ -1,12 +1,12 @@
<script lang="ts">
import {objOf} from 'ramda'
import {onMount} from 'svelte'
import {nip19} from 'nostr-tools'
import {warn} from 'src/util/logger'
import Content from 'src/partials/Content.svelte'
import NoteDetail from 'src/views/notes/NoteDetail.svelte'
import Person from 'src/routes/PersonDetail.svelte'
import {sampleRelays} from 'src/agent/relays'
import {objOf} from "ramda"
import {onMount} from "svelte"
import {nip19} from "nostr-tools"
import {warn} from "src/util/logger"
import Content from "src/partials/Content.svelte"
import NoteDetail from "src/views/notes/NoteDetail.svelte"
import Person from "src/routes/PersonDetail.svelte"
import {sampleRelays} from "src/agent/relays"
export let entity
@ -14,8 +14,8 @@
onMount(() => {
try {
({type, data} = nip19.decode(entity) as {type: string, data: any})
relays = sampleRelays((data.relays || []).map(objOf('url')))
;({type, data} = nip19.decode(entity) as {type: string; data: any})
relays = sampleRelays((data.relays || []).map(objOf("url")))
} catch (e) {
warn(e)
}
@ -39,4 +39,3 @@
<div>Sorry, we weren't able to find "{entity}".</div>
</Content>
{/if}

View File

@ -1,42 +1,42 @@
<script lang="ts">
import {assoc} from 'ramda'
import {updateIn} from 'hurdak/lib/hurdak'
import {now, formatTimestamp} from 'src/util/misc'
import {toHex} from 'src/util/nostr'
import Channel from 'src/partials/Channel.svelte'
import Badge from 'src/partials/Badge.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import user from 'src/agent/user'
import {getRelaysForEventChildren, sampleRelays} from 'src/agent/relays'
import network from 'src/agent/network'
import database from 'src/agent/database'
import cmd from 'src/agent/cmd'
import {modal} from 'src/app/ui'
import {lastChecked} from 'src/app/alerts'
import {renderNote} from 'src/app'
import {assoc} from "ramda"
import {updateIn} from "hurdak/lib/hurdak"
import {now, formatTimestamp} from "src/util/misc"
import {toHex} from "src/util/nostr"
import Channel from "src/partials/Channel.svelte"
import Badge from "src/partials/Badge.svelte"
import Anchor from "src/partials/Anchor.svelte"
import user from "src/agent/user"
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
import network from "src/agent/network"
import database from "src/agent/database"
import cmd from "src/agent/cmd"
import {modal} from "src/app/ui"
import {lastChecked} from "src/app/alerts"
import {renderNote} from "src/app"
export let entity
const id = toHex(entity)
const room = database.watch('rooms', t => t.get(id) || {id})
const room = database.watch("rooms", t => t.get(id) || {id})
const getRelays = () => sampleRelays($room ? getRelaysForEventChildren($room) : [])
const listenForMessages = onChunk =>
network.listen({
relays: getRelays(),
filter: [{kinds: [42], '#e': [id], since: now()}],
filter: [{kinds: [42], "#e": [id], since: now()}],
onChunk,
})
const loadMessages = (cursor, onChunk) =>
network.load({
relays: getRelays(),
filter: {kinds: [42], '#e': [id], ...cursor.getFilter()},
filter: {kinds: [42], "#e": [id], ...cursor.getFilter()},
onChunk,
})
const edit = () => {
modal.set({type: 'room/edit', room: $room})
modal.set({type: "room/edit", room: $room})
}
const sendMessage = async content => {
@ -52,21 +52,21 @@
</script>
<Channel {loadMessages} {listenForMessages} {sendMessage}>
<div slot="header" class="flex gap-4 items-start">
<div slot="header" class="flex items-start gap-4">
<div class="flex items-center gap-4">
<Anchor type="unstyled" class="fa fa-arrow-left text-2xl cursor-pointer" href="/chat" />
<Anchor type="unstyled" class="fa fa-arrow-left cursor-pointer text-2xl" href="/chat" />
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-12 w-12 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({$room.picture})" />
</div>
<div class="w-full flex flex-col gap-2">
<div class="flex items-center justify-between w-full">
<div class="flex w-full flex-col gap-2">
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-4">
<div class="text-lg font-bold">{$room.name || ''}</div>
<div class="text-lg font-bold">{$room.name || ""}</div>
{#if $room?.pubkey === user.getPubkey()}
<button class="text-sm cursor-pointer" on:click={edit}>
<i class="fa-solid fa-edit" /> Edit
</button>
<button class="cursor-pointer text-sm" on:click={edit}>
<i class="fa-solid fa-edit" /> Edit
</button>
{/if}
</div>
<div class="flex items-center gap-2">
@ -74,17 +74,17 @@
<span class="text-light">Public</span>
</div>
</div>
<div>{$room.about || ''}</div>
<div>{$room.about || ""}</div>
</div>
</div>
<div slot="message" let:message>
{#if message.showPerson}
<div class="flex gap-4 items-center justify-between">
<Badge person={message.person} />
<p class="text-sm text-light">{formatTimestamp(message.created_at)}</p>
</div>
<div class="flex items-center justify-between gap-4">
<Badge person={message.person} />
<p class="text-sm text-light">{formatTimestamp(message.created_at)}</p>
</div>
{/if}
<div class="overflow-hidden text-ellipsis ml-6 my-1">
<div class="my-1 ml-6 overflow-hidden text-ellipsis">
{@html renderNote(message, {showEntire: true})}
</div>
</div>

View File

@ -1,26 +1,26 @@
<script>
import {onMount} from 'svelte'
import {onMount} from "svelte"
import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import ChatListItem from "src/views/chat/ChatListItem.svelte"
import database from 'src/agent/database'
import network from 'src/agent/network'
import {getUserReadRelays} from 'src/agent/relays'
import {modal} from 'src/app/ui'
import database from "src/agent/database"
import network from "src/agent/network"
import {getUserReadRelays} from "src/agent/relays"
import {modal} from "src/app/ui"
let q = ""
let search
let results = []
const userRooms = database.watch('rooms', t => t.all({joined: true}))
const otherRooms = database.watch('rooms', t => t.all({'joined:!eq': true}))
const userRooms = database.watch("rooms", t => t.all({joined: true}))
const otherRooms = database.watch("rooms", t => t.all({"joined:!eq": true}))
$: search = fuzzy($otherRooms, {keys: ['name', 'about']})
$: search = fuzzy($otherRooms, {keys: ["name", "about"]})
$: results = search(q).slice(0, 50)
document.title = 'Chat'
document.title = "Chat"
onMount(() => {
const sub = network.listen({
@ -35,22 +35,22 @@
</script>
<Content>
<div class="flex justify-between mt-10">
<div class="flex gap-2 items-center">
<div class="mt-10 flex justify-between">
<div class="flex items-center gap-2">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your rooms</h2>
</div>
<Anchor type="button-accent" on:click={() => modal.set({type: 'room/edit'})}>
<Anchor type="button-accent" on:click={() => modal.set({type: "room/edit"})}>
<i class="fa-solid fa-plus" /> Create Room
</Anchor>
</div>
{#each $userRooms as room (room.id)}
<ChatListItem {room} />
<ChatListItem {room} />
{:else}
<p class="text-center py-8">You haven't yet joined any rooms.</p>
<p class="text-center py-8">You haven't yet joined any rooms.</p>
{/each}
<div class="pt-2 mb-2 border-b border-solid border-medium" />
<div class="flex gap-2 items-center">
<div class="mb-2 border-b border-solid border-medium pt-2" />
<div class="flex items-center gap-2">
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other rooms</h2>
</div>
@ -60,15 +60,13 @@
</Input>
</div>
{#if results.length > 0}
{#each results as room (room.id)}
<ChatListItem room={room} />
{/each}
<small class="text-center">
Showing {Math.min(50, $otherRooms.length)} of {$otherRooms.length} known rooms
</small>
{#each results as room (room.id)}
<ChatListItem {room} />
{/each}
<small class="text-center">
Showing {Math.min(50, $otherRooms.length)} of {$otherRooms.length} known rooms
</small>
{:else}
<small class="text-center">
No matching rooms found
</small>
<small class="text-center"> No matching rooms found </small>
{/if}
</Content>

View File

@ -1,19 +1,20 @@
<script lang="ts">
import {flatten} from 'ramda'
import {fly} from 'svelte/transition'
import {logs} from 'src/util/logger.js'
import {formatTimestamp} from 'src/util/misc'
import Content from 'src/partials/Content.svelte'
import {flatten} from "ramda"
import {fly} from "svelte/transition"
import {logs} from "src/util/logger.js"
import {formatTimestamp} from "src/util/misc"
import Content from "src/partials/Content.svelte"
document.title = "Debug"
</script>
<Content>
{#each flatten($logs) as {created_at, message}}
<div in:fly={{y: 20}} class="text-sm flex flex-col gap-2">
<div class="text-light underline">{formatTimestamp(created_at/1000)}</div>
<pre>{message.map(m => JSON.stringify(m, null, 2)).join(' ')}</pre>
{#each flatten($logs) as { created_at, message }}
<div in:fly={{y: 20}} class="flex flex-col gap-2 text-sm">
<div class="text-light underline">
{formatTimestamp(created_at / 1000)}
</div>
<pre>{message.map(m => JSON.stringify(m, null, 2)).join(" ")}</pre>
</div>
{/each}
</Content>

View File

@ -1,13 +1,13 @@
<script>
import {navigate} from 'svelte-routing'
import {toTitle} from 'hurdak/lib/hurdak'
import {navigate} from "svelte-routing"
import {toTitle} from "hurdak/lib/hurdak"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Tabs from "src/partials/Tabs.svelte"
import NewNoteButton from "src/views/notes/NewNoteButton.svelte"
import Follows from "src/views/feed/Follows.svelte"
import Network from "src/views/feed/Network.svelte"
import user from 'src/agent/user'
import user from "src/agent/user"
export let activeTab
@ -18,17 +18,19 @@
<Content>
{#if !user.getProfile()}
<Content size="lg" class="text-center">
<p class="text-xl">Don't have an account?</p>
<p>Click <Anchor href="/login">here</Anchor> to join the nostr network.</p>
</Content>
<Content size="lg" class="text-center">
<p class="text-xl">Don't have an account?</p>
<p>
Click <Anchor href="/login">here</Anchor> to join the nostr network.
</p>
</Content>
{/if}
<div>
<Tabs tabs={['follows', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'follows'}
<Follows />
<Tabs tabs={["follows", "network"]} {activeTab} {setActiveTab} />
{#if activeTab === "follows"}
<Follows />
{:else}
<Network />
<Network />
{/if}
</div>
</Content>

View File

@ -1,26 +1,22 @@
<script>
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import {nip19} from 'nostr-tools'
import {nip19} from "nostr-tools"
import {copyToClipboard} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.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 {toast} from "src/app/ui"
const {pubkey, privkey} = keys
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
const keypairUrl = "https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/"
const copyKey = type => {
copyToClipboard(
type === 'private'
? nip19.nsecEncode($privkey)
: nip19.npubEncode($pubkey)
)
copyToClipboard(type === "private" ? nip19.nsecEncode($privkey) : nip19.npubEncode($pubkey))
toast.show("info", `Your ${type} key has been copied to the clipboard.`)
}
@ -31,50 +27,51 @@
}
})
document.title = 'Keys'
document.title = "Keys"
</script>
<div in:fly={{y: 20}}>
<Content>
<div class="flex justify-center items-center flex-col mb-4">
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>Your Keys</Heading>
<p>
Your account is identified across the network using
a public/private <Anchor href={keypairUrl} external>keypair</Anchor>.
This allows you to fully own your account, and move to another app if needed.
Your account is identified across the network using a public/private <Anchor
href={keypairUrl}
external>keypair</Anchor
>. This allows you to fully own your account, and move to another app if needed.
</p>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Public Key</strong>
<Input disabled value={$pubkey ? nip19.npubEncode($pubkey) : ''}>
<Input disabled value={$pubkey ? nip19.npubEncode($pubkey) : ""}>
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey('public')} />
on:click={() => copyKey("public")} />
</Input>
<p class="text-sm text-light">
Your public key identifies your account. You can share this with people
trying to find you on nostr.
Your public key identifies your account. You can share this with people trying to find you
on nostr.
</p>
</div>
{#if $privkey}
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input disabled type="password" value={nip19.nsecEncode($privkey)}>
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey('private')} />
</Input>
<p class="text-sm text-light">
Your private key is used to prove your identity by cryptographically signing
messages. <strong>Do not share this with anyone.</strong> Be careful about
copying this into other apps - instead, consider using
a <Anchor href={nip07} external>compatible browser extension</Anchor> to securely
store your key.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input disabled type="password" value={nip19.nsecEncode($privkey)}>
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey("private")} />
</Input>
<p class="text-sm text-light">
Your private key is used to prove your identity by cryptographically signing messages. <strong
>Do not share this with anyone.</strong>
Be careful about copying this into other apps - instead, consider using a <Anchor
href={nip07}
external>compatible browser extension</Anchor> to securely store your key.
</p>
</div>
{/if}
</div>
</Content>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from 'src/partials/Heading.svelte'
import user from 'src/agent/user'
import Heading from "src/partials/Heading.svelte"
import user from "src/agent/user"
import {modal} from "src/app/ui"
import {login} from "src/app"
@ -14,22 +14,22 @@
const {nostr} = window as any
if (nostr) {
login('extension', await nostr.getPublicKey())
login("extension", await nostr.getPublicKey())
} else {
modal.set({type: 'login/privkey'})
modal.set({type: "login/privkey"})
}
}
const signUp = () => {
modal.set({type: 'onboarding', stage: 'intro'})
modal.set({type: "onboarding", stage: "intro"})
}
const pubkeyLogIn = () => {
modal.set({type: 'login/pubkey'})
modal.set({type: "login/pubkey"})
}
if (user.getPubkey()) {
navigate('/')
navigate("/")
}
document.title = "Log In"
@ -37,17 +37,16 @@
<div in:fly={{y: 20}}>
<Content size="lg" class="text-center">
<div class="flex flex-col gap-8 max-w-2xl">
<div class="flex justify-center items-center flex-col mb-4">
<div class="flex max-w-2xl flex-col gap-8">
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>Welcome!</Heading>
<i>To the Nostr Network</i>
</div>
<p class="text-center">
Click below to log in or create an account. If you have
a <Anchor href={nip07} external>compatible browser extension</Anchor> installed,
we will use that.
Click below to log in or create an account. If you have a <Anchor href={nip07} external
>compatible browser extension</Anchor> installed, we will use that.
</p>
<div class="flex flex-col gap-4 items-center">
<div class="flex flex-col items-center gap-4">
<div class="flex gap-4">
<Anchor class="w-32 text-center" type="button-accent" on:click={autoLogIn}>Log In</Anchor>
<Anchor class="w-32 text-center" type="button" on:click={signUp}>Sign Up</Anchor>

View File

@ -1,8 +1,8 @@
<script>
import {fly} from 'svelte/transition'
import Anchor from 'src/partials/Anchor.svelte'
import {fly} from "svelte/transition"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import database from 'src/agent/database'
import database from "src/agent/database"
let confirmed = false
@ -17,7 +17,7 @@
// Give them a moment to see the state transition. Dexie
// also apparently needs some time
setTimeout(() => {
window.location.href = '/login'
window.location.href = "/login"
}, 1000)
}
@ -26,11 +26,11 @@
<Content size="lg" class="text-center">
{#if confirmed}
<div in:fly={{y:20}}>Clearing your local database...</div>
<div in:fly={{y: 20}}>Clearing your local database...</div>
{:else}
<div class="flex flex-col gap-8 items-center" in:fly={{y:20}}>
<div>Are you sure you want to log out? All data will be cleared.</div>
<Anchor type="button" on:click={confirm}>Log out</Anchor>
</div>
<div class="flex flex-col items-center gap-8" in:fly={{y: 20}}>
<div>Are you sure you want to log out? All data will be cleared.</div>
<Anchor type="button" on:click={confirm}>Log out</Anchor>
</div>
{/if}
</Content>

View File

@ -1,27 +1,27 @@
<script lang="ts">
import cx from 'classnames'
import {assoc} from 'ramda'
import {renameProp} from 'hurdak/lib/hurdak'
import {toHex, displayPerson} from 'src/util/nostr'
import {now, formatTimestamp} from 'src/util/misc'
import {Tags} from 'src/util/nostr'
import Channel from 'src/partials/Channel.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {getAllPubkeyRelays, sampleRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import keys from 'src/agent/keys'
import user from 'src/agent/user'
import cmd from 'src/agent/cmd'
import {routes} from 'src/app/ui'
import {lastChecked} from 'src/app/alerts'
import {renderNote} from 'src/app'
import cx from "classnames"
import {assoc} from "ramda"
import {renameProp} from "hurdak/lib/hurdak"
import {toHex, displayPerson} from "src/util/nostr"
import {now, formatTimestamp} from "src/util/misc"
import {Tags} from "src/util/nostr"
import Channel from "src/partials/Channel.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
import database from "src/agent/database"
import network from "src/agent/network"
import keys from "src/agent/keys"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
import {routes} from "src/app/ui"
import {lastChecked} from "src/app/alerts"
import {renderNote} from "src/app"
export let entity
let crypt = keys.getCrypt()
let pubkey = toHex(entity)
let person = database.watch('people', () => database.getPersonWithFallback(pubkey))
let person = database.watch("people", () => database.getPersonWithFallback(pubkey))
lastChecked.update(assoc(pubkey, now()))
@ -32,19 +32,17 @@
const decryptMessages = async events => {
// Gotta do it in serial because of extension limitations
for (const event of events) {
const key = event.pubkey === pubkey
? pubkey
: Tags.from(event).type("p").values().first()
const key = event.pubkey === pubkey ? pubkey : Tags.from(event).type("p").values().first()
event.decryptedContent = await crypt.decrypt(key, event.content)
}
return events.map(renameProp('decryptedContent', 'content'))
return events.map(renameProp("decryptedContent", "content"))
}
const getFilters = extra => [
{kinds: [4], authors: [user.getPubkey()], '#p': [pubkey], ...extra},
{kinds: [4], authors: [pubkey], '#p': [user.getPubkey()], ...extra},
{kinds: [4], authors: [user.getPubkey()], "#p": [pubkey], ...extra},
{kinds: [4], authors: [pubkey], "#p": [user.getPubkey()], ...extra},
]
const listenForMessages = onChunk =>
@ -73,15 +71,15 @@
</script>
<Channel {loadMessages} {listenForMessages} {sendMessage}>
<div slot="header" class="flex gap-4 items-start">
<div slot="header" class="flex items-start gap-4">
<div class="flex items-center gap-4">
<Anchor type="unstyled" class="fa fa-arrow-left text-2xl cursor-pointer" href="/messages" />
<Anchor type="unstyled" class="fa fa-arrow-left cursor-pointer text-2xl" href="/messages" />
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-12 w-12 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({$person.kind0?.picture})" />
</div>
<div class="w-full flex flex-col gap-2">
<div class="flex items-center justify-between w-full">
<div class="flex w-full flex-col gap-2">
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-4">
<Anchor type="unstyled" href={routes.person(pubkey)} class="text-lg font-bold">
{displayPerson($person)}
@ -92,18 +90,24 @@
<span class="text-light">Encrypted</span>
</div>
</div>
<div>{$person.kind0?.about || ''}</div>
<div>{$person.kind0?.about || ""}</div>
</div>
</div>
<div slot="message" let:message class={cx("flex overflow-hidden text-ellipsis", {
'ml-12 justify-end': message.person.pubkey === user.getPubkey(),
'mr-12': message.person.pubkey !== user.getPubkey(),
})}>
<div class={cx('rounded-2xl py-2 px-4 max-w-xl inline-block', {
'bg-white text-black rounded-br-none text-end': message.person.pubkey === user.getPubkey(),
'bg-dark rounded-bl-none': message.person.pubkey !== user.getPubkey(),
<div
slot="message"
let:message
class={cx("flex overflow-hidden text-ellipsis", {
"ml-12 justify-end": message.person.pubkey === user.getPubkey(),
"mr-12": message.person.pubkey !== user.getPubkey(),
})}>
<div class="break-words">{@html renderNote(message, {showEntire: true})}</div>
<div
class={cx("inline-block max-w-xl rounded-2xl py-2 px-4", {
"rounded-br-none bg-white text-end text-black": message.person.pubkey === user.getPubkey(),
"rounded-bl-none bg-dark": message.person.pubkey !== user.getPubkey(),
})}>
<div class="break-words">
{@html renderNote(message, {showEntire: true})}
</div>
<small
class="mt-1"
class:text-dark={message.person.pubkey === user.getPubkey()}

View File

@ -1,32 +1,33 @@
<script>
import {sortBy} from 'ramda'
import {toTitle} from 'hurdak/lib/hurdak'
import {sortBy} from "ramda"
import {toTitle} from "hurdak/lib/hurdak"
import Tabs from "src/partials/Tabs.svelte"
import Content from "src/partials/Content.svelte"
import MessagesListItem from "src/views/messages/MessagesListItem.svelte"
import database from 'src/agent/database'
import database from "src/agent/database"
let activeTab = 'messages'
let activeTab = "messages"
const setActiveTab = tab => {
activeTab = tab
}
const accepted = database.watch('contacts', t => t.all({accepted: true}))
const requests = database.watch('contacts', t => t.all({'accepted:!eq': true}))
const accepted = database.watch("contacts", t => t.all({accepted: true}))
const requests = database.watch("contacts", t => t.all({"accepted:!eq": true}))
const getContacts = tab =>
sortBy(c => -c.lastMessage, tab === 'messages' ? $accepted : $requests)
const getContacts = tab => sortBy(c => -c.lastMessage, tab === "messages" ? $accepted : $requests)
const getDisplay = tab =>
({title: toTitle(tab), badge: getContacts(tab).length})
const getDisplay = tab => ({
title: toTitle(tab),
badge: getContacts(tab).length,
})
document.title = 'Direct Messages'
document.title = "Direct Messages"
</script>
<Content>
<Tabs tabs={['messages', 'requests']} {activeTab} {setActiveTab} {getDisplay} />
<Tabs tabs={["messages", "requests"]} {activeTab} {setActiveTab} {getDisplay} />
{#each getContacts(activeTab) as contact (contact.pubkey)}
<MessagesListItem {contact} />
<MessagesListItem {contact} />
{/each}
</Content>

View File

@ -1,7 +1,7 @@
<script>
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
import user from 'src/agent/user'
import {onMount} from "svelte"
import {navigate} from "svelte-routing"
import user from "src/agent/user"
onMount(() => navigate(user.getProfile() ? '/notes/follows' : '/login'))
onMount(() => navigate(user.getProfile() ? "/notes/follows" : "/login"))
</script>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import {last} from 'ramda'
import {onMount} from 'svelte'
import {tweened} from 'svelte/motion'
import {fly, fade} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {log} from 'src/util/logger'
import {renderContent} from 'src/util/html'
import {displayPerson, Tags, toHex} from 'src/util/nostr'
import {last} from "ramda"
import {onMount} from "svelte"
import {tweened} from "svelte/motion"
import {fly, fade} from "svelte/transition"
import {navigate} from "svelte-routing"
import {log} from "src/util/logger"
import {renderContent} from "src/util/html"
import {displayPerson, Tags, toHex} from "src/util/nostr"
import Tabs from "src/partials/Tabs.svelte"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
@ -44,30 +44,37 @@
actions = []
if (showActions) {
actions.push({onClick: share, label: 'Share', icon: 'share-nodes'})
actions.push({onClick: share, label: "Share", icon: "share-nodes"})
if (following) {
actions.push({onClick: unfollow, label: 'Unfollow', icon: 'user-minus'})
actions.push({onClick: unfollow, label: "Unfollow", icon: "user-minus"})
} else if (user.getPubkey() !== pubkey) {
actions.push({onClick: follow, label: 'Follow', icon: 'user-plus'})
actions.push({onClick: follow, label: "Follow", icon: "user-plus"})
}
if ($canPublish) {
actions.push({onClick: () => navigate(`/messages/${npub}`), label: 'Message', icon: 'envelope'})
actions.push({onClick: openAdvanced, label: 'Advanced', icon: 'sliders'})
actions.push({
onClick: () => navigate(`/messages/${npub}`),
label: "Message",
icon: "envelope",
})
actions.push({onClick: openAdvanced, label: "Advanced", icon: "sliders"})
}
actions.push({onClick: openProfileInfo, label: 'Profile', icon: 'info'})
actions.push({onClick: openProfileInfo, label: "Profile", icon: "info"})
if (user.getPubkey() === pubkey && $canPublish) {
actions.push({onClick: () => navigate('/profile'), label: 'Edit', icon: 'edit'})
actions.push({
onClick: () => navigate("/profile"),
label: "Edit",
icon: "edit",
})
}
}
}
onMount(async () => {
log('Person', npub, person)
log("Person", npub, person)
document.title = displayPerson(person)
@ -89,7 +96,7 @@
await network.load({
shouldProcess: false,
relays: getRelays(),
filter: [{kinds: [3], '#p': [pubkey]}],
filter: [{kinds: [3], "#p": [pubkey]}],
onChunk: events => {
for (const e of events) {
followers.add(e.pubkey)
@ -109,12 +116,12 @@
const showFollows = () => {
const pubkeys = Tags.wrap(person.petnames).pubkeys()
modal.set({type: 'person/list', pubkeys})
}
modal.set({type: "person/list", pubkeys})
}
const showFollowers = () => {
modal.set({type: 'person/list', pubkeys: Array.from(followers)})
}
modal.set({type: "person/list", pubkeys: Array.from(followers)})
}
const follow = async () => {
const [{url}] = getRelays()
@ -127,22 +134,25 @@
}
const openAdvanced = () => {
modal.set({type: 'person/settings', person})
modal.set({type: "person/settings", person})
}
const openProfileInfo = () => {
modal.set({type: 'person/info', person})
modal.set({type: "person/info", person})
}
const share = () => {
modal.set({type: 'person/share', person})
modal.set({type: "person/share", person})
}
</script>
<svelte:window on:click={() => { showActions = false }} />
<svelte:window
on:click={() => {
showActions = false
}} />
<div
class="absolute w-full h-64 left-0"
class="absolute left-0 h-64 w-full"
style="z-index: -1;
background-size: cover;
background-image:
@ -152,75 +162,73 @@
<Content>
<div class="flex gap-4">
<div
class="overflow-hidden w-16 h-16 sm:w-32 sm:h-32 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-16 w-16 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center sm:h-32 sm:w-32"
style="background-image: url({person.kind0?.picture})" />
<div class="flex flex-col gap-4 flex-grow">
<div class="flex justify-between items-start gap-4">
<div class="flex-grow flex flex-col gap-2">
<div class="flex flex-grow flex-col gap-4">
<div class="flex items-start justify-between gap-4">
<div class="flex flex-grow flex-col gap-2">
<div class="flex items-center gap-2">
<h1 class="text-2xl">{displayPerson(person)}</h1>
</div>
{#if person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last(person.verified_as.split('@'))}</span>
</div>
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last(person.verified_as.split("@"))}</span>
</div>
{/if}
</div>
<div class="whitespace-nowrap flex gap-3 flex-wrap relative">
<div on:click|stopPropagation={toggleActions} class="px-5 py-2 cursor-pointer">
<div class="relative flex flex-wrap gap-3 whitespace-nowrap">
<div on:click|stopPropagation={toggleActions} class="cursor-pointer px-5 py-2">
<i class="fa fa-xl fa-ellipsis-vertical" />
</div>
<div class="absolute top-0 right-0 mt-12 flex flex-col gap-2 opacity-90 z-10">
<div class="absolute top-0 right-0 z-10 mt-12 flex flex-col gap-2 opacity-90">
<div
class="absolute inset-0 bg-black rounded-full"
class="absolute inset-0 rounded-full bg-black"
class:hidden={!showActions}
style="filter: blur(15px)"
transition:fade|local />
{#each actions as {onClick, href, label, icon}, i}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
in:fly|local={{y: 20, delay: i * 30}}
out:fly|local={{y: 20, delay: (actions.length - i - 1) * 30}}
on:click={onClick}>
<div class="text-light">{label}</div>
<Anchor type="button-circle">
<i class={`fa fa-${icon}`} />
</Anchor>
</div>
{#each actions as { onClick, href, label, icon }, i}
<div
class="z-10 flex cursor-pointer items-center justify-end gap-2"
in:fly|local={{y: 20, delay: i * 30}}
out:fly|local={{y: 20, delay: (actions.length - i - 1) * 30}}
on:click={onClick}>
<div class="text-light">{label}</div>
<Anchor type="button-circle">
<i class={`fa fa-${icon}`} />
</Anchor>
</div>
{/each}
</div>
</div>
</div>
<p>{@html renderContent(person?.kind0?.about || '')}</p>
<p>{@html renderContent(person?.kind0?.about || "")}</p>
{#if person?.petnames}
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
<strong>{person.petnames.length}</strong> following
</button>
<button on:click={showFollowers}>
<strong>{$followersCount}</strong> followers
</button>
</div>
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
<strong>{person.petnames.length}</strong> following
</button>
<button on:click={showFollowers}>
<strong>{$followersCount}</strong> followers
</button>
</div>
{/if}
</div>
</div>
<Tabs tabs={['notes', 'likes', 'relays']} {activeTab} {setActiveTab} />
<Tabs tabs={["notes", "likes", "relays"]} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'}
<Notes {pubkey} />
{:else if activeTab === 'likes'}
<Likes {pubkey} />
{:else if activeTab === 'relays'}
{#if activeTab === "notes"}
<Notes {pubkey} />
{:else if activeTab === "likes"}
<Likes {pubkey} />
{:else if activeTab === "relays"}
{#if person?.relays}
<Relays person={person} />
<Relays {person} />
{:else if loading}
<Spinner />
<Spinner />
{:else}
<Content size="lg" class="text-center">
Unable to show network for this person.
</Content>
<Content size="lg" class="text-center">Unable to show network for this person.</Content>
{/if}
{/if}
</Content>

View File

@ -1,14 +1,14 @@
<script lang="ts">
import {find, propEq} from 'ramda'
import {onMount} from 'svelte'
import {find, propEq} from "ramda"
import {onMount} from "svelte"
import {poll, stringToColor} from "src/util/misc"
import {displayRelay} from "src/util/nostr"
import {between} from 'hurdak/lib/hurdak'
import {between} from "hurdak/lib/hurdak"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/views/feed/Feed.svelte"
import database from 'src/agent/database'
import pool from 'src/agent/pool'
import database from "src/agent/database"
import pool from "src/agent/pool"
import user from "src/agent/user"
export let url
@ -22,14 +22,14 @@
const {relays} = user
$: joined = find(propEq('url', relay.url), $relays)
$: joined = find(propEq("url", relay.url), $relays)
onMount(() => {
return poll(10_000, async () => {
const conn = await pool.getConnection(relay.url)
if (conn) {
[quality, message] = conn.getQuality()
;[quality, message] = conn.getQuality()
} else {
quality = null
message = "Not connected"
@ -41,57 +41,58 @@
</script>
<Content>
<div class="flex gap-2 items-center justify-between">
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
<span
class="border-b border-solid"
style={`border-color: ${stringToColor(relay.url)}`}>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("wss") ? "fa fa-lock" : "fa fa-unlock"} />
<span class="border-b border-solid" style={`border-color: ${stringToColor(relay.url)}`}>
{displayRelay(relay)}
</span>
<span
on:mouseout={() => {showStatus = false}}
on:mouseover={() => {showStatus = true}}
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
class:bg-medium={message === 'Not connected'}
class:bg-danger={quality <= 0.3 && message !== 'Not connected'}
on:mouseout={() => {
showStatus = false
}}
on:mouseover={() => {
showStatus = true
}}
class="h-2 w-2 cursor-pointer rounded-full bg-medium"
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>
class:bg-success={quality > 0.7} />
<p
class="text-light text-sm transition-all hidden sm:block"
class="hidden text-sm text-light transition-all sm:block"
class:opacity-0={!showStatus}
class:opacity-1={showStatus}>
{message}
</p>
</div>
<div class="whitespace-nowrap flex gap-3 items-center flex-wrap">
<div class="flex flex-wrap items-center gap-3 whitespace-nowrap">
{#if relay.contact}
<Anchor type="button-circle" href={`mailto:${relay.contact}`}>
<i class="fa fa-envelope" />
</Anchor>
<Anchor type="button-circle" href={`mailto:${relay.contact}`}>
<i class="fa fa-envelope" />
</Anchor>
{/if}
{#if joined}
{#if $relays.length > 1}
<Anchor
type="button"
class="flex gap-2 items-center rounded-full"
on:click={() => user.removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</Anchor>
{/if}
{#if $relays.length > 1}
<Anchor
type="button"
class="flex items-center gap-2 rounded-full"
on:click={() => user.removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</Anchor>
{/if}
{:else}
<Anchor
type="button"
class="flex gap-2 items-center rounded-full"
on:click={() => user.addRelay(relay.url)}>
<i class="fa fa-right-to-bracket" /> Join
</Anchor>
<Anchor
type="button"
class="flex items-center gap-2 rounded-full"
on:click={() => user.addRelay(relay.url)}>
<i class="fa fa-right-to-bracket" /> Join
</Anchor>
{/if}
</div>
</div>
{#if relay.description}
<p>{relay.description}</p>
<p>{relay.description}</p>
{/if}
</Content>
<div class="border-b border-solid border-medium" />

View File

@ -1,5 +1,5 @@
<script>
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import RelaySearch from "src/views/relays/RelaySearch.svelte"
@ -9,29 +9,29 @@
const {relays} = user
document.title = 'Relays'
document.title = "Relays"
</script>
<div in:fly={{y: 20}}>
<Content>
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your relays</h2>
</div>
<Anchor type="button-accent" on:click={() => modal.set({type: 'relay/add'})}>
<Anchor type="button-accent" on:click={() => modal.set({type: "relay/add"})}>
<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.
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 mt-8 flex gap-2 justify-center items-center">
<i class="fa fa-triangle-exclamation" />
No relays connected
</div>
<div class="mt-8 flex items-center justify-center gap-2 text-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)}
@ -39,14 +39,14 @@
{/each}
</div>
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
<div class="pt-2 mb-2 border-b border-solid border-medium" />
<div class="flex gap-2 items-center">
<div class="mb-2 border-b border-solid border-medium pt-2" />
<div class="flex items-center gap-2">
<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.
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>
<RelaySearch />
</div>

View File

@ -1,23 +1,26 @@
<script lang="ts">
import QrScanner from 'qr-scanner'
import {onDestroy} from 'svelte'
import {navigate} from 'svelte-routing'
import {find} from 'ramda'
import {nip05, nip19} from 'nostr-tools'
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import QrScanner from "qr-scanner"
import {onDestroy} from "svelte"
import {navigate} from "svelte-routing"
import {find} from "ramda"
import {nip05, nip19} from "nostr-tools"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Content from "src/partials/Content.svelte"
import {toast} from "src/app/ui"
let video, value, scanner, status = ''
let video,
value,
scanner,
status = ""
const onDecode = result => {
handleInput(result.data)
}
const handleInput = async input => {
input = input.replace('nostr:', '')
input = input.replace("nostr:", "")
if (find(s => input.startsWith(s), ["note1", "npub1", "nevent1", "nprofile1"])) {
navigate("/" + input)
@ -39,7 +42,7 @@
}
const showVideo = async () => {
status = 'loading'
status = "loading"
scanner = new QrScanner(video, onDecode, {
returnDetailedScanResult: true,
@ -47,7 +50,7 @@
await scanner.start()
status = 'ready'
status = "ready"
}
onDestroy(async () => {
@ -60,7 +63,7 @@
<Content>
<form class="flex gap-2" on:submit|preventDefault={() => handleInput(value)}>
<Input placeholder="nprofile..." bind:value={value} wrapperClass="flex-grow" />
<Input placeholder="nprofile..." bind:value wrapperClass="flex-grow" />
<Anchor type="button" on:click={() => handleInput(value)}>
<i class="fa fa-arrow-right" />
</Anchor>
@ -72,10 +75,8 @@
Enter any nostr identifier (npub, nevent, nprofile, note or user@domain.tld), or click on the
camera icon to scan with your device's camera instead.
</div>
{#if status === 'loading'}
<Spinner>
Loading your camera...
</Spinner>
{#if status === "loading"}
<Spinner>Loading your camera...</Spinner>
{/if}
<video bind:this={video} />
</Content>

View File

@ -1,6 +1,6 @@
<script>
import Content from 'src/partials/Content.svelte'
import PersonSearch from 'src/views/person/PersonSearch.svelte'
import Content from "src/partials/Content.svelte"
import PersonSearch from "src/views/person/PersonSearch.svelte"
</script>
<Content>

View File

@ -1,4 +1,4 @@
import {writable} from 'svelte/store'
import {writable} from "svelte/store"
export const logs = writable([])
@ -7,6 +7,6 @@ const logAndAppend = (level, ...message) => {
console[level](...message)
}
export const log = (...message) => logAndAppend('log', ...message)
export const warn = (...message) => logAndAppend('warn', ...message)
export const error = (...message) => logAndAppend('error', ...message)
export const log = (...message) => logAndAppend("log", ...message)
export const warn = (...message) => logAndAppend("warn", ...message)
export const error = (...message) => logAndAppend("error", ...message)

View File

@ -6,8 +6,8 @@
import RelaySearch from "src/views/relays/RelaySearch.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
import PersonSearch from "src/views/person/PersonSearch.svelte"
import database from 'src/agent/database'
import user from 'src/agent/user'
import database from "src/agent/database"
import user from "src/agent/user"
export let enforceRelays = true
export let enforcePeople = true
@ -16,82 +16,77 @@
const needsRelays = () => $relays.length === 0 && enforceRelays
const needsPeople = () => $petnamePubkeys.length === 0 && enforcePeople
let modal = needsRelays() ? 'relays' : (needsPeople() ? 'people' : null)
let modal = needsRelays() ? "relays" : needsPeople() ? "people" : null
const closeModal = () => {
modal = null
}
</script>
{#if modal === 'relays'}
<Modal onEscape={closeModal}>
<Content>
{#if $relays.length > 0}
<h1 class="text-2xl">Your Relays</h1>
{/if}
{#each $relays as relay (relay.url)}
<RelayCard showControls {relay} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
{#if modal === "relays"}
<Modal onEscape={closeModal}>
<Content>
{#if $relays.length > 0}
<h1 class="text-2xl">Your Relays</h1>
{/if}
{#each $relays as relay (relay.url)}
<RelayCard showControls {relay} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>Search below to find one to join.</div>
</div>
{/each}
<RelaySearch />
</Content>
</Modal>
{:else if needsRelays()}
<Content size="lg">
<div class="mt-12 flex flex-col items-center gap-4">
<div class="flex items-center gap-2 text-xl">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Search below to find one to join.
Click <Anchor href="/relays">here</Anchor> to find one to join.
</div>
</div>
{/each}
<RelaySearch />
</Content>
</Modal>
{:else if needsRelays()}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Click <Anchor href="/relays">here</Anchor> to find one to join.
</div>
</div>
</Content>
{:else if modal === 'people'}
<Modal onEscape={closeModal}>
<Content>
{#if $petnamePubkeys.length > 0}
<h1 class="text-2xl">Your Follows</h1>
{/if}
{#each $petnamePubkeys as pubkey (pubkey)}
<PersonInfo person={database.getPersonWithFallback(pubkey)} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
{:else if modal === "people"}
<Modal onEscape={closeModal}>
<Content>
{#if $petnamePubkeys.length > 0}
<h1 class="text-2xl">Your Follows</h1>
{/if}
{#each $petnamePubkeys as pubkey (pubkey)}
<PersonInfo person={database.getPersonWithFallback(pubkey)} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>Search below to find some interesting people.</div>
</div>
{/each}
<PersonSearch hideFollowing />
</Content>
</Modal>
{:else if needsPeople()}
<Content size="lg">
<div class="mt-12 flex flex-col items-center gap-4">
<div class="flex items-center gap-2 text-xl">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Search below to find some interesting people.
Click <Anchor href="/search/people">here</Anchor> to find some interesting people.
</div>
</div>
{/each}
<PersonSearch hideFollowing />
</Content>
</Modal>
{:else if needsPeople()}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Click <Anchor href="/search/people">here</Anchor> to find some
interesting people.
</div>
</div>
</Content>
{:else}
<slot />
<slot />
{/if}

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import Input from "src/partials/Input.svelte"
import ImageInput from "src/partials/ImageInput.svelte"
@ -10,16 +10,17 @@
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
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 {routes} from "src/app/ui"
import {publishWithToast} from 'src/app'
import {publishWithToast} from "src/app"
let values = user.getProfile().kind0 || {}
const nip05Url = "https://github.com/nostr-protocol/nips/blob/master/05.md"
const lud16Url = "https://blog.getalby.com/create-your-lightning-address/"
const pseudUrl = "https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
const pseudUrl =
"https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
onMount(async () => {
if (!user.getProfile()) {
@ -30,7 +31,7 @@
const submit = async event => {
event?.preventDefault()
publishWithToast(getUserWriteRelays(), cmd.updateUser(values))
navigate(routes.person(user.getPubkey(), 'profile'))
navigate(routes.person(user.getPubkey(), "profile"))
}
document.title = "Profile"
@ -38,7 +39,7 @@
<form on:submit={submit} in:fly={{y: 20}}>
<Content>
<div class="flex justify-center items-center flex-col mb-4">
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>About You</Heading>
<p>
Give people a friendly way to recognize you. We recommend you do not use your real name or
@ -46,7 +47,7 @@
<Anchor external href={pseudUrl}>pseudonymous</Anchor>.
</p>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Username</strong>
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.name}>
@ -63,8 +64,7 @@
<i slot="before" class="fa-solid fa-user-check" />
</Input>
<p class="text-sm text-light">
Enter a <Anchor external href={nip05Url}>NIP-05</Anchor> address to verify
your public key.
Enter a <Anchor external href={nip05Url}>NIP-05</Anchor> address to verify your public key.
</p>
</div>
<div class="flex flex-col gap-1">
@ -73,8 +73,8 @@
<i slot="before" class="fa-solid fa-bolt" />
</Input>
<p class="text-sm text-light">
Enter a <Anchor external href={lud16Url}>LUD-16</Anchor> address to enable
sending and receiving lightning tips (LUD-06 will also work).
Enter a <Anchor external href={lud16Url}>LUD-16</Anchor> address to enable sending and receiving
lightning tips (LUD-06 will also work).
</p>
</div>
<div class="flex flex-col gap-1">
@ -86,10 +86,12 @@
</div>
<div class="flex flex-col gap-1">
<strong>Profile Picture</strong>
<ImageInput bind:value={values.picture} icon="image-portrait" maxWidth={480} maxHeight={480} />
<p class="text-sm text-light">
Please be mindful of others and only use small images.
</p>
<ImageInput
bind:value={values.picture}
icon="image-portrait"
maxWidth={480}
maxHeight={480} />
<p class="text-sm text-light">Please be mindful of others and only use small images.</p>
</div>
<div class="flex flex-col gap-1">
<strong>Profile Banner</strong>

View File

@ -1,13 +1,13 @@
<script>
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import Toggle from "src/partials/Toggle.svelte"
import Input from "src/partials/Input.svelte"
import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import user from 'src/agent/user'
import user from "src/agent/user"
import {toast} from "src/app/ui"
let values = {...user.getSettings()}
@ -31,25 +31,23 @@
<form on:submit={submit} in:fly={{y: 20}}>
<Content>
<div class="flex justify-center items-center flex-col mb-4">
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>App Settings</Heading>
<p>
Tweak Coracle to work the way you want it to.
</p>
<p>Tweak Coracle to work the way you want it to.</p>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<strong>Show images and link previews</strong>
<Toggle bind:value={values.showMedia} />
</div>
<p class="text-sm text-light">
If enabled, coracle will automatically retrieve a link preview for the last link
in any note.
If enabled, coracle will automatically retrieve a link preview for the last link in any
note.
</p>
</div>
<div class="flex flex-col gap-1">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<strong>Default zap amount</strong>
<Input bind:value={values.defaultZap} />
</div>
@ -63,9 +61,9 @@
<div>{values.relayLimit} relays</div>
</div>
<Input type="range" bind:value={values.relayLimit} min={1} max={50} />
<p class="text-sm text-light mt-2">
This controls how many relays to max out at when loading feeds and event context.
More is faster, but will require more bandwidth and processing power.
<p class="mt-2 text-sm text-light">
This controls how many relays to max out at when loading feeds and event context. More is
faster, but will require more bandwidth and processing power.
</p>
</div>
<div class="flex flex-col gap-1">
@ -74,18 +72,18 @@
<i slot="before" class="fa-solid fa-server" />
</Input>
<p class="text-sm text-light">
Enter a custom url for Coracle's helper application. Dufflepud is used for
hosting images and loading link previews.
Enter a custom url for Coracle's helper application. Dufflepud is used for hosting images
and loading link previews.
</p>
</div>
<div class="flex flex-col gap-1">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<strong>Report errors and analytics</strong>
<Toggle bind:value={values.reportAnalytics} />
</div>
<p class="text-sm text-light">
Keep this enabled if you would like the Coracle developers to be able to
know what features are used, and to diagnose and fix bugs.
Keep this enabled if you would like the Coracle developers to be able to know what
features are used, and to diagnose and fix bugs.
</p>
</div>
<Button type="submit" class="text-center">Save</Button>

View File

@ -1,9 +1,9 @@
<script lang="ts">
import {displayPerson} from 'src/util/nostr'
import user from 'src/agent/user'
import {menuIsOpen, installPrompt, routes} from 'src/app/ui'
import {newAlerts, newDirectMessages, newChatMessages} from 'src/app/alerts'
import {slowConnections} from 'src/app/connection'
import {displayPerson} from "src/util/nostr"
import user from "src/agent/user"
import {menuIsOpen, installPrompt, routes} from "src/app/ui"
import {newAlerts, newDirectMessages, newChatMessages} from "src/app/alerts"
import {slowConnections} from "src/app/connection"
const {profile} = user
@ -23,106 +23,102 @@
</script>
<ul
class="mt-16 pt-4 pb-20 lg:mt-0 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
border-r border-medium text-white overflow-hidden z-20 lg:ml-0"
class:-ml-56={!$menuIsOpen}
>
class="fixed top-0 bottom-0 left-0 z-20 mt-16 w-56 overflow-hidden border-r border-medium bg-dark pt-4
pb-20 text-white shadow-xl transition-all lg:mt-0 lg:ml-0"
class:-ml-56={!$menuIsOpen}>
{#if $profile}
<li>
<a href={routes.person($profile.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({$profile.kind0?.picture})" />
<span class="text-lg font-bold">{displayPerson($profile)}</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 fa-bell mr-2" /> Notifications
{#if $newAlerts}
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
{/if}
</a>
</li>
<li>
<a href={routes.person($profile.pubkey)} class="flex items-center gap-2 px-4 py-2 pb-6">
<div
class="h-6 w-6 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({$profile.kind0?.picture})" />
<span class="text-lg font-bold">{displayPerson($profile)}</span>
</a>
</li>
<li class="relative cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/alerts">
<i class="fa fa-bell mr-2" /> Notifications
{#if $newAlerts}
<div class="absolute top-3 left-6 h-2 w-2 rounded bg-accent" />
{/if}
</a>
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/follows">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/notes/follows">
<i class="fa fa-rss mr-2" /> Feed
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/search">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/search">
<i class="fa fa-search mr-2" /> Search
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/scan">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/scan">
<i class="fa fa-qrcode mr-2" /> Scan
</a>
</li>
{#if $profile}
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/messages">
<i class="fa fa-envelope mr-2" /> Messages
{#if $newDirectMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-7" />
{/if}
</a>
</li>
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
<i class="fa fa-comment mr-2" /> Chat
{#if $newChatMessages}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-7" />
{/if}
</a>
</li>
<li class="relative cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/messages">
<i class="fa fa-envelope mr-2" /> Messages
{#if $newDirectMessages}
<div class="absolute top-2 left-7 h-2 w-2 rounded bg-accent" />
{/if}
</a>
</li>
<li class="relative cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/chat">
<i class="fa fa-comment mr-2" /> Chat
{#if $newChatMessages}
<div class="absolute top-2 left-7 h-2 w-2 rounded bg-accent" />
{/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">
<li class="mx-3 my-4 h-px bg-medium" />
<li class="relative cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/relays">
<i class="fa fa-server mr-2" /> Relays
{#if $slowConnections.length > 0}
<div class="w-2 h-2 rounded bg-accent absolute top-2 left-8" />
<div class="absolute top-2 left-8 h-2 w-2 rounded bg-accent" />
{/if}
</a>
</li>
{#if $profile}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/keys">
<i class="fa 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 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 fa-right-from-bracket mr-2" /> Logout
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/keys">
<i class="fa fa-key mr-2" /> Keys
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/settings">
<i class="fa fa-gear mr-2" /> Settings
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/logout">
<i class="fa 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 fa-right-to-bracket mr-2" /> Login
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/login">
<i class="fa 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 fa-bug mr-2" /> Debug
</a>
</li>
{#if import.meta.env.VITE_SHOW_DEBUG_ROUTE === "true"}
<li class="cursor-pointer">
<a class="block px-4 py-2 transition-all hover:bg-accent" href="/debug">
<i class="fa fa-bug mr-2" /> Debug
</a>
</li>
{/if}
{#if $installPrompt}
<li
class="cursor-pointer px-4 py-2 hover:bg-accent transition-all"
on:click={install}>
<i class="fa fa-rocket mr-2" /> Install
</li>
<li class="cursor-pointer px-4 py-2 transition-all hover:bg-accent" on:click={install}>
<i class="fa fa-rocket mr-2" /> Install
</li>
{/if}
</ul>

View File

@ -1,38 +1,38 @@
<script lang="ts">
import cx from 'classnames'
import {is} from 'ramda'
import {fly} from 'svelte/transition'
import cx from "classnames"
import {is} from "ramda"
import {fly} from "svelte/transition"
import {toast} from "src/app/ui"
</script>
{#if $toast}
{#key 'key'}
<div
class="fixed top-0 left-0 right-0 z-30 pointer-events-none flex justify-center"
transition:fly={{y: -50, duration: 300}}>
<div
class={cx(
"rounded shadow-xl m-2 ml-16 sm:ml-2 p-3 text-center border pointer-events-auto",
"max-w-xl flex-grow transition-all",
{
'bg-dark border-medium text-white': $toast.type === 'info',
'bg-dark border-warning text-white': $toast.type === 'warning',
'bg-dark border-danger text-white': $toast.type === 'error',
}
)}>
{#if is(String, $toast.message)}
{$toast.message}
{:else}
<div>
{$toast.message.text}
{#if $toast.message.link}
<a class="ml-1 underline" href={$toast.message.link.href}>
{$toast.message.link.text}
</a>
{/if}
{#key "key"}
<div
class="pointer-events-none fixed top-0 left-0 right-0 z-30 flex justify-center"
transition:fly={{y: -50, duration: 300}}>
<div
class={cx(
"pointer-events-auto m-2 ml-16 rounded border p-3 text-center shadow-xl sm:ml-2",
"max-w-xl flex-grow transition-all",
{
"border-medium bg-dark text-white": $toast.type === "info",
"border-warning bg-dark text-white": $toast.type === "warning",
"border-danger bg-dark text-white": $toast.type === "error",
}
)}>
{#if is(String, $toast.message)}
{$toast.message}
{:else}
<div>
{$toast.message.text}
{#if $toast.message.link}
<a class="ml-1 underline" href={$toast.message.link.href}>
{$toast.message.link.text}
</a>
{/if}
</div>
{/if}
</div>
</div>
{/if}
</div>
</div>
{/key}
{/key}
{/if}

View File

@ -1,14 +1,14 @@
<script lang="ts">
import {onMount} from 'svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {menuIsOpen} from 'src/app/ui'
import {onMount} from "svelte"
import Anchor from "src/partials/Anchor.svelte"
import {menuIsOpen} from "src/app/ui"
import {newAlerts} from "src/app/alerts"
const toggleMenu = () => menuIsOpen.update(x => !x)
onMount(() => {
document.querySelector("html").addEventListener("click", e => {
if (!(e.target as any).matches('.fa-bars')) {
if (!(e.target as any).matches(".fa-bars")) {
menuIsOpen.set(false)
}
})
@ -16,15 +16,18 @@
</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 h-16"
>
<div 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">
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b
border-medium bg-dark p-4 text-white">
<div class="fa fa-bars fa-2xl cursor-pointer lg:hidden" 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/logo.png" class="w-8" />
<h1 class="staatliches text-3xl">Coracle</h1>
</Anchor>
{#if $newAlerts}
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12 lg:hidden" />
<div class="absolute top-4 left-12 h-2 w-2 rounded bg-accent lg:hidden" />
{/if}
</div>

View File

@ -1,11 +1,11 @@
<script>
import {fly} from 'svelte/transition'
import {ellipsize, quantify, switcher} from 'hurdak/lib/hurdak'
import {fly} from "svelte/transition"
import {ellipsize, quantify, switcher} from "hurdak/lib/hurdak"
import Badge from "src/partials/Badge.svelte"
import {formatTimestamp} from 'src/util/misc'
import {killEvent} from 'src/util/html'
import database from 'src/agent/database'
import {modal} from 'src/app/ui'
import {formatTimestamp} from "src/util/misc"
import {killEvent} from "src/util/html"
import database from "src/agent/database"
import {modal} from "src/app/ui"
export let note
export let type
@ -17,9 +17,9 @@
})
const actionText = switcher(type, {
replies: 'replied to your note',
likes: 'liked your note',
zaps: 'zapped your note',
replies: "replied to your note",
likes: "liked your note",
zaps: "zapped your note",
})
let isOpen = false
@ -38,24 +38,25 @@
</script>
<button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all w-full
border border-solid border-black hover:border-medium hover:bg-dark text-left"
on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative w-full">
class="flex w-full cursor-pointer flex-col gap-2 border border-solid border-black py-2
px-3 text-left text-white transition-all hover:border-medium hover:bg-dark"
on:click={() => modal.set({type: "note/detail", note})}>
<div class="relative flex w-full items-center justify-between gap-2">
<button class="cursor-pointer" on:click={openPopover}>
{quantify(pubkeys.length, 'person', 'people')} {actionText}.
{quantify(pubkeys.length, "person", "people")}
{actionText}.
</button>
{#if isOpen}
<button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
<button
on:click={killEvent}
in:fly={{y: 20}}
class="absolute top-0 mt-8 py-2 px-4 rounded border border-solid border-medium
bg-dark grid grid-cols-2 gap-y-2 gap-x-4 z-20">
{#each pubkeys as pubkey}
<Badge person={database.getPersonWithFallback(pubkey)} />
{/each}
</button>
<button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
<button
on:click={killEvent}
in:fly={{y: 20}}
class="absolute top-0 z-20 mt-8 grid grid-cols-2 gap-y-2 gap-x-4 rounded
border border-solid border-medium bg-dark py-2 px-4">
{#each pubkeys as pubkey}
<Badge person={database.getPersonWithFallback(pubkey)} />
{/each}
</button>
{/if}
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import {ellipsize} from 'hurdak/lib/hurdak'
import {formatTimestamp} from 'src/util/misc'
import {displayPerson} from 'src/util/nostr'
import {ellipsize} from "hurdak/lib/hurdak"
import {formatTimestamp} from "src/util/misc"
import {displayPerson} from "src/util/nostr"
import ImageCircle from "src/partials/ImageCircle.svelte"
import Popover from "src/partials/Popover.svelte"
import PersonSummary from "src/views/person/PersonSummary.svelte"
import database from 'src/agent/database'
import {modal} from 'src/app/ui'
import database from "src/agent/database"
import {modal} from "src/app/ui"
export let note
@ -14,11 +14,11 @@
</script>
<button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all w-full
border border-solid border-black hover:border-medium hover:bg-dark text-left"
on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative w-full">
<div class="flex gap-2 items-center">
class="flex w-full cursor-pointer flex-col gap-2 border border-solid border-black py-2
px-3 text-left text-white transition-all hover:border-medium hover:bg-dark"
on:click={() => modal.set({type: "note/detail", note})}>
<div class="relative flex w-full items-center justify-between gap-2">
<div class="flex items-center gap-2">
<ImageCircle src={person.kind0?.picture} />
<div on:click|stopPropagation>
<Popover class="inline-block">

View File

@ -1,29 +1,29 @@
<script lang="ts">
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import {error} from "src/util/logger"
import {stripExifData} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Button from "src/partials/Button.svelte"
import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import {getUserWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import cmd from "src/agent/cmd"
import {toast, modal} from "src/app/ui"
import {publishWithToast} from 'src/app'
import {publishWithToast} from "src/app"
export let room = {name: null, id: null, about: null, picture: null}
onMount(async () => {
document.querySelector('[name=picture]').addEventListener('change', async e => {
document.querySelector("[name=picture]").addEventListener("change", async e => {
const target = e.target as HTMLInputElement
const [file] = target.files
if (file) {
const reader = new FileReader()
reader.onerror = error
reader.onload = () => room.picture = reader.result
reader.onload = () => (room.picture = reader.result)
reader.readAsDataURL(await stripExifData(file))
} else {
room.picture = null
@ -55,18 +55,16 @@
<form on:submit={submit} class="flex justify-center py-12" in:fly={{y: 20}}>
<Content>
<div class="flex justify-center items-center flex-col mb-4">
<div class="mb-4 flex flex-col items-center justify-center">
<h1 class="staatliches text-6xl">Name your room</h1>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Room name</strong>
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={room.name}>
<i slot="before" class="fa-solid fa-tag" />
</Input>
<p class="text-sm text-light">
The room's name can be changed anytime.
</p>
<p class="text-sm text-light">The room's name can be changed anytime.</p>
</div>
<div class="flex flex-col gap-1">
<strong>Room information</strong>
@ -78,12 +76,9 @@
<div class="flex flex-col gap-1">
<strong>Picture</strong>
<input type="file" name="picture" />
<p class="text-sm text-light">
A picture to help people remember your room.
</p>
<p class="text-sm text-light">A picture to help people remember your room.</p>
</div>
<Button type="submit" class="text-center">Done</Button>
</div>
</Content>
</form>

View File

@ -1,10 +1,10 @@
<script>
import {nip19} from 'nostr-tools'
import {navigate} from 'svelte-routing'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import Anchor from 'src/partials/Anchor.svelte'
import database from 'src/agent/database'
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak"
import Anchor from "src/partials/Anchor.svelte"
import database from "src/agent/database"
export let room
@ -14,34 +14,46 @@
</script>
<button
class="flex gap-4 px-4 py-6 cursor-pointer hover:bg-medium transition-all rounded border border-solid border-medium bg-dark"
class="flex cursor-pointer gap-4 rounded border border-solid border-medium bg-dark px-4 py-6 transition-all hover:bg-medium"
on:click={enter}
in:fly={{y: 20}}>
<div
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-14 w-14 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({room.picture})" />
<div class="flex flex-grow flex-col justify-start gap-2 min-w-0">
<div class="flex min-w-0 flex-grow flex-col justify-start gap-2">
<div class="flex flex-grow items-start justify-between gap-2">
<div class="flex gap-2 items-center overflow-hidden">
<div class="flex items-center gap-2 overflow-hidden">
<i class="fa fa-lock-open text-light" />
<h2 class="text-lg">{room.name || ''}</h2>
<h2 class="text-lg">{room.name || ""}</h2>
</div>
{#if room.joined}
<Anchor type="button" class="flex items-center gap-2" on:click={e => { e.stopPropagation(); leave() }}>
<i class="fa fa-right-from-bracket" />
<span>Leave</span>
</Anchor>
<Anchor
type="button"
class="flex items-center gap-2"
on:click={e => {
e.stopPropagation()
leave()
}}>
<i class="fa fa-right-from-bracket" />
<span>Leave</span>
</Anchor>
{:else}
<Anchor type="button" class="flex items-center gap-2" on:click={e => { e.stopPropagation(); join() }}>
<i class="fa fa-right-to-bracket" />
<span>Join</span>
</Anchor>
<Anchor
type="button"
class="flex items-center gap-2"
on:click={e => {
e.stopPropagation()
join()
}}>
<i class="fa fa-right-to-bracket" />
<span>Join</span>
</Anchor>
{/if}
</div>
{#if room.about}
<p class="text-light text-start">
{ellipsize(room.about, 300)}
</p>
<p class="text-start text-light">
{ellipsize(room.about, 300)}
</p>
{/if}
</div>
</button>

View File

@ -1,15 +1,15 @@
<script lang="ts">
import {onMount} from 'svelte'
import {partition, propEq, uniqBy, sortBy, prop} from 'ramda'
import {slide} from 'svelte/transition'
import {quantify} from 'hurdak/lib/hurdak'
import {createScroller, now, Cursor} from 'src/util/misc'
import {asDisplayEvent, mergeFilter} from 'src/util/nostr'
import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte'
import {onMount} from "svelte"
import {partition, propEq, uniqBy, sortBy, prop} from "ramda"
import {slide} from "svelte/transition"
import {quantify} from "hurdak/lib/hurdak"
import {createScroller, now, Cursor} from "src/util/misc"
import {asDisplayEvent, mergeFilter} from "src/util/nostr"
import Spinner from "src/partials/Spinner.svelte"
import Content from "src/partials/Content.svelte"
import Note from "src/views/notes/Note.svelte"
import user from 'src/agent/user'
import network from 'src/agent/network'
import user from "src/agent/user"
import network from "src/agent/network"
import {modal} from "src/app/ui"
import {mergeParents} from "src/app"
@ -36,9 +36,9 @@
// Load parents before showing the notes so we have hierarchy. Give it a short
// timeout, since this is really just a nice-to-have
const combined = uniqBy(
prop('id'),
prop("id"),
newNotes
.filter(propEq('kind', 1))
.filter(propEq("kind", 1))
.concat(await network.loadParents(newNotes, {timeout: parentsTimeout}))
.map(asDisplayEvent)
)
@ -61,15 +61,11 @@
const loadBufferedNotes = () => {
// Drop notes at the end if there are a lot
notes = uniqBy(
prop('id'),
notesBuffer.concat(notes).slice(0, maxNotes)
)
notes = uniqBy(prop("id"), notesBuffer.concat(notes).slice(0, maxNotes))
notesBuffer = []
}
const onChunk = async newNotes => {
const chunk = sortBy(e => -e.created_at, await processNewNotes(newNotes))
const [bottom, top] = partition(e => e.created_at < since, chunk)
@ -79,21 +75,29 @@
}
// Slice new notes in case someone leaves the tab open for a long time
notes = uniqBy(prop('id'), notes.concat(bottom))
notes = uniqBy(prop("id"), notes.concat(bottom))
notesBuffer = top.concat(notesBuffer).slice(0, maxNotes)
cursor.update(notes)
}
onMount(() => {
const sub = network.listen({relays, filter: mergeFilter(filter, {since}), onChunk})
const sub = network.listen({
relays,
filter: mergeFilter(filter, {since}),
onChunk,
})
const scroller = createScroller(() => {
if ($modal) {
return
}
return network.load({relays, filter: mergeFilter(filter, cursor.getFilter()), onChunk})
return network.load({
relays,
filter: mergeFilter(filter, cursor.getFilter()),
onChunk,
})
})
return () => {
@ -105,17 +109,17 @@
<Content size="inherit" class="pt-6">
{#if notesBuffer.length > 0}
<button
in:slide
class="cursor-pointer text-center underline text-light"
on:click={loadBufferedNotes}>
Load {quantify(notesBuffer.length, 'new note')}
</button>
<button
in:slide
class="cursor-pointer text-center text-light underline"
on:click={loadBufferedNotes}>
Load {quantify(notesBuffer.length, "new note")}
</button>
{/if}
<div class="flex flex-col gap-4">
{#each notes as note (note.id)}
<Note depth={2} {note} />
<Note depth={2} {note} />
{/each}
</div>

View File

@ -1,15 +1,18 @@
<script>
import {shuffle} from 'src/util/misc'
import {shuffle} from "src/util/misc"
import Feed from "src/views/feed/Feed.svelte"
import {getUserFollows} from 'src/agent/social'
import {sampleRelays, getAllPubkeyWriteRelays} from 'src/agent/relays'
import {getUserFollows} from "src/agent/social"
import {sampleRelays, getAllPubkeyWriteRelays} from "src/agent/relays"
const authors = shuffle(getUserFollows()).slice(0, 256)
const relays = sampleRelays(getAllPubkeyWriteRelays(authors))
// Separate notes and reactions into two queries since otherwise reactions dominate,
// we never find their parents (or reactions are mostly to a few posts), and the feed sucks
const filter = [{kinds: [1], authors}, {kinds: [7], authors}]
const filter = [
{kinds: [1], authors},
{kinds: [7], authors},
]
</script>
<Feed {relays} {filter} />

View File

@ -1,8 +1,8 @@
<script>
import {shuffle} from 'src/util/misc'
import {shuffle} from "src/util/misc"
import Feed from "src/views/feed/Feed.svelte"
import {getUserNetwork} from 'src/agent/social'
import {sampleRelays, getAllPubkeyWriteRelays} from 'src/agent/relays'
import {getUserNetwork} from "src/agent/social"
import {sampleRelays, 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.
@ -11,7 +11,10 @@
// Separate notes and reactions into two queries since otherwise reactions dominate,
// we never find their parents (or reactions are mostly to a few posts), and the feed sucks
const filter = [{kinds: [1], authors}, {kinds: [7], authors}]
const filter = [
{kinds: [1], authors},
{kinds: [7], authors},
]
</script>
<Feed {relays} {filter} />

View File

@ -1,23 +1,23 @@
<script lang="ts">
import type {Relay} from 'src/util/types'
import {isNil, find, all, last} from 'ramda'
import {onDestroy, onMount} from 'svelte'
import {navigate} from 'svelte-routing'
import {sleep, shuffle} from 'src/util/misc'
import {isRelay} from 'src/util/nostr'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Input from 'src/partials/Input.svelte'
import Heading from 'src/partials/Heading.svelte'
import RelayCardSimple from 'src/partials/RelayCardSimple.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Modal from 'src/partials/Modal.svelte'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import user from 'src/agent/user'
import {loadAppData} from 'src/app'
import {toast} from 'src/app/ui'
import type {Relay} from "src/util/types"
import {isNil, find, all, last} from "ramda"
import {onDestroy, onMount} from "svelte"
import {navigate} from "svelte-routing"
import {sleep, shuffle} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Input from "src/partials/Input.svelte"
import Heading from "src/partials/Heading.svelte"
import RelayCardSimple from "src/partials/RelayCardSimple.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Modal from "src/partials/Modal.svelte"
import {getUserReadRelays} from "src/agent/relays"
import database from "src/agent/database"
import network from "src/agent/network"
import user from "src/agent/user"
import {loadAppData} from "src/app"
import {toast} from "src/app/ui"
let modal = null
let customRelayUrl = null
@ -25,7 +25,7 @@
let currentRelays = {} as Record<number, Relay>
let attemptedRelays = new Set()
let customRelays = []
let knownRelays = database.watch('relays', table => shuffle(table.all()))
let knownRelays = database.watch("relays", table => shuffle(table.all()))
let allRelays = []
$: allRelays = $knownRelays.concat(customRelays)
@ -49,38 +49,34 @@
attemptedRelays.add(relay.url)
currentRelays[i] = relay
network.loadPeople([user.getPubkey()], {relays: [relay], force: true})
.then(async () => {
// Wait a bit before removing the relay to smooth out the ui
await sleep(1000)
network.loadPeople([user.getPubkey()], {relays: [relay], force: true}).then(async () => {
// Wait a bit before removing the relay to smooth out the ui
await sleep(1000)
currentRelays[i] = null
currentRelays[i] = null
if (searching && getUserReadRelays().length > 0) {
searching = false
modal = 'success'
if (searching && getUserReadRelays().length > 0) {
searching = false
modal = "success"
await Promise.all([
loadAppData(user.getPubkey()),
sleep(3000),
])
await Promise.all([loadAppData(user.getPubkey()), sleep(3000)])
navigate('/notes/follows')
}
})
navigate("/notes/follows")
}
})
}
if (all(isNil, Object.values(currentRelays)) && isNil(customRelayUrl)) {
modal = 'failure'
customRelayUrl = ''
modal = "failure"
customRelayUrl = ""
}
setTimeout(searchForRelays, 300)
}
const addCustomRelay = () => {
if (!customRelayUrl.startsWith('ws')) {
customRelayUrl = 'wss://' + last(customRelayUrl.split('://'))
if (!customRelayUrl.startsWith("ws")) {
customRelayUrl = "wss://" + last(customRelayUrl.split("://"))
}
if (!isRelay(customRelayUrl)) {
@ -104,51 +100,59 @@
<Content size="lg">
<Heading class="text-center">Connect to Nostr</Heading>
<p class="text-left">
We're searching for your profile on the network. If you'd like to select your
relays manually instead,
click <Anchor on:click={() => { customRelayUrl = ''; modal = 'custom' }}>here</Anchor>.
We're searching for your profile on the network. If you'd like to select your relays manually
instead, click <Anchor
on:click={() => {
customRelayUrl = ""
modal = "custom"
}}>here</Anchor
>.
</p>
{#if Object.values(currentRelays).length > 0}
<p>Currently searching:</p>
{#each Object.values(currentRelays) as relay}
<div class="h-12">
{#if relay}
<RelayCardSimple relay={{...relay, description: null}} />
{/if}
</div>
{/each}
<p>Currently searching:</p>
{#each Object.values(currentRelays) as relay}
<div class="h-12">
{#if relay}
<RelayCardSimple relay={{...relay, description: null}} />
{/if}
</div>
{/each}
{/if}
</Content>
{#if modal}
<Modal onEscape={modal === 'success' ? null : () => { modal = null }}>
<Content>
{#if modal === 'success'}
<div class="text-center my-12">Success! Just a moment while we get things set up.</div>
<Spinner delay={0} />
{:else if modal === 'failure'}
<div class="text-center my-12">
We didn't have any luck finding your profile data - you'll need to select your
relays manually to continue. You can skip this step by clicking
<Anchor href="/relays">here</Anchor>, but be aware that any new relay selections
will replace your old ones.
</div>
{:else if modal === 'custom'}
<div class="text-center my-12">
Enter the url of a relay you've used in the past to store your profile
and we'll check there.
</div>
{/if}
<Modal
onEscape={modal === "success"
? null
: () => {
modal = null
}}>
<Content>
{#if modal === "success"}
<div class="my-12 text-center">Success! Just a moment while we get things set up.</div>
<Spinner delay={0} />
{:else if modal === "failure"}
<div class="my-12 text-center">
We didn't have any luck finding your profile data - you'll need to select your relays
manually to continue. You can skip this step by clicking
<Anchor href="/relays">here</Anchor>, but be aware that any new relay selections will
replace your old ones.
</div>
{:else if modal === "custom"}
<div class="my-12 text-center">
Enter the url of a relay you've used in the past to store your profile and we'll check
there.
</div>
{/if}
{#if ['failure', 'custom'].includes(modal)}
<form class="flex gap-2" on:submit|preventDefault={addCustomRelay}>
<Input bind:value={customRelayUrl} wrapperClass="flex-grow">
<i slot="before" class="fa fa-search" />
</Input>
<Anchor type="button" on:click={addCustomRelay}>Search relay</Anchor>
</form>
{/if}
</Content>
</Modal>
{#if ["failure", "custom"].includes(modal)}
<form class="flex gap-2" on:submit|preventDefault={addCustomRelay}>
<Input bind:value={customRelayUrl} wrapperClass="flex-grow">
<i slot="before" class="fa fa-search" />
</Input>
<Anchor type="button" on:click={addCustomRelay}>Search relay</Anchor>
</form>
{/if}
</Content>
</Modal>
{/if}

View File

@ -1,32 +1,29 @@
<script lang="ts">
import {toHex} 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 Heading from 'src/partials/Heading.svelte'
import {toHex} 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 Heading from "src/partials/Heading.svelte"
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 logIn = () => {
const privkey = nsec.startsWith('nsec') ? toHex(nsec) : nsec
const privkey = nsec.startsWith("nsec") ? toHex(nsec) : nsec
if (!privkey?.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")
} else {
login('privkey', privkey)
login("privkey", privkey)
}
}
</script>
<Content size="lg" class="text-center">
<Heading>Login with your Private Key</Heading>
<p>
To give Coracle full access to your nostr identity, enter your private key below.
</p>
<p>To give Coracle full access to your nostr identity, enter your private key below.</p>
<div class="flex gap-2">
<div class="flex-grow">
<Input type="password" bind:value={nsec} placeholder="nsec...">
@ -35,9 +32,9 @@
</div>
<Anchor type="button" on:click={logIn}>Log In</Anchor>
</div>
<p class="bg-black rounded border-2 border-solid border-warning py-3 px-6">
Note that sharing your private key directly is not recommended, instead you should use
a <Anchor href={nip07} external>compatible browser extension</Anchor> to securely store your key.
<p class="rounded border-2 border-solid border-warning bg-black py-3 px-6">
Note that sharing your private key directly is not recommended, instead you should use a <Anchor
href={nip07}
external>compatible browser extension</Anchor> to securely store your key.
</p>
</Content>

View File

@ -1,31 +1,30 @@
<script lang="ts">
import {toHex} 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 Heading from 'src/partials/Heading.svelte'
import {toHex} 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 Heading from "src/partials/Heading.svelte"
import {toast} from "src/app/ui"
import {login} from "src/app"
let npub = ''
let npub = ""
const logIn = () => {
const pubkey = npub.startsWith('npub') ? toHex(npub) : npub
const pubkey = npub.startsWith("npub") ? toHex(npub) : npub
if (!pubkey?.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid public key.")
} else {
login('pubkey', pubkey)
login("pubkey", pubkey)
}
}
</script>
<Content size="lg" class="text-center">
<Heading>Login with your Public Key</Heading>
<p>
For read-only access, enter your public key (or someone else's) below. Your
key should start with "npub".
For read-only access, enter your public key (or someone else's) below. Your key should start
with "npub".
</p>
<div class="flex gap-2">
<div class="flex-grow">
@ -36,4 +35,3 @@
<Anchor type="button" on:click={logIn}>Log In</Anchor>
</div>
</Content>

View File

@ -1,11 +1,11 @@
<script>
import {nip19} from 'nostr-tools'
import {navigate} from 'svelte-routing'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import {displayPerson} from 'src/util/nostr'
import database from 'src/agent/database'
import {lastChecked} from 'src/app/alerts'
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak"
import {displayPerson} from "src/util/nostr"
import database from "src/agent/database"
import {lastChecked} from "src/app/alerts"
export let contact
@ -15,31 +15,31 @@
</script>
<button
class="flex gap-4 px-4 py-6 cursor-pointer hover:bg-medium transition-all rounded
border border-solid border-medium bg-dark"
class="flex cursor-pointer gap-4 rounded border border-solid border-medium bg-dark
px-4 py-6 transition-all hover:bg-medium"
on:click={enter}
in:fly={{y: 20}}>
<div
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border
border-solid border-white"
class="h-14 w-14 shrink-0 overflow-hidden rounded-full border border-solid border-white
bg-cover bg-center"
style="background-image: url({person.kind0?.picture})" />
<div class="flex flex-grow flex-col justify-start gap-2 min-w-0">
<div class="flex min-w-0 flex-grow flex-col justify-start gap-2">
<div class="flex flex-grow items-start justify-between gap-2">
<div class="flex gap-2 items-center overflow-hidden">
<div class="flex items-center gap-2 overflow-hidden">
<i class="fa fa-lock text-light" />
<h2 class="text-lg">{displayPerson(person)}</h2>
</div>
<div class="relative">
<i class="fa fa-bell" class:text-light={!newMessages} />
{#if newMessages}
<div class="w-1 h-1 rounded-full bg-accent top-0 right-0 absolute mt-1" />
<div class="absolute top-0 right-0 mt-1 h-1 w-1 rounded-full bg-accent" />
{/if}
</div>
</div>
{#if person.kind0?.about}
<p class="text-light text-start">
{ellipsize(person.kind0.about, 300)}
</p>
<p class="text-start text-light">
{ellipsize(person.kind0.about, 300)}
</p>
{/if}
</div>
</button>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import user from 'src/agent/user'
import user from "src/agent/user"
import {modal} from "src/app/ui"
export let pubkey = null
@ -8,12 +8,12 @@
</script>
{#if $canPublish}
<div class="fixed bottom-0 right-0 m-8">
<button
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
items-center border border-dark shadow-2xl"
on:click={() => modal.set({type: 'note/create', pubkey})}>
<span class="fa-sold fa-plus fa-2xl" />
</button>
</div>
<div class="fixed bottom-0 right-0 m-8">
<button
class="color-white flex h-16 w-16 items-center justify-center rounded-full
border border-dark bg-accent shadow-2xl"
on:click={() => modal.set({type: "note/create", pubkey})}>
<span class="fa-sold fa-plus fa-2xl" />
</button>
</div>
{/if}

View File

@ -1,40 +1,40 @@
<script lang="ts">
import cx from 'classnames'
import {nip19} from 'nostr-tools'
import {find, sum, last, whereEq, without, uniq, pluck, reject, propEq} from 'ramda'
import {onMount} from 'svelte'
import {tweened} from 'svelte/motion'
import {slide} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {quantify} from 'hurdak/lib/hurdak'
import {Tags, findRootId, findReplyId, displayPerson, isLike} from 'src/util/nostr'
import {formatTimestamp, now, tryJson, stringToColor, formatSats, fetchJson} from 'src/util/misc'
import cx from "classnames"
import {nip19} from "nostr-tools"
import {find, sum, last, whereEq, without, uniq, pluck, reject, propEq} from "ramda"
import {onMount} from "svelte"
import {tweened} from "svelte/motion"
import {slide} from "svelte/transition"
import {navigate} from "svelte-routing"
import {quantify} from "hurdak/lib/hurdak"
import {Tags, findRootId, findReplyId, displayPerson, isLike} from "src/util/nostr"
import {formatTimestamp, now, tryJson, stringToColor, formatSats, fetchJson} from "src/util/misc"
import {extractUrls, isMobile} from "src/util/html"
import {invoiceAmount} from 'src/util/lightning'
import ImageCircle from 'src/partials/ImageCircle.svelte'
import QRCode from 'src/partials/QRCode.svelte'
import ImageInput from 'src/partials/ImageInput.svelte'
import Input from 'src/partials/Input.svelte'
import Textarea from 'src/partials/Textarea.svelte'
import Content from 'src/partials/Content.svelte'
import PersonSummary from 'src/views/person/PersonSummary.svelte'
import Popover from 'src/partials/Popover.svelte'
import RelayCard from 'src/views/relays/RelayCard.svelte'
import Modal from 'src/partials/Modal.svelte'
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {invoiceAmount} from "src/util/lightning"
import ImageCircle from "src/partials/ImageCircle.svelte"
import QRCode from "src/partials/QRCode.svelte"
import ImageInput from "src/partials/ImageInput.svelte"
import Input from "src/partials/Input.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Content from "src/partials/Content.svelte"
import PersonSummary from "src/views/person/PersonSummary.svelte"
import Popover from "src/partials/Popover.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
import Modal from "src/partials/Modal.svelte"
import Preview from "src/partials/Preview.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {toast, modal} from "src/app/ui"
import {renderNote} from "src/app"
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import user from 'src/agent/user'
import keys from 'src/agent/keys'
import network from 'src/agent/network'
import {getEventPublishRelays, getRelaysForEventParent} from 'src/agent/relays'
import database from 'src/agent/database'
import cmd from 'src/agent/cmd'
import {routes} from 'src/app/ui'
import {publishWithToast} from 'src/app'
import user from "src/agent/user"
import keys from "src/agent/keys"
import network from "src/agent/network"
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
import database from "src/agent/database"
import cmd from "src/agent/cmd"
import {routes} from "src/app/ui"
import {publishWithToast} from "src/app"
export let note
export let depth = 0
@ -60,7 +60,7 @@
const links = extractUrls(note.content)
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey))
const person = database.watch("people", () => database.getPersonWithFallback(note.pubkey))
let likes, flags, zaps, like, flag, border, childrenContainer, noteContainer, canZap
@ -71,7 +71,7 @@
const repliesCount = tweened(0, {interpolate})
$: likes = note.reactions.filter(n => isLike(n.content))
$: flags = note.reactions.filter(whereEq({content: '-'}))
$: flags = note.reactions.filter(whereEq({content: "-"}))
$: zaps = note.zaps
.map(zap => {
const zapMeta = Tags.from(zap).asMeta()
@ -83,35 +83,35 @@
}))
})
.filter(zap => {
if (!zap) {
return false
}
if (!zap) {
return false
}
// Don't count zaps that the user sent himself
if (zap.request.pubkey === $person.pubkey) {
return false
}
// Don't count zaps that the user sent himself
if (zap.request.pubkey === $person.pubkey) {
return false
}
const {invoiceAmount, request} = zap
const reqMeta = Tags.from(request).asMeta()
const {invoiceAmount, request} = zap
const reqMeta = Tags.from(request).asMeta()
// Verify that the zapper actually sent the requested amount (if it was supplied)
if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
return false
}
// Verify that the zapper actually sent the requested amount (if it was supplied)
if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
return false
}
// If the sending client provided an lnurl tag, verify that too
if (reqMeta.lnurl && reqMeta.lnurl !== $person?.lnurl) {
return false
}
// If the sending client provided an lnurl tag, verify that too
if (reqMeta.lnurl && reqMeta.lnurl !== $person?.lnurl) {
return false
}
// Verify that the zap note actually came from the recipient's zapper
if ($person.zapper?.nostrPubkey !== zap.pubkey) {
return false
}
// Verify that the zap note actually came from the recipient's zapper
if ($person.zapper?.nostrPubkey !== zap.pubkey) {
return false
}
return true
})
return true
})
$: like = find(whereEq({pubkey: $profile?.pubkey}), likes)
$: flag = find(whereEq({pubkey: $profile?.pubkey}), flags)
@ -120,42 +120,42 @@
$: $flagsCount = flags.length
$: $zapsTotal = sum(zaps.map(zap => zap.invoiceAmount)) / 1000
$: $repliesCount = note.replies.length
$: visibleNotes = note.replies.filter(r => showContext ? true : !r.isContext)
$: visibleNotes = note.replies.filter(r => (showContext ? true : !r.isContext))
$: canZap = $person?.zapper
const onClick = e => {
const target = e.target as HTMLElement
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
modal.set({type: 'note/detail', note})
if (interactive && !["I"].includes(target.tagName) && !target.closest("a")) {
modal.set({type: "note/detail", note})
}
}
const goToParent = async () => {
const relays = getRelaysForEventParent(note)
modal.set({type: 'note/detail', note: {id: findReplyId(note)}, relays})
modal.set({type: "note/detail", note: {id: findReplyId(note)}, relays})
}
const goToRoot = async () => {
const relays = getRelaysForEventParent(note)
modal.set({type: 'note/detail', note: {id: findRootId(note)}, relays})
modal.set({type: "note/detail", note: {id: findRootId(note)}, relays})
}
const react = async content => {
if (!$profile) {
return navigate('/login')
return navigate("/login")
}
const relays = getEventPublishRelays(note)
const [event] = await cmd.createReaction(note, content).publish(relays)
if (content === '+') {
if (content === "+") {
likes = likes.concat(event)
}
if (content === '-') {
if (content === "-") {
flags = flags.concat(event)
}
}
@ -163,12 +163,12 @@
const deleteReaction = e => {
cmd.deleteEvent([e.id]).publish(getEventPublishRelays(note))
if (e.content === '+') {
likes = reject(propEq('pubkey', $profile.pubkey), likes)
if (e.content === "+") {
likes = reject(propEq("pubkey", $profile.pubkey), likes)
}
if (e.content === '-') {
flags = reject(propEq('pubkey', $profile.pubkey), flags)
if (e.content === "-") {
flags = reject(propEq("pubkey", $profile.pubkey), flags)
}
}
@ -176,7 +176,7 @@
if ($profile) {
reply = reply || true
} else {
navigate('/login')
navigate("/login")
}
}
@ -190,7 +190,7 @@
}
const onReplyKeydown = e => {
if (e.key === 'Escape') {
if (e.key === "Escape") {
e.stopPropagation()
resetReply()
}
@ -200,7 +200,7 @@
let {content, mentions, topics} = reply.parse()
if (image) {
content = (content + '\n' + image).trim()
content = (content + "\n" + image).trim()
}
if (content) {
@ -215,11 +215,13 @@
toast.show("info", {
text: `Your note has been created!`,
link: {
text: 'View',
href: "/" + nip19.neventEncode({
id: event.id,
relays: pluck('url', relays.slice(0, 3)),
}),
text: "View",
href:
"/" +
nip19.neventEncode({
id: event.id,
relays: pluck("url", relays.slice(0, 3)),
}),
},
})
}
@ -231,8 +233,8 @@
const startZap = async () => {
zap = {
amount: user.getSetting('defaultZap'),
message: '',
amount: user.getSetting("defaultZap"),
message: "",
invoice: null,
loading: false,
startedAt: now(),
@ -245,18 +247,16 @@
const {zapper, lnurl} = $person
const amount = zap.amount * 1000
const relays = getEventPublishRelays(note)
const urls = pluck('url', relays)
const urls = pluck("url", relays)
const publishable = cmd.requestZap(urls, zap.message, note.pubkey, note.id, amount, lnurl)
const event = encodeURI(JSON.stringify(await keys.sign(publishable.event)))
const res = await fetchJson(
`${zapper.callback}?amount=${amount}&nostr=${event}&lnurl=${lnurl}`
)
const res = await fetchJson(`${zapper.callback}?amount=${amount}&nostr=${event}&lnurl=${lnurl}`)
zap.invoice = res.pr
zap.loading = false
// Open up alby or whatever
const {webln} = (window as {webln?: any})
const {webln} = window as {webln?: any}
if (webln) {
await webln.enable()
@ -269,7 +269,7 @@
filter: {
kinds: [9735],
authors: [zapper.nostrPubkey],
'#p': [$person.pubkey],
"#p": [$person.pubkey],
since: zap.startedAt,
},
onChunk: chunk => {
@ -299,18 +299,16 @@
if (childrenContainer && noteContainer) {
const lastChild = last(
[].slice.apply(childrenContainer.children)
.filter(e => e.matches('.note'))
[].slice.apply(childrenContainer.children).filter(e => e.matches(".note"))
)
if (lastChild) {
const height = (
66
+ getHeight(childrenContainer)
- getHeight(lastChild)
- getHeight(lastChild.nextElementSibling)
- (lastChild.nextElementSibling ? 16 : 0)
)
const height =
66 +
getHeight(childrenContainer) -
getHeight(lastChild) -
getHeight(lastChild.nextElementSibling) -
(lastChild.nextElementSibling ? 16 : 0)
border.style = `height: ${height - 21}px`
}
@ -330,207 +328,233 @@
<svelte:body on:click={onBodyClick} />
{#if $person}
<div bind:this={noteContainer} class="note relative">
<Card class="flex gap-4 relative" on:click={onClick} {interactive} {invertColors}>
{#if !showParent}
<div class={`absolute h-px -ml-4 w-4 bg-${borderColor} z-10`} style="left: 0px; top: 27px;" />
{/if}
<div>
<Anchor class="text-lg font-bold" href={routes.person($person.pubkey)}>
<ImageCircle size={10} src={$person.kind0?.picture} />
</Anchor>
</div>
<div class="flex flex-col gap-2 flex-grow min-w-0">
<div class="flex items-center justify-between">
<Popover triggerType={isMobile ? 'click' : 'mouseenter'}>
<div slot="trigger">
<Anchor
type="unstyled"
class="text-lg font-bold flex gap-2 items-center"
href={!isMobile && routes.person($person.pubkey)}>
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
<i class="fa fa-circle-check text-accent text-sm" />
{/if}
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={$person.pubkey} />
</div>
</Popover>
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on]})}
class="text-sm text-light"
type="unstyled">
{timestamp}
<div bind:this={noteContainer} class="note relative">
<Card class="relative flex gap-4" on:click={onClick} {interactive} {invertColors}>
{#if !showParent}
<div
class={`absolute -ml-4 h-px w-4 bg-${borderColor} z-10`}
style="left: 0px; top: 27px;" />
{/if}
<div>
<Anchor class="text-lg font-bold" href={routes.person($person.pubkey)}>
<ImageCircle size={10} src={$person.kind0?.picture} />
</Anchor>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
{#if findReplyId(note) && showParent}
<small class="text-light">
<i class="fa fa-code-merge" />
<Anchor on:click={goToParent}>View Parent</Anchor>
</small>
{/if}
{#if findRootId(note) && findRootId(note) !== findReplyId(note) && showParent}
<small class="text-light">
<i class="fa fa-code-pull-request" />
<Anchor on:click={goToRoot}>View Thread</Anchor>
</small>
<div class="flex min-w-0 flex-grow flex-col gap-2">
<div class="flex items-center justify-between">
<Popover triggerType={isMobile ? "click" : "mouseenter"}>
<div slot="trigger">
<Anchor
type="unstyled"
class="flex items-center gap-2 text-lg font-bold"
href={!isMobile && routes.person($person.pubkey)}>
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />
{/if}
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={$person.pubkey} />
</div>
</Popover>
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on]})}
class="text-sm text-light"
type="unstyled">
{timestamp}
</Anchor>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
{#if findReplyId(note) && showParent}
<small class="text-light">
<i class="fa fa-code-merge" />
<Anchor on:click={goToParent}>View Parent</Anchor>
</small>
{/if}
{#if findRootId(note) && findRootId(note) !== findReplyId(note) && showParent}
<small class="text-light">
<i class="fa fa-code-pull-request" />
<Anchor on:click={goToRoot}>View Thread</Anchor>
</small>
{/if}
</div>
{#if flag}
<p class="border-l-2 border-solid border-medium pl-4 text-light">
You have flagged this content as offensive.
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
</p>
{:else}
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
<p>{@html renderNote(note, {showEntire})}</p>
{#if user.getSetting("showMedia") && links.length > 0}
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview url={last(links)} />
</button>
{/if}
</div>
<div class="flex justify-between text-light">
<div class="flex">
<button class="w-16 text-left" on:click|stopPropagation={startReply}>
<i class="fa fa-reply cursor-pointer" />
{$repliesCount}
</button>
<button
class="w-16 text-left"
class:text-accent={like}
on:click|stopPropagation={() => (like ? deleteReaction(like) : react("+"))}>
<i
class={cx("fa fa-heart cursor-pointer", {
"fa-beat fa-beat-custom": like,
})} />
{$likesCount}
</button>
<button
class={cx("w-20 text-left", {
"pointer-events-none opacity-50": !canZap,
})}
class:text-accent={zapped}
on:click|stopPropagation={startZap}>
<i class="fa fa-bolt cursor-pointer" />
{formatSats($zapsTotal)}
</button>
<button class="w-16 text-left" on:click|stopPropagation={() => react("-")}>
<i class="fa fa-flag cursor-pointer" />
{$flagsCount}
</button>
</div>
<div
class="hidden cursor-pointer items-center gap-1 sm:flex"
on:click|stopPropagation={() => {
showRelays = true
}}>
<i class="fa fa-server" />
<div
class="h-1 w-1 rounded-full"
style={`background: ${stringToColor(note.seen_on)}`} />
</div>
</div>
{/if}
</div>
{#if flag}
<p class="text-light border-l-2 border-solid border-medium pl-4">
You have flagged this content as offensive.
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
</p>
{:else}
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
<p>{@html renderNote(note, {showEntire})}</p>
{#if user.getSetting('showMedia') && links.length > 0}
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview url={last(links)} />
</div>
</Card>
</div>
{#if reply}
<div
transition:slide
class={`note-reply relative z-10 border border-${borderColor} rounded border-solid`}
bind:this={replyContainer}>
<div class="bg-dark" class:rounded-b={replyMentions.length === 0} on:keydown={onReplyKeydown}>
<Compose bind:this={reply} onSubmit={sendReply}>
<button
slot="addon"
on:click={sendReply}
class="flex cursor-pointer flex-col justify-center gap-2 border-l border-solid border-dark p-4
py-8 text-white transition-all hover:bg-accent">
<i class="fa fa-paper-plane fa-xl" />
</button>
{/if}
</Compose>
</div>
{#if image}
<div class="bg-dark p-2">
<Preview
url={image}
onClose={() => {
image = null
}} />
</div>
<div class="flex justify-between text-light">
<div class="flex">
<button class="w-16 text-left" on:click|stopPropagation={startReply}>
<i class="fa fa-reply cursor-pointer" />
{$repliesCount}
</button>
<button class="w-16 text-left" class:text-accent={like}
on:click|stopPropagation={() => like ? deleteReaction(like) : react("+")}>
<i class={cx('fa fa-heart cursor-pointer', {'fa-beat fa-beat-custom': like})} />
{$likesCount}
</button>
{/if}
<div class={`h-px bg-${borderColor}`} />
<div class="h-12 rounded-b bg-black p-2 text-sm text-white">
<div class="mr-2 inline-block border-r border-solid border-medium py-2 pl-1 pr-3">
<div class="flex cursor-pointer items-center gap-3">
<ImageInput bind:value={image} icon="image" hideInput>
<i slot="button" class="fa fa-paperclip" />
</ImageInput>
<i class="fa fa-at" />
</div>
</div>
{#each replyMentions as p}
<div class="mr-1 inline-block rounded-full border border-solid border-light py-1 px-2">
<button
class={cx("w-20 text-left", {'pointer-events-none opacity-50': !canZap})}
class:text-accent={zapped}
on:click|stopPropagation={startZap}>
<i class="fa fa-bolt cursor-pointer" />
{formatSats($zapsTotal)}
</button>
<button class="w-16 text-left" on:click|stopPropagation={() => react("-")}>
<i class="fa fa-flag cursor-pointer" />
{$flagsCount}
</button>
class="fa fa-times cursor-pointer"
on:click|stopPropagation={() => removeMention(p)} />
{displayPerson(database.getPersonWithFallback(p))}
</div>
<div
class="cursor-pointer gap-1 items-center hidden sm:flex"
on:click|stopPropagation={() => { showRelays = true }}>
<i class="fa fa-server" />
<div
class="h-1 w-1 rounded-full"
style={`background: ${stringToColor(note.seen_on)}`} />
</div>
</div>
{/if}
{:else}
<div class="text-light inline-block">No mentions</div>
{/each}
<div class="-mt-2" />
</div>
</div>
</Card>
</div>
{#if reply}
<div transition:slide
class={`note-reply relative z-10 border border-${borderColor} border-solid rounded`}
bind:this={replyContainer}>
<div
class="bg-dark"
class:rounded-b={replyMentions.length === 0}
on:keydown={onReplyKeydown}>
<Compose bind:this={reply} onSubmit={sendReply}>
<button
slot="addon"
on:click={sendReply}
class="flex flex-col py-8 p-4 justify-center gap-2 border-l border-solid border-dark
hover:bg-accent transition-all cursor-pointer text-white">
<i class="fa fa-paper-plane fa-xl" />
</button>
</Compose>
</div>
{#if image}
<div class="p-2 bg-dark">
<Preview url={image} onClose={() => { image = null }} />
</div>
{/if}
<div class={`h-px bg-${borderColor}`} />
<div class="text-white text-sm p-2 rounded-b bg-black h-12">
<div class="inline-block py-2 pl-1 pr-3 mr-2 border-r border-solid border-medium">
<div class="flex gap-3 items-center cursor-pointer">
<ImageInput bind:value={image} icon="image" hideInput>
<i slot="button" class="fa fa-paperclip" />
</ImageInput>
<i class="fa fa-at" />
{#if visibleNotes.length > 0 && depth > 0}
<div class="relative">
<div class={`absolute w-px bg-${borderColor} z-10 -mt-4 ml-4 h-0`} bind:this={border} />
<div class="note-children relative ml-8 flex flex-col gap-4" bind:this={childrenContainer}>
{#if !showEntire && note.replies.length > visibleNotes.length}
<button class="ml-5 cursor-pointer py-2 text-light" on:click={onClick}>
<i class="fa fa-up-down pr-2 text-sm" />
Show {quantify(
note.replies.length - visibleNotes.length,
"other reply",
"more replies"
)}
</button>
{/if}
{#each visibleNotes as r (r.id)}
<svelte:self
showParent={false}
note={r}
depth={depth - 1}
{invertColors}
{anchorId}
{showContext} />
{/each}
</div>
</div>
{#each replyMentions as p}
<div class="inline-block py-1 px-2 mr-1 rounded-full border border-solid border-light">
<button class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
{displayPerson(database.getPersonWithFallback(p))}
</div>
{:else}
<div class="text-light inline-block">No mentions</div>
{/each}
<div class="-mt-2" />
</div>
</div>
{/if}
{/if}
{#if showRelays}
<Modal
onEscape={() => {
showRelays = false
}}>
<Content>
<RelayCard theme="black" showControls relay={{url: note.seen_on}} />
</Content>
</Modal>
{/if}
{#if visibleNotes.length > 0 && depth > 0}
<div class="relative">
<div class={`absolute w-px bg-${borderColor} z-10 -mt-4 ml-4 h-0`} bind:this={border} />
<div class="ml-8 note-children relative flex flex-col gap-4" bind:this={childrenContainer}>
{#if !showEntire && note.replies.length > visibleNotes.length}
<button class="ml-5 py-2 text-light cursor-pointer" on:click={onClick}>
<i class="fa fa-up-down text-sm pr-2" />
Show {quantify(note.replies.length - visibleNotes.length, 'other reply', 'more replies')}
</button>
{/if}
{#each visibleNotes as r (r.id)}
<svelte:self showParent={false} note={r} depth={depth - 1} {invertColors} {anchorId} {showContext} />
{/each}
</div>
</div>
{/if}
{#if showRelays}
<Modal onEscape={() => { showRelays = false }}>
<Content>
<RelayCard theme="black" showControls relay={{url: note.seen_on}} />
</Content>
</Modal>
{/if}
{#if zap}
<Modal onEscape={cleanupZap}>
<Content size="lg">
<div class="text-center">
<h1 class="staatliches text-2xl">Send a zap</h1>
<p>to {displayPerson($person)}</p>
</div>
{#if zap.invoice}
<QRCode code={zap.invoice} />
<div class="text-center text-light">
Copy or scan using a lightning wallet to pay your zap.
</div>
{:else}
<Textarea bind:value={zap.message} placeholder="Add an optional message" />
<div class="flex items-center gap-2">
<label class="flex-grow">Custom amount:</label>
<Input bind:value={zap.amount}>
<i slot="before" class="fa fa-bolt" />
<span slot="after" class="-mt-1">sats</span>
</Input>
<Anchor loading={zap.loading} type="button-accent" on:click={loadZapInvoice}>
Zap!
</Anchor>
</div>
{/if}
</Content>
</Modal>
{/if}
{#if zap}
<Modal onEscape={cleanupZap}>
<Content size="lg">
<div class="text-center">
<h1 class="staatliches text-2xl">Send a zap</h1>
<p>to {displayPerson($person)}</p>
</div>
{#if zap.invoice}
<QRCode code={zap.invoice} />
<div class="text-center text-light">
Copy or scan using a lightning wallet to pay your zap.
</div>
{:else}
<Textarea bind:value={zap.message} placeholder="Add an optional message" />
<div class="flex items-center gap-2">
<label class="flex-grow">Custom amount:</label>
<Input bind:value={zap.amount}>
<i slot="before" class="fa fa-bolt" />
<span slot="after" class="-mt-1">sats</span>
</Input>
<Anchor loading={zap.loading} type="button-accent" on:click={loadZapInvoice}>
Zap!
</Anchor>
</div>
{/if}
</Content>
</Modal>
{/if}
{/if}

View File

@ -1,9 +1,9 @@
<script>
import {onMount} from "svelte"
import {nip19} from 'nostr-tools'
import {quantify} from 'hurdak/lib/hurdak'
import {last, reject, pluck, propEq} from 'ramda'
import {fly} from 'svelte/transition'
import {nip19} from "nostr-tools"
import {quantify} from "hurdak/lib/hurdak"
import {last, reject, pluck, propEq} from "ramda"
import {fly} from "svelte/transition"
import {fuzzy} from "src/util/misc"
import {displayPerson} from "src/util/nostr"
import Button from "src/partials/Button.svelte"
@ -14,12 +14,12 @@
import RelayCardSimple from "src/partials/RelayCardSimple.svelte"
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import Heading from 'src/partials/Heading.svelte'
import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import Heading from "src/partials/Heading.svelte"
import {getUserWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import cmd from "src/agent/cmd"
import {toast, modal} from "src/app/ui"
import {publishWithToast} from 'src/app'
import {publishWithToast} from "src/app"
export let pubkey = null
@ -27,13 +27,13 @@
let input = null
let relays = getUserWriteRelays()
let showSettings = false
let q = ''
let q = ""
let search
const knownRelays = database.watch('relays', t => t.all())
const knownRelays = database.watch("relays", t => t.all())
$: {
const joined = new Set(pluck('url', relays))
const joined = new Set(pluck("url", relays))
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
@ -45,7 +45,7 @@
let {content, mentions, topics} = input.parse()
if (image) {
content = (content + '\n' + image).trim()
content = (content + "\n" + image).trim()
}
if (content) {
@ -53,17 +53,20 @@
const [event, promise] = await publishWithToast(relays, thunk)
promise.then(() =>
setTimeout(() =>
toast.show("info", {
text: `Your note has been created!`,
link: {
text: 'View',
href: "/" + nip19.neventEncode({
id: event.id,
relays: pluck('url', relays.slice(0, 3)),
}),
},
}),
setTimeout(
() =>
toast.show("info", {
text: `Your note has been created!`,
link: {
text: "View",
href:
"/" +
nip19.neventEncode({
id: event.id,
relays: pluck("url", relays.slice(0, 3)),
}),
},
}),
3000
)
)
@ -73,25 +76,25 @@
}
const closeSettings = () => {
q = ''
q = ""
showSettings = false
}
const addRelay = relay => {
q = ''
q = ""
relays = relays.concat(relay)
}
const removeRelay = relay => {
relays = reject(propEq('url', relay.url), relays)
relays = reject(propEq("url", relay.url), relays)
}
onMount(() => {
if (pubkey) {
const person = database.getPersonWithFallback(pubkey)
input.type('@' + displayPerson(person))
input.trigger({key: 'Enter'})
input.type("@" + displayPerson(person))
input.trigger({key: "Enter"})
}
})
</script>
@ -99,7 +102,7 @@
<form on:submit|preventDefault={onSubmit} in:fly={{y: 20}}>
<Content size="lg">
<Heading class="text-center">Create a note</Heading>
<div class="flex flex-col gap-4 w-full">
<div class="flex w-full flex-col gap-4">
<div class="flex flex-col gap-2">
<strong>What do you want to say?</strong>
<div class="border-l-2 border-solid border-medium pl-4">
@ -107,16 +110,22 @@
</div>
</div>
{#if image}
<Preview url={image} onClose={() => { image = null }} />
<Preview
url={image}
onClose={() => {
image = null
}} />
{/if}
<div class="flex gap-2">
<Button type="submit" class="text-center flex-grow">Send</Button>
<Button type="submit" class="flex-grow text-center">Send</Button>
<ImageInput bind:value={image} icon="image" hideInput />
</div>
<small
class="flex justify-end items-center gap-1 cursor-pointer"
on:click={() => { showSettings = true }}>
<span>Publishing to {quantify(relays.length, 'relay')}</span>
class="flex cursor-pointer items-center justify-end gap-1"
on:click={() => {
showSettings = true
}}>
<span>Publishing to {quantify(relays.length, "relay")}</span>
<i class="fa fa-edit" />
</small>
</div>
@ -124,34 +133,37 @@
</form>
{#if showSettings}
<Modal onEscape={closeSettings}>
<form on:submit|preventDefault={closeSettings}>
<Content>
<div class="flex justify-center items-center mb-4">
<Heading>Note relays</Heading>
</div>
<div>Select which relays to publish to:</div>
<div>
{#each relays as relay}
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
<button type="button" class="fa fa-times cursor-pointer" on:click={() => removeRelay(relay)} />
{last(relay.url.split('//'))}
<Modal onEscape={closeSettings}>
<form on:submit|preventDefault={closeSettings}>
<Content>
<div class="mb-4 flex items-center justify-center">
<Heading>Note relays</Heading>
</div>
<div>Select which relays to publish to:</div>
<div>
{#each relays as relay}
<div
class="mr-1 mb-2 inline-block rounded-full border border-solid border-light py-1 px-2">
<button
type="button"
class="fa fa-times cursor-pointer"
on:click={() => removeRelay(relay)} />
{last(relay.url.split("//"))}
</div>
{/each}
</div>
<Input bind:value={q} placeholder="Search for other relays">
<i slot="before" class="fa fa-search" />
</Input>
{#each (q ? search(q) : []).slice(0, 3) as relay (relay.url)}
<RelayCardSimple {relay}>
<button slot="actions" class="underline" on:click={() => addRelay(relay)}>
Add relay
</button>
</RelayCardSimple>
{/each}
</div>
<Input bind:value={q} placeholder="Search for other relays">
<i slot="before" class="fa fa-search" />
</Input>
{#each (q ? search(q) : []).slice(0, 3) as relay (relay.url)}
<RelayCardSimple {relay}>
<button slot="actions" class="underline" on:click={() => addRelay(relay)}>
Add relay
</button>
</RelayCardSimple>
{/each}
<Button type="submit" class="text-center">Done</Button>
</Content>
</form>
</Modal>
<Button type="submit" class="text-center">Done</Button>
</Content>
</form>
</Modal>
{/if}

View File

@ -1,16 +1,16 @@
<script>
import {onMount} from 'svelte'
import {nip19} from 'nostr-tools'
import {fly} from 'svelte/transition'
import {first} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {asDisplayEvent} from 'src/util/nostr'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Note from 'src/views/notes/Note.svelte'
import user from 'src/agent/user'
import network from 'src/agent/network'
import {sampleRelays} from 'src/agent/relays'
import {onMount} from "svelte"
import {nip19} from "nostr-tools"
import {fly} from "svelte/transition"
import {first} from "hurdak/lib/hurdak"
import {log} from "src/util/logger"
import {asDisplayEvent} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/views/notes/Note.svelte"
import user from "src/agent/user"
import network from "src/agent/network"
import {sampleRelays} from "src/agent/relays"
export let note
export let relays = []
@ -34,7 +34,7 @@
}
if (note) {
log('NoteDetail', nip19.noteEncode(note.id), note)
log("NoteDetail", nip19.noteEncode(note.id), note)
network.streamContext({
depth: 6,
@ -50,17 +50,15 @@
</script>
{#if !loading && !found}
<div in:fly={{y: 20}}>
<Content size="lg" class="text-center">
Sorry, we weren't able to find this note.
</Content>
</div>
<div in:fly={{y: 20}}>
<Content size="lg" class="text-center">Sorry, we weren't able to find this note.</Content>
</div>
{:else if note.pubkey}
<div in:fly={{y: 20}} class="flex flex-col gap-4 p-4">
<Note showContext depth={6} anchorId={note.id} note={asDisplayEvent(note)} {invertColors} />
</div>
<div in:fly={{y: 20}} class="flex flex-col gap-4 p-4">
<Note showContext depth={6} anchorId={note.id} note={asDisplayEvent(note)} {invertColors} />
</div>
{/if}
{#if loading}
<Spinner />
<Spinner />
{/if}

View File

@ -1,21 +1,21 @@
<script lang="ts">
import {uniq} from 'ramda'
import {onMount} from 'svelte'
import {generatePrivateKey} from 'nostr-tools'
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {shuffle} from 'src/util/misc'
import {displayPerson} from 'src/util/nostr'
import OnboardingIntro from 'src/views/onboarding/OnboardingIntro.svelte'
import OnboardingKey from 'src/views/onboarding/OnboardingKey.svelte'
import OnboardingRelays from 'src/views/onboarding/OnboardingRelays.svelte'
import OnboardingFollows from 'src/views/onboarding/OnboardingFollows.svelte'
import OnboardingComplete from 'src/views/onboarding/OnboardingComplete.svelte'
import {getFollows} from 'src/agent/social'
import {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import user from 'src/agent/user'
import {uniq} from "ramda"
import {onMount} from "svelte"
import {generatePrivateKey} from "nostr-tools"
import {fly} from "svelte/transition"
import {navigate} from "svelte-routing"
import {shuffle} from "src/util/misc"
import {displayPerson} from "src/util/nostr"
import OnboardingIntro from "src/views/onboarding/OnboardingIntro.svelte"
import OnboardingKey from "src/views/onboarding/OnboardingKey.svelte"
import OnboardingRelays from "src/views/onboarding/OnboardingRelays.svelte"
import OnboardingFollows from "src/views/onboarding/OnboardingFollows.svelte"
import OnboardingComplete from "src/views/onboarding/OnboardingComplete.svelte"
import {getFollows} from "src/agent/social"
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
import database from "src/agent/database"
import network from "src/agent/network"
import user from "src/agent/user"
import keys from "src/agent/keys"
import {loadAppData} from "src/app"
import {modal} from "src/app/ui"
@ -23,10 +23,10 @@
export let stage
let relays = [
{url: 'wss://nostr-pub.wellorder.net'},
{url: 'wss://nostr.zebedee.cloud'},
{url: 'wss://nos.lol'},
{url: 'wss://brb.io'},
{url: "wss://nostr-pub.wellorder.net"},
{url: "wss://nostr.zebedee.cloud"},
{url: "wss://nos.lol"},
{url: "wss://brb.io"},
]
let follows = [
@ -39,19 +39,21 @@
const privkey = generatePrivateKey()
const signup = async () => {
await keys.login('privkey', privkey)
await keys.login("privkey", privkey)
await user.updateRelays(() => relays)
await user.updatePetnames(() => follows.map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
const name = displayPerson(database.getPersonWithFallback(pubkey))
await user.updatePetnames(() =>
follows.map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
const name = displayPerson(database.getPersonWithFallback(pubkey))
return ["p", pubkey, url, name]
}))
return ["p", pubkey, url, name]
})
)
loadAppData(user.getPubkey())
modal.set(null)
navigate('/notes/follows')
navigate("/notes/follows")
}
// Prime our people cache for hardcoded follows and a sample of people they follow
@ -65,17 +67,17 @@
</script>
{#key stage}
<div in:fly={{y: 20}}>
{#if stage === 'intro'}
<OnboardingIntro />
{:else if stage === 'key'}
<OnboardingKey {privkey} />
{:else if stage === 'relays'}
<OnboardingRelays bind:relays={relays} />
{:else if stage === 'follows'}
<OnboardingFollows bind:follows={follows} />
{:else}
<OnboardingComplete {signup} />
{/if}
</div>
<div in:fly={{y: 20}}>
{#if stage === "intro"}
<OnboardingIntro />
{:else if stage === "key"}
<OnboardingKey {privkey} />
{:else if stage === "relays"}
<OnboardingRelays bind:relays />
{:else if stage === "follows"}
<OnboardingFollows bind:follows />
{:else}
<OnboardingComplete {signup} />
{/if}
</div>
{/key}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import Anchor from 'src/partials/Anchor.svelte'
import Heading from 'src/partials/Heading.svelte'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
export let signup
@ -17,11 +17,11 @@
<Content size="lg" class="text-center">
<Heading>Welcome to Nostr</Heading>
<p>
Youre all set! If have any questions, or need any help, just ask. Your fellow
nostriches are always happy to lend a hand.
Youre all set! If have any questions, or need any help, just ask. Your fellow nostriches are
always happy to lend a hand.
</p>
<Anchor {loading} type="button-accent" on:click={startSignup}>Get on Nostr</Anchor>
{#if loading}
<Spinner />
<Spinner />
{/if}
</Content>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import {without} from 'ramda'
import {without} from "ramda"
import {fuzzy} from "src/util/misc"
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Heading from 'src/partials/Heading.svelte'
import Content from 'src/partials/Content.svelte'
import PersonInfo from 'src/partials/PersonInfo.svelte'
import database from 'src/agent/database'
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import PersonInfo from "src/partials/PersonInfo.svelte"
import database from "src/agent/database"
import {modal} from "src/app/ui"
export let follows
@ -14,7 +14,7 @@
let q = ""
let search
const knownPeople = database.watch('people', t => t.all({'kind0.name:!nil': null}))
const knownPeople = database.watch("people", t => t.all({"kind0.name:!nil": null}))
$: search = fuzzy(
$knownPeople.filter(p => !follows.includes(p.pubkey)),
@ -34,32 +34,30 @@
<Content class="text-center">
<Heading>Find Your People</Heading>
<p>
To get you started, weve added some interesting people to your follow list.
You can update your follows list at any time.
To get you started, weve added some interesting people to your follow list. You can update
your follows list at any time.
</p>
<Anchor
type="button-accent"
on:click={() => modal.set({type: 'onboarding', stage: 'complete'})}>
on:click={() => modal.set({type: "onboarding", stage: "complete"})}>
Continue
</Anchor>
</Content>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<i class="fa fa-user-astronaut fa-lg" />
<h2 class="staatliches text-2xl">Your follows</h2>
</div>
{#if follows.length === 0}
<div class="text-center mt-8 flex gap-2 justify-center items-center">
<i class="fa fa-triangle-exclamation" />
<span>No follows selected</span>
</div>
<div class="mt-8 flex items-center justify-center gap-2 text-center">
<i class="fa fa-triangle-exclamation" />
<span>No follows selected</span>
</div>
{:else}
{#each follows as pubkey}
<PersonInfo person={database.getPersonWithFallback(pubkey)} {removePetname} />
{/each}
{#each follows as pubkey}
<PersonInfo person={database.getPersonWithFallback(pubkey)} {removePetname} />
{/each}
{/if}
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other people</h2>
</div>
@ -67,6 +65,6 @@
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each search(q).slice(0, 50) as person (person.pubkey)}
<PersonInfo {person} {addPetname} />
<PersonInfo {person} {addPetname} />
{/each}
</Content>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Anchor from 'src/partials/Anchor.svelte'
import Heading from 'src/partials/Heading.svelte'
import Content from 'src/partials/Content.svelte'
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import {modal} from "src/app/ui"
const tutorialUrl = "https://nostr.how/"
@ -10,15 +10,15 @@
<Content size="lg" class="text-center">
<Heading>Create an Account</Heading>
<p>
New to Nostr? Click <Anchor external href={tutorialUrl}>here</Anchor> or watch the video
below for a crash course on what it is, and how to use it.
New to Nostr? Click <Anchor external href={tutorialUrl}>here</Anchor> or watch the video below for
a crash course on what it is, and how to use it.
</p>
<video controls src="" class="object-center object-contain" />
<video controls src="" class="object-contain object-center" />
<p>
When youre ready to dive in, click below and well guide you through
the process of creating an account.
When youre ready to dive in, click below and well guide you through the process of creating an
account.
</p>
<Anchor type="button-accent" on:click={() => modal.set({type: 'onboarding', stage: 'key'})}>
<Anchor type="button-accent" on:click={() => modal.set({type: "onboarding", stage: "key"})}>
Let's go!
</Anchor>
</Content>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {nip19} from "nostr-tools"
import {copyToClipboard} from "src/util/html"
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Heading from 'src/partials/Heading.svelte'
import Content from 'src/partials/Content.svelte'
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import {modal, toast} from "src/app/ui"
export let privkey
@ -21,21 +21,21 @@
<Content size="lg" class="text-center">
<Heading>Generate a Key</Heading>
<p>
Your private key is your password, and gives you total control over your Nostr account.
We've generated a fresh one for you below store it somewhere safe!
Your private key is your password, and gives you total control over your Nostr account. We've
generated a fresh one for you below store it somewhere safe!
</p>
<div class="flex gap-2">
<Input disabled placeholder={"•".repeat(53)} wrapperClass="flex-grow">
<i slot="before" class="fa fa-lock" />
<button slot="after" class="cursor-pointer fa fa-copy" on:click={copyKey} />
<button slot="after" class="fa fa-copy cursor-pointer" on:click={copyKey} />
</Input>
<Anchor type="button-accent" on:click={() => modal.set({type: 'onboarding', stage: 'relays'})}>
<Anchor type="button-accent" on:click={() => modal.set({type: "onboarding", stage: "relays"})}>
Log in
</Anchor>
</div>
<p>
Avoid pasting your key into too many apps and websites. Instead, use
a <Anchor href={nip07} external>compatible browser extension</Anchor> to
securely store your key.
Avoid pasting your key into too many apps and websites. Instead, use a <Anchor
href={nip07}
external>compatible browser extension</Anchor> to securely store your key.
</p>
</Content>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import {reject, pluck, whereEq} from 'ramda'
import {reject, pluck, whereEq} from "ramda"
import {fuzzy} from "src/util/misc"
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Heading from 'src/partials/Heading.svelte'
import Content from 'src/partials/Content.svelte'
import RelayCard from 'src/partials/RelayCard.svelte'
import database from 'src/agent/database'
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import database from "src/agent/database"
import {modal} from "src/app/ui"
export let relays
@ -14,10 +14,10 @@
let q = ""
let search
const knownRelays = database.watch('relays', t => t.all())
const knownRelays = database.watch("relays", t => t.all())
$: {
const joined = new Set(pluck('url', relays))
const joined = new Set(pluck("url", relays))
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
@ -38,33 +38,31 @@
<Content class="text-center">
<Heading>Get Connected</Heading>
<p>
Nostr is a protocol, not a platform. This means that <i>you</i> choose where to store your
data. Select your preferred storage relays below, or click "continue" to use some
reasonable defaults. You can change your selection any time.
Nostr is a protocol, not a platform. This means that <i>you</i> choose where to store your data.
Select your preferred storage relays below, or click "continue" to use some reasonable defaults.
You can change your selection any time.
</p>
<Anchor
type="button-accent"
on:click={() => modal.set({type: 'onboarding', stage: 'follows'})}>
<Anchor type="button-accent" on:click={() => modal.set({type: "onboarding", stage: "follows"})}>
Continue
</Anchor>
</Content>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your relays</h2>
</div>
{#if relays.length === 0}
<div class="text-center mt-8 flex gap-2 justify-center items-center">
<i class="fa fa-triangle-exclamation" />
<span>No relays connected</span>
</div>
<div class="mt-8 flex items-center justify-center gap-2 text-center">
<i class="fa fa-triangle-exclamation" />
<span>No relays connected</span>
</div>
{:else}
<div class="grid grid-cols-1 gap-4">
{#each relays as relay (relay.url)}
<RelayCard {relay} {removeRelay} />
{/each}
</div>
<div class="grid grid-cols-1 gap-4">
{#each relays as relay (relay.url)}
<RelayCard {relay} {removeRelay} />
{/each}
</div>
{/if}
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other relays</h2>
</div>
@ -72,7 +70,7 @@
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay} {addRelay} />
<RelayCard {relay} {addRelay} />
{/each}
<small class="text-center">
Showing {Math.min($knownRelays.length - relays.length, 50)}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Feed from "src/views/feed/Feed.svelte"
import {isLike} from 'src/util/nostr'
import {sampleRelays, getPubkeyWriteRelays} from 'src/agent/relays'
import {isLike} from "src/util/nostr"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
export let pubkey
@ -11,4 +11,3 @@
</script>
<Feed {relays} {filter} {shouldDisplay} parentsTimeout={10_000} />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import Feed from "src/views/feed/Feed.svelte"
import {sampleRelays, getPubkeyWriteRelays} from 'src/agent/relays'
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
export let pubkey

View File

@ -1,8 +1,8 @@
<script lang="ts">
import {displayPerson} from "src/util/nostr"
import PersonInfo from 'src/partials/PersonInfo.svelte'
import {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
import user from 'src/agent/user'
import PersonInfo from "src/partials/PersonInfo.svelte"
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
import user from "src/agent/user"
export let person

View File

@ -1,18 +1,18 @@
<script type="ts">
import Content from 'src/partials/Content.svelte'
import PersonInfo from 'src/views/person/PersonInfo.svelte'
import database from 'src/agent/database'
import network from 'src/agent/network'
import Content from "src/partials/Content.svelte"
import PersonInfo from "src/views/person/PersonInfo.svelte"
import database from "src/agent/database"
import network from "src/agent/network"
export let pubkeys
const people = database.watch('people', t => pubkeys.map(database.getPersonWithFallback))
const people = database.watch("people", t => pubkeys.map(database.getPersonWithFallback))
network.loadPeople(pubkeys)
</script>
<Content gap={2}>
{#each ($people || []) as person}
<PersonInfo {person} />
{#each $people || [] as person}
<PersonInfo {person} />
{/each}
</Content>

View File

@ -33,7 +33,7 @@
// fetch data
nip05
.queryProfile(person.verified_as)
.then((data) => {
.then(data => {
nip05ProfileData = data
// recalculate nprofile using NIP05 relay data, if specified.
@ -45,7 +45,7 @@
})
}
})
.catch((err) => {
.catch(err => {
warn("NIP05 profile retrieval failed")
})
.finally(() => {
@ -63,12 +63,12 @@
let name, domain
if (identifier.match(/^.*@.*$/)) {
[name, domain] = identifier.split("@")
;[name, domain] = identifier.split("@")
} else {
// In case of no name (domain-only), mimick the reasonable
// (but somewhat questionable) behaviour of nostr-tools/nip05,
// which defaults the name value
[name, domain] = ["_", identifier]
;[name, domain] = ["_", identifier]
}
return `https://${domain}/.well-known/nostr.json?name=${name}`
}
@ -84,62 +84,51 @@
<Content>
<h1 class="staatliches text-2xl">Profile Details</h1>
<div>
<div class="text-lg mb-1">Public Key (Hex)</div>
<div class="text-sm font-mono">
<button
class="fa-solid fa-copy cursor-pointer"
on:click={() => copy(person.pubkey)}
/>
<div class="mb-1 text-lg">Public Key (Hex)</div>
<div class="font-mono text-sm">
<button class="fa-solid fa-copy cursor-pointer" on:click={() => copy(person.pubkey)} />
{person.pubkey}
</div>
</div>
<div>
<div class="text-lg mb-1">Public Key (npub)</div>
<div class="text-sm font-mono">
<div class="mb-1 text-lg">Public Key (npub)</div>
<div class="font-mono text-sm">
{#if npub}
<button
class="fa-solid fa-copy cursor-pointer"
on:click={() => copy(npub)}
/>
<button class="fa-solid fa-copy cursor-pointer" on:click={() => copy(npub)} />
{/if}
{npub}
</div>
</div>
{#if nProfile}
<div>
<div class="text-lg mb-1">nprofile</div>
<div class="text-sm font-mono break-all">
<button
class="fa-solid fa-copy cursor-pointer inline"
on:click={() => copy(nProfile)}
/>
{nProfile}
<div>
<div class="mb-1 text-lg">nprofile</div>
<div class="break-all font-mono text-sm">
<button class="fa-solid fa-copy inline cursor-pointer" on:click={() => copy(nProfile)} />
{nProfile}
</div>
</div>
</div>
{/if}
<h1 class="staatliches text-2xl mt-4">NIP05</h1>
<h1 class="staatliches mt-4 text-2xl">NIP05</h1>
{#if loaded && person.verified_as}
<div>
<div class="text-lg mb-1">NIP05 Identifier</div>
<div class="text-sm font-mono">
<div class="mb-1 text-lg">NIP05 Identifier</div>
<div class="font-mono text-sm">
{#if person.verified_as}
<button
class="fa-solid fa-copy cursor-pointer inline"
on:click={() => copy(person.verified_as)}
/>
class="fa-solid fa-copy inline cursor-pointer"
on:click={() => copy(person.verified_as)} />
{/if}
{person.verified_as || "?"}
</div>
</div>
<div>
<div class="text-lg mb-1">NIP05 Validation Endpoint</div>
<div class="text-sm font-mono">
<div class="mb-1 text-lg">NIP05 Validation Endpoint</div>
<div class="font-mono text-sm">
{#if nip05QueryEndpoint}
<button
class="fa-solid fa-copy cursor-pointer inline"
on:click={() => copy(nip05QueryEndpoint)}
/>
class="fa-solid fa-copy inline cursor-pointer"
on:click={() => copy(nip05QueryEndpoint)} />
{/if}
{nip05QueryEndpoint || "?"}
@ -148,19 +137,19 @@
{#if nip05ProfileData}
<div>
<div class="text-lg mb-2">NIP05 Relay Configuration</div>
<div class="mb-2 text-lg">NIP05 Relay Configuration</div>
{#if nip05ProfileData?.relays?.length}
<p class="text-sm mb-4 text-light">
<p class="mb-4 text-sm text-light">
These relays are advertised by the NIP05 identifier's validation endpoint.
</p>
<div class="grid grid-cols-1 gap-4">
{#each nip05ProfileData?.relays as url}
<RelayCard relay={{url}} />
<RelayCard relay={{url}} />
{/each}
</div>
{:else}
<p class="text-sm mb-4 text-light">
<p class="mb-4 text-sm text-light">
<i class="fa-solid fa-info-circle" />
No relays are advertised by the NIP05 identifier's validation endpoint.
</p>
@ -168,12 +157,12 @@
</div>
{:else}
<p>
<i class="fa-solid fa-warning text-warning mr-2" />
<i class="fa-solid fa-warning mr-2 text-warning" />
Could not fetch NIP05 data.
</p>
{/if}
{:else}
<p class="text-sm mb-4 text-light">
<p class="mb-4 text-sm text-light">
<i class="fa-solid fa-info-circle" />
NIP05 identifier not available.
</p>

View File

@ -3,11 +3,11 @@
import {personKinds} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import Spinner from "src/partials/Spinner.svelte"
import PersonInfo from 'src/views/person/PersonInfo.svelte'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import user from 'src/agent/user'
import PersonInfo from "src/views/person/PersonInfo.svelte"
import {getUserReadRelays} from "src/agent/relays"
import database from "src/agent/database"
import network from "src/agent/network"
import user from "src/agent/user"
export let hideFollowing = false
@ -15,11 +15,10 @@
let results = []
const {petnamePubkeys} = user
const search = database.watch('people', t =>
fuzzy(
t.all({'kind0.name:!nil': null}),
{keys: ["kind0.name", "kind0.about", "pubkey"]}
)
const search = database.watch("people", t =>
fuzzy(t.all({"kind0.name:!nil": null}), {
keys: ["kind0.name", "kind0.about", "pubkey"],
})
)
$: results = $search(q).slice(0, 50)
@ -38,8 +37,8 @@
</Input>
{#each results as person (person.pubkey)}
{#if person.pubkey !== user.getPubkey() && !(hideFollowing && $petnamePubkeys.includes(person.pubkey))}
<PersonInfo {person} />
<PersonInfo {person} />
{/if}
{:else}
<Spinner />
<Spinner />
{/each}

View File

@ -1,19 +1,19 @@
<script>
import {last} from 'ramda'
import {switcher, first} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import {last} from "ramda"
import {switcher, first} from "hurdak/lib/hurdak"
import {fly} from "svelte/transition"
import Button from "src/partials/Button.svelte"
import Content from 'src/partials/Content.svelte'
import Content from "src/partials/Content.svelte"
import SelectButton from "src/partials/SelectButton.svelte"
import user from 'src/agent/user'
import {getUserWriteRelays} from 'src/agent/relays'
import cmd from 'src/agent/cmd'
import {publishWithToast} from 'src/app'
import user from "src/agent/user"
import {getUserWriteRelays} from "src/agent/relays"
import cmd from "src/agent/cmd"
import {publishWithToast} from "src/app"
export let person
const muffle = user.getProfile().muffle || []
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
const muffleOptions = ["Never", "Sometimes", "Often", "Always"]
const muffleValue = parseFloat(first(muffle.filter(t => t[1] === person.pubkey).map(last)) || 1)
const values = {
@ -41,16 +41,14 @@
<Content class="text-white">
<div class="flex flex-col gap-2">
<h1 class="text-3xl">Advanced Follow</h1>
<p>
Fine grained controls for interacting with other people.
</p>
<p>Fine grained controls for interacting with other people.</p>
</div>
<div class="flex flex-col gap-1">
<strong>How often do you want to see notes from this person?</strong>
<SelectButton bind:value={values.muffle} options={muffleOptions} />
<p class="text-sm text-light">
"Never" is effectively a mute, while "Always" will show posts whenever available.
If you want a middle ground, choose "Sometimes" or "Often".
"Never" is effectively a mute, while "Always" will show posts whenever available. If you
want a middle ground, choose "Sometimes" or "Often".
</p>
</div>
<Button type="submit" class="text-center">Done</Button>

View File

@ -1,20 +1,18 @@
<script lang="ts">
import {prop} from 'ramda'
import {nip19} from 'nostr-tools'
import Content from 'src/partials/Content.svelte'
import QRCode from 'src/partials/QRCode.svelte'
import {getPubkeyWriteRelays} from 'src/agent/relays'
import {prop} from "ramda"
import {nip19} from "nostr-tools"
import Content from "src/partials/Content.svelte"
import QRCode from "src/partials/QRCode.svelte"
import {getPubkeyWriteRelays} from "src/agent/relays"
export let person
const {pubkey} = person
const relays = [prop('url', getPubkeyWriteRelays(pubkey))]
const relays = [prop("url", getPubkeyWriteRelays(pubkey))]
const nprofile = nip19.nprofileEncode({pubkey, relays})
</script>
<Content size="lg">
<QRCode code={nprofile} />
<div class="text-center text-light">
Copy or scan from a nostr app to share this profile.
</div>
<div class="text-center text-light">Copy or scan from a nostr app to share this profile.</div>
</Content>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import {last} from 'ramda'
import {navigate} from 'svelte-routing'
import {renderContent} from 'src/util/html'
import {displayPerson} from 'src/util/nostr'
import {last} from "ramda"
import {navigate} from "svelte-routing"
import {renderContent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import Anchor from "src/partials/Anchor.svelte"
import user from "src/agent/user"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
@ -13,7 +13,7 @@
const {petnamePubkeys, canPublish} = user
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
const person = database.watch('people', () => database.getPersonWithFallback(pubkey))
const person = database.watch("people", () => database.getPersonWithFallback(pubkey))
let following = false
@ -30,12 +30,12 @@
}
</script>
<div class="flex flex-col gap-4 py-2 px-3 relative">
<div class="relative flex flex-col gap-4 py-2 px-3">
<div class="flex gap-4">
<div
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
class="h-14 w-14 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style="background-image: url({$person.kind0?.picture})" />
<div class="flex-grow flex flex-col gap-2">
<div class="flex flex-grow flex-col gap-2">
<Anchor
type="unstyled"
class="flex items-center gap-2"
@ -43,25 +43,25 @@
<h2 class="text-lg">{displayPerson($person)}</h2>
</Anchor>
{#if $person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last($person.verified_as.split('@'))}</span>
</div>
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last($person.verified_as.split("@"))}</span>
</div>
{/if}
</div>
<div class="flex gap-2">
{#if $canPublish}
{#if following}
<Anchor type="button-circle" on:click={unfollow}>
<i class="fa fa-user-minus" />
</Anchor>
{:else}
<Anchor type="button-circle" on:click={follow}>
<i class="fa fa-user-plus" />
</Anchor>
{/if}
{#if following}
<Anchor type="button-circle" on:click={unfollow}>
<i class="fa fa-user-minus" />
</Anchor>
{:else}
<Anchor type="button-circle" on:click={follow}>
<i class="fa fa-user-plus" />
</Anchor>
{/if}
{/if}
</div>
</div>
<p>{@html renderContent($person?.kind0?.about || '')}</p>
<p>{@html renderContent($person?.kind0?.about || "")}</p>
</div>

View File

@ -1,5 +1,5 @@
<script>
import {fly} from 'svelte/transition'
import {fly} from "svelte/transition"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
@ -9,16 +9,16 @@
<div in:fly={{y: 20}}>
<Content>
<p>
Below are the relays this user publishes to. Join one or more to make sure you never
miss their updates.
Below are the relays this user publishes to. Join one or more to make sure you never miss
their updates.
</p>
{#if (person.relays || []).length === 0}
<div class="pt-8 text-center">No relays found</div>
<div class="pt-8 text-center">No relays found</div>
{:else}
{#each person.relays as relay (relay.url)}
{#if relay.write !== '!'}
<RelayCard {relay} />
{/if}
{#if relay.write !== "!"}
<RelayCard {relay} />
{/if}
{/each}
{/if}
</Content>

View File

@ -1,12 +1,12 @@
<script>
import {fly} from 'svelte/transition'
import Input from 'src/partials/Input.svelte'
import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte'
import Button from 'src/partials/Button.svelte'
import user from 'src/agent/user'
import {fly} from "svelte/transition"
import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import Button from "src/partials/Button.svelte"
import user from "src/agent/user"
import {toast, modal} from "src/app/ui"
import {loadAppData} from 'src/app'
import {loadAppData} from "src/app"
let url = $modal.url
@ -14,8 +14,8 @@
e.preventDefault()
url = url.trim()
if (!url.includes('://')) {
url = 'wss://' + url
if (!url.includes("://")) {
url = "wss://" + url
}
try {
@ -24,7 +24,7 @@
return toast.show("error", "That isn't a valid url")
}
if (!url.match('^wss?://')) {
if (!url.match("^wss?://")) {
return toast.show("error", "That isn't a valid websocket url")
}
@ -40,18 +40,16 @@
<form on:submit={submit} in:fly={{y: 20}}>
<Content>
<div class="flex justify-center items-center flex-col mb-4">
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>Add a relay</Heading>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Relay URL</strong>
<Input autofocus bind:value={url} placeholder="wss://relay.example.com">
<i slot="before" class="fa-solid fa-link" />
</Input>
<p class="text-sm text-light">
The url where the relay is hosted.
</p>
<p class="text-sm text-light">The url where the relay is hosted.</p>
</div>
<Button type="submit" class="text-center">Done</Button>
</div>

View File

@ -1,15 +1,15 @@
<script lang="ts">
import {find, propEq} from 'ramda'
import {onMount} from 'svelte'
import {find, propEq} from "ramda"
import {onMount} from "svelte"
import {poll} from "src/util/misc"
import Toggle from "src/partials/Toggle.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import pool from 'src/agent/pool'
import pool from "src/agent/pool"
import user from "src/agent/user"
import {loadAppData} from 'src/app'
import {loadAppData} from "src/app"
export let relay
export let theme = 'dark'
export let theme = "dark"
export let showControls = false
let quality = null
@ -18,7 +18,7 @@
const {relays, canPublish} = user
$: joined = find(propEq('url', relay.url), $relays)
$: joined = find(propEq("url", relay.url), $relays)
const removeRelay = ({url}) => user.removeRelay(url)
@ -35,7 +35,7 @@
const conn = await pool.getConnection(relay.url)
if (conn) {
[quality, message] = conn.getQuality()
;[quality, message] = conn.getQuality()
} else {
quality = null
message = "Not connected"
@ -45,15 +45,16 @@
</script>
<RelayCard
{relay} {theme}
{relay}
{theme}
addRelay={!joined && $canPublish ? addRelay : null}
removeRelay={joined && $relays.length > 1 && canPublish ? removeRelay : null}>
<div slot="controls" class="flex justify-between gap-2">
{#if showControls && $canPublish}
<span>Publish to this relay?</span>
<Toggle
value={relay.write}
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
<span>Publish to this relay?</span>
<Toggle
value={relay.write}
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
{/if}
</div>
</RelayCard>

View File

@ -1,19 +1,19 @@
<script>
import {pluck} from 'ramda'
import {pluck} from "ramda"
import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
import database from 'src/agent/database'
import database from "src/agent/database"
import user from "src/agent/user"
let q = ""
let search
let knownRelays = database.watch('relays', t => t.all())
let knownRelays = database.watch("relays", t => t.all())
const {relays} = user
$: {
const joined = new Set(pluck('url', $relays))
const joined = new Set(pluck("url", $relays))
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
@ -26,7 +26,7 @@
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay} />
<RelayCard {relay} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)}