mirror of
https://github.com/coracle-social/coracle.git
synced 2024-10-06 11:43:30 +00:00
Add person popover
This commit is contained in:
parent
0a293ca354
commit
c500c7354f
@ -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
|
||||
|
||||
|
10
ROADMAP.md
10
ROADMAP.md
@ -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.
|
||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -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"
|
||||
}
|
||||
|
19
src/app.css
19
src/app.css
@ -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;
|
||||
}
|
||||
|
62
src/partials/Popover.svelte
Normal file
62
src/partials/Popover.svelte
Normal 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>
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
|
41
src/views/alerts/Mention.svelte
Normal file
41
src/views/alerts/Mention.svelte
Normal 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>
|
@ -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)}>
|
||||
<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"
|
||||
|
@ -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>
|
||||
{/each}
|
||||
</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}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<p>{@html renderContent(person?.kind0?.about || '')}</p>
|
||||
|
72
src/views/person/PersonSummary.svelte
Normal file
72
src/views/person/PersonSummary.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user