Add typescript

This commit is contained in:
Jonathan Staab 2023-02-03 17:01:29 -06:00
parent 233c4b6ad6
commit a45ecb61be
47 changed files with 210 additions and 150 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -7,7 +7,8 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src/*/** --quiet"
"lint": "eslint src/*/** --quiet",
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.1.0",
@ -23,6 +24,7 @@
"@bugsnag/js": "^7.18.0",
"@fortawesome/fontawesome-free": "^6.2.1",
"@noble/secp256k1": "^1.7.0",
"@tsconfig/svelte": "^3.0.0",
"classnames": "^2.3.2",
"compressorjs": "^1.1.1",
"dexie": "^3.2.2",
@ -31,8 +33,10 @@
"hurdak": "github:ConsignCloud/hurdak",
"nostr-tools": "^1.1.1",
"ramda": "^0.28.0",
"svelte-check": "^3.0.3",
"svelte-link-preview": "^0.3.3",
"svelte-loading-spinners": "^0.3.4",
"svelte-preprocess": "^5.0.1",
"svelte-routing": "^1.6.0",
"svelte-switch": "^0.0.5",
"throttle-debounce": "^5.0.0",

View File

@ -130,7 +130,7 @@
document.body.style.position = `fixed`
}
} else {
document.body.style = ''
document.body.setAttribute('style', '')
window.scrollTo(0, scrollY)
}
})
@ -151,18 +151,18 @@
<Route path="/notes/:activeTab" component={Notes} />
<Route path="/people/:npub/:activeTab" let:params>
{#key params.npub}
<Person {...params} />
<Person npub={params.npub} activeTab={params.activeTab} />
{/key}
</Route>
<Route path="/chat" component={Chat} />
<Route path="/chat/:entity" let:params>
{#key params.entity}
<ChatRoom {...params} />
<ChatRoom entity={params.entity} />
{/key}
</Route>
<Route path="/messages/:entity" let:params>
{#key params.entity}
<Messages {...params} />
<Messages entity={params.entity} />
{/key}
</Route>
<Route path="/keys" component={Keys} />
@ -173,7 +173,7 @@
<Route path="/logout" component={Logout} />
<Route path="/:entity" let:params>
{#key params.entity}
<Bech32Entity {...params} />
<Bech32Entity entity={params.entity} />
{/key}
</Route>
<Route path="*" component={NotFound} />
@ -259,10 +259,10 @@
border-b border-medium z-10"
>
<div class="lg:hidden">
<i class="fa-solid fa-bars fa-2xl cursor-pointer" bind:this={menuIcon} on:click={toggleMenu} />
<button class="fa-solid fa-bars fa-2xl cursor-pointer" bind:this={menuIcon} on:click={toggleMenu} />
</div>
<Anchor external type="unstyled" href="https://github.com/staab/coracle" class="flex items-center gap-2">
<img src="/images/favicon.png" class="w-8" />
<img alt="Coracle Logo" src="/images/favicon.png" class="w-8" />
<h1 class="staatliches text-3xl">Coracle</h1>
</Anchor>
{#if $mostRecentAlert > $lastCheckedAlerts || hasNewMessages}
@ -272,12 +272,12 @@
{#if keys.canSign()}
<div class="fixed bottom-0 right-0 m-8">
<a
<button
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
items-center border border-dark shadow-2xl cursor-pointer"
items-center border border-dark shadow-2xl"
on:click={() => modal.set({type: 'note/create'})}>
<span class="fa-sold fa-plus fa-2xl" />
</a>
</button>
</div>
{/if}

View File

@ -31,7 +31,7 @@ export const ready = writable(false)
export const people = writable([])
// Bootstrap our people observable
db.people.toArray().then($p => {
db.table('people').toArray().then($p => {
people.set(createMap('pubkey', $p))
ready.set(true)
})
@ -51,7 +51,7 @@ export const updatePeople = async updates => {
people.update($people => ({...$people, ...updates}))
// Sync to our database
await db.people.bulkPut(Object.values(updates))
await db.table('people').bulkPut(Object.values(updates))
}
// Hooks
@ -126,7 +126,7 @@ const processProfileEvents = async events => {
}
},
default: () => {
console.log(`Received unsupported event type ${event.kind}`)
console.log(`Received unsupported event type ${e.kind}`)
},
}),
updated_at: now(),
@ -155,7 +155,7 @@ const processRoomEvents = async events => {
continue
}
const room = await db.rooms.get(roomId)
const room = await db.table('rooms').get(roomId)
// Merge edits but don't let old ones override new ones
if (room?.edited_at > e.created_at) {
@ -178,7 +178,7 @@ const processRoomEvents = async events => {
}
}
await db.rooms.bulkPut(Object.values(updates))
await db.table('rooms').bulkPut(Object.values(updates))
}
const processMessages = async events => {
@ -187,7 +187,7 @@ const processMessages = async events => {
.map(e => ({...e, recipient: Tags.from(e).type("p").values().first()}))
if (messages.length > 0) {
await db.messages.bulkPut(messages)
await db.table('messages').bulkPut(messages)
}
}

View File

@ -37,7 +37,7 @@ export const getFollows = pubkey => {
return Tags.wrap(person?.petnames || defaults.petnames).values().all()
}
export const getRelays = pubkey => {
export const getRelays = (pubkey?: string) => {
let relays = getPerson(pubkey)?.relays
if (!relays?.length) {
@ -71,7 +71,7 @@ export const publish = async (relays, event) => {
return signedEvent
}
export const load = async (relays, filter, opts) => {
export const load = async (relays, filter, opts?) => {
const events = await pool.request(relays, filter, opts)
await processEvents(events)
@ -79,7 +79,7 @@ export const load = async (relays, filter, opts) => {
return events
}
export const listen = async (relays, filter, onEvent, {shouldProcess = true} = {}) => {
export const listen = async (relays, filter, onEvent, {shouldProcess = true}: any = {}) => {
const sub = await pool.subscribe(relays, filter)
sub.onEvent(e => {

View File

@ -7,8 +7,8 @@ let signingFunction
const pubkey = synced('agent/user/pubkey')
const privkey = synced('agent/user/privkey')
const hasExtension = () => Boolean(window.nostr)
const canSign = () => Boolean(hasExtension() || get(privkey))
const getExtension = () => (window as {nostr?: any}).nostr
const canSign = () => Boolean(getExtension() || get(privkey))
const setPrivateKey = _privkey => {
privkey.set(_privkey)
@ -16,9 +16,11 @@ const setPrivateKey = _privkey => {
}
const setPublicKey = _pubkey => {
if (window.nostr) {
const nostr = getExtension()
if (nostr) {
signingFunction = async event => {
const {sig} = await window.nostr.signEvent(event)
const {sig} = await nostr.signEvent(event)
return sig
}
@ -44,8 +46,9 @@ const sign = async event => {
const getCrypt = () => {
const $privkey = get(privkey)
const nostr = getExtension()
if (!$privkey && !hasExtension()) {
if (!$privkey && !nostr) {
throw new Error('No encryption method available.')
}
@ -53,13 +56,13 @@ const getCrypt = () => {
encrypt: (pubkey, message) => {
return $privkey
? nip04.encrypt($privkey, pubkey, message)
: window.nostr.nip04.encrypt(pubkey, message)
: nostr.nip04.encrypt(pubkey, message)
},
decrypt: async (pubkey, message) => {
try {
return $privkey
? nip04.decrypt($privkey, pubkey, message)
: await window.nostr.nip04.decrypt(pubkey, message)
: await nostr.nip04.decrypt(pubkey, message)
} catch (e) {
console.error(e)
return `<Failed to decrypt message: ${e}>`
@ -72,6 +75,6 @@ const getCrypt = () => {
setPublicKey(get(pubkey))
export default {
pubkey, privkey, hasExtension, canSign, setPrivateKey, setPublicKey, clear,
pubkey, privkey, canSign, setPrivateKey, setPublicKey, clear,
sign, getCrypt,
}

View File

@ -71,7 +71,7 @@ const findConnection = url => find(whereEq({url}), connections)
const connect = async url => {
const conn = findConnection(url) || new Connection(url)
await db.relays.put({url})
await db.table('relays').put({url})
await Promise.race([conn.connect(), sleep(5000)])
if (conn.status === 'ready') {

View File

@ -17,7 +17,7 @@ const onChunk = async (relays, pubkey, events) => {
const context = await loaders.loadContext(relays, events, {threshold: 2})
const notes = threadify(events, context, {muffle: getMuffle()})
await db.alerts.bulkPut(notes)
await db.table('alerts').bulkPut(notes)
mostRecentAlert.update($t => events.reduce((t, e) => Math.max(t, e.created_at), $t))
}

View File

@ -14,7 +14,7 @@ import loaders from 'src/app/loaders'
export {toast, modal, settings, alerts, messages, logUsage}
export const login = async ({privkey, pubkey}, usingExtension = false) => {
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}, usingExtension = false) => {
if (privkey) {
keys.setPrivateKey(privkey)
} else {
@ -79,7 +79,7 @@ export const setRelayWriteCondition = async (url, write) => {
}
}
export const render = (note, {showEntire = false}) => {
export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const $people = get(people)
const peopleByPubkey = createMap(

View File

@ -44,7 +44,7 @@ const loadNetwork = async (relays, pubkey) => {
await loadPeople(tags.relays(), tags.values().all())
}
const loadContext = async (relays, notes, {loadParents = false, depth = 0, ...opts} = {}) => {
const loadContext = async (relays, notes, {loadParents = false, depth = 0, ...opts}: any = {}) => {
notes = ensurePlural(notes)
if (notes.length === 0) {
@ -58,7 +58,7 @@ const loadContext = async (relays, notes, {loadParents = false, depth = 0, ...op
const parentTags = uniq(chunk.map(findReply).filter(identity))
const parentIds = Tags.wrap(parentTags).values().all()
const combinedRelays = uniq(relays.concat(Tags.wrap(parentTags).relays()))
const filter = [{kinds: [1, 7], '#e': chunkIds}]
const filter = [{kinds: [1, 7], '#e': chunkIds} as {}]
if (authors.length > 0) {
filter.push({kinds: personKinds, authors})

View File

@ -25,7 +25,7 @@ const listen = async (relays, pubkey) => {
// Reload annotated messages, don't alert about messages to self
const messages = reject(
e => e.pubkey === e.recipient,
await db.messages.toArray()
await db.table('messages').toArray()
)
if (messages.length > 0) {

View File

@ -1,6 +1,7 @@
import Bugsnag from "@bugsnag/js"
import {prop} from "ramda"
import {uuid} from "hurdak/lib/hurdak"
import type {Writable} from 'svelte/store'
import {navigate} from "svelte-routing"
import {nip19} from 'nostr-tools'
import {writable, get} from "svelte/store"
@ -15,7 +16,11 @@ export const routes = {
// Toast
export const toast = writable(null)
export interface Toast<T> extends Writable<T> {
show(type: string, message: string, timeout?: number): void
}
export const toast = writable(null) as Toast<any>
toast.show = (type, message, timeout = 5) => {
const id = uuid()
@ -29,6 +34,7 @@ toast.show = (type, message, timeout = 5) => {
}, timeout * 1000)
}
// Modals
export const modal = {

View File

@ -9,9 +9,6 @@ Bugsnag.start({
import App from 'src/App.svelte'
// Annoying global always fails silently. TODO: figure out an eslint rule instead
window.find = null
const app = new App({
target: document.getElementById('app')
})

View File

@ -6,7 +6,7 @@
export let invertColors = false
</script>
<li
<div
on:click
in:fly={{y: 20}}
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
@ -15,4 +15,4 @@
"hover:bg-medium": interactive && invertColors,
})}>
<slot />
</li>
</div>

View File

@ -9,10 +9,10 @@
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import {user, getPerson} from 'src/agent'
import {render} from 'src/app'
import {renderNote} from 'src/app'
export let name
export let link
export let link = null
export let about
export let picture
export let loadMessages
@ -42,7 +42,7 @@
}
// flex-col means the first is the last
const getLastListItem = () => document.querySelector('ul[name=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
@ -123,7 +123,7 @@
<div class="flex gap-4 h-full">
<div class="relative w-full">
<div class="flex flex-col pt-20 pb-28 h-full">
<ul class="pb-6 p-4 overflow-auto flex-grow flex flex-col-reverse justify-start" name="messages">
<ul class="pb-6 p-4 overflow-auto flex-grow flex flex-col-reverse justify-start channel-messages">
{#each annotatedMessages as m (m.id)}
<li in:fly={{y: 20}} class="py-1 flex flex-col gap-2">
{#if type === 'chat' && m.showPerson}
@ -142,7 +142,7 @@
'bg-light text-black rounded-br-none': type === 'dm' && m.person.pubkey === $user.pubkey,
'bg-dark rounded-bl-none': type === 'dm' && m.person.pubkey !== $user.pubkey,
})}>
{@html render(m, {showEntire: true})}
{@html renderNote(m, {showEntire: true})}
</div>
</div>
</li>
@ -157,7 +157,9 @@
<div class="fixed z-10 top-16 w-full lg:-ml-56 lg:pl-56 border-b border-solid border-medium bg-dark">
<div class="p-4 flex items-start gap-4">
<div class="flex items-center gap-4">
<i class="fa fa-arrow-left text-2xl cursor-pointer" on:click={() => navigate("/chat")} />
<button
class="fa fa-arrow-left text-2xl cursor-pointer"
on:click={() => navigate("/chat")} />
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({picture})" />
@ -171,9 +173,9 @@
<div class="text-lg font-bold">{name || ''}</div>
{/if}
{#if editRoom}
<small class="cursor-pointer" on:click={editRoom}>
<button class="text-sm cursor-pointer" on:click={editRoom}>
<i class="fa-solid fa-edit" /> Edit
</small>
</button>
{/if}
</div>
<div class="flex items-center gap-2">
@ -199,12 +201,12 @@
on:keypress={onKeyPress}
class="w-full p-2 text-white bg-medium
placeholder:text-light outline-0 resize-none" />
<div
<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 ">
<i class="fa-solid fa-paper-plane fa-xl" />
</div>
</button>
</div>
</div>
{#if showNewMessages}

View File

@ -180,13 +180,13 @@
{#if suggestions.length > 0}
<div class="rounded border border-solid border-medium mt-2" in:fly={{y: 20}}>
{#each suggestions as person, i (person.pubkey)}
<div
<button
class="py-2 px-4 cursor-pointer"
class:bg-black={index !== i}
class:bg-dark={index === i}
on:click={() => pickSuggestion(person)}>
<Badge inert {person} />
</div>
</button>
{/each}
</div>
{/if}

View File

@ -2,7 +2,7 @@
import cx from "classnames"
export let wrapperClass = ""
export let value
export let value = ""
const className = cx(
$$props.class,

View File

@ -24,29 +24,29 @@
}
</script>
<div
<button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all
border border-solid border-black hover:border-medium hover:bg-dark"
on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative">
<span class="cursor-pointer" on:click={openPopover}>
<button class="cursor-pointer" on:click={openPopover}>
{quantify(note.people.length, 'person', 'people')} liked your note.
</span>
</button>
{#if isOpen}
<div in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
<div
<button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
<button
on:click={killEvent}
infly={{y: 20}}
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-3 gap-y-2 gap-x-4 z-20">
{#each uniqBy(prop('pubkey'), note.people) as person (person.pubkey)}
<Badge {person} />
{/each}
</div>
</button>
{/if}
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>
<div class="ml-6 text-light">
{ellipsize(note.content, 120)}
</div>
</div>
</button>

View File

@ -12,7 +12,7 @@
}} />
<div class="fixed inset-0 z-10">
<div
<button
class="absolute inset-0 opacity-75 bg-black cursor-pointer"
transition:fade
on:click={onEscape} />

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import cx from 'classnames'
import extractUrls from 'extract-urls'
import {nip19} from 'nostr-tools'
@ -9,7 +9,7 @@
import {Tags, findReply, findReplyId, displayPerson, isLike} from "src/util/nostr"
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {settings, modal, render} from "src/app"
import {settings, modal, renderNote} from "src/app"
import {formatTimestamp} from 'src/util/misc'
import Badge from "src/partials/Badge.svelte"
import Compose from "src/partials/Compose.svelte"
@ -45,7 +45,9 @@
$: flag = find(whereEq({pubkey: $user?.pubkey}), flags)
const onClick = e => {
if (interactive && !['I'].includes(e.target.tagName) && !e.target.closest('a')) {
const target = e.target as HTMLElement
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
modal.set({type: 'note/detail', note, relays})
}
}
@ -112,14 +114,18 @@
resetReply()
}
}
const onBodyClick = e => {
const target = e.target as HTMLElement
if (!target.closest('.fa-reply') && !target.closest('.note-reply')) {
resetReply()
}
}
</script>
<svelte:body
on:click={e => {
if (!e.target.closest('.fa-reply') && !e.target.closest('.note-reply')) {
resetReply()
}
}}
on:click={onBodyClick}
on:keydown={e => {
if (e.key === 'Escape') {
resetReply()
@ -150,30 +156,28 @@
</p>
{:else}
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
<p>{@html render(note, {showEntire})}</p>
<p>{@html renderNote(note, {showEntire})}</p>
{#each links.slice(-2) as link}
<div>
<div class="inline-block" on:click={e => e.stopPropagation()}>
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
</div>
</button>
</div>
{/each}
</div>
<div class="flex gap-6 text-light">
<div>
<i
class="fa fa-reply cursor-pointer"
on:click={startReply} />
<button class="fa fa-reply cursor-pointer" on:click|stopPropagation={startReply} />
{note.replies.length}
</div>
<div class={cx({'text-accent': like})}>
<i
<button
class="fa fa-heart cursor-pointer"
on:click={() => like ? deleteReaction(like) : react("+")} />
on:click|stopPropagation={() => like ? deleteReaction(like) : react("+")} />
{likes.length}
</div>
<div>
<i class="fa fa-flag cursor-pointer" on:click={() => react("-")} />
<button class="fa fa-flag cursor-pointer" on:click|stopPropagation={() => react("-")} />
{flags.length}
</div>
</div>
@ -185,13 +189,13 @@
<div transition:slide class="note-reply">
<div class="bg-medium border-medium border border-solid">
<Compose bind:this={reply} onSubmit={sendReply}>
<div
<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" />
</div>
</button>
</Compose>
</div>
{#if replyMentions.length > 0}
@ -201,7 +205,7 @@
</div>
{#each replyMentions as p}
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
<i class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
<button class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
{displayPerson(getPerson(p, true))}
</div>
{/each}
@ -214,10 +218,10 @@
{#if depth > 0}
<div class="ml-5 border-l border-solid border-medium">
{#if !showEntire && note.replies.length > 3}
<div class="ml-5 py-2 text-light cursor-pointer" on:click={onClick}>
<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 - 3, 'other reply', 'more replies')}
</div>
</button>
{/if}
{#each note.replies.slice(showEntire ? 0 : -3) as r (r.id)}
<svelte:self showParent={false} note={r} depth={depth - 1} {invertColors} {anchorId} />

View File

@ -48,12 +48,12 @@
<Content size="inherit" class="pt-6">
{#if newNotes.length > 0}
<div
<button
in:slide
class="cursor-pointer text-center underline text-light"
on:click={showNewNotes}>
Load {quantify(newNotes.length, 'new note')}
</div>
</button>
{/if}
<div>

View File

@ -36,7 +36,7 @@
href={url}
class="rounded border border-solid border-medium flex flex-col bg-white overflow-hidden">
{#if preview.image}
<img src={preview.image} />
<img alt="Link preview" src={preview.image} />
<div class="h-px bg-medium" />
{/if}
{#if preview.title}

View File

@ -33,7 +33,7 @@
})
</script>
<li
<button
class="flex gap-4 px-4 py-6 cursor-pointer hover:bg-medium transition-all rounded border border-solid border-medium bg-dark"
on:click={() => setRoom(room)}
in:fly={{y: 20}}>
@ -81,4 +81,4 @@
</p>
{/if}
</div>
</li>
</button>

View File

@ -3,7 +3,7 @@
export let setRoom
</script>
<div
<button
class="flex gap-4 px-2 py-4 cursor-pointer hover:bg-dark transition-all"
on:click={() => setRoom(room.id)}>
<div
@ -17,4 +17,4 @@
</p>
{/if}
</div>
</div>
</button>

View File

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

View File

@ -7,13 +7,13 @@
export let setActiveTab
</script>
<ul class="border-b border-solid border-dark flex pt-2" in:fly={{y: 20}}>
<div class="border-b border-solid border-dark flex pt-2" in:fly={{y: 20}}>
{#each tabs as tab}
<li
<button
class="cursor-pointer hover:border-b border-solid border-medium px-8 py-4"
class:border-b={activeTab === tab}
on:click={() => setActiveTab(tab)}>
{toTitle(tab)}
</li>
</button>
{/each}
</ul>
</div>

View File

@ -18,7 +18,7 @@
return createScroller(async () => {
limit += 10
const events = await db.alerts.toArray()
const events = await db.table('alerts').toArray()
const notes = events.filter(e => e.kind === 1)
const likes = events.filter(e => e.kind === 7)

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {objOf} from 'ramda'
import {nip19} from 'nostr-tools'
import Content from 'src/partials/Content.svelte'
@ -7,7 +7,7 @@
export let entity
const {type, data} = nip19.decode(entity)
const {type, data} = nip19.decode(entity) as {type: string, data: any}
const relays = (data.relays || []).map(objOf('url'))
</script>

View File

@ -19,8 +19,8 @@
const {mostRecentByPubkey} = messages
const rooms = lq(async () => {
const rooms = await db.rooms.where('joined').equals(1).toArray()
const messages = await db.messages.toArray()
const rooms = await db.table('rooms').where('joined').equals(1).toArray()
const messages = await db.table('messages').toArray()
const pubkeys = without([$user.pubkey], uniq(messages.flatMap(m => [m.pubkey, m.recipient])))
await loaders.loadPeople(getRelays(), pubkeys)
@ -31,7 +31,7 @@
})
const search = lq(async () => {
const rooms = await db.rooms.where('joined').equals(0).toArray()
const rooms = await db.table('rooms').where('joined').equals(0).toArray()
roomsCount = rooms.length
@ -49,11 +49,11 @@
}
const joinRoom = id => {
db.rooms.where('id').equals(id).modify({joined: 1})
db.table('rooms').where('id').equals(id).modify({joined: 1})
}
const leaveRoom = id => {
db.rooms.where('id').equals(id).modify({joined: 0})
db.table('rooms').where('id').equals(id).modify({joined: 0})
}
onMount(() => {

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {liveQuery} from 'dexie'
import {pluck} from 'ramda'
import {nip19} from 'nostr-tools'
@ -11,8 +11,8 @@
export let entity
let {data: roomId} = nip19.decode(entity)
let room = liveQuery(() => db.rooms.where('id').equals(roomId).first())
let {data: roomId} = nip19.decode(entity) as {data: string}
let room = liveQuery(() => db.table('rooms').where('id').equals(roomId).first())
const getRoomRelays = $room => {
let relays = getRelays()
@ -26,7 +26,7 @@
const listenForMessages = async cb => {
// Make sure we have our room so we can calculate relays
const $room = await db.rooms.where('id').equals(roomId).first()
const $room = await db.table('rooms').where('id').equals(roomId).first()
const relays = getRoomRelays($room)
return listen(

View File

@ -46,7 +46,10 @@
<div class="flex flex-col gap-1">
<strong>Public Key</strong>
<Input disabled value={$pubkey ? nip19.npubEncode($pubkey) : ''}>
<i slot="after" class="fa-solid fa-copy cursor-pointer" on:click={() => copyKey('public')} />
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey('public')} />
</Input>
<p class="text-sm text-light">
Your public key identifies your account. You can share this with people
@ -57,7 +60,10 @@
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input disabled type="password" value={nip19.nsecEncode($privkey)}>
<i slot="after" class="fa-solid fa-copy cursor-pointer" on:click={() => copyKey('private')} />
<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

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {fly} from 'svelte/transition'
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
@ -8,8 +8,10 @@
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const autoLogIn = async () => {
if (window.nostr) {
await login({pubkey: await window.nostr.getPublicKey()}, true)
const {nostr} = window as any
if (nostr) {
await login({pubkey: await nostr.getPublicKey()}, true)
} else {
modal.set({type: 'login/privkey'})
}

View File

@ -21,7 +21,7 @@
// Give them a moment to see the state transition. Dexie
// also apparently needs some time
setTimeout(() => {
window.location = '/login'
window.location.href = '/login'
}, 1000)
}
</script>

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {liveQuery} from 'dexie'
import {nip19} from 'nostr-tools'
import {sortBy, pluck} from 'ramda'
@ -13,8 +13,8 @@
export let entity
let crypt = keys.getCrypt()
let {data: pubkey} = nip19.decode(entity)
let person = liveQuery(() => db.people.get(pubkey))
let {data: pubkey} = nip19.decode(entity) as {data: string}
let person = liveQuery(() => db.table('people').get(pubkey))
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
@ -37,15 +37,20 @@
batch(300, async events => {
// Reload from db since we annotate messages there
const messageIds = pluck('id', events.filter(e => e.kind === 4))
const messages = await db.messages.where('id').anyOf(messageIds).toArray()
const messages = await db.table('messages').where('id').anyOf(messageIds).toArray()
cb(await decryptMessages(messages))
})
)
const loadMessages = async ({until, limit}) => {
const fromThem = await db.messages.where('pubkey').equals(pubkey).toArray()
const toThem = await db.messages.where('recipient').equals(pubkey).toArray()
const a = db.table('messages')
const b = a.where('pubkey')
const c = b.equals(pubkey)
const d = c.toArray()
const e = await d
const fromThem = await db.table('messages').where('pubkey').equals(pubkey).toArray()
const toThem = await db.table('messages').where('recipient').equals(pubkey).toArray()
const events = fromThem.concat(toThem).filter(e => e.created_at < until)
const messages = sortBy(e => -e.created_at, events).slice(0, limit)

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {last, find, reject} from 'ramda'
import {onMount, onDestroy} from 'svelte'
import {nip19} from 'nostr-tools'
@ -26,7 +26,7 @@
export let relays = null
let subs = []
let pubkey = nip19.decode(npub).data
let pubkey = nip19.decode(npub).data as string
let following = false
let followers = new Set()
let followersCount = 0

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
@ -28,7 +28,8 @@
values = pick(Object.keys(values), $user)
document.querySelector('[name=picture]').addEventListener('change', async e => {
const [file] = e.target.files
const target = e.target as HTMLInputElement
const [file] = target.files
if (file) {
const reader = new FileReader()

View File

@ -24,15 +24,15 @@
const {relays} = await res.json()
for (const url of relays) {
db.relays.put({url})
db.table('relays').put({url})
}
}).catch(noop)
for (const relay of defaults.relays) {
db.relays.put(relay)
db.table('relays').put(relay)
}
const knownRelays = liveQuery(() => db.relays.toArray())
const knownRelays = liveQuery(() => db.table('relays').toArray())
$: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
@ -93,7 +93,7 @@
<i class={url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
{last(url.split('://'))}
</strong>
<i class="fa fa-times cursor-pointer" on:click={() => leave(url)}/>
<button class="fa fa-times cursor-pointer" on:click={() => leave(url)} />
</div>
<p class="text-light">
{#if status[url] === 'error'}
@ -151,9 +151,9 @@
<strong>{name || url}</strong>
<p class="text-light">{description || ''}</p>
</div>
<a class="underline cursor-pointer" on:click={() => join(url)}>
<button class="underline cursor-pointer" on:click={() => join(url)}>
Join
</a>
</button>
</div>
{/if}
{/each}

View File

@ -12,7 +12,7 @@ export const copyToClipboard = text => {
const result = document.execCommand("copy")
document.body.removeChild(input)
activeElement.focus()
;(activeElement as HTMLElement).focus()
return result
}
@ -62,7 +62,7 @@ export const killEvent = e => {
e.stopImmediatePropagation()
}
export const fromParentOffset = (element, offset) => {
export const fromParentOffset = (element, offset): [HTMLElement, number] => {
for (const child of element.childNodes) {
if (offset <= child.textContent.length) {
return [child, offset]
@ -70,6 +70,8 @@ export const fromParentOffset = (element, offset) => {
offset -= child.textContent.length
}
throw new Error("Unable to find parent offset")
}
export const renderContent = content => {

View File

@ -73,7 +73,7 @@ export const formatTimestampRelative = ts => {
numeric: 'auto',
})
return formatter.format(-delta, unit)
return formatter.format(-delta, unit as Intl.RelativeTimeFormatUnit)
}
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
@ -147,6 +147,8 @@ export const getLastSync = (k, fallback = 0) => {
}
export class Cursor {
until: number
limit: number
constructor(limit = 10) {
this.until = now()
this.limit = limit

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {stripExifData} from "src/util/html"
@ -10,11 +10,12 @@
import {toast, modal} from "src/app"
import cmd from "src/app/cmd"
export let room = {}
export let room = {name: null, id: null, about: null, picture: null}
onMount(async () => {
document.querySelector('[name=picture]').addEventListener('change', async e => {
const [file] = e.target.files
const target = e.target as HTMLInputElement
const [file] = target.files
if (file) {
const reader = new FileReader()
@ -37,7 +38,7 @@
? await cmd.updateRoom(getRelays(), room)
: await cmd.createRoom(getRelays(), room)
await db.rooms.where('id').equals(room.id).modify({joined: 1})
await db.table('rooms').where('id').equals(room.id).modify({joined: 1})
toast.show("info", `Your room has been ${room.id ? 'updated' : 'created'}!`)

View File

@ -2,7 +2,7 @@
import {onMount} from 'svelte'
import {nip19} from 'nostr-tools'
import {fly} from 'svelte/transition'
import {load} from 'src/agent'
import {load, getRelays} from 'src/agent'
import {annotate} from 'src/app'
import loaders from 'src/app/loaders'
import Note from 'src/partials/Note.svelte'
@ -10,7 +10,7 @@
import Spinner from 'src/partials/Spinner.svelte'
export let note
export let relays
export let relays = getRelays()
let loading = true

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {nip19} from 'nostr-tools'
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
@ -10,7 +10,7 @@
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
const privkey = nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec
const privkey = (nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec) as string
if (!privkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {nip19} from 'nostr-tools'
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
@ -9,7 +9,7 @@
let npub = ''
const logIn = async () => {
const pubkey = npub.startsWith('npub') ? nip19.decode(npub).data : npub
const pubkey = (npub.startsWith('npub') ? nip19.decode(npub).data : npub) as string
if (!pubkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid public key.")

View File

@ -1 +1,3 @@
<script lang="ts">
export let q
</script>

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {nip19, generatePrivateKey} from 'nostr-tools'
import {copyToClipboard} from "src/util/html"
import Input from 'src/partials/Input.svelte'
@ -11,7 +11,7 @@
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
await login({privkey: nip19.decode(nsec).data})
await login({privkey: nip19.decode(nsec).data as string})
}
const copyKey = () => {
@ -31,7 +31,7 @@
<div class="flex-grow">
<Input disabled placeholder={"•".repeat(63)}>
<i slot="before" class="fa fa-key" />
<i slot="after" class="cursor-pointer fa fa-copy" on:click={copyKey} />
<button slot="after" class="cursor-pointer fa fa-copy" on:click={copyKey} />
</Input>
</div>
<Anchor type="button" on:click={logIn}>Log In</Anchor>

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"allowSyntheticDefaultImports": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"isolatedModules": false,
"importsNotUsedAsValues": "preserve"
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

View File

@ -1,5 +1,6 @@
import * as path from 'path'
import { defineConfig } from 'vite'
import sveltePreprocess from 'svelte-preprocess'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
@ -17,8 +18,14 @@ export default defineConfig({
protocolImports: true,
}),
svelte({
preprocess: sveltePreprocess(),
onwarn: (warning, handler) => {
if (warning.code.startsWith("a11y-")) return
const isA11y = warning.code.startsWith('a11y-')
if (["a11y-autofocus"].includes(warning.code)) return
if (warning.filename.includes("node_modules")) return
if (warning.filename.includes("Card.svelte") && isA11y) return
handler(warning)
},
}),