forked from coracle/flotilla
Flesh out people tab
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import Link from '@lib/components/Link.svelte'
|
||||
import Button from '@lib/components/Button.svelte'
|
||||
import ContentToken from '@app/components/ContentToken.svelte'
|
||||
import ContentCode from '@app/components/ContentCode.svelte'
|
||||
import ContentLinkInline from '@app/components/ContentLinkInline.svelte'
|
||||
@@ -32,7 +31,7 @@
|
||||
export let minLength = 500
|
||||
export let maxLength = 700
|
||||
export let showEntire = false
|
||||
export let skipMedia = false
|
||||
export let hideMedia = false
|
||||
export let expandable = true
|
||||
export let depth = 0
|
||||
|
||||
@@ -65,14 +64,11 @@
|
||||
|
||||
$: shortContent = showEntire
|
||||
? fullContent
|
||||
: truncate(
|
||||
fullContent.filter(p => !skipMedia || (isLink(p) && p.value.isMedia)),
|
||||
{
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: 200,
|
||||
},
|
||||
)
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMedia ? 20 : 200,
|
||||
})
|
||||
|
||||
$: ellipsize = expandable && shortContent.find(isEllipsis)
|
||||
</script>
|
||||
@@ -90,7 +86,7 @@
|
||||
{:else if isCashu(parsed) || isInvoice(parsed)}
|
||||
<ContentToken value={parsed.value} />
|
||||
{:else if isLink(parsed)}
|
||||
{#if isStartOrEnd(i)}
|
||||
{#if isStartOrEnd(i) && !hideMedia}
|
||||
<ContentLinkBlock value={parsed.value} />
|
||||
{:else}
|
||||
<ContentLinkInline value={parsed.value} />
|
||||
@@ -98,10 +94,10 @@
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{#if isStartOrEnd(i) && depth < 2}
|
||||
{#if isStartOrEnd(i) && depth < 2 && !hideMedia}
|
||||
<ContentQuote value={parsed.value} {depth}>
|
||||
<div slot="note-content" let:event>
|
||||
<svelte:self {event} depth={depth + 1} />
|
||||
<svelte:self {hideMedia} {event} depth={depth + 1} />
|
||||
</div>
|
||||
</ContentQuote>
|
||||
{:else}
|
||||
@@ -120,6 +116,8 @@
|
||||
|
||||
{#if ellipsize}
|
||||
<div class="z-feature relative -mt-24 mb-0 flex justify-center bg-gradient-to-t from-base-100 pt-12" class:-ml-12={depth > 0}>
|
||||
<Button class="btn" on:click={expand}>See more</Button>
|
||||
<button type="button" class="btn" on:click|stopPropagation|preventDefault={expand}>
|
||||
See more
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {getAddress, Address} from "@welshman/util"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import {deriveEvent} from "@app/state"
|
||||
|
||||
@@ -14,8 +15,14 @@
|
||||
$: isGroup = address.match(/^(34550|35834):/)
|
||||
</script>
|
||||
|
||||
<button class="text-left my-2" on:click|stopPropagation>
|
||||
<NoteCard event={$event} class="p-4 rounded-box bg-base-300">
|
||||
<slot name="note-content" event={$event} {depth} />
|
||||
</NoteCard>
|
||||
<button class="text-left my-2 max-w-full" on:click|stopPropagation>
|
||||
{#if $event}
|
||||
<NoteCard event={$event} class="p-4 rounded-box bg-base-300">
|
||||
<slot name="note-content" event={$event} {depth} />
|
||||
</NoteCard>
|
||||
{:else}
|
||||
<div class="p-4 rounded-box bg-base-300">
|
||||
<Spinner loading>Loading event...</Spinner>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from 'svelte'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {ago, append, first, sortBy, max, WEEK, ctx} from '@welshman/lib'
|
||||
import {NOTE, getAncestorTags, getListValues} from '@welshman/util'
|
||||
import type {Filter} from '@welshman/util'
|
||||
import {deriveEvents} from '@welshman/store'
|
||||
import {repository, load, loadRelaySelections, userFollows, formatTimestamp, formatTimestampRelative} from '@welshman/app'
|
||||
import Link from '@lib/components/Link.svelte'
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import {entityLink} from '@app/state'
|
||||
|
||||
export let pubkey
|
||||
|
||||
const filters: Filter[] = [{kinds: [NOTE], authors: [pubkey], since: ago(WEEK)}]
|
||||
const events = deriveEvents(repository, {filters})
|
||||
|
||||
$: roots = $events.filter(e => getAncestorTags(e.tags).replies.length === 0)
|
||||
|
||||
onMount(async () => {
|
||||
// Make sure we have their relay selections before we load their posts
|
||||
await loadRelaySelections(pubkey)
|
||||
|
||||
// Load at least one note, regardless of time frame
|
||||
load({
|
||||
filters: append({kinds: [NOTE], authors: [pubkey], limit: 1}, filters),
|
||||
relays: ctx.app.router.FromPubkeys([pubkey]).getUrls(),
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<Profile {pubkey} />
|
||||
<ProfileInfo {pubkey} />
|
||||
{#if roots.length > 0}
|
||||
{@const event = first(sortBy(e => -e.created_at, roots))}
|
||||
{@const relays = ctx.app.router.Event(event).getUrls()}
|
||||
{@const nevent = nip19.neventEncode({id: event.id, relays})}
|
||||
{@const following = getListValues("p", $userFollows).includes(pubkey)}
|
||||
<div class="divider" />
|
||||
<Link external class="chat chat-start" href={entityLink(nevent)}>
|
||||
<div class="chat-bubble">
|
||||
<Content hideMedia={!following} {event} />
|
||||
<p class="text-xs text-right">{formatTimestamp(event.created_at)}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<div class="flex gap-2">
|
||||
<div class="badge badge-neutral">{roots.length} recent {roots.length === 1 ? 'note' : 'notes'}</div>
|
||||
<div class="badge badge-neutral">Last posted {formatTimestampRelative(event.created_at)}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +1,34 @@
|
||||
<script lang="ts">
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {deriveProfile, deriveHandleForPubkey, displayHandle, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {derived} from 'svelte/store'
|
||||
import {displayPubkey, getListValues} from "@welshman/util"
|
||||
import {userFollows, deriveUserWotScore, deriveProfile, deriveHandleForPubkey, displayHandle, deriveProfileDisplay, formatTimestamp, getUserWotScore, followsByPubkey} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import WotScore from "@lib/components/WotScore.svelte"
|
||||
import {entityLink} from '@app/state'
|
||||
|
||||
export let pubkey
|
||||
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
const profile = deriveProfile(pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
const score = deriveUserWotScore(pubkey)
|
||||
|
||||
$: following = getListValues("p", $userFollows).includes(pubkey)
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 max-w-full">
|
||||
<div class="py-1">
|
||||
<div class="flex gap-3 max-w-full">
|
||||
<Link external href={entityLink(npub)} class="py-1">
|
||||
<Avatar src={$profile?.picture} size={10} />
|
||||
</div>
|
||||
</Link>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="text-bold text-ellipsis overflow-hidden">
|
||||
{$profileDisplay}
|
||||
<div class="flex gap-2 items-center">
|
||||
<Link external class="text-bold text-ellipsis overflow-hidden" href={entityLink(npub)}>
|
||||
{$profileDisplay}
|
||||
</Link>
|
||||
<WotScore score={$score} active={following} />
|
||||
</div>
|
||||
<div class="text-sm opacity-75 text-ellipsis overflow-hidden">
|
||||
{$handle ? displayHandle($handle) : displayPubkey(pubkey)}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const profile = deriveProfile(pubkey)
|
||||
</script>
|
||||
|
||||
{#if $profile}
|
||||
<Content event={{content: $profile.about, tags: []}} hideMedia />
|
||||
{/if}
|
||||
@@ -3,18 +3,18 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
export let src
|
||||
export let alt = ""
|
||||
export let size = 7
|
||||
export let icon = "user-rounded"
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx($$props.class, "!flex overflow-hidden rounded-full")}
|
||||
class:center={!src}
|
||||
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px;`}>
|
||||
{#if src}
|
||||
<img {alt} {src} />
|
||||
{:else}
|
||||
{#if src}
|
||||
<div
|
||||
class={cx($$props.class, "shrink-0 overflow-hidden rounded-full bg-cover bg-center")}
|
||||
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; background-image: url(${src}); ${$$props.style || ""}`} />
|
||||
{:else}
|
||||
<div
|
||||
class={cx($$props.class, "rounded-full !flex center")}
|
||||
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; ${$$props.style || ""}`}>
|
||||
<Icon {icon} size={Math.round(size * 0.7)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<style>
|
||||
.wot-background {
|
||||
fill: transparent;
|
||||
stroke: var(--base-content);
|
||||
opacity: 30%;
|
||||
}
|
||||
|
||||
.wot-highlight {
|
||||
fill: transparent;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 100 100;
|
||||
transform-origin: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {clamp} from '@welshman/lib'
|
||||
|
||||
export let score
|
||||
export let max = 100
|
||||
export let active = false
|
||||
|
||||
const radius = 6
|
||||
const center = radius + 1
|
||||
|
||||
$: normalizedScore = clamp([0, max], score) / max
|
||||
$: dashOffset = 100 - 44 * normalizedScore
|
||||
$: style = `transform: rotate(${135 - normalizedScore * 180}deg)`
|
||||
$: stroke = active ? 'var(--primary)' : 'var(--base-content)'
|
||||
</script>
|
||||
|
||||
<div class="relative h-[14px] w-[14px]">
|
||||
<svg height="14" width="14" class="absolute">
|
||||
<circle class="wot-background" cx={center} cy={center} r={radius} />
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
class="wot-highlight"
|
||||
stroke-dashoffset={dashOffset}
|
||||
{style}
|
||||
{stroke} />
|
||||
</svg>
|
||||
</div>
|
||||
+11
-6
@@ -17,26 +17,31 @@ export const copyToClipboard = (text: string) => {
|
||||
}
|
||||
|
||||
type ScrollerOpts = {
|
||||
onScroll: () => any
|
||||
element: Element
|
||||
threshold?: number
|
||||
reverse?: boolean
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export const createScroller = (
|
||||
loadMore: () => Promise<void>,
|
||||
{delay = 1000, threshold = 2000, reverse = false}: ScrollerOpts = {},
|
||||
) => {
|
||||
export const createScroller = ({
|
||||
onScroll,
|
||||
element,
|
||||
delay = 1000,
|
||||
threshold = 2000,
|
||||
reverse = false,
|
||||
}: ScrollerOpts) => {
|
||||
let done = false
|
||||
const check = async () => {
|
||||
// While we have empty space, fill it
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight, scrollTop} = document.querySelector('.max-h-screen')!
|
||||
const {scrollHeight, scrollTop} = element
|
||||
const offset = Math.abs(scrollTop || scrollY)
|
||||
const shouldLoad = offset + innerHeight + threshold > scrollHeight
|
||||
|
||||
// Only trigger loading the first time we reach the threshold
|
||||
if (shouldLoad) {
|
||||
await loadMore()
|
||||
await onScroll()
|
||||
}
|
||||
|
||||
// No need to check all that often
|
||||
|
||||
@@ -10,16 +10,18 @@
|
||||
|
||||
let term = ""
|
||||
let limit = 20
|
||||
|
||||
const loadMore = async () => {
|
||||
limit += 20
|
||||
}
|
||||
let element: Element
|
||||
|
||||
$: relays = $relaySearch.searchOptions(term).slice(0, limit)
|
||||
|
||||
onMount(() => {
|
||||
const sub = discoverRelays()
|
||||
const scroller = createScroller(loadMore)
|
||||
const scroller = createScroller({
|
||||
element: element.closest('.max-h-screen')!,
|
||||
onScroll: () => {
|
||||
limit += 20
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
sub.close()
|
||||
@@ -28,7 +30,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="content column gap-4">
|
||||
<div class="content column gap-4" bind:this={element}>
|
||||
<h1 class="superheading mt-20">Discover Spaces</h1>
|
||||
<p class="text-center">Find communities all across the nostr network</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 100}}>
|
||||
<SecondaryNavItem href="/home/notes">
|
||||
<Icon icon="clipboard-text" /> Saved Notes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 150}}>
|
||||
<SecondaryNavHeader>
|
||||
Chats
|
||||
<div class="cursor-pointer">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from 'svelte'
|
||||
import {createScroller} from '@lib/html'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import {shuffle} from '@welshman/lib'
|
||||
import {getListValues} from '@welshman/util'
|
||||
import {profileSearch, userFollows} from '@welshman/app'
|
||||
import PeopleItem from '@app/components/PeopleItem.svelte'
|
||||
|
||||
const defaultPubkeys = shuffle(getListValues("p", $userFollows))
|
||||
|
||||
let term = ""
|
||||
let limit = 10
|
||||
let element: Element
|
||||
|
||||
$: pubkeys = term
|
||||
? $profileSearch.searchValues(term)
|
||||
: defaultPubkeys
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element: element.closest('.max-h-screen')!,
|
||||
onScroll: () => {
|
||||
limit += 10
|
||||
},
|
||||
})
|
||||
|
||||
return () => scroller.stop()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div class="content column gap-4" bind:this={element}>
|
||||
<h1 class="superheading mt-20">People</h1>
|
||||
<p class="text-center">Get the latest from people in your network</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="magnifer" />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search for people..." />
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
||||
<PeopleItem {pubkey} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user