mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Move stuff to views
This commit is contained in:
parent
f4a706e12b
commit
03ce671814
@ -39,6 +39,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
|
||||
|
||||
- [ ] Check firefox - in dev it won't work, but it should in production
|
||||
- [ ] Re-implement muffle
|
||||
- [ ] Rename users/accounts to people
|
||||
- https://vitejs.dev/guide/features.html#web-workers
|
||||
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
|
||||
- https://web.dev/module-workers/
|
||||
|
@ -16,9 +16,9 @@
|
||||
import {modal, logout, alerts} from "src/state/app"
|
||||
import {user} from 'src/state/user'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import NoteDetail from "src/partials/NoteDetail.svelte"
|
||||
import NoteDetail from "src/views/NoteDetail.svelte"
|
||||
import NotFound from "src/routes/NotFound.svelte"
|
||||
// import Search from "src/routes/Search.svelte"
|
||||
import Search from "src/routes/Search.svelte"
|
||||
import Alerts from "src/routes/Alerts.svelte"
|
||||
import Notes from "src/routes/Notes.svelte"
|
||||
import Login from "src/routes/Login.svelte"
|
||||
@ -27,7 +27,7 @@
|
||||
import Keys from "src/routes/Keys.svelte"
|
||||
import RelayList from "src/routes/RelayList.svelte"
|
||||
import AddRelay from "src/routes/AddRelay.svelte"
|
||||
// import UserDetail from "src/routes/UserDetail.svelte"
|
||||
import UserDetail from "src/routes/UserDetail.svelte"
|
||||
// import UserAdvanced from "src/routes/UserAdvanced.svelte"
|
||||
import NoteCreate from "src/routes/NoteCreate.svelte"
|
||||
// import Chat from "src/routes/Chat.svelte"
|
||||
@ -54,8 +54,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', e => console.error(e))
|
||||
|
||||
onMount(() => {
|
||||
// Poll for new notifications
|
||||
(async function pollForNotifications() {
|
||||
@ -96,13 +94,7 @@
|
||||
<div use:links class="h-full">
|
||||
<div class="pt-16 text-white h-full">
|
||||
<Route path="/alerts" component={Alerts} />
|
||||
<!--
|
||||
<Route path="/search/:type" let:params>
|
||||
{#key params.type}
|
||||
<Search {...params} />
|
||||
{/key}
|
||||
</Route>
|
||||
-->
|
||||
<Route path="/search/:type" component={Search} />
|
||||
<Route path="/notes/:activeTab" component={Notes} />
|
||||
<Route path="/notes/new" component={NoteCreate} />
|
||||
<!--
|
||||
@ -114,12 +106,12 @@
|
||||
{/key}
|
||||
</Route>
|
||||
<Route path="/chat/:room/edit" component={ChatEdit} />
|
||||
-->
|
||||
<Route path="/users/:pubkey/:activeTab" let:params>
|
||||
{#key params.pubkey + params.activeTab}
|
||||
{#key params.pubkey}
|
||||
<UserDetail {...params} />
|
||||
{/key}
|
||||
</Route>
|
||||
-->
|
||||
<Route path="/keys" component={Keys} />
|
||||
<Route path="/relays" component={RelayList} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
|
@ -1,79 +0,0 @@
|
||||
<script>
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {uniqBy, sortBy, prop, identity} from 'ramda'
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {createScroller, notesListener, annotateNotes} from "src/util/notes"
|
||||
import {Cursor, epoch, findReply, channels} from 'src/state/nostr'
|
||||
import {modal} from "src/state/app"
|
||||
|
||||
export let author
|
||||
export let notes
|
||||
|
||||
let cursor
|
||||
let listener
|
||||
let scroller
|
||||
let modalUnsub
|
||||
let interval
|
||||
let loading = true
|
||||
|
||||
const addLikes = async likes => {
|
||||
const noteIds = likes.filter(e => e.content === '+').map(findReply).filter(identity)
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const chunk = await channels.getter.all({kinds: [1], ids: noteIds})
|
||||
const annotated = await annotateNotes(chunk, {showParent: false})
|
||||
|
||||
notes.update($notes => sortBy(n => -n.created_at, uniqBy(prop('id'), $notes.concat(annotated))))
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
cursor = new Cursor({kinds: [7], authors: [author]})
|
||||
listener = notesListener(notes, {kinds: [1, 5, 7], authors: [author]})
|
||||
scroller = createScroller(cursor, addLikes)
|
||||
|
||||
// Track loading based on cursor cutoff date
|
||||
interval = setInterval(() => {
|
||||
loading = cursor.since > epoch
|
||||
}, 1000)
|
||||
|
||||
// When a modal opens, suspend our subscriptions
|
||||
modalUnsub = modal.subscribe(async $modal => {
|
||||
if ($modal) {
|
||||
cursor.stop()
|
||||
listener.stop()
|
||||
scroller.stop()
|
||||
} else {
|
||||
cursor.start()
|
||||
listener.start()
|
||||
scroller.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
scroller?.stop()
|
||||
modalUnsub?.()
|
||||
clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={scroller?.start} />
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each $notes as n (n.id)}
|
||||
<li><Note interactive noReply note={n} depth={1} /></li>
|
||||
{:else}
|
||||
{#if loading}
|
||||
<li><Spinner /></li>
|
||||
{:else}
|
||||
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
@ -1,52 +0,0 @@
|
||||
<script>
|
||||
import {onMount} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import {notesListener, annotateNotes} from "src/util/notes"
|
||||
import {channels} from "src/state/nostr"
|
||||
import {modal} from "src/state/app"
|
||||
import {user} from "src/state/user"
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
|
||||
export let note
|
||||
|
||||
let notes = writable([])
|
||||
let cursor
|
||||
let listener
|
||||
|
||||
onMount(() => {
|
||||
channels.getter
|
||||
.all({kinds: [1], ids: [note.id]})
|
||||
.then(async $notes => {
|
||||
notes.set(await annotateNotes($notes))
|
||||
})
|
||||
|
||||
listener = notesListener(notes, [
|
||||
{kinds: [1, 5, 7], '#e': [note.id]},
|
||||
// We can't target reaction deletes by e tag, so get them
|
||||
// all so we can support toggling like/flags for our user
|
||||
{kinds: [5], authors: $user ? [$user.pubkey] : []}
|
||||
])
|
||||
|
||||
// Populate our initial empty space
|
||||
listener.start()
|
||||
|
||||
// Unsubscribe when modal closes so that others can re-subscribe sooner
|
||||
const unsubModal = modal.subscribe($modal => {
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubModal()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each $notes as n (n.id)}
|
||||
<div n:fly={{y: 20}}>
|
||||
<Note showEntire showParent invertColors anchorId={note.id} note={n} depth={2} />
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/each}
|
@ -1,59 +0,0 @@
|
||||
<script>
|
||||
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
|
||||
import {createMap} from 'hurdak/lib/hurdak'
|
||||
import {findReply, findRoot} from 'src/util/nostr'
|
||||
import {fly} from 'svelte/transition'
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let filter
|
||||
export let shouldMuffle = false
|
||||
|
||||
const toThread = async notes => {
|
||||
const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity)
|
||||
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()
|
||||
|
||||
const allNotes = uniqBy(prop('id'), notes.concat(ancestors))
|
||||
const notesById = createMap('id', allNotes)
|
||||
const notesByRoot = groupBy(
|
||||
n => {
|
||||
const rootId = findRoot(n)
|
||||
const parentId = findReply(n)
|
||||
|
||||
// Actually dereference the notes in case we weren't able to retrieve them
|
||||
if (notesById[rootId]) {
|
||||
return rootId
|
||||
}
|
||||
|
||||
if (notesById[parentId]) {
|
||||
return parentId
|
||||
}
|
||||
|
||||
return n.id
|
||||
},
|
||||
allNotes
|
||||
)
|
||||
|
||||
return Object.keys(notesByRoot).map(id => notesById[id])
|
||||
}
|
||||
|
||||
const notes = relay.lq(async () => {
|
||||
return await toThread(
|
||||
await relay.filterEvents(filter).limit(10).reverse().sortBy('created_at')
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each ($notes || []) as n (n.id)}
|
||||
<li><Note interactive note={n} depth={2} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if $notes?.length === 0}
|
||||
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
No notes found.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
@ -3,8 +3,8 @@ import {filterTags} from 'src/util/nostr'
|
||||
|
||||
export const db = new Dexie('coracle/relay')
|
||||
|
||||
db.version(2).stores({
|
||||
events: '++id, pubkey, created_at, kind, content',
|
||||
db.version(3).stores({
|
||||
events: '++id, pubkey, created_at, kind, content, reply, root',
|
||||
users: '++pubkey, name, about',
|
||||
tags: '++key, event, value',
|
||||
})
|
||||
|
@ -18,13 +18,21 @@ const lq = f => liveQuery(async () => {
|
||||
|
||||
const ensureContext = async e => {
|
||||
// We can't return a promise, so use setTimeout instead
|
||||
const user = await db.users.where('pubkey').equals(e.pubkey).first()
|
||||
const user = await db.users.where('pubkey').equals(e.pubkey).first() || {
|
||||
muffle: [],
|
||||
petnames: [],
|
||||
updated_at: 0,
|
||||
pubkey: e.pubkey,
|
||||
}
|
||||
|
||||
// Throttle updates for users
|
||||
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
|
||||
await pool.updateUser(user || {pubkey: e.pubkey, updated_at: 0})
|
||||
if (user.updated_at < now() - timedelta(1, 'hours')) {
|
||||
Object.assign(user, await pool.getUserInfo({pubkey: e.pubkey, ...user}))
|
||||
}
|
||||
|
||||
// Even if we didn't find a match, save it so we don't keep trying to refresh
|
||||
db.users.put({...user, updated_at: now()})
|
||||
|
||||
// TODO optimize this like user above so we're not double-fetching
|
||||
await pool.fetchContext(e)
|
||||
}
|
||||
@ -79,6 +87,25 @@ const findReaction = async (id, filter) =>
|
||||
const countReactions = async (id, filter) =>
|
||||
(await filterReactions(id, filter)).length
|
||||
|
||||
const findNote = async id => {
|
||||
const [note, children] = await Promise.all([
|
||||
db.events.get(id),
|
||||
db.events.where('reply').equals(id),
|
||||
])
|
||||
|
||||
const [replies, reactions, user, html] = await Promise.all([
|
||||
children.clone().filter(e => e.kind === 1).toArray(),
|
||||
children.clone().filter(e => e.kind === 7).toArray(),
|
||||
db.users.get(note.pubkey),
|
||||
renderNote(note, {showEntire: false}),
|
||||
])
|
||||
|
||||
return {
|
||||
...note, reactions, user, html,
|
||||
replies: await Promise.all(replies.map(r => findNote(r.id))),
|
||||
}
|
||||
}
|
||||
|
||||
const renderNote = async (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
|
||||
@ -105,5 +132,5 @@ const renderNote = async (note, {showEntire = false}) => {
|
||||
|
||||
export default {
|
||||
db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions,
|
||||
findReaction, filterReplies, renderNote,
|
||||
findReaction, filterReplies, findNote, renderNote,
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {relayPool, getPublicKey} from 'nostr-tools'
|
||||
import {noop, switcherFn, uuid} from 'hurdak/lib/hurdak'
|
||||
import {now, timedelta} from "src/util/misc"
|
||||
import {filterTags} from "src/util/nostr"
|
||||
import {now, randomChoice, timedelta} from "src/util/misc"
|
||||
import {filterTags, findReply, findRoot} from "src/util/nostr"
|
||||
import {db} from 'src/relay/db'
|
||||
|
||||
// ============================================================================
|
||||
@ -9,43 +10,78 @@ import {db} from 'src/relay/db'
|
||||
|
||||
const pool = relayPool()
|
||||
|
||||
const post = (topic, payload) => postMessage({topic, payload})
|
||||
|
||||
const req = ({filter, onEvent, onEose = noop}) => {
|
||||
// If we don't have any relays, we'll wait forever for an eose, but
|
||||
// we already know we're done. Use a timeout since callers are
|
||||
// expecting this to be async and we run into errors otherwise.
|
||||
if (pool.relays.length === 0) {
|
||||
onEose()
|
||||
|
||||
return {unsub: noop}
|
||||
class Channel {
|
||||
constructor(name) {
|
||||
this.name = name
|
||||
this.p = Promise.resolve()
|
||||
}
|
||||
async sub(filter, onEvent, onEose = noop) {
|
||||
// If we don't have any relays, we'll wait forever for an eose, but
|
||||
// we already know we're done. Use a timeout since callers are
|
||||
// expecting this to be async and we run into errors otherwise.
|
||||
if (Object.keys(pool.relays).length === 0) {
|
||||
setTimeout(onEose)
|
||||
|
||||
const eoseRelays = []
|
||||
return pool.sub({filter, cb: onEvent}, uuid(), r => {
|
||||
eoseRelays.push(r)
|
||||
|
||||
if (eoseRelays.length === pool.relays.length) {
|
||||
onEose()
|
||||
return {unsub: noop}
|
||||
}
|
||||
})
|
||||
|
||||
// Grab our spot in the queue, save resolve for later
|
||||
let resolve
|
||||
let p = this.p
|
||||
this.p = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
|
||||
// Make sure callers have to wait for the previous sub to be done
|
||||
// before they can get a new one.
|
||||
await p
|
||||
|
||||
// Start our subscription, wait for all relays to eose before
|
||||
// calling it done
|
||||
const eoseRelays = []
|
||||
const sub = pool.sub({filter, cb: onEvent}, this.name, r => {
|
||||
eoseRelays.push(r)
|
||||
|
||||
if (eoseRelays.length === Object.keys(pool.relays).length) {
|
||||
onEose()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
unsub: () => {
|
||||
sub.unsub()
|
||||
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
all(filter) {
|
||||
/* eslint no-async-promise-executor: 0 */
|
||||
return new Promise(async resolve => {
|
||||
const result = []
|
||||
|
||||
const sub = await this.sub(
|
||||
filter,
|
||||
e => result.push(e),
|
||||
r => {
|
||||
sub.unsub()
|
||||
|
||||
resolve(uniqBy(prop('id'), result))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Start up a subscription to get recent data and listen for new stuff
|
||||
export const channels = [
|
||||
new Channel('a'),
|
||||
new Channel('b'),
|
||||
new Channel('c'),
|
||||
]
|
||||
|
||||
const lastSync = now() - timedelta(1, 'days')
|
||||
const req = filter => randomChoice(channels).all(filter)
|
||||
|
||||
req({
|
||||
filter: {
|
||||
kinds: [1],
|
||||
since: lastSync,
|
||||
limit: 10,
|
||||
},
|
||||
onEvent: e => {
|
||||
post('events/put', e)
|
||||
},
|
||||
})
|
||||
const prepEvent = e => ({...e, root: findRoot(e), reply: findReply(e)})
|
||||
|
||||
// ============================================================================
|
||||
// Listen to messages posted from the main application
|
||||
@ -81,47 +117,37 @@ const setPublicKey = pubkey => {
|
||||
|
||||
const publishEvent = event => {
|
||||
pool.publish(event)
|
||||
db.events.put(event)
|
||||
db.events.put(prepEvent(event))
|
||||
}
|
||||
|
||||
const updateUser = async user => {
|
||||
if (!user.pubkey) throw new Error("Invalid user")
|
||||
const loadEvents = async filter => {
|
||||
const events = await req(filter)
|
||||
|
||||
user = {muffle: [], petnames: [], ...user}
|
||||
db.events.bulkPut(events.map(prepEvent))
|
||||
}
|
||||
|
||||
const sub = req({
|
||||
filter: {
|
||||
kinds: [0, 3, 12165],
|
||||
authors: [user.pubkey],
|
||||
since: user.updated_at,
|
||||
},
|
||||
onEvent: e => {
|
||||
switcherFn(e.kind, {
|
||||
0: () => Object.assign(user, JSON.parse(e.content)),
|
||||
3: () => Object.assign(user, {petnames: e.tags}),
|
||||
12165: () => Object.assign(user, {muffle: e.tags}),
|
||||
})
|
||||
},
|
||||
onEose: () => {
|
||||
sub.unsub()
|
||||
const getUserInfo = async user => {
|
||||
for (const e of await req({kinds: [0, 3, 12165], authors: [user.pubkey]})) {
|
||||
switcherFn(e.kind, {
|
||||
0: () => Object.assign(user, JSON.parse(e.content)),
|
||||
3: () => Object.assign(user, {petnames: e.tags}),
|
||||
12165: () => Object.assign(user, {muffle: e.tags}),
|
||||
})
|
||||
}
|
||||
|
||||
db.users.put({...user, updated_at: now()})
|
||||
},
|
||||
})
|
||||
return user
|
||||
}
|
||||
|
||||
const fetchContext = async event => {
|
||||
const sub = req({
|
||||
filter: [
|
||||
{kinds: [5, 7], '#e': [event.id]},
|
||||
{kinds: [5], 'ids': filterTags({tag: "e"}, event)},
|
||||
],
|
||||
onEvent: e => post('events/put', e),
|
||||
onEose: () => sub.unsub(),
|
||||
})
|
||||
const events = await req([
|
||||
{kinds: [5, 7], '#e': [event.id]},
|
||||
{kinds: [5], 'ids': filterTags({tag: "e"}, event)},
|
||||
])
|
||||
|
||||
db.events.bulkPut(events.map(prepEvent))
|
||||
}
|
||||
|
||||
export default {
|
||||
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
|
||||
publishEvent, updateUser, fetchContext,
|
||||
publishEvent, loadEvents, getUserInfo, fetchContext,
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
import {user} from 'src/state/user'
|
||||
import {alerts, modal} from 'src/state/app'
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
import Note from 'src/views/Note.svelte'
|
||||
|
||||
const events = relay.lq(async () => {
|
||||
const events = await relay
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {timedelta} from 'src/util/misc'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import Notes from "src/views/Notes.svelte"
|
||||
import {relays} from "src/state/nostr"
|
||||
import {user} from "src/state/user"
|
||||
|
||||
|
@ -1,70 +1,14 @@
|
||||
<script>
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {uniqBy, pluck, prop} from 'ramda'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {createScroller, annotateNotes} from "src/util/notes"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {relays, Cursor} from "src/state/nostr"
|
||||
import {user} from "src/state/user"
|
||||
import {ensureAccounts, accounts, modal} from "src/state/app"
|
||||
import SearchPeople from 'src/views/SearchPeople.svelte'
|
||||
import SearchNotes from 'src/views/SearchNotes.svelte'
|
||||
|
||||
export let type
|
||||
|
||||
const data = writable([])
|
||||
let q = ''
|
||||
let search
|
||||
let results
|
||||
let cursor
|
||||
let scroller
|
||||
let modalUnsub
|
||||
|
||||
$: search = fuzzy($data, {keys: type === 'people' ? ["name", "about", "pubkey"] : ["content"]})
|
||||
|
||||
$: {
|
||||
scroller?.start()
|
||||
results = search(q)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
cursor = new Cursor({kinds: type === 'people' ? [0] : [1]})
|
||||
scroller = createScroller(cursor, async chunk => {
|
||||
if (type === 'people') {
|
||||
await ensureAccounts(pluck('pubkey', chunk))
|
||||
|
||||
data.set(Object.values($accounts))
|
||||
} else {
|
||||
const annotated = await annotateNotes(chunk, {showParent: false})
|
||||
|
||||
data.update($data => uniqBy(prop('id'), $data.concat(annotated)))
|
||||
}
|
||||
})
|
||||
|
||||
// When a modal opens, suspend our subscriptions
|
||||
modalUnsub = modal.subscribe(async $modal => {
|
||||
if ($modal) {
|
||||
cursor.stop()
|
||||
scroller.stop()
|
||||
} else {
|
||||
cursor.start()
|
||||
scroller.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cursor?.stop()
|
||||
scroller?.stop()
|
||||
modalUnsub?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={scroller?.start} />
|
||||
|
||||
<ul class="border-b border-solid border-dark flex max-w-xl m-auto pt-2" in:fly={{y: 20}}>
|
||||
<li
|
||||
class="cursor-pointer hover:border-b border-solid border-medium"
|
||||
@ -79,51 +23,13 @@
|
||||
</ul>
|
||||
|
||||
<div class="max-w-xl m-auto mt-4" in:fly={{y: 20}}>
|
||||
<Input bind:value={q} placeholder="Search for {type}">
|
||||
<Input bind:value={q} placeholder="Search for people">
|
||||
<i slot="before" class="fa-solid fa-search" />
|
||||
</Input>
|
||||
</div>
|
||||
|
||||
{#if type === 'people'}
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each (results || []) as e (e.pubkey)}
|
||||
{#if e.pubkey !== $user.pubkey}
|
||||
<li in:fly={{y: 20}}>
|
||||
<a href="/users/{e.pubkey}/notes" class="flex gap-4 my-4">
|
||||
<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({e.picture})" />
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-2xl">{e.name || e.pubkey.slice(0, 8)}</h1>
|
||||
<p>{e.about || ''}</p>
|
||||
</div>
|
||||
</a>
|
||||
<li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
<SearchPeople {q} />
|
||||
{:else if type === 'notes'}
|
||||
<SearchNotes {q} />
|
||||
{/if}
|
||||
|
||||
{#if type === 'notes'}
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each (results || []) as e (e.id)}
|
||||
<li in:fly={{y: 20}}>
|
||||
<Note interactive showParent note={e} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<!-- This will always be sitting at the bottom in case infinite scrolling can't keep up -->
|
||||
<Spinner />
|
||||
|
||||
{#if $relays.length === 0}
|
||||
<div class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
You aren't yet connected to any relays. Please click <Anchor href="/relays"
|
||||
>here</Anchor
|
||||
> to get started.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -1,24 +1,20 @@
|
||||
<script>
|
||||
import {find} from 'ramda'
|
||||
import {writable} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import Likes from "src/partials/Likes.svelte"
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import Notes from "src/views/Notes.svelte"
|
||||
import Likes from "src/views/Likes.svelte"
|
||||
import {user as currentUser} from 'src/state/user'
|
||||
import {t, dispatch} from 'src/state/dispatch'
|
||||
import {accounts, modal} from "src/state/app"
|
||||
import {modal} from "src/state/app"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let pubkey
|
||||
export let activeTab
|
||||
|
||||
const user = $accounts[pubkey]
|
||||
const notes = writable([])
|
||||
const likes = writable([])
|
||||
const network = writable([])
|
||||
const authors = $currentUser ? $currentUser.petnames.map(t => t[1]) : []
|
||||
const user = relay.lq(() => relay.db.users.get(pubkey))
|
||||
|
||||
let following = $currentUser && find(t => t[1] === pubkey, $currentUser.petnames)
|
||||
|
||||
@ -26,7 +22,7 @@
|
||||
|
||||
const follow = () => {
|
||||
const petnames = $currentUser.petnames
|
||||
.concat([t("p", pubkey, user?.name)])
|
||||
.concat([t("p", pubkey, $user?.name)])
|
||||
|
||||
dispatch('account/petnames', petnames)
|
||||
|
||||
@ -43,7 +39,7 @@
|
||||
}
|
||||
|
||||
const openAdvanced = () => {
|
||||
modal.set({form: 'user/advanced', user: user || {pubkey}})
|
||||
modal.set({form: 'user/advanced', user: $user || {pubkey}})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -52,15 +48,15 @@
|
||||
<div class="flex gap-4">
|
||||
<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({user?.picture})" />
|
||||
style="background-image: url({$user?.picture})" />
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-2xl">{user?.name || pubkey.slice(0, 8)}</h1>
|
||||
<h1 class="text-2xl">{$user?.name || pubkey.slice(0, 8)}</h1>
|
||||
{#if $currentUser && $currentUser.pubkey !== pubkey}
|
||||
<i class="fa-solid fa-sliders cursor-pointer" on:click={openAdvanced} />
|
||||
{/if}
|
||||
</div>
|
||||
<p>{user?.about || ''}</p>
|
||||
<p>{$user?.about || ''}</p>
|
||||
</div>
|
||||
<div class="whitespace-nowrap">
|
||||
{#if $currentUser?.pubkey === pubkey}
|
||||
@ -80,11 +76,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} />
|
||||
{#if activeTab === 'notes'}
|
||||
<Notes notes={notes} filter={{kinds: [1], authors: [pubkey]}} />
|
||||
<Notes filter={{kinds: [1], authors: [pubkey]}} />
|
||||
{:else if activeTab === 'likes'}
|
||||
<Likes notes={likes} author={pubkey} />
|
||||
<Likes author={pubkey} />
|
||||
{:else if activeTab === 'network'}
|
||||
<Notes notes={network} filter={{kinds: [1], authors}} shouldMuffle />
|
||||
{#if $user}
|
||||
<Notes shouldMuffle filter={{kinds: [1], authors: $user.petnames.map(t => t[1])}} />
|
||||
{:else}
|
||||
<div class="py-16 max-w-xl m-auto flex justify-center">
|
||||
Unable to show network for this user.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -23,10 +23,10 @@ dispatch.addMethod("account/init", async (topic, { privkey, pubkey }) => {
|
||||
})
|
||||
|
||||
// Make sure we have data for this user
|
||||
await ensureAccounts([pubkey], {force: true})
|
||||
const {name} = await relay.pool.updateUser({pubkey})
|
||||
|
||||
// Tell the caller whether this user was found
|
||||
return {found: Boolean(get(user).name)}
|
||||
return {found: Boolean(name)}
|
||||
})
|
||||
|
||||
dispatch.addMethod("account/update", async (topic, updates) => {
|
||||
@ -58,8 +58,6 @@ dispatch.addMethod("account/muffle", async (topic, muffle) => {
|
||||
})
|
||||
|
||||
dispatch.addMethod("relay/join", async (topic, url) => {
|
||||
const $user = get(user)
|
||||
|
||||
relays.update(r => r.concat(url))
|
||||
})
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import pluck from "ramda/src/pluck"
|
||||
import {pluck} from "ramda"
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
|
||||
export const fuzzy = (data, opts = {}) => {
|
||||
@ -50,3 +51,28 @@ export const formatTimestamp = ts => {
|
||||
|
||||
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export const createScroller = loadMore => {
|
||||
const onScroll = debounce(1000, async () => {
|
||||
/* eslint no-constant-condition: 0 */
|
||||
|
||||
while (true) {
|
||||
// While we have empty space, fill it
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight} = document.body
|
||||
|
||||
if (scrollY + innerHeight + 600 < scrollHeight) {
|
||||
break
|
||||
}
|
||||
|
||||
loadMore()
|
||||
|
||||
await sleep(1000)
|
||||
}
|
||||
})
|
||||
|
||||
onScroll()
|
||||
|
||||
return onScroll
|
||||
}
|
||||
|
||||
export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)]
|
||||
|
26
src/views/Likes.svelte
Normal file
26
src/views/Likes.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {propEq} from 'ramda'
|
||||
import Note from "src/views/Note.svelte"
|
||||
import {findReply} from 'src/util/nostr'
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let author
|
||||
|
||||
const notes = relay.lq(async () => {
|
||||
const reactions = await relay.db.events
|
||||
.where('pubkey').equals(author).filter(propEq('kind', 7)).toArray()
|
||||
|
||||
return Promise.all(reactions.map(r => relay.findNote(findReply(r))))
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $notes}
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each $notes as n (n.id)}
|
||||
<li><Note interactive noReply note={n} depth={1} /></li>
|
||||
{:else}
|
||||
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import cx from 'classnames'
|
||||
import {liveQuery} from 'dexie'
|
||||
import {whereEq, find} from 'ramda'
|
||||
import {slide} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {hasParent, findLink} from 'src/util/html'
|
||||
@ -19,19 +19,21 @@
|
||||
export let depth = 0
|
||||
export let anchorId = null
|
||||
export let showParent = false
|
||||
export let showEntire = false
|
||||
export let invertColors = false
|
||||
|
||||
let reply = null
|
||||
|
||||
const link = $settings.showLinkPreviews ? findLink(note.content) : null
|
||||
const interactive = !anchorId || anchorId !== note.id
|
||||
const like = liveQuery(() => relay.findReaction(note.id, {content: "+", pubkey: $user?.pubkey}))
|
||||
const flag = liveQuery(() => relay.findReaction(note.id, {content: "-", pubkey: $user?.pubkey}))
|
||||
const likes = liveQuery(() => relay.countReactions(note.id, {content: "+"}))
|
||||
const flags = liveQuery(() => relay.countReactions(note.id, {content: "-"}))
|
||||
const replies = liveQuery(() => relay.filterReplies(note.id))
|
||||
const account = liveQuery(() => relay.db.users.get(note.pubkey))
|
||||
|
||||
let likes, flags, like, flag
|
||||
|
||||
$: {
|
||||
likes = note.reactions.filter(whereEq({content: '+'}))
|
||||
flags = note.reactions.filter(whereEq({content: '-'}))
|
||||
like = find(whereEq({pubkey: $user?.pubkey}), likes)
|
||||
flag = find(whereEq({pubkey: $user?.pubkey}), flags)
|
||||
}
|
||||
|
||||
relay.ensureContext(note)
|
||||
|
||||
@ -41,10 +43,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
const goToParent = () => {
|
||||
const parentId = findReply(note)
|
||||
|
||||
modal.set({note: {id: parentId}})
|
||||
const goToParent = async () => {
|
||||
modal.set({note: {id: findReply(note)}})
|
||||
}
|
||||
|
||||
const react = content => {
|
||||
@ -98,7 +98,7 @@
|
||||
|
||||
<Card on:click={onClick} {interactive} {invertColors}>
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<UserBadge user={{...$account, pubkey: note.pubkey}} />
|
||||
<UserBadge user={{...note.user, pubkey: note.pubkey}} />
|
||||
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
|
||||
</div>
|
||||
<div class="ml-6 flex flex-col gap-2">
|
||||
@ -107,17 +107,14 @@
|
||||
Reply to <Anchor on:click={goToParent}>{findReply(note).slice(0, 8)}</Anchor>
|
||||
</small>
|
||||
{/if}
|
||||
{#if $flag}
|
||||
{#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>
|
||||
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-ellipsis overflow-hidden">
|
||||
{#await relay.renderNote(note, {showEntire})}
|
||||
{:then content}
|
||||
{@html content}
|
||||
{/await}
|
||||
{@html note.html}
|
||||
{#if link}
|
||||
<div class="mt-2" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
@ -129,17 +126,17 @@
|
||||
<i
|
||||
class="fa-solid fa-reply cursor-pointer"
|
||||
on:click={startReply} />
|
||||
{$replies?.length}
|
||||
{note.replies.length}
|
||||
</div>
|
||||
<div class={cx({'text-accent': $like})}>
|
||||
<div class={cx({'text-accent': like})}>
|
||||
<i
|
||||
class="fa-solid fa-heart cursor-pointer"
|
||||
on:click={() => $like ? deleteReaction($like) : react("+")} />
|
||||
{$likes}
|
||||
on:click={() => like ? deleteReaction(like) : react("+")} />
|
||||
{likes.length}
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-flag cursor-pointer" on:click={() => react("-")} />
|
||||
{$flags}
|
||||
{flags.length}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -168,7 +165,7 @@
|
||||
{/if}
|
||||
|
||||
{#if depth > 0}
|
||||
{#each ($replies || []) as r (r.id)}
|
||||
{#each note.replies as r (r.id)}
|
||||
<div class="ml-5 border-l border-solid border-medium">
|
||||
<svelte:self note={r} depth={depth - 1} {invertColors} {anchorId} />
|
||||
</div>
|
14
src/views/NoteDetail.svelte
Normal file
14
src/views/NoteDetail.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import relay from 'src/relay'
|
||||
import Note from 'src/views/Note.svelte'
|
||||
|
||||
export let note
|
||||
|
||||
const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true}))
|
||||
</script>
|
||||
|
||||
{#if $observable}
|
||||
<div n:fly={{y: 20}}>
|
||||
<Note showParent invertColors anchorId={note.id} note={$observable} depth={2} />
|
||||
</div>
|
||||
{/if}
|
80
src/views/Notes.svelte
Normal file
80
src/views/Notes.svelte
Normal file
@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
|
||||
import {createMap} from 'hurdak/lib/hurdak'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import {findReply, findRoot} from 'src/util/nostr'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {createScroller} from 'src/util/misc'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Note from "src/views/Note.svelte"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let filter
|
||||
export let shouldMuffle = false
|
||||
|
||||
let limit = 10, init = now(), offset = 0, notes
|
||||
|
||||
const onScroll = createScroller(async () => {
|
||||
limit += 10
|
||||
offset += 1
|
||||
|
||||
const delta = timedelta(1, 'minutes')
|
||||
const since = init - delta * offset
|
||||
const until = init - delta * (offset - 1)
|
||||
|
||||
await relay.pool.loadEvents({...filter, since, until})
|
||||
|
||||
createNotesObservable()
|
||||
})
|
||||
|
||||
const createNotesObservable = () => {
|
||||
notes = relay.lq(async () => {
|
||||
const notes = await relay.filterEvents(filter).limit(limit).reverse().sortBy('created_at')
|
||||
const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity)
|
||||
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()
|
||||
|
||||
const allNotes = uniqBy(prop('id'), notes.concat(ancestors))
|
||||
const notesById = createMap('id', allNotes)
|
||||
const notesByRoot = groupBy(
|
||||
n => {
|
||||
const rootId = findRoot(n)
|
||||
const parentId = findReply(n)
|
||||
|
||||
// Actually dereference the notes in case we weren't able to retrieve them
|
||||
if (notesById[rootId]) {
|
||||
return rootId
|
||||
}
|
||||
|
||||
if (notesById[parentId]) {
|
||||
return parentId
|
||||
}
|
||||
|
||||
return n.id
|
||||
},
|
||||
allNotes
|
||||
)
|
||||
|
||||
return await Promise.all(Object.keys(notesByRoot).map(relay.findNote))
|
||||
})
|
||||
}
|
||||
|
||||
createNotesObservable()
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={onScroll} />
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each ($notes || []) as n (n.id)}
|
||||
<li><Note interactive note={n} depth={2} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if $notes?.length === 0}
|
||||
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
No notes found.
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
35
src/views/SearchNotes.svelte
Normal file
35
src/views/SearchNotes.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Note from "src/views/Note.svelte"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let q
|
||||
|
||||
let results = []
|
||||
|
||||
const search = relay.lq(async () => {
|
||||
const notes = await relay.filterEvents({kinds: [1]})
|
||||
.limit(5000).reverse().sortBy('created_at')
|
||||
|
||||
return fuzzy(notes, {keys: ["content"]})
|
||||
})
|
||||
|
||||
$: {
|
||||
if ($search) {
|
||||
Promise.all(
|
||||
$search(q).map(n => relay.findNote(n.id))
|
||||
).then(notes => {
|
||||
results = notes
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each results as e (e.id)}
|
||||
<li in:fly={{y: 20}}>
|
||||
<Note interactive note={e} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
42
src/views/SearchPeople.svelte
Normal file
42
src/views/SearchPeople.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {user} from "src/state/user"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let q
|
||||
|
||||
let results = []
|
||||
|
||||
const search = relay.lq(async () => {
|
||||
return fuzzy(await relay.db.users.toArray(), {keys: ["name", "about", "pubkey"]})
|
||||
})
|
||||
|
||||
$: {
|
||||
if ($search) {
|
||||
Promise.all(
|
||||
$search(q).map(n => relay.findNote(n.id))
|
||||
).then(notes => {
|
||||
results = notes
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each results as e (e.pubkey)}
|
||||
{#if e.pubkey !== $user.pubkey}
|
||||
<li in:fly={{y: 20}}>
|
||||
<a href="/users/{e.pubkey}/notes" class="flex gap-4 my-4">
|
||||
<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({e.picture})" />
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-2xl">{e.name || e.pubkey.slice(0, 8)}</h1>
|
||||
<p>{e.about || ''}</p>
|
||||
</div>
|
||||
</a>
|
||||
<li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
Loading…
Reference in New Issue
Block a user