mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Add better relay review support
This commit is contained in:
parent
6b293b33a2
commit
5fc98007d2
@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.2.31
|
||||||
|
|
||||||
|
- [x] Added the ability to view and write reviews on relays, with ratings
|
||||||
|
|
||||||
# 0.2.30
|
# 0.2.30
|
||||||
|
|
||||||
- [x] Prefer followed users when mentioning people
|
- [x] Prefer followed users when mentioning people
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
- [ ] List detail pages with follow all and add all to list
|
- [ ] List detail pages with follow all and add all to list
|
||||||
- [ ] Use vida to stream development
|
- [ ] Use vida to stream development
|
||||||
- [ ] Fix connection management stuff. Have GPT help
|
- [ ] Fix connection management stuff. Have GPT help
|
||||||
- [ ] Relay reviews
|
|
||||||
- Add curated relay list, and an easy way to view content on the relays
|
|
||||||
- Deploy ontology.coracle.social
|
|
||||||
- [ ] Add preview proxy thing
|
- [ ] Add preview proxy thing
|
||||||
- [ ] White-labeled
|
- [ ] White-labeled
|
||||||
- [ ] Add invite code registration for relay
|
- [ ] Add invite code registration for relay
|
||||||
@ -21,6 +18,7 @@
|
|||||||
|
|
||||||
# Core
|
# Core
|
||||||
|
|
||||||
|
- [ ] Deploy ontology.coracle.social
|
||||||
- [ ] Add threads - replies by self get shown at the top of replies?
|
- [ ] Add threads - replies by self get shown at the top of replies?
|
||||||
- [ ] Show link previews when posting
|
- [ ] Show link previews when posting
|
||||||
- [ ] Embedded music players for Spotify, youtube, etc
|
- [ ] Embedded music players for Spotify, youtube, etc
|
||||||
|
@ -255,7 +255,7 @@ const loadParents = (notes, opts = {}) => {
|
|||||||
|
|
||||||
return load({
|
return load({
|
||||||
relays: sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3),
|
relays: sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3),
|
||||||
filter: {kinds: [1], ids: notesWithParent.map(findReplyId)},
|
filter: {ids: notesWithParent.map(findReplyId)},
|
||||||
...opts,
|
...opts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -268,7 +268,7 @@ const streamContext = ({notes, onChunk, maxDepth = 2}) => {
|
|||||||
|
|
||||||
const loadChunk = (events, depth) => {
|
const loadChunk = (events, depth) => {
|
||||||
// Remove anything from the chunk we've already seen
|
// Remove anything from the chunk we've already seen
|
||||||
events = events.filter(e => e.kind === 1 && !seen.has(e.id))
|
events = events.filter(e => ![7, 9735].includes(e.kind) && !seen.has(e.id))
|
||||||
|
|
||||||
// If we have no new information, no need to re-subscribe
|
// If we have no new information, no need to re-subscribe
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
@ -333,7 +333,7 @@ const streamContext = ({notes, onChunk, maxDepth = 2}) => {
|
|||||||
const applyContext = (notes, context) => {
|
const applyContext = (notes, context) => {
|
||||||
context = context.map(assoc("isContext", true))
|
context = context.map(assoc("isContext", true))
|
||||||
|
|
||||||
const replies = context.filter(propEq("kind", 1))
|
const replies = context.filter(e => ![7, 9735].includes(e.kind))
|
||||||
const reactions = context.filter(propEq("kind", 7))
|
const reactions = context.filter(propEq("kind", 7))
|
||||||
const zaps = context.filter(propEq("kind", 9735))
|
const zaps = context.filter(propEq("kind", 9735))
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
export let shouldDisplay = always(true)
|
export let shouldDisplay = always(true)
|
||||||
export let parentsTimeout = 500
|
export let parentsTimeout = 500
|
||||||
export let invertColors = false
|
export let invertColors = false
|
||||||
|
export let onEvent = null
|
||||||
|
|
||||||
let notes = []
|
let notes = []
|
||||||
let notesBuffer = []
|
let notesBuffer = []
|
||||||
@ -90,6 +91,11 @@
|
|||||||
// Show replies grouped by parent whenever possible
|
// Show replies grouped by parent whenever possible
|
||||||
const merged = sortBy(e => -e.created_at, mergeParents(combined))
|
const merged = sortBy(e => -e.created_at, mergeParents(combined))
|
||||||
|
|
||||||
|
// Notify caller if they asked for it
|
||||||
|
for (const e of merged) {
|
||||||
|
onEvent?.(e)
|
||||||
|
}
|
||||||
|
|
||||||
// Split into notes before and after we started loading
|
// Split into notes before and after we started loading
|
||||||
const [bottom, top] = partition(e => e.created_at < since, merged)
|
const [bottom, top] = partition(e => e.created_at < since, merged)
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {objOf, reverse} from "ramda"
|
import {objOf, reverse} from "ramda"
|
||||||
|
import {nip19} from 'nostr-tools'
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {splice} from "hurdak/lib/hurdak"
|
import {splice, switcher, switcherFn} from "hurdak/lib/hurdak"
|
||||||
import {warn} from "src/util/logger"
|
import {warn} from "src/util/logger"
|
||||||
import {displayPerson, parseContent, Tags} from "src/util/nostr"
|
import {displayPerson, parseContent, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
||||||
import {modal} from "src/partials/state"
|
import {modal} from "src/partials/state"
|
||||||
import MediaSet from "src/partials/MediaSet.svelte"
|
import MediaSet from "src/partials/MediaSet.svelte"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||||
import {sampleRelays} from "src/agent/relays"
|
import {sampleRelays} from "src/agent/relays"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
@ -25,6 +27,7 @@
|
|||||||
const shouldTruncate = !showEntire && note.content.length > maxLength
|
const shouldTruncate = !showEntire && note.content.length > maxLength
|
||||||
|
|
||||||
let content = parseContent(note)
|
let content = parseContent(note)
|
||||||
|
let rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
||||||
|
|
||||||
const links = []
|
const links = []
|
||||||
const ranges = []
|
const ranges = []
|
||||||
@ -118,6 +121,31 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
|
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
|
||||||
<p>
|
<p>
|
||||||
|
{#if rating}
|
||||||
|
{@const [type, value] = Tags.from(note).reject(t => ['l', 'L'].includes(t[0])).first()}
|
||||||
|
{@const action = switcher(type, {
|
||||||
|
r: () => modal.push({type: 'relay/detail', url: value}),
|
||||||
|
p: () => modal.push({type: 'person/feed', pubkey: value}),
|
||||||
|
e: () => modal.push({type: 'note/detail', note: {id: value}}),
|
||||||
|
})}
|
||||||
|
{@const display = switcherFn(type, {
|
||||||
|
r: () => displayRelay({url: value}),
|
||||||
|
p: () => displayPerson(getPersonWithFallback(value)),
|
||||||
|
e: () => "a note",
|
||||||
|
default: "something"
|
||||||
|
})}
|
||||||
|
<div class="flex items-center gap-2 pl-2 mb-4 border-l-2 border-solid border-gray-5">
|
||||||
|
Rated
|
||||||
|
{#if action}
|
||||||
|
<Anchor on:click={action}>{display}</Anchor>
|
||||||
|
{:else}
|
||||||
|
{display}
|
||||||
|
{/if}
|
||||||
|
<div class="text-sm">
|
||||||
|
<Rating inert value={rating} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#each content as { type, value }, i}
|
{#each content as { type, value }, i}
|
||||||
{#if type === "newline"}
|
{#if type === "newline"}
|
||||||
{#each value as _}
|
{#each value as _}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
export let relays
|
export let relays
|
||||||
export let invertColors = false
|
export let invertColors = false
|
||||||
|
|
||||||
const filter = {kinds: [1], authors: [pubkey]}
|
const filter = {kinds: [1, 1985], authors: [pubkey]}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Feed {relays} {filter} {invertColors} parentsTimeout={3000} delta={timedelta(1, "days")} />
|
<Feed {relays} {filter} {invertColors} parentsTimeout={3000} delta={timedelta(1, "days")} />
|
||||||
|
@ -8,12 +8,14 @@
|
|||||||
import {displayRelay} from "src/util/nostr"
|
import {displayRelay} from "src/util/nostr"
|
||||||
import {modal} from "src/partials/state"
|
import {modal} from "src/partials/state"
|
||||||
import Toggle from "src/partials/Toggle.svelte"
|
import Toggle from "src/partials/Toggle.svelte"
|
||||||
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {loadAppData} from "src/app/state"
|
import {loadAppData} from "src/app/state"
|
||||||
|
|
||||||
export let relay
|
export let relay
|
||||||
|
export let rating = null
|
||||||
export let theme = "gray-8"
|
export let theme = "gray-8"
|
||||||
export let showStatus = false
|
export let showStatus = false
|
||||||
export let hideActions = false
|
export let hideActions = false
|
||||||
@ -82,6 +84,11 @@
|
|||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if rating}
|
||||||
|
<div class="px-4 text-sm" in:fly={{y: 20}}>
|
||||||
|
<Rating inert value={rating} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !hideActions}
|
{#if !hideActions}
|
||||||
<slot name="actions">
|
<slot name="actions">
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {pluck} from "ramda"
|
import {onMount} from "svelte"
|
||||||
|
import {pluck, groupBy} from "ramda"
|
||||||
|
import {mapValues} from "hurdak/lib/hurdak"
|
||||||
import {fuzzy} from "src/util/misc"
|
import {fuzzy} from "src/util/misc"
|
||||||
import {normalizeRelayUrl} from "src/util/nostr"
|
import {normalizeRelayUrl, Tags, getAvgQuality} from "src/util/nostr"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||||
|
import {getUserReadRelays} from "src/agent/relays"
|
||||||
|
import network from "src/agent/network"
|
||||||
import {watch} from "src/agent/db"
|
import {watch} from "src/agent/db"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
@ -14,8 +18,14 @@
|
|||||||
export let hideIfEmpty = false
|
export let hideIfEmpty = false
|
||||||
|
|
||||||
let search
|
let search
|
||||||
|
let reviews = []
|
||||||
let knownRelays = watch("relays", t => t.all())
|
let knownRelays = watch("relays", t => t.all())
|
||||||
|
|
||||||
|
$: ratings = mapValues(
|
||||||
|
events => getAvgQuality("review/relay", events),
|
||||||
|
groupBy(e => Tags.from(e).getMeta("r"), reviews)
|
||||||
|
)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const joined = new Set(pluck("url", $relays))
|
const joined = new Set(pluck("url", $relays))
|
||||||
|
|
||||||
@ -24,6 +34,18 @@
|
|||||||
{keys: ["name", "description", "url"]}
|
{keys: ["name", "description", "url"]}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
reviews = await network.load({
|
||||||
|
relays: getUserReadRelays(),
|
||||||
|
filter: {
|
||||||
|
limit: 1000,
|
||||||
|
kinds: [1985],
|
||||||
|
"#l": ["review/relay"],
|
||||||
|
"#L": ["social.coracle.ontology"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@ -35,7 +57,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#each !q && hideIfEmpty ? [] : search(q).slice(0, limit) as relay (relay.url)}
|
{#each !q && hideIfEmpty ? [] : search(q).slice(0, limit) as relay (relay.url)}
|
||||||
<slot name="item" {relay}>
|
<slot name="item" {relay}>
|
||||||
<RelayCard {relay} />
|
<RelayCard rating={ratings[relay.url]} {relay} />
|
||||||
</slot>
|
</slot>
|
||||||
{/each}
|
{/each}
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
import {displayRelay} from "src/util/nostr"
|
import {displayRelay} from "src/util/nostr"
|
||||||
import {webSocketURLToPlainOrBase64} from "src/util/misc"
|
import {webSocketURLToPlainOrBase64} from "src/util/misc"
|
||||||
import {poll, stringToHue, hsl} from "src/util/misc"
|
import {poll, stringToHue, hsl} from "src/util/misc"
|
||||||
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
|
|
||||||
export let relay
|
export let relay
|
||||||
|
export let rating = null
|
||||||
|
|
||||||
let quality = null
|
let quality = null
|
||||||
let message = null
|
let message = null
|
||||||
@ -47,4 +49,9 @@
|
|||||||
class:opacity-1={showStatus}>
|
class:opacity-1={showStatus}>
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
|
{#if rating}
|
||||||
|
<div class="px-4 text-sm">
|
||||||
|
<Rating inert value={rating} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
if (!displayNote.pubkey) {
|
if (!displayNote.pubkey) {
|
||||||
await network.load({
|
await network.load({
|
||||||
relays: sampleRelays(relays),
|
relays: sampleRelays(relays),
|
||||||
filter: {kinds: [1], ids: [displayNote.id]},
|
filter: {ids: [displayNote.id]},
|
||||||
onChunk: events => {
|
onChunk: events => {
|
||||||
displayNote = asDisplayEvent(first(events))
|
displayNote = asDisplayEvent(first(events))
|
||||||
},
|
},
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelay, normalizeRelayUrl} from "src/util/nostr"
|
import {batch} from "src/util/misc"
|
||||||
|
import {displayRelay, normalizeRelayUrl, getAvgQuality} from "src/util/nostr"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
import Tabs from "src/partials/Tabs.svelte"
|
import Tabs from "src/partials/Tabs.svelte"
|
||||||
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import RelayTitle from "src/app/shared/RelayTitle.svelte"
|
import RelayTitle from "src/app/shared/RelayTitle.svelte"
|
||||||
import RelayActions from "src/app/shared/RelayActions.svelte"
|
import RelayActions from "src/app/shared/RelayActions.svelte"
|
||||||
import {relays} from "src/agent/db"
|
import {relays} from "src/agent/db"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
let reviews = []
|
||||||
let activeTab = "reviews"
|
let activeTab = "reviews"
|
||||||
|
|
||||||
url = normalizeRelayUrl(url)
|
url = normalizeRelayUrl(url)
|
||||||
|
|
||||||
|
$: rating = getAvgQuality("review/relay", reviews)
|
||||||
|
|
||||||
const relay = relays.get(url) || {url}
|
const relay = relays.get(url) || {url}
|
||||||
const tabs = ["reviews", "notes"]
|
const tabs = ["reviews", "notes"]
|
||||||
const setActiveTab = tab => {
|
const setActiveTab = tab => {
|
||||||
activeTab = tab
|
activeTab = tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onReview = batch(1000, chunk => {
|
||||||
|
reviews = reviews.concat(chunk)
|
||||||
|
})
|
||||||
|
|
||||||
document.title = displayRelay(relay)
|
document.title = displayRelay(relay)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -27,6 +36,11 @@
|
|||||||
<RelayTitle {relay} />
|
<RelayTitle {relay} />
|
||||||
<RelayActions {relay} />
|
<RelayActions {relay} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if rating}
|
||||||
|
<div class="text-sm">
|
||||||
|
<Rating inert value={rating} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if relay.description}
|
{#if relay.description}
|
||||||
<p>{relay.description}</p>
|
<p>{relay.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@ -34,9 +48,10 @@
|
|||||||
{#if activeTab === "reviews"}
|
{#if activeTab === "reviews"}
|
||||||
<Feed
|
<Feed
|
||||||
invertColors
|
invertColors
|
||||||
|
onEvent={onReview}
|
||||||
filter={{
|
filter={{
|
||||||
kinds: [1985],
|
kinds: [1985],
|
||||||
"#l": ["review"],
|
"#l": ["review/relay"],
|
||||||
"#L": ["social.coracle.ontology"],
|
"#L": ["social.coracle.ontology"],
|
||||||
"#r": [relay.url],
|
"#r": [relay.url],
|
||||||
}} />
|
}} />
|
||||||
|
@ -4,24 +4,26 @@
|
|||||||
import Button from "src/partials/Button.svelte"
|
import Button from "src/partials/Button.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import Textarea from "src/partials/Textarea.svelte"
|
import Compose from "src/partials/Compose.svelte"
|
||||||
import Rating from "src/partials/Rating.svelte"
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import cmd from "src/agent/cmd"
|
import cmd from "src/agent/cmd"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
let review
|
let compose
|
||||||
let rating
|
let rating
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
|
const review = compose.parse()
|
||||||
|
|
||||||
cmd
|
cmd
|
||||||
.createLabel({
|
.createLabel({
|
||||||
content: review,
|
content: review,
|
||||||
tagClient: false,
|
tagClient: false,
|
||||||
tags: [
|
tags: [
|
||||||
["L", "social.coracle.ontology"],
|
["L", "social.coracle.ontology"],
|
||||||
["l", "review", "social.coracle.ontology"],
|
["l", "review/relay", "social.coracle.ontology", JSON.stringify({quality: rating})],
|
||||||
["r", url],
|
["r", url],
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@ -41,7 +43,11 @@
|
|||||||
<Rating bind:value={rating} />
|
<Rating bind:value={rating} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Textarea bind:value={review} placeholder="Share your thoughts..." />
|
<Compose
|
||||||
|
{onSubmit}
|
||||||
|
class="shadow-inset rounded bg-input text-black"
|
||||||
|
style="min-height: 6rem"
|
||||||
|
bind:this={compose} />
|
||||||
<Button type="submit" class="flex-grow text-center">Send</Button>
|
<Button type="submit" class="flex-grow text-center">Send</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -217,7 +217,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ContentEditable bind:this={contenteditable} on:keydown={onKeyDown} on:keyup={onKeyUp} />
|
<ContentEditable
|
||||||
|
style={$$props.style}
|
||||||
|
class={$$props.class}
|
||||||
|
bind:this={contenteditable}
|
||||||
|
on:keydown={onKeyDown}
|
||||||
|
on:keyup={onKeyUp} />
|
||||||
<slot name="addon" />
|
<slot name="addon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
|
|
||||||
let input = null
|
let input = null
|
||||||
|
|
||||||
// Line breaks are wrapped in divs sometimes
|
// Line breaks are wrapped in divs sometimes
|
||||||
@ -86,7 +88,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full min-w-0 p-2 text-gray-2 outline-0"
|
style={$$props.style}
|
||||||
|
class={cx($$props.class, "w-full min-w-0 p-2 text-gray-2 outline-0")}
|
||||||
autofocus
|
autofocus
|
||||||
contenteditable
|
contenteditable
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import {range} from "hurdak/lib/hurdak"
|
import {range} from "hurdak/lib/hurdak"
|
||||||
|
|
||||||
|
export let inert = false
|
||||||
export let value = 1
|
export let value = 1
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
{#each range(0, 5) as x}
|
{#each range(0, 5) as x}
|
||||||
<i
|
<i
|
||||||
class="fa fa-star cursor-pointer text-gray-5 hover:text-warning hover:opacity-75"
|
class={cx("fa fa-star text-gray-5", {
|
||||||
class:text-warning={value >= x / 5}
|
"cursor-pointer hover:text-warning hover:opacity-75": !inert,
|
||||||
|
"text-warning": value >= x / 5,
|
||||||
|
})}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
value = x / 5
|
value = x / 5
|
||||||
}} />
|
}} />
|
||||||
|
@ -219,7 +219,7 @@ export const defer = (): Deferred<any> => {
|
|||||||
return Object.assign(p, {resolve, reject})
|
return Object.assign(p, {resolve, reject})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const avg = xs => sum(xs) / xs.length
|
export const avg = xs => (xs.length > 0 ? sum(xs) / xs.length : 0)
|
||||||
|
|
||||||
// https://stackoverflow.com/a/21682946
|
// https://stackoverflow.com/a/21682946
|
||||||
export const stringToHue = value => {
|
export const stringToHue = value => {
|
||||||
|
@ -2,7 +2,7 @@ import type {DisplayEvent} from "src/util/types"
|
|||||||
import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from "ramda"
|
import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from "ramda"
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {ensurePlural, ellipsize, first} from "hurdak/lib/hurdak"
|
import {ensurePlural, ellipsize, first} from "hurdak/lib/hurdak"
|
||||||
import {tryJson} from "src/util/misc"
|
import {tryJson, avg} from "src/util/misc"
|
||||||
import {invoiceAmount} from "src/util/lightning"
|
import {invoiceAmount} from "src/util/lightning"
|
||||||
|
|
||||||
export const personKinds = [0, 2, 3, 10001, 10002]
|
export const personKinds = [0, 2, 3, 10001, 10002]
|
||||||
@ -60,6 +60,9 @@ export class Tags {
|
|||||||
filter(f) {
|
filter(f) {
|
||||||
return new Tags(this.tags.filter(f))
|
return new Tags(this.tags.filter(f))
|
||||||
}
|
}
|
||||||
|
reject(f) {
|
||||||
|
return new Tags(this.tags.filter(t => !f(t)))
|
||||||
|
}
|
||||||
any(f) {
|
any(f) {
|
||||||
return this.filter(f).exists()
|
return this.filter(f).exists()
|
||||||
}
|
}
|
||||||
@ -363,3 +366,9 @@ export const processZaps = (zaps, author) =>
|
|||||||
export const fromNostrURI = s => s.replace(/^[\w\+]+:\/?\/?/, "")
|
export const fromNostrURI = s => s.replace(/^[\w\+]+:\/?\/?/, "")
|
||||||
|
|
||||||
export const toNostrURI = s => `web+nostr://${s}`
|
export const toNostrURI = s => `web+nostr://${s}`
|
||||||
|
|
||||||
|
export const getLabelQuality = (label, event) =>
|
||||||
|
tryJson(() => JSON.parse(last(Tags.from(event).type("l").equals(label).first())).quality)
|
||||||
|
|
||||||
|
export const getAvgQuality = (label, events) =>
|
||||||
|
avg(events.map(e => getLabelQuality(label, e)).filter(identity))
|
||||||
|
Loading…
Reference in New Issue
Block a user