Add PersonFeed

This commit is contained in:
Jonathan Staab 2023-04-13 16:21:34 -05:00
parent b2cd58cf8b
commit 51b7de9c29
15 changed files with 306 additions and 214 deletions

View File

@ -12,6 +12,7 @@
import Onboarding from "src/app/views/Onboarding.svelte"
import NoteCreate from "src/app/views/NoteCreate.svelte"
import NoteDetail from "src/app/views/NoteDetail.svelte"
import PersonFeed from "src/app/views/PersonFeed.svelte"
import PersonList from "src/app/shared/PersonList.svelte"
import PersonProfileInfo from "src/app/views/PersonProfileInfo.svelte"
import PersonShare from "src/app/views/PersonShare.svelte"
@ -47,6 +48,8 @@
<LoginPubKey />
{:else if m.type === "login/connect"}
<LoginConnect />
{:else if m.type === "person/feed"}
<PersonFeed pubkey={m.pubkey} />
{:else if m.type === "person/info"}
<PersonProfileInfo person={m.person} />
{:else if m.type === "person/share"}

View File

@ -19,6 +19,7 @@
export let delta = timedelta(6, "hours")
export let shouldDisplay = always(true)
export let parentsTimeout = 500
export let invertColors = false
let notes = []
let notesBuffer = []
@ -145,7 +146,7 @@
<div class="flex flex-col gap-4">
{#each notes as note (note.id)}
<Note depth={2} {note} {feedRelay} {setFeedRelay} />
<Note depth={2} {note} {feedRelay} {setFeedRelay} {invertColors} />
{/each}
</div>

View File

@ -2,6 +2,7 @@
import {nip19} from "nostr-tools"
import {find, last} from "ramda"
import {onMount} from "svelte"
import {navigate} from "svelte-routing"
import {quantify} from "hurdak/lib/hurdak"
import {findRootId, findReplyId, displayPerson} from "src/util/nostr"
import {formatTimestamp} from "src/util/misc"
@ -63,6 +64,14 @@
}
}
const goToAuthor = () => {
if (document.querySelector(".modal-content")) {
navigate(routes.person(note.pubkey))
} else {
modal.push({type: "person/feed", pubkey: note.pubkey})
}
}
const goToParent = async () => {
const relays = getRelaysForEventParent(note)
@ -122,22 +131,34 @@
</div>
<div class="flex min-w-0 flex-grow flex-col gap-2">
<div class="flex flex-col items-start justify-between sm:flex-row sm:items-center">
<Popover triggerType={isMobile ? "click" : "mouseenter"}>
<div slot="trigger">
<Anchor
type="unstyled"
class="flex items-center gap-2 pr-16 text-lg font-bold sm:pr-0"
href={isMobile ? null : routes.person($author.pubkey)}>
<span>{displayPerson($author)}</span>
{#if $author.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />
{/if}
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={$author.pubkey} />
</div>
</Popover>
{#if isMobile}
<Anchor
type="unstyled"
class="flex items-center gap-2 pr-16 text-lg font-bold"
on:click={goToAuthor}>
<span>{displayPerson($author)}</span>
{#if $author.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />
{/if}
</Anchor>
{:else}
<Popover triggerType="mouseenter">
<div slot="trigger">
<Anchor
type="unstyled"
class="flex items-center gap-2 pr-16 text-lg font-bold"
on:click={goToAuthor}>
<span>{displayPerson($author)}</span>
{#if $author.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />
{/if}
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={$author.pubkey} />
</div>
</Popover>
{/if}
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: note.seen_on})}
class="text-sm text-gray-1"

View File

@ -0,0 +1,95 @@
<script lang="ts">
import {find} from "ramda"
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {displayPerson} from "src/util/nostr"
import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {getPubkeyWriteRelays} from "src/agent/relays"
import user from "src/agent/user"
import pool from "src/agent/pool"
export let person
const npub = nip19.npubEncode(person.pubkey)
const {petnamePubkeys, canPublish, mutes} = user
let actions = []
$: muted = find(m => m[1] === person.pubkey, $mutes)
$: following = $petnamePubkeys.includes(person.pubkey)
$: {
actions = []
actions.push({onClick: share, label: "Share", icon: "share-nodes"})
if (user.getPubkey() !== person.pubkey && $canPublish) {
actions.push({
onClick: () => navigate(`/messages/${npub}`),
label: "Message",
icon: "envelope",
})
if (muted) {
actions.push({onClick: unmute, label: "Unmute", icon: "microphone"})
} else if (user.getPubkey() !== person.pubkey) {
actions.push({onClick: mute, label: "Mute", icon: "microphone-slash"})
}
}
if (pool.forceUrls.length === 0) {
actions.push({onClick: openProfileInfo, label: "Details", icon: "info"})
}
if (user.getPubkey() === person.pubkey && $canPublish) {
actions.push({
onClick: () => navigate("/profile"),
label: "Edit",
icon: "edit",
})
}
}
const follow = async () => {
const [{url}] = getPubkeyWriteRelays(person.pubkey)
user.addPetname(person.pubkey, url, displayPerson(person))
}
const unfollow = async () => {
user.removePetname(person.pubkey)
}
const mute = async () => {
user.addMute("p", person.pubkey)
}
const unmute = async () => {
user.removeMute(person.pubkey)
}
const openProfileInfo = () => {
modal.push({type: "person/info", person})
}
const share = () => {
modal.push({type: "person/share", person})
}
</script>
<div class="flex items-center gap-3">
{#if $canPublish}
<Popover triggerType="mouseenter">
<div slot="trigger">
{#if following}
<i class="fa fa-user-minus cursor-pointer" on:click={unfollow} />
{:else if user.getPubkey() !== person.pubkey}
<i class="fa fa-user-plus cursor-pointer" on:click={follow} />
{/if}
</div>
<div slot="tooltip">{following ? "Unfollow" : "Follow"}</div>
</Popover>
{/if}
<OverflowMenu {actions} />
</div>

View File

@ -4,8 +4,9 @@
export let pubkey
export let relays
export let invertColors = false
const filter = {kinds: [1], authors: [pubkey]}
</script>
<Feed {relays} {filter} parentsTimeout={3000} delta={timedelta(1, "days")} />
<Feed {relays} {filter} {invertColors} parentsTimeout={3000} delta={timedelta(1, "days")} />

View File

@ -0,0 +1,59 @@
<script lang="ts">
import {onMount} from "svelte"
import {fly} from "svelte/transition"
import {tweened} from "svelte/motion"
import {numberFmt} from "src/util/misc"
import {modal} from "src/partials/state"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import pool from "src/agent/pool"
export let person
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
let followersCount = tweened(0, {interpolate, duration: 1000})
const showFollows = () => {
modal.push({type: "person/follows", pubkey: person.pubkey})
}
const showFollowers = () => {
modal.push({type: "person/followers", pubkey: person.pubkey})
}
onMount(async () => {
// Get our followers count
const count = await pool.count({kinds: [3], "#p": [person.pubkey]})
if (count) {
followersCount.set(count)
} else {
const followers = new Set()
await network.load({
relays: sampleRelays(getPubkeyWriteRelays(person.pubkey)),
shouldProcess: false,
filter: [{kinds: [3], "#p": [person.pubkey]}],
onChunk: events => {
for (const e of events) {
followers.add(e.pubkey)
}
followersCount.set(followers.size)
},
})
}
})
</script>
{#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>{numberFmt.format($followersCount)}</strong> followers
</button>
</div>
{/if}

View File

@ -1,13 +1,10 @@
<script lang="ts">
import {last, nth} from "ramda"
import {navigate} from "svelte-routing"
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"
import {getPersonWithFallback} from "src/agent/db"
import {watch} from "src/agent/db"
import {routes} from "src/app/state"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
@ -37,12 +34,7 @@
<div class="flex gap-4">
<PersonCircle size={14} person={$person} />
<div class="flex flex-grow flex-col gap-2">
<Anchor
type="unstyled"
class="flex items-center gap-2"
on:click={() => navigate(routes.person(pubkey))}>
<h2 class="text-lg">{displayPerson($person)}</h2>
</Anchor>
<h2 class="text-lg">{displayPerson($person)}</h2>
{#if $person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />

View File

@ -1,7 +1,5 @@
<script lang="ts">
import {find, last, propEq} from "ramda"
import Anchor from "src/partials/Anchor.svelte"
import Popover from "src/partials/Popover.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import user from "src/agent/user"
import {getRelayWithFallback} from "src/agent/db"
@ -42,15 +40,4 @@
}
</script>
{#if actions.length > 0}
{#if actions.length === 1}
<Popover triggerType="mouseenter">
<Anchor slot="trigger" type="button-circle" on:click={actions[0].onClick}>
<i class={`fa fa-${actions[0].icon}`} />
</Anchor>
<p slot="tooltip">{actions[0].label}</p>
</Popover>
{:else}
<OverflowMenu {actions} />
{/if}
{/if}
<OverflowMenu {actions} />

View File

@ -1,5 +1,4 @@
<script lang="ts">
import {displayRelay} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import RelayTitle from "src/app/shared/RelayTitle.svelte"
@ -22,9 +21,8 @@
{#if feedRelay.description}
<p>{feedRelay.description}</p>
{/if}
<p class="text-gray-4">
<i class="fa fa-info-circle" />
Below is your current feed including only notes seen on {displayRelay(feedRelay)}
<p class="border-l-2 border-gray-6 pl-4 text-gray-4">
Below is your current feed including only notes seen on this relay.
</p>
<div class="flex flex-col gap-4">

View File

@ -1,45 +1,34 @@
<script lang="ts">
import {last, identity, find} from "ramda"
import {onMount} from "svelte"
import {tweened} from "svelte/motion"
import {fly} from "svelte/transition"
import {last, identity} from "ramda"
import {navigate} from "svelte-routing"
import {log} from "src/util/logger"
import {parseHex} from "src/util/html"
import {numberFmt} from "src/util/misc"
import {displayPerson, toHex} from "src/util/nostr"
import {modal, theme, getThemeColor} from "src/partials/state"
import {theme, getThemeColor} from "src/partials/state"
import Tabs from "src/partials/Tabs.svelte"
import Content from "src/partials/Content.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Notes from "src/app/views/PersonNotes.svelte"
import Likes from "src/app/views/PersonLikes.svelte"
import Relays from "src/app/views/PersonRelays.svelte"
import user from "src/agent/user"
import PersonActions from "src/app/shared/PersonActions.svelte"
import PersonNotes from "src/app/shared/PersonNotes.svelte"
import PersonLikes from "src/app/shared/PersonLikes.svelte"
import PersonRelays from "src/app/shared/PersonRelays.svelte"
import pool from "src/agent/pool"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import {getPersonWithFallback, watch} from "src/agent/db"
import {routes} from "src/app/state"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
import PersonStats from "src/app/shared/PersonStats.svelte"
export let npub
export let activeTab
export let relays = []
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
const {petnamePubkeys, canPublish, mutes} = user
const tabs = ["notes", "likes", pool.forceUrls.length === 0 && "relays"].filter(identity)
const pubkey = toHex(npub)
const person = watch("people", () => getPersonWithFallback(pubkey))
let following = false
let muted = false
let followersCount = tweened(0, {interpolate, duration: 1000})
let loading = true
let actions = []
let rgb, rgba
$: ownRelays = getPubkeyWriteRelays(pubkey)
@ -52,118 +41,11 @@
rgb = `rgba(${color.join(", ")})`
}
$: following = $petnamePubkeys.includes(pubkey)
$: muted = find(m => m[1] === pubkey, $mutes)
log("Person", npub, $person)
$: {
actions = []
if ($canPublish) {
if (following) {
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: share, label: "Share", icon: "share-nodes"})
if (user.getPubkey() !== pubkey && $canPublish) {
actions.push({
onClick: () => navigate(`/messages/${npub}`),
label: "Message",
icon: "envelope",
})
if (muted) {
actions.push({onClick: unmute, label: "Unmute", icon: "microphone"})
} else if (user.getPubkey() !== pubkey) {
actions.push({onClick: mute, label: "Mute", icon: "microphone-slash"})
}
}
if (pool.forceUrls.length === 0) {
actions.push({onClick: openProfileInfo, label: "Details", icon: "info"})
}
if (user.getPubkey() === pubkey && $canPublish) {
actions.push({
onClick: () => navigate("/profile"),
label: "Edit",
icon: "edit",
})
}
}
onMount(async () => {
log("Person", npub, $person)
document.title = displayPerson($person)
// Refresh our person
network.loadPeople([pubkey], {relays, force: true}).then(() => {
ownRelays = getPubkeyWriteRelays(pubkey)
loading = false
})
// Get our followers count
const count = await pool.count({kinds: [3], "#p": [pubkey]})
if (count) {
followersCount.set(count)
} else {
const followers = new Set()
await network.load({
relays,
shouldProcess: false,
filter: [{kinds: [3], "#p": [pubkey]}],
onChunk: events => {
for (const e of events) {
followers.add(e.pubkey)
}
followersCount.set(followers.size)
},
})
}
})
document.title = displayPerson($person)
const setActiveTab = tab => navigate(routes.person(pubkey, tab))
const showFollows = () => {
modal.push({type: "person/follows", pubkey})
}
const showFollowers = () => {
modal.push({type: "person/followers", pubkey})
}
const follow = async () => {
const [{url}] = relays
user.addPetname(pubkey, url, displayPerson($person))
}
const unfollow = async () => {
user.removePetname(pubkey)
}
const mute = async () => {
user.addMute("p", pubkey)
}
const unmute = async () => {
user.removeMute(pubkey)
}
const openProfileInfo = () => {
modal.push({type: "person/info", person: $person})
}
const share = () => {
modal.push({type: "person/share", person: $person})
}
</script>
<div
@ -190,31 +72,22 @@
</div>
{/if}
</div>
<OverflowMenu {actions} />
<PersonActions person={$person} />
</div>
<PersonAbout person={$person} />
{#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>{numberFmt.format($followersCount)}</strong> followers
</button>
</div>
{/if}
<PersonStats person={$person} />
</div>
</div>
<Tabs {tabs} {activeTab} {setActiveTab} />
{#if activeTab === "notes"}
<Notes {pubkey} {relays} />
<PersonNotes {pubkey} {relays} />
{:else if activeTab === "likes"}
<Likes {pubkey} {relays} />
<PersonLikes {pubkey} {relays} />
{:else if activeTab === "relays"}
{#if ownRelays.length > 0}
<Relays relays={ownRelays} />
<PersonRelays relays={ownRelays} />
{:else if loading}
<Spinner />
{:else}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import {last} from "ramda"
import {displayPerson} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import PersonActions from "src/app/shared/PersonActions.svelte"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import {getPersonWithFallback, watch} from "src/agent/db"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
import PersonNotes from "src/app/shared/PersonNotes.svelte"
import PersonStats from "src/app/shared/PersonStats.svelte"
import {routes} from "src/app/state"
export let pubkey
const person = watch("people", () => getPersonWithFallback(pubkey))
$: relays = sampleRelays(getPubkeyWriteRelays(pubkey))
document.title = displayPerson($person)
</script>
<Content>
<div class="flex gap-4 text-gray-1">
<PersonCircle person={$person} size={16} class="sm:h-32 sm:w-32" />
<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">
<Anchor type="unstyled" href={routes.person(pubkey)}>
<h1 class="text-2xl">
{displayPerson($person)}
</h1>
</Anchor>
</div>
{#if $person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-gray-1">{last($person.verified_as.split("@"))}</span>
</div>
{/if}
</div>
<PersonActions person={$person} />
</div>
<PersonAbout person={$person} />
<PersonStats person={$person} />
</div>
</div>
<PersonNotes invertColors {pubkey} {relays} />
</Content>

View File

@ -32,15 +32,14 @@
class:cursor-pointer={onEscape}
on:click={onEscape}>
<div class="mt-12 min-h-full">
{#if onEscape}
<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-accent-light bg-accent text-white">
<i class="fa fa-times fa-lg" />
</div>
<div class="pointer-events-none sticky top-0 z-10 flex w-full justify-end p-2">
<div
class:opacity-0={!onEscape}
class="pointer-events-auto flex h-10 w-10 cursor-pointer items-center justify-center
rounded-full border border-solid border-accent-light bg-accent text-white">
<i class="fa fa-times fa-lg" />
</div>
{/if}
</div>
<div class="absolute mt-12 h-full w-full bg-gray-7" />
<div
class="relative h-full w-full cursor-auto border-t border-solid border-gray-6 bg-gray-7 pt-2 pb-10"

View File

@ -6,24 +6,36 @@
export let size = ""
</script>
<Popover theme="transparent">
<div slot="trigger" class="cursor-pointer px-2">
<i class={`fa fa-${size} fa-ellipsis-v`} />
</div>
<div
slot="tooltip"
let:instance
class="relative flex flex-col gap-2"
on:click={() => instance.hide()}>
<div
class="absolute top-0 right-0 bottom-0 w-32 rounded-full bg-gray-8"
style="filter: blur(15px) opacity(0.75)" />
{#each actions as { label, icon, onClick }}
<div class="relative z-10 cursor-pointer" on:click={onClick}>
<span class="absolute right-0 mr-12 mt-2">{label}</span>
<Anchor type="button-circle"><i class={`fa fa-${icon}`} /></Anchor>
{#if actions.length > 0}
{#if actions.length === 1}
<Popover triggerType="mouseenter">
<i
slot="trigger"
class={`fa fa-${actions[0].icon} cursor-pointer`}
on:click={actions[0].onClick} />
<p slot="tooltip">{actions[0].label}</p>
</Popover>
{:else}
<Popover theme="transparent">
<div slot="trigger" class="cursor-pointer px-2">
<i class={`fa fa-${size} fa-ellipsis-v`} />
</div>
{/each}
</div>
</Popover>
<div
slot="tooltip"
let:instance
class="relative flex flex-col gap-2"
on:click={() => instance.hide()}>
<div
class="absolute top-0 right-0 bottom-0 w-32 rounded-full bg-gray-8"
style="filter: blur(15px) opacity(0.75)" />
{#each actions as { label, icon, onClick }}
<div class="relative z-10 cursor-pointer" on:click={onClick}>
<span class="absolute right-0 mr-12 mt-2">{label}</span>
<Anchor type="button-circle"><i class={`fa fa-${icon}`} /></Anchor>
</div>
{/each}
</div>
</Popover>
{/if}
{/if}