Add likes/network tabs

This commit is contained in:
Jonathan Staab 2022-12-08 09:48:39 -08:00
parent 70ca44226c
commit 65c8f63721
10 changed files with 152 additions and 54 deletions

View File

@ -1,10 +1,13 @@
Bugs
- [ ] Permalink note detail (share/permalink button?)
- [ ] Back button is pretty broken
- [ ] Permalink note detail (share/permalink button?). Permalinks don't work
- [ ] Prevent tabs from re-mounting (or at least re- animating)
- [ ] Go "back" after adding a note
- [ ] uniq and sortBy are sprinkled all over the place, figure out a better solution
- [ ] With link/image previews, remove the url from the note body if it's on a separate last line
- [ ] Search page is slow and likes don't show up. Probably move this server-side
- [ ] Replies counts aren't showing on replies
Features

View File

@ -102,7 +102,7 @@
{/key}
</Route>
<Route path="/chat/:room/edit" component={ChatEdit} />
<Route path="/users/:pubkey" let:params>
<Route path="/users/:pubkey/:activeTab" let:params>
{#key params.pubkey}
<UserDetail {...params} />
{/key}
@ -122,7 +122,7 @@
>
{#if $user}
<li>
<a href={`/users/${$user.pubkey}`} class="flex gap-2 px-4 py-2 pb-6 items-center">
<a href={`/users/${$user.pubkey}/notes`} class="flex gap-2 px-4 py-2 pb-6 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$user.picture})" />

86
src/partials/Likes.svelte Normal file
View File

@ -0,0 +1,86 @@
<script>
import {onMount, onDestroy} from 'svelte'
import {fly} from 'svelte/transition'
import {uniqBy, sortBy, prop, identity} from 'ramda'
import {switcherFn} from 'hurdak/lib/hurdak'
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/partials/Note.svelte"
import {Cursor, Listener, epoch, findReplyTo, channels} from 'src/state/nostr'
import {createScroller, annotateNotes, modal} from "src/state/app"
export let filter
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(findReplyTo).filter(identity)
if (noteIds.length === 0) {
return
}
const chunk = await channels.getter.all({kinds: [1], ids: noteIds})
const annotated = await annotateNotes(chunk, {showParents: true})
notes.update($notes => sortBy(n => -n.created_at, uniqBy(prop('id'), $notes.concat(annotated))))
}
onMount(async () => {
cursor = new Cursor(filter)
listener = new Listener(filter, e => switcherFn(e.kind, {5: () => addLikes([e])}))
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 note={n} />
{#each n.replies as r (r.id)}
<div class="ml-6 border-l border-solid border-medium">
<Note interactive isReply note={r} />
</div>
{/each}
</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>

View File

@ -1,7 +1,7 @@
<script>
import {onMount, onDestroy} from 'svelte'
import {fly} from 'svelte/transition'
import {uniqBy, reject, prop} from 'ramda'
import {uniqBy, sortBy, reject, prop} from 'ramda'
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/partials/Note.svelte"
import {Cursor, epoch} from 'src/state/nostr'
@ -29,7 +29,7 @@
const annotated = await annotateNotes(chunk, {showParents: true})
notes.update($notes => uniqBy(prop('id'), $notes.concat(annotated)))
notes.update($notes => sortBy(n => -n.created_at, uniqBy(prop('id'), $notes.concat(annotated))))
})
// Track loading based on cursor cutoff date

View File

@ -2,9 +2,9 @@
export let user
</script>
<a href={`/users/${user?.pubkey}`} class="flex gap-2 items-center">
<a href={`/users/${user.pubkey}/notes`} class="flex gap-2 items-center">
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({user?.picture})" />
<span class="text-lg font-bold">{user?.name || user?.pubkey.slice(0, 8)}</span>
style="background-image: url({user.picture})" />
<span class="text-lg font-bold">{user.name || user.pubkey.slice(0, 8)}</span>
</a>

View File

@ -43,7 +43,7 @@
} else {
await dispatch("account/update", values)
navigate(`/users/${$user.pubkey}`)
navigate(`/users/${$user.pubkey}/profile`)
toast.show("info", "Your profile has been updated!")
}

View File

@ -88,7 +88,7 @@
{#each (results || []) as e (e.pubkey)}
{#if e.pubkey !== $user.pubkey}
<li in:fly={{y: 20}}>
<a href="/users/{e.pubkey}" class="flex gap-4 my-4">
<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})" />

View File

@ -1,19 +1,27 @@
<script>
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 {user as currentUser} from 'src/state/user'
import {t, dispatch} from 'src/state/dispatch'
import {accounts, getFollow, modal} from "src/state/app"
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 authorNotes = writable([])
let following = getFollow(pubkey)
let user
$: user = $accounts[pubkey]
const setActiveTab = tab => navigate(`/users/${pubkey}/${tab}`)
const follow = () => {
const petnames = $currentUser.petnames
@ -38,21 +46,20 @@
}
</script>
{#if user}
<div class="max-w-2xl m-auto flex flex-col gap-4 py-8 px-4">
<div class="max-w-xl m-auto flex flex-col gap-4 py-8 px-4">
<div class="flex flex-col gap-4" in:fly={{y: 20}}>
<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}</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}
@ -71,7 +78,12 @@
</div>
</div>
</div>
<div class="h-px bg-medium" in:fly={{y: 20, delay: 200}} />
<Notes notes={authorNotes} filter={{kinds: [1], authors: [pubkey]}} />
</div>
<Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'}
<Notes notes={notes} filter={{kinds: [1], authors: [pubkey]}} />
{:else if activeTab === 'likes'}
<Likes notes={likes} filter={{kinds: [7], authors: [pubkey]}} />
{:else if activeTab === 'network'}
<Notes notes={network} filter={{kinds: [1], authors}} shouldMuffle />
{/if}

View File

@ -1,8 +1,8 @@
import {when, assoc, prop, identity, whereEq, reverse, uniq, sortBy, uniqBy, find, last, pluck, groupBy} from 'ramda'
import {when, prop, identity, whereEq, reverse, uniq, sortBy, uniqBy, find, last, pluck, groupBy} from 'ramda'
import {debounce} from 'throttle-debounce'
import {writable, get} from 'svelte/store'
import {navigate} from "svelte-routing"
import {switcherFn, ensurePlural} from 'hurdak/lib/hurdak'
import {switcherFn} from 'hurdak/lib/hurdak'
import {getLocalJson, setLocalJson, now, timedelta, sleep} from "src/util/misc"
import {user} from 'src/state/user'
import {epoch, filterMatches, Listener, channels, relays, findReplyTo} from 'src/state/nostr'
@ -179,38 +179,35 @@ export const notesListener = (notes, filter, {shouldMuffle = false} = {}) => {
reactions: n.reactions.filter(e => !ids.includes(e.id)),
}))
return new Listener(
ensurePlural(filter).map(assoc('since', now())),
e => switcherFn(e.kind, {
1: async () => {
const id = findReplyTo(e)
return new Listener(filter, e => switcherFn(e.kind, {
1: async () => {
const id = findReplyTo(e)
if (shouldMuffle && Math.random() > getMuffleValue(e.pubkey)) {
return
}
if (id) {
const [reply] = await annotateNotes([e])
updateNote(id, n => ({...n, replies: n.replies.concat(reply)}))
} else if (filterMatches(filter, e)) {
const [note] = await annotateNotes([e])
notes.update($notes => uniqBy(prop('id'), [note].concat($notes)))
}
},
5: () => {
const ids = e.tags.map(t => t[1])
notes.update($notes => deleteNotes($notes, ids))
},
7: () => {
const id = findReplyTo(e)
updateNote(id, n => ({...n, reactions: n.reactions.concat(e)}))
if (shouldMuffle && Math.random() > getMuffleValue(e.pubkey)) {
return
}
})
)
if (id) {
const [reply] = await annotateNotes([e])
updateNote(id, n => ({...n, replies: n.replies.concat(reply)}))
} else if (filterMatches(filter, e)) {
const [note] = await annotateNotes([e])
notes.update($notes => uniqBy(prop('id'), [note].concat($notes)))
}
},
5: () => {
const ids = e.tags.map(t => t[1])
notes.update($notes => deleteNotes($notes, ids))
},
7: () => {
const id = findReplyTo(e)
updateNote(id, n => ({...n, reactions: n.reactions.concat(e)}))
}
}))
}
// UI

View File

@ -1,6 +1,6 @@
import {writable, get} from 'svelte/store'
import {relayPool, getPublicKey} from 'nostr-tools'
import {last, find, intersection, uniqBy, prop} from 'ramda'
import {assoc, last, find, intersection, uniqBy, prop} from 'ramda'
import {first, noop, ensurePlural} from 'hurdak/lib/hurdak'
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
@ -184,7 +184,7 @@ export class Cursor {
export class Listener {
constructor(filter, onEvent) {
this.filter = ensurePlural(filter)
this.filter = ensurePlural(filter).map(assoc('since', now()))
this.onEvent = onEvent
this.since = now()
this.sub = null