Add person popover

This commit is contained in:
Jonathan Staab 2023-02-28 14:02:20 -06:00
parent 0a293ca354
commit c500c7354f
12 changed files with 287 additions and 103 deletions

View File

@ -5,6 +5,9 @@
- [x] Improve paste support
- [x] Add timestamps to messages
- [x] Support installation as a PWA
- [x] Fix social share image, add description
- [x] Clean up person detail actions, maybe click one circle and show the rest
- [x] Add person popover to notes/alerts
## 0.2.13

View File

@ -1,6 +1,8 @@
# Current
- [ ] Fix iOS
- [ ] Hover badge to view profile like twitter
- [ ] Cache follower numbers to avoid re-fetching so much
- [ ] Make the note relays button modal make sense, one relay with no explanation is not good
# Lightning
@ -18,22 +20,17 @@
# More
- [ ] Allow the user to disable likes/zaps
- [ ] Polls
- Find the best implementation https://github.com/nostr-protocol/nips/search?q=poll&type=issues
- Comment on all three nip drafts which one I implemented
- [ ] Micro app DSL
- [ ] Fix social share image, add description
- [ ] Sort feeds by created date on profile page?
- [ ] Implement https://media.nostr.band/
- [ ] Cache follower numbers to avoid re-fetching so much
- [ ] Groups - may need a new NIP, or maybe use topics
- [ ] Support https://github.com/nostr-protocol/nips/pull/211 as a bech32 entity
- [ ] Add new DM button to dms list
- [ ] Add suggested relays based on follows or topics
- [ ] Combine alerts/messages and any other top-level subscriptions to avoid sub limit
- [ ] Clean up person detail actions, maybe click one circle and show the rest
- [ ] Hover badge to view profile like twitter
- [ ] Show created date as bitcoin block height (add a setting?)
- [ ] Support relay auth
- [ ] Following indicator on person info
- [ ] Share button for notes, shows qr code and nevent
@ -75,6 +72,7 @@
- [ ] Release to android
- https://svelte-native.technology/docs
- https://ionic.io/blog/capacitor-everything-youve-ever-wanted-to-know
- Or just wrap it
- [ ] When publishing fails, enqueue and retry
- Track which relays the events should be published to, and which ones have succeeded
- Maybe notify and ask user which events to re-publish.

31
package-lock.json generated
View File

@ -33,6 +33,7 @@
"svelte-routing": "^1.6.0",
"svelte-switch": "^0.0.5",
"throttle-debounce": "^5.0.0",
"tippy.js": "^6.3.7",
"vite-plugin-node-polyfills": "^0.5.0",
"vite-plugin-pwa": "^0.14.4"
},
@ -2096,6 +2097,15 @@
"@octokit/openapi-types": "^16.0.0"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -7673,6 +7683,14 @@
"node": ">=0.6.0"
}
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@ -9982,6 +10000,11 @@
"@octokit/openapi-types": "^16.0.0"
}
},
"@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
},
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -13911,6 +13934,14 @@
"setimmediate": "^1.0.4"
}
},
"tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"requires": {
"@popperjs/core": "^2.9.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View File

@ -51,6 +51,7 @@
"svelte-routing": "^1.6.0",
"svelte-switch": "^0.0.5",
"throttle-debounce": "^5.0.0",
"tippy.js": "^6.3.7",
"vite-plugin-node-polyfills": "^0.5.0",
"vite-plugin-pwa": "^0.14.4"
}

View File

@ -57,3 +57,22 @@
html, body, #app, #app > div {
height: 100%;
}
/* Tippy */
.tippy-box {
background-color: #0f0f0e !important;
border: 1px solid #403D39;
box-shadow: 3px 3px 20px #0f0f0e,
3px -3px 20px #0f0f0e,
-3px 3px 20px #0f0f0e,
-3px -3px 20px #0f0f0e;
}
.tippy-box[data-placement^=top]>.tippy-arrow:before {
border-top-color: #403D39 !important;
}
.tippy-box[data-placement^=top]>.tippy-arrow {
bottom: -1px !important;
}

View File

@ -0,0 +1,62 @@
<script lang="ts">
import 'tippy.js/dist/tippy.css'
import 'tippy.js/animations/shift-away.css'
import tippy from 'tippy.js'
import {onMount} from 'svelte'
let trigger
let tooltip
let instance
onMount(() => {
instance = tippy(trigger, {
appendTo: () => document.body,
allowHTML: true,
interactive: true,
trigger: 'click',
animation: 'shift-away',
onShow: () => {
const [tooltipContents] = tooltip.children
instance.popper.querySelector('.tippy-content').appendChild(tooltipContents)
instance.popper.addEventListener('mouseleave', e => instance.hide())
instance.popper.addEventListener('click', e => {
if (e.target.closest('.tippy-close')) {
instance.hide()
}
})
},
onHidden: () => {
const [tooltipContents] = instance.popper.querySelector('.tippy-content').children
tooltip.appendChild(tooltipContents)
},
})
return () => {
instance.destroy()
}
})
</script>
<svelte:window
on:scroll={e => {
instance.hide()
}} />
<svelte:body
on:keydown={e => {
if (e.key === 'Escape') {
instance.hide()
}
}} />
<div bind:this={trigger}>
<slot name="trigger" />
</div>
<div bind:this={tooltip} class="hidden">
<div>
<slot name="tooltip" />
</div>
</div>

View File

@ -23,7 +23,7 @@
</script>
<ul
class="mt-16 pt-4 pb-20 lg:mt-4 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
class="mt-16 pt-4 pb-20 lg:mt-0 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
border-r border-medium text-white overflow-hidden z-20 lg:ml-0"
class:-ml-56={!$menuIsOpen}
>

View File

@ -2,18 +2,14 @@
import {sortBy, assoc} from 'ramda'
import {onMount} from 'svelte'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import {displayPerson} from 'src/util/nostr'
import {now, formatTimestamp, createScroller} from 'src/util/misc'
import {now, createScroller} from 'src/util/misc'
import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import ImageCircle from "src/partials/ImageCircle.svelte"
import Alert from 'src/views/alerts/Alert.svelte'
import Mention from 'src/views/alerts/Mention.svelte'
import database from 'src/agent/database'
import user from 'src/agent/user'
import {lastChecked} from 'src/app/alerts'
import {modal, routes} from 'src/app/ui'
let limit = 0
let notes = null
@ -37,29 +33,13 @@
{#if notes}
<Content>
{#each notes as note (note.id)}
{@const person = database.getPersonWithFallback(note.pubkey)}
<div in:fly={{y: 20}}>
{#if note.replies.length > 0}
<Alert type="replies" {note} />
{:else if note.likedBy.length > 0}
<Alert type="likes" {note} />
{:else}
<button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all w-full
border border-solid border-black hover:border-medium hover:bg-dark text-left"
on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative w-full">
<Anchor type="unstyled" href={routes.person(person.pubkey)} class="align-middle">
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold ml-1">{displayPerson(person)}</span>
<span>mentioned you.</span>
</Anchor>
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>
<div class="ml-6 text-light">
{ellipsize(note.content, 120)}
</div>
</button>
<Mention {note} />
{/if}
</div>
{:else}

View File

@ -0,0 +1,41 @@
<script lang="ts">
import {ellipsize} from 'hurdak/lib/hurdak'
import {formatTimestamp} from 'src/util/misc'
import {displayPerson} from 'src/util/nostr'
import Anchor from 'src/partials/Anchor.svelte'
import ImageCircle from "src/partials/ImageCircle.svelte"
import Popover from "src/partials/Popover.svelte"
import PersonSummary from "src/views/person/PersonSummary.svelte"
import database from 'src/agent/database'
import {modal} from 'src/app/ui'
export let note
const person = database.getPersonWithFallback(note.pubkey)
</script>
<button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all w-full
border border-solid border-black hover:border-medium hover:bg-dark text-left"
on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative w-full">
<div class="flex gap-1 items-center" on:click|stopPropagation>
<Popover>
<div slot="trigger">
<Anchor type="unstyled" class="text-lg font-bold flex gap-2 items-center">
<ImageCircle src={person.kind0?.picture} />
<span class="text-lg font-bold ml-1">{displayPerson(person)}</span>
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={note.pubkey} />
</div>
</Popover>
<span>mentioned you.</span>
</div>
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>
<div class="ml-6 text-light">
{ellipsize(note.content, 120)}
</div>
</button>

View File

@ -11,6 +11,8 @@
import {extractUrls} from "src/util/html"
import ImageCircle from 'src/partials/ImageCircle.svelte'
import Content from 'src/partials/Content.svelte'
import PersonSummary from 'src/views/person/PersonSummary.svelte'
import Popover from 'src/partials/Popover.svelte'
import RelayCard from 'src/views/relays/RelayCard.svelte'
import Modal from 'src/partials/Modal.svelte'
import Preview from 'src/partials/Preview.svelte'
@ -225,12 +227,19 @@
</div>
<div class="flex flex-col gap-2 flex-grow min-w-0">
<div class="flex items-center justify-between">
<Anchor type="unstyled" class="text-lg font-bold flex gap-2 items-center" href={routes.person($person.pubkey)}>
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
<i class="fa fa-circle-check text-accent text-sm" />
{/if}
</Anchor>
<Popover>
<div slot="trigger">
<Anchor type="unstyled" class="text-lg font-bold flex gap-2 items-center">
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
<i class="fa fa-circle-check text-accent text-sm" />
{/if}
</Anchor>
</div>
<div slot="tooltip">
<PersonSummary pubkey={$person.pubkey} />
</div>
</Popover>
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on]})}
class="text-sm text-light"

View File

@ -36,9 +36,34 @@
let person = database.getPersonWithFallback(pubkey)
let loading = true
let showActions = false
let actions = []
$: following = $petnamePubkeys.includes(pubkey)
$: {
actions = []
if (showActions) {
actions.push({onClick: share, label: 'Share', icon: 'share-nodes'})
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'})
}
if ($canPublish) {
actions.push({href: `/messages/${npub}`, label: 'Message', icon: 'envelope'})
actions.push({onClick: openAdvanced, label: 'Advanced', icon: 'sliders'})
}
if (user.getPubkey() === pubkey) {
actions.push({href: '/profile', label: 'Edit', icon: 'edit'})
}
}
}
onMount(async () => {
log('Person', npub, person)
@ -75,12 +100,6 @@
showActions = !showActions
}
const makeGetTransition = () => {
let i = 0
return () => ({y: 20, delay: i++ * 30})
}
const setActiveTab = tab => navigate(routes.person(pubkey, tab))
const showFollows = () => {
@ -142,78 +161,27 @@
</div>
<div class="whitespace-nowrap flex gap-3 flex-wrap relative">
<div on:click|stopPropagation={toggleActions} class="px-5 py-2 cursor-pointer">
<i class="fa fa-ellipsis-vertical" />
<i class="fa fa-xl fa-ellipsis-vertical" />
</div>
{#if showActions}
{@const getTransition = makeGetTransition()}
<div class="absolute top-0 right-0 mt-12 flex flex-col gap-2 opacity-90">
<div
class="absolute inset-0 bg-black rounded-full"
class:hidden={!showActions}
style="filter: blur(15px)"
transition:fade />
transition:fade|local />
{#each actions as {onClick, href, label, icon}, i}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={share}>
<div class="text-light">Share</div>
in:fly|local={{y: 20, delay: i * 30}}
out:fly|local={{y: 20, delay: (actions.length - i - 1) * 30}}
on:click={onClick}>
<div class="text-light">{label}</div>
<Anchor type="button-circle">
<i class="fa fa-share-nodes" />
<i class={`fa fa-${icon}`} />
</Anchor>
</div>
{#if following}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={unfollow}>
<div class="text-light">Unfollow</div>
<Anchor type="button-circle">
<i class="fa fa-user-minus" />
</Anchor>
</div>
{:else if user.getPubkey() !== pubkey}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={follow}>
<div class="text-light">Follow</div>
<Anchor type="button-circle">
<i class="fa fa-user-plus" />
</Anchor>
</div>
{/if}
{#if $canPublish}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={() => navigate(`/messages/${npub}`)}>
<div class="text-light">Message</div>
<Anchor type="button-circle">
<i class="fa fa-envelope" />
</Anchor>
</div>
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={openAdvanced}>
<div class="text-light">Advanced</div>
<Anchor type="button-circle">
<i class="fa fa-sliders" />
</Anchor>
</div>
{/if}
{#if user.getPubkey() === pubkey}
<div
class="flex gap-2 justify-end items-center z-10 cursor-pointer"
transition:fly={getTransition()}
on:click={() => navigate("/profile")}>
<div class="text-light">Edit</div>
<Anchor type="button-circle">
<i class="fa fa-edit" />
</Anchor>
</div>
{/if}
{/each}
</div>
{/if}
</div>
</div>
<p>{@html renderContent(person?.kind0?.about || '')}</p>

View File

@ -0,0 +1,72 @@
<script lang="ts">
import {last} from 'ramda'
import {navigate} from 'svelte-routing'
import {renderContent} from 'src/util/html'
import {displayPerson} from 'src/util/nostr'
import Anchor from "src/partials/Anchor.svelte"
import user from "src/agent/user"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import {routes, modal} from "src/app/ui"
export let pubkey
const {petnamePubkeys} = user
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
let following = false
let person = database.getPersonWithFallback(pubkey)
$: following = $petnamePubkeys.includes(pubkey)
const follow = async () => {
const [{url}] = getRelays()
user.addPetname(pubkey, url, displayPerson(person))
}
const unfollow = async () => {
user.removePetname(pubkey)
}
const share = () => {
modal.set({type: 'person/share', person})
}
</script>
<div class="flex flex-col gap-4 py-2 px-3 relative">
<div class="flex gap-4">
<div
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.kind0?.picture})" />
<div class="flex-grow flex flex-col gap-2">
<Anchor
type="unstyled"
class="flex items-center gap-2"
on:click={() => navigate(routes.person(pubkey))}>
<h1 class="text-2xl">{displayPerson(person)}</h1>
</Anchor>
{#if person.verified_as}
<div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" />
<span class="text-light">{last(person.verified_as.split('@'))}</span>
</div>
{/if}
</div>
<div class="flex gap-2">
<Anchor class="tippy-close" type="button-circle" on:click={share}>
<i class="fa fa-share-nodes" />
</Anchor>
{#if following}
<Anchor type="button-circle" on:click={unfollow}>
<i class="fa fa-user-minus" />
</Anchor>
{:else}
<Anchor type="button-circle" on:click={follow}>
<i class="fa fa-user-plus" />
</Anchor>
{/if}
</div>
</div>
<p>{@html renderContent(person?.kind0?.about || '')}</p>
</div>