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
|
||||
|
||||
# 0.2.31
|
||||
|
||||
- [x] Added the ability to view and write reviews on relays, with ratings
|
||||
|
||||
# 0.2.30
|
||||
|
||||
- [x] Prefer followed users when mentioning people
|
||||
|
@ -3,9 +3,6 @@
|
||||
- [ ] List detail pages with follow all and add all to list
|
||||
- [ ] Use vida to stream development
|
||||
- [ ] 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
|
||||
- [ ] White-labeled
|
||||
- [ ] Add invite code registration for relay
|
||||
@ -21,6 +18,7 @@
|
||||
|
||||
# Core
|
||||
|
||||
- [ ] Deploy ontology.coracle.social
|
||||
- [ ] Add threads - replies by self get shown at the top of replies?
|
||||
- [ ] Show link previews when posting
|
||||
- [ ] Embedded music players for Spotify, youtube, etc
|
||||
|
@ -255,7 +255,7 @@ const loadParents = (notes, opts = {}) => {
|
||||
|
||||
return load({
|
||||
relays: sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3),
|
||||
filter: {kinds: [1], ids: notesWithParent.map(findReplyId)},
|
||||
filter: {ids: notesWithParent.map(findReplyId)},
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
@ -268,7 +268,7 @@ const streamContext = ({notes, onChunk, maxDepth = 2}) => {
|
||||
|
||||
const loadChunk = (events, depth) => {
|
||||
// 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 (events.length === 0) {
|
||||
@ -333,7 +333,7 @@ const streamContext = ({notes, onChunk, maxDepth = 2}) => {
|
||||
const applyContext = (notes, context) => {
|
||||
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 zaps = context.filter(propEq("kind", 9735))
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
export let shouldDisplay = always(true)
|
||||
export let parentsTimeout = 500
|
||||
export let invertColors = false
|
||||
export let onEvent = null
|
||||
|
||||
let notes = []
|
||||
let notesBuffer = []
|
||||
@ -90,6 +91,11 @@
|
||||
// Show replies grouped by parent whenever possible
|
||||
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
|
||||
const [bottom, top] = partition(e => e.created_at < since, merged)
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {objOf, reverse} from "ramda"
|
||||
import {nip19} from 'nostr-tools'
|
||||
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 {displayPerson, parseContent, Tags} from "src/util/nostr"
|
||||
import {displayPerson, parseContent, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
||||
import {modal} from "src/partials/state"
|
||||
import MediaSet from "src/partials/MediaSet.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Rating from "src/partials/Rating.svelte"
|
||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||
import {sampleRelays} from "src/agent/relays"
|
||||
import user from "src/agent/user"
|
||||
@ -25,6 +27,7 @@
|
||||
const shouldTruncate = !showEntire && note.content.length > maxLength
|
||||
|
||||
let content = parseContent(note)
|
||||
let rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
||||
|
||||
const links = []
|
||||
const ranges = []
|
||||
@ -118,6 +121,31 @@
|
||||
|
||||
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
|
||||
<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}
|
||||
{#if type === "newline"}
|
||||
{#each value as _}
|
||||
|
@ -6,7 +6,7 @@
|
||||
export let relays
|
||||
export let invertColors = false
|
||||
|
||||
const filter = {kinds: [1], authors: [pubkey]}
|
||||
const filter = {kinds: [1, 1985], authors: [pubkey]}
|
||||
</script>
|
||||
|
||||
<Feed {relays} {filter} {invertColors} parentsTimeout={3000} delta={timedelta(1, "days")} />
|
||||
|
@ -8,12 +8,14 @@
|
||||
import {displayRelay} from "src/util/nostr"
|
||||
import {modal} from "src/partials/state"
|
||||
import Toggle from "src/partials/Toggle.svelte"
|
||||
import Rating from "src/partials/Rating.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import pool from "src/agent/pool"
|
||||
import user from "src/agent/user"
|
||||
import {loadAppData} from "src/app/state"
|
||||
|
||||
export let relay
|
||||
export let rating = null
|
||||
export let theme = "gray-8"
|
||||
export let showStatus = false
|
||||
export let hideActions = false
|
||||
@ -82,6 +84,11 @@
|
||||
{message}
|
||||
</p>
|
||||
{/if}
|
||||
{#if rating}
|
||||
<div class="px-4 text-sm" in:fly={{y: 20}}>
|
||||
<Rating inert value={rating} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !hideActions}
|
||||
<slot name="actions">
|
||||
|
@ -1,9 +1,13 @@
|
||||
<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 {normalizeRelayUrl} from "src/util/nostr"
|
||||
import {normalizeRelayUrl, Tags, getAvgQuality} from "src/util/nostr"
|
||||
import Input from "src/partials/Input.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 user from "src/agent/user"
|
||||
|
||||
@ -14,8 +18,14 @@
|
||||
export let hideIfEmpty = false
|
||||
|
||||
let search
|
||||
let reviews = []
|
||||
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))
|
||||
|
||||
@ -24,6 +34,18 @@
|
||||
{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>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@ -35,7 +57,7 @@
|
||||
{/if}
|
||||
{#each !q && hideIfEmpty ? [] : search(q).slice(0, limit) as relay (relay.url)}
|
||||
<slot name="item" {relay}>
|
||||
<RelayCard {relay} />
|
||||
<RelayCard rating={ratings[relay.url]} {relay} />
|
||||
</slot>
|
||||
{/each}
|
||||
<slot name="footer">
|
||||
|
@ -4,10 +4,12 @@
|
||||
import {displayRelay} from "src/util/nostr"
|
||||
import {webSocketURLToPlainOrBase64} 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 pool from "src/agent/pool"
|
||||
|
||||
export let relay
|
||||
export let rating = null
|
||||
|
||||
let quality = null
|
||||
let message = null
|
||||
@ -47,4 +49,9 @@
|
||||
class:opacity-1={showStatus}>
|
||||
{message}
|
||||
</p>
|
||||
{#if rating}
|
||||
<div class="px-4 text-sm">
|
||||
<Rating inert value={rating} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -34,7 +34,7 @@
|
||||
if (!displayNote.pubkey) {
|
||||
await network.load({
|
||||
relays: sampleRelays(relays),
|
||||
filter: {kinds: [1], ids: [displayNote.id]},
|
||||
filter: {ids: [displayNote.id]},
|
||||
onChunk: events => {
|
||||
displayNote = asDisplayEvent(first(events))
|
||||
},
|
||||
|
@ -1,24 +1,33 @@
|
||||
<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 Feed from "src/app/shared/Feed.svelte"
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Rating from "src/partials/Rating.svelte"
|
||||
import RelayTitle from "src/app/shared/RelayTitle.svelte"
|
||||
import RelayActions from "src/app/shared/RelayActions.svelte"
|
||||
import {relays} from "src/agent/db"
|
||||
|
||||
export let url
|
||||
|
||||
let reviews = []
|
||||
let activeTab = "reviews"
|
||||
|
||||
url = normalizeRelayUrl(url)
|
||||
|
||||
$: rating = getAvgQuality("review/relay", reviews)
|
||||
|
||||
const relay = relays.get(url) || {url}
|
||||
const tabs = ["reviews", "notes"]
|
||||
const setActiveTab = tab => {
|
||||
activeTab = tab
|
||||
}
|
||||
|
||||
const onReview = batch(1000, chunk => {
|
||||
reviews = reviews.concat(chunk)
|
||||
})
|
||||
|
||||
document.title = displayRelay(relay)
|
||||
</script>
|
||||
|
||||
@ -27,6 +36,11 @@
|
||||
<RelayTitle {relay} />
|
||||
<RelayActions {relay} />
|
||||
</div>
|
||||
{#if rating}
|
||||
<div class="text-sm">
|
||||
<Rating inert value={rating} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if relay.description}
|
||||
<p>{relay.description}</p>
|
||||
{/if}
|
||||
@ -34,9 +48,10 @@
|
||||
{#if activeTab === "reviews"}
|
||||
<Feed
|
||||
invertColors
|
||||
onEvent={onReview}
|
||||
filter={{
|
||||
kinds: [1985],
|
||||
"#l": ["review"],
|
||||
"#l": ["review/relay"],
|
||||
"#L": ["social.coracle.ontology"],
|
||||
"#r": [relay.url],
|
||||
}} />
|
||||
|
@ -4,24 +4,26 @@
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import Content from "src/partials/Content.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 user from "src/agent/user"
|
||||
import cmd from "src/agent/cmd"
|
||||
|
||||
export let url
|
||||
|
||||
let review
|
||||
let compose
|
||||
let rating
|
||||
|
||||
const onSubmit = () => {
|
||||
const review = compose.parse()
|
||||
|
||||
cmd
|
||||
.createLabel({
|
||||
content: review,
|
||||
tagClient: false,
|
||||
tags: [
|
||||
["L", "social.coracle.ontology"],
|
||||
["l", "review", "social.coracle.ontology"],
|
||||
["l", "review/relay", "social.coracle.ontology", JSON.stringify({quality: rating})],
|
||||
["r", url],
|
||||
],
|
||||
})
|
||||
@ -41,7 +43,11 @@
|
||||
<Rating bind:value={rating} />
|
||||
</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>
|
||||
</div>
|
||||
</Content>
|
||||
|
@ -217,7 +217,12 @@
|
||||
</script>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
|
||||
let input = null
|
||||
|
||||
// Line breaks are wrapped in divs sometimes
|
||||
@ -86,7 +88,8 @@
|
||||
</script>
|
||||
|
||||
<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
|
||||
contenteditable
|
||||
bind:this={input}
|
||||
|
@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {range} from "hurdak/lib/hurdak"
|
||||
|
||||
export let inert = false
|
||||
export let value = 1
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1">
|
||||
{#each range(0, 5) as x}
|
||||
<i
|
||||
class="fa fa-star cursor-pointer text-gray-5 hover:text-warning hover:opacity-75"
|
||||
class:text-warning={value >= x / 5}
|
||||
class={cx("fa fa-star text-gray-5", {
|
||||
"cursor-pointer hover:text-warning hover:opacity-75": !inert,
|
||||
"text-warning": value >= x / 5,
|
||||
})}
|
||||
on:click={() => {
|
||||
value = x / 5
|
||||
}} />
|
||||
|
@ -219,7 +219,7 @@ export const defer = (): Deferred<any> => {
|
||||
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
|
||||
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 {nip19} from "nostr-tools"
|
||||
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"
|
||||
|
||||
export const personKinds = [0, 2, 3, 10001, 10002]
|
||||
@ -60,6 +60,9 @@ export class Tags {
|
||||
filter(f) {
|
||||
return new Tags(this.tags.filter(f))
|
||||
}
|
||||
reject(f) {
|
||||
return new Tags(this.tags.filter(t => !f(t)))
|
||||
}
|
||||
any(f) {
|
||||
return this.filter(f).exists()
|
||||
}
|
||||
@ -363,3 +366,9 @@ export const processZaps = (zaps, author) =>
|
||||
export const fromNostrURI = s => s.replace(/^[\w\+]+:\/?\/?/, "")
|
||||
|
||||
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