Use new wot score indicator, revert to one-off style, show loading when searching profiles

This commit is contained in:
Jon Staab 2024-02-20 17:40:59 -08:00
parent 6fd36ccecc
commit dde8e47f34
17 changed files with 182 additions and 69 deletions

View File

@ -8,6 +8,8 @@
- [x] Add zap splits - [x] Add zap splits
- [x] Add default platform split amount - [x] Add default platform split amount
- [x] Add invite link generation - [x] Add invite link generation
- [x] Use new web of trust display
- [x] Show loading when searching profiles
# 0.4.2 # 0.4.2

View File

@ -55,7 +55,7 @@
external external
href="https://info.coracle.social"> href="https://info.coracle.social">
<img alt="App Logo" src={import.meta.env.VITE_LOGO_URL || "/images/logo.png"} class="w-12" /> <img alt="App Logo" src={import.meta.env.VITE_LOGO_URL || "/images/logo.png"} class="w-12" />
<h1 class="staatliches text-4xl leading-none">{appName}</h1> <h1 class="staatliches text-[2.6em] leading-none -mb-[0.1em]">{appName}</h1>
</Anchor> </Anchor>
<MenuDesktopItem path="/notes">Feed</MenuDesktopItem> <MenuDesktopItem path="/notes">Feed</MenuDesktopItem>
{#if !$env.FORCE_GROUP && $env.FORCE_RELAYS.length === 0} {#if !$env.FORCE_GROUP && $env.FORCE_RELAYS.length === 0}

View File

@ -65,7 +65,7 @@
{#if $searchTerm} {#if $searchTerm}
<div <div
class="absolute right-0 top-10 max-h-[70vh] w-96 overflow-auto rounded bg-cocoa shadow-2xl"> class="absolute right-0 top-10 max-h-[70vh] w-96 overflow-auto rounded bg-cocoa shadow-2xl">
<SearchResults term={searchTerm}> <SearchResults showLoading term={searchTerm}>
<div slot="result" let:result class="px-4 py-2 transition-colors hover:bg-dark"> <div slot="result" let:result class="px-4 py-2 transition-colors hover:bg-dark">
{#if result.type === "topic"} {#if result.type === "topic"}
#{result.topic.name} #{result.topic.name}

View File

@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import {nip19} from "nostr-tools" import {nip19} from "nostr-tools"
import {debounce, throttle} from "throttle-debounce" import {throttle} from "throttle-debounce"
import {createEventDispatcher} from "svelte" import {createEventDispatcher} from "svelte"
import {last, partition, whereEq} from "ramda" import {last, partition, whereEq} from "ramda"
import PersonBadge from "src/app/shared/PersonBadge.svelte" import PersonBadge from "src/app/shared/PersonBadge.svelte"
import ContentEditable from "src/partials/ContentEditable.svelte" import ContentEditable from "src/partials/ContentEditable.svelte"
import Suggestions from "src/partials/Suggestions.svelte" import Suggestions from "src/partials/Suggestions.svelte"
import { import {
load,
follows, follows,
derivePerson, derivePerson,
displayPerson, displayPerson,
searchableRelays, searchableRelays,
getPubkeyHints, getPubkeyHints,
searchPeople, searchPeople,
createPeopleLoader,
} from "src/engine" } from "src/engine"
export let onSubmit export let onSubmit
@ -24,6 +24,11 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const {loading: loadingPeople, load: loadPeople} = createPeopleLoader({
shouldLoad: (term: string) => term.startsWith("@"),
onEvent: () => applySearch(getInfo().word),
})
const pubkeyEncoder = { const pubkeyEncoder = {
encode: pubkey => { encode: pubkey => {
const relays = getPubkeyHints.limit(3).getHints(pubkey, "write") const relays = getPubkeyHints.limit(3).getHints(pubkey, "write")
@ -37,16 +42,6 @@
}, },
} }
const loadPeople = debounce(500, search => {
if (search.length > 2 && search.startsWith("@")) {
load({
relays: $searchableRelays,
filters: [{kinds: [0], search, limit: 10}],
onEvent: () => applySearch(getInfo().word),
})
}
})
const applySearch = throttle(300, word => { const applySearch = throttle(300, word => {
let results = [] let results = []
if (word.length > 1 && word.startsWith("@")) { if (word.length > 1 && word.startsWith("@")) {
@ -273,7 +268,10 @@
<slot name="addon" /> <slot name="addon" />
</div> </div>
<Suggestions bind:this={suggestions} select={person => autocomplete({person})}> <Suggestions
bind:this={suggestions}
select={person => autocomplete({person})}
loading={$loadingPeople}>
<div slot="item" let:item> <div slot="item" let:item>
<PersonBadge inert pubkey={item.pubkey} /> <PersonBadge inert pubkey={item.pubkey} />
</div> </div>

View File

@ -14,7 +14,6 @@
deriveAdminKeyForGroup, deriveAdminKeyForGroup,
publishGroupExitRequest, publishGroupExitRequest,
publishGroupEntryRequest, publishGroupEntryRequest,
deriveGroup,
deriveGroupStatus, deriveGroupStatus,
resetGroupAccess, resetGroupAccess,
} from "src/engine" } from "src/engine"

View File

@ -254,7 +254,7 @@
</div> </div>
<div class="flex min-w-0 flex-grow flex-col gap-2"> <div class="flex min-w-0 flex-grow flex-col gap-2">
<div class="flex min-w-0 flex-col items-start justify-between sm:flex-row"> <div class="flex min-w-0 flex-col items-start justify-between sm:flex-row">
<Anchor type="unstyled" class="mr-4 min-w-0 text-lg font-bold" on:click={showPerson}> <Anchor type="unstyled" class="mr-4 min-w-0" on:click={showPerson}>
<PersonName pubkey={event.pubkey} /> <PersonName pubkey={event.pubkey} />
</Anchor> </Anchor>
<Anchor <Anchor

View File

@ -1,29 +1,76 @@
<style>
.wot-background {
fill: transparent;
stroke: var(--mid);
}
.wot-highlight {
fill: transparent;
stroke-width: 1;
stroke-dasharray: 100 100;
transform-origin: center;
}
</style>
<script context="module">
import {writable} from "src/engine"
const maxWot = writable(10)
</script>
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {themeColors} from "src/partials/state"
import Popover from "src/partials/Popover.svelte" import Popover from "src/partials/Popover.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import {deriveFollowing, derivePerson, displayPerson, session, getWotScore} from "src/engine" import {
deriveFollowing,
derivePerson,
displayPerson,
displayNpub,
session,
getWotScore,
} from "src/engine"
export let pubkey export let pubkey
const person = derivePerson(pubkey) const person = derivePerson(pubkey)
const following = deriveFollowing(pubkey) const following = deriveFollowing(pubkey)
const wotScore = getWotScore($session?.pubkey, pubkey) const wotScore = getWotScore($session?.pubkey, pubkey)
const npubDisplay = displayNpub(pubkey)
maxWot.update(x => Math.max(x, wotScore + 10))
$: dashOffset = 100 - (Math.max(0, wotScore) / $maxWot) * 100
$: style = `transform: rotate(${dashOffset * 1.8 - 4}deg)`
$: stroke = $themeColors[$following || pubkey === $session?.pubkey ? "accent" : "white"]
$: personDisplay = displayPerson($person)
</script> </script>
<div class={cx("flex gap-1", $$props.class)}> <div class={cx("flex gap-1", $$props.class)}>
<span class="cy-person-name overflow-hidden text-ellipsis"> <div class="flex flex-col overflow-hidden text-ellipsis">
{displayPerson($person)} <span class="cy-person-name">{personDisplay}</span>
</span> {#if personDisplay != npubDisplay}
<small class="text-xs">{npubDisplay}</small>
{/if}
</div>
{#if $session} {#if $session}
<div class="flex gap-1 font-normal"> <div class="flex gap-1 font-normal">
<Popover triggerType="mouseenter"> <Popover triggerType="mouseenter">
<span slot="trigger" class="whitespace-nowrap px-2 py-1 text-xs"> <span
{#if $following} slot="trigger"
<i class="fa fa-check-circle text-accent" /> class="relative flex h-10 w-10 items-center justify-center whitespace-nowrap px-4 text-xs">
{:else} <svg height="32" width="32" class="absolute">
<i class="fa fa-diagram-project text-accent" /> <circle class="wot-background" cx="16" cy="16" r="15" />
{/if} <circle
cx="16"
cy="16"
r="15"
class="wot-highlight"
stroke-dashoffset={dashOffset}
{style}
{stroke} />
</svg>
{wotScore} {wotScore}
</span> </span>
<Anchor modal slot="tooltip" class="flex items-center gap-1" href="/help/web-of-trust"> <Anchor modal slot="tooltip" class="flex items-center gap-1" href="/help/web-of-trust">

View File

@ -1,13 +1,15 @@
<script lang="ts"> <script lang="ts">
import {throttle} from "throttle-debounce" import {throttle} from "throttle-debounce"
import {slide} from "src/util/transition"
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import {parseAnything} from "src/util/nostr" import {parseAnything} from "src/util/nostr"
import {router} from "src/app/router" import {router} from "src/app/router"
import type {Person, Topic} from "src/engine" import type {Person, Topic} from "src/engine"
import {topics, derived, searchPeople, loadPeople} from "src/engine" import {topics, derived, searchPeople, createPeopleLoader} from "src/engine"
export let term export let term
export let replace = false export let replace = false
export let showLoading = false
const openTopic = topic => router.at("topics").of(topic).open({replace}) const openTopic = topic => router.at("topics").of(topic).open({replace})
@ -39,6 +41,8 @@
}, },
) )
const {loading: loadingPeople, load: loadPeople} = createPeopleLoader()
const searchTopics = topics const searchTopics = topics
.throttle(1000) .throttle(1000)
.derived($topics => fuzzy($topics, {keys: ["name"], threshold: 0.5, shouldSort: true})) .derived($topics => fuzzy($topics, {keys: ["name"], threshold: 0.5, shouldSort: true}))
@ -72,3 +76,11 @@
{:else} {:else}
<p class="text-center py-12">No results found.</p> <p class="text-center py-12">No results found.</p>
{/each} {/each}
{#if showLoading && $loadingPeople}
<div transition:slide|local class="flex gap-2 bg-cocoa px-4 py-2 text-lighter absolute bottom-0 left-0 right-0">
<div>
<i class="fa fa-circle-notch fa-spin" />
</div>
Loading more options...
</div>
{/if}

View File

@ -11,7 +11,15 @@
import PersonSummary from "src/app/shared/PersonSummary.svelte" import PersonSummary from "src/app/shared/PersonSummary.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte" import RelayCard from "src/app/shared/RelayCard.svelte"
import type {Relay} from "src/engine" import type {Relay} from "src/engine"
import {env, lists, urlToRelay, mention, loadPeople, searchPeople, searchRelays} from "src/engine" import {
env,
lists,
urlToRelay,
mention,
createPeopleLoader,
searchPeople,
searchRelays,
} from "src/engine"
export let relays export let relays
export let petnames export let petnames
@ -24,6 +32,8 @@
let showPersonSearch let showPersonSearch
let showRelaySearch let showRelaySearch
const {load: loadPeople} = createPeopleLoader()
const prev = () => setStage("profile") const prev = () => setStage("profile")
const next = () => setStage("note") const next = () => setStage("note")
@ -138,8 +148,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Anchor button on:click={prev}><i class="fa fa-arrow-left" /> Back</Anchor> <Anchor button on:click={prev}><i class="fa fa-arrow-left" /> Back</Anchor>
<Anchor button accent class="flex-grow" on:click={() => next()} <Anchor button accent class="flex-grow" on:click={() => next()}>Continue</Anchor>
>Continue</Anchor>
</div> </div>
{#if showList} {#if showList}

View File

@ -78,17 +78,19 @@
<i slot="before" class="fa fa-search" /> <i slot="before" class="fa fa-search" />
<i slot="after" class="fa fa-qrcode cursor-pointer" on:click={startScanner} /> <i slot="after" class="fa fa-qrcode cursor-pointer" on:click={startScanner} />
</Input> </Input>
<SearchResults replace term={searchTerm}> <div class="relative max-h-full">
<div slot="result" let:result> <SearchResults replace term={searchTerm}>
{#if result.type === "topic"} <div slot="result" let:result>
<Card interactive> {#if result.type === "topic"}
#{result.topic.name} <Card interactive>
</Card> #{result.topic.name}
{:else if result.type === "profile"} </Card>
<Card interactive> {:else if result.type === "profile"}
<PersonSummary inert hideActions pubkey={result.id} /> <Card interactive>
</Card> <PersonSummary inert hideActions pubkey={result.id} />
{/if} </Card>
</div> {/if}
</SearchResults> </div>
</SearchResults>
</div>
{/if} {/if}

View File

@ -1,13 +1,41 @@
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {always} from "ramda"
import {noop, sleep} from "hurdak"
import {writable} from "src/engine/core/utils"
import {load} from "src/engine/network/utils" import {load} from "src/engine/network/utils"
import {searchableRelays} from "src/engine/relays/derived" import {searchableRelays} from "src/engine/relays/derived"
import type {Event} from "src/engine/events/model"
export const loadPeople = debounce(500, search => { type PeopleLoaderOpts = {
// Only search if we have a query shouldLoad?: (term: string) => boolean
if (search.length > 2) { onEvent?: (e: Event) => void
load({ }
relays: searchableRelays.get(),
filters: [{kinds: [0], search, limit: 100}], export const createPeopleLoader = ({
}) shouldLoad = always(true),
onEvent = noop,
}: PeopleLoaderOpts = {}) => {
const loading = writable(false)
return {
loading,
load: debounce(500, term => {
if (term.length > 2 && shouldLoad(term)) {
const now = Date.now()
loading.set(true)
load({
onEvent,
relays: searchableRelays.get(),
filters: [{kinds: [0], search: term, limit: 100}],
onClose: async () => {
await sleep(Math.min(1000, Date.now() - now))
loading.set(false)
},
})
}
}),
} }
}) }

View File

@ -34,6 +34,12 @@ export const personHasName = ({profile: p}: Person) => Boolean(p?.name || p?.dis
export const getPersonWithDefault = pubkey => ({pubkey, ...people.key(pubkey).get()}) export const getPersonWithDefault = pubkey => ({pubkey, ...people.key(pubkey).get()})
export const displayNpub = pubkey => {
const d = nip19.npubEncode(pubkey)
return d.slice(0, 8) + "..." + d.slice(-5)
}
export const displayPerson = ({pubkey, profile}: Person) => { export const displayPerson = ({pubkey, profile}: Person) => {
if (profile) { if (profile) {
const {display_name, name} = profile const {display_name, name} = profile
@ -48,7 +54,7 @@ export const displayPerson = ({pubkey, profile}: Person) => {
} }
try { try {
return nip19.npubEncode(pubkey).slice(-8) return displayNpub(pubkey)
} catch (e) { } catch (e) {
logger.error(e) logger.error(e)

View File

@ -1,10 +1,9 @@
import {whereEq, when, reject, uniqBy, prop, inc} from "ramda" import {whereEq, when, reject, uniqBy, prop, inc} from "ramda"
import {now, normalizeRelayUrl, isShareableRelay} from "paravel" import {now, normalizeRelayUrl, isShareableRelay, createEvent} from "paravel"
import {people} from "src/engine/people/state" import {people} from "src/engine/people/state"
import {canSign, stateKey} from "src/engine/session/derived" import {canSign, signer, stateKey} from "src/engine/session/derived"
import {updateStore} from "src/engine/core/commands" import {updateStore} from "src/engine/core/commands"
import {pool} from "src/engine/network/state" import {createAndPublish, getClientTags, Publisher} from "src/engine/network/utils"
import {createAndPublish, getClientTags} from "src/engine/network/utils"
import type {RelayPolicy} from "./model" import type {RelayPolicy} from "./model"
import {relays} from "./state" import {relays} from "./state"
import {relayPolicies} from "./derived" import {relayPolicies} from "./derived"
@ -58,16 +57,16 @@ export const publishRelays = ($relays: RelayPolicy[]) => {
} }
} }
export const joinRelay = (url: string, claim?: string) => { export const joinRelay = async (url: string, claim?: string) => {
// Fire off the claim to join the relay // Fire off the claim to join the relay
if (claim) { if (claim) {
Publisher.publish({ Publisher.publish({
relays: [url], relays: [url],
event: createEvent(28934, { event: await signer.get().signAsUser(
"tags": [ createEvent(28934, {
["claim", claim], tags: [["claim", claim]],
], })
}) ),
}) })
} }

View File

@ -23,12 +23,11 @@
className = cx({ className = cx({
"border-r-4 border-transparent": border, "border-r-4 border-transparent": border,
"cursor-pointer transition-colors": interactive, "cursor-pointer transition-colors": interactive,
"hover:border-cocoa": border && interactive && isAlt, "hover:border-accent": border && interactive,
"hover:border-dark-l": border && interactive && !isAlt,
"bg-cocoa": isAlt, "bg-cocoa": isAlt,
"hover:bg-cocoa-d": isAlt && interactive, // "hover:bg-cocoa-d": isAlt && interactive && !border,
"bg-dark": !isAlt, "bg-dark": !isAlt,
"hover:bg-dark-d": !isAlt && interactive, // "hover:bg-dark-d": !isAlt && interactive && !border,
}) })
}) })
</script> </script>

View File

@ -44,7 +44,7 @@
<AlternatingBackground <AlternatingBackground
border border
{interactive} {interactive}
class={cx($$props.class, "rounded p-3 text-lightest")}> class={cx($$props.class, "rounded py-5 px-7 text-lightest")}>
<slot /> <slot />
</AlternatingBackground> </AlternatingBackground>
</div> </div>

View File

@ -18,6 +18,7 @@
export let displayItem = getKey export let displayItem = getKey
export let autofocus = false export let autofocus = false
export let multiple = false export let multiple = false
export let loading = false
export let defaultOptions = [] export let defaultOptions = []
export let term = multiple ? "" : displayItem(value) export let term = multiple ? "" : displayItem(value)
@ -134,6 +135,7 @@
<Suggestions <Suggestions
bind:this={suggestions} bind:this={suggestions}
create={termToItem ? create : null} create={termToItem ? create : null}
{loading}
{select} {select}
{term} {term}
{getKey}> {getKey}>

View File

@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import {identity} from "ramda" import {identity} from "ramda"
import {slide} from "src/util/transition"
export let select export let select
export let term = null export let term = null
export let create = null export let create = null
export let loading = false
export let getKey = identity export let getKey = identity
let data = [] let data = []
@ -37,7 +39,7 @@
{#if data.length > 0 || (create && term)} {#if data.length > 0 || (create && term)}
<div <div
class="mt-2 flex max-h-[350px] flex-col overflow-y-auto overflow-x-hidden rounded border border-solid border-mid"> class="mt-2 flex max-h-[350px] flex-col overflow-y-auto overflow-x-hidden border border-solid border-mid">
{#if create && term} {#if create && term}
{@const i = data.length} {@const i = data.length}
<button <button
@ -65,3 +67,11 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if loading}
<div transition:slide|local class="flex gap-2 bg-cocoa px-4 py-2 text-lighter">
<div>
<i class="fa fa-circle-notch fa-spin" />
</div>
Loading more options...
</div>
{/if}