mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-28 16:00:52 +00:00
Add relay symbol to notes, publish optimistically, reduce how many relays replies are published to, add more logging, re-work thread layout, optimize note loading by switching from debounce to throttle
This commit is contained in:
parent
233d754258
commit
f7e6d46fcf
10
README.md
10
README.md
@ -21,6 +21,7 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
|
||||
# Snacks
|
||||
|
||||
- [ ] Pinned posts ala snort
|
||||
- [ ] Add nip05 verification on feed
|
||||
- [ ] Linkify follow/followers numbers
|
||||
- [ ] Support key delegation
|
||||
@ -30,10 +31,10 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
- [ ] Attachments (a tag w/content type and url)
|
||||
- [ ] Linkify bech32 entities w/ NIP 21 https://github.com/nostr-protocol/nips/blob/master/21.md
|
||||
- [ ] Sign in as user with one click to view things from their pubkey's perspective - do this with multiple accounts
|
||||
- [ ] QR code generation/scanner to share nprofile https://cdn.jb55.com/s/d966a729777c2021.MP4
|
||||
|
||||
# Missions
|
||||
|
||||
- [ ] Topics/hashtag views
|
||||
- [ ] Support paid relays
|
||||
- atlas.nostr.land
|
||||
- eden.nostr.land
|
||||
@ -74,17 +75,16 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
|
||||
# Current
|
||||
|
||||
- [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html
|
||||
- [_] Add nip 05 to calculation
|
||||
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
|
||||
- nevent1qqspjcqw2hu5gfcpkrjhs0aqvxuzjgtp50l375mcqjfpmk48cg5hevgpr3mhxue69uhkummnw3ez6un9d3shjtnhd3m8xtnnwpskxegpzamhxue69uhkummnw3ezuendwsh8w6t69e3xj7spramhxue69uhkummnw3ez6un9d3shjtnwdahxxefwv93kzer9d4usz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcpr9mhxue69uhkummnw3ezuer9d3hjuum0ve68wctjv5n8hwfg
|
||||
- [ ] Show notification at top of feeds: "Showing notes from 3 relays". Click to customize.
|
||||
- [ ] Click through on relays page to view a feed for only that relay.
|
||||
- [ ] Custom views: slider between fast/complete with a warning at either extreme
|
||||
- [ ] Deterministically calculate color for relays, show it on notes. User popper?
|
||||
- [ ] Likes list
|
||||
- [ ] Fix anon/new user experience
|
||||
- [ ] Likes are slow
|
||||
- [ ] Show loading on replies/notes
|
||||
- [ ] Show loading on replies/new notes
|
||||
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
|
||||
|
||||
# Changelog
|
||||
|
||||
|
@ -26,6 +26,7 @@
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Modal from 'src/partials/Modal.svelte'
|
||||
import RelayCard from "src/partials/RelayCard.svelte"
|
||||
import SignUp from "src/views/SignUp.svelte"
|
||||
import PersonList from "src/views/PersonList.svelte"
|
||||
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
|
||||
@ -361,6 +362,12 @@
|
||||
<NoteCreate />
|
||||
{:else if $modal.type === 'relay/add'}
|
||||
<AddRelay />
|
||||
{:else if $modal.type === 'relay/list'}
|
||||
<Content>
|
||||
{#each $modal.relays as relay}
|
||||
<RelayCard showControls {relay} />
|
||||
{/each}
|
||||
</Content>
|
||||
{:else if $modal.type === 'signUp'}
|
||||
<SignUp />
|
||||
{:else if $modal.type === 'room/edit'}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type {MyEvent} from 'src/util/types'
|
||||
import {prop, pick, join, uniqBy, last} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
@ -80,18 +81,19 @@ const deleteEvent = (relays, ids) =>
|
||||
|
||||
// Utils
|
||||
|
||||
const publishEvent = (relays, kind, {content = '', tags = []} = {}) => {
|
||||
const publishEvent = (relays, kind, {content = '', tags = []} = {}): [MyEvent, Promise<MyEvent>] => {
|
||||
if (relays.length === 0) {
|
||||
throw new Error("Unable to publish, no relays specified")
|
||||
}
|
||||
|
||||
const pubkey = get(keys.pubkey)
|
||||
const createdAt = Math.round(new Date().valueOf() / 1000)
|
||||
const event = {kind, content, tags, pubkey, created_at: createdAt}
|
||||
const event = {kind, content, tags, pubkey, created_at: createdAt} as MyEvent
|
||||
|
||||
log("Publishing", event, relays)
|
||||
|
||||
return network.publish(relays, event)
|
||||
// Return the event synchronously, separate from the promise
|
||||
return [event, network.publish(relays, event)]
|
||||
}
|
||||
|
||||
export default {
|
@ -1,7 +1,7 @@
|
||||
import type {Person, MyEvent} from 'src/util/types'
|
||||
import type {Readable} from 'svelte/store'
|
||||
import {isEmpty, pick, identity, sortBy, uniq, reject, groupBy, last, propEq, uniqBy, prop} from 'ramda'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {first, ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import {derived, get} from 'svelte/store'
|
||||
import {Tags} from 'src/util/nostr'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
@ -61,17 +61,25 @@ export const getTopRelays = (pubkeys, mode = 'all') => {
|
||||
export const getBestRelay = (pubkey, mode = 'all') =>
|
||||
first(getTopRelays([pubkey], mode).concat(getPubkeyRelays(pubkey, mode)))
|
||||
|
||||
export const getEventRelays = event => {
|
||||
export const getAllEventRelays = events => {
|
||||
return uniqBy(
|
||||
prop('url'),
|
||||
getPubkeyRelays(event.pubkey, 'write')
|
||||
.concat(Tags.from(event).relays())
|
||||
.concat({url: event.seen_on})
|
||||
ensurePlural(events)
|
||||
.flatMap(event =>
|
||||
getPubkeyRelays(event.pubkey, 'write')
|
||||
.concat(Tags.from(event).relays())
|
||||
.concat({url: event.seen_on})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const getTopRelaysFromEvents = (events: Array<MyEvent>) =>
|
||||
uniqBy(prop('url'), events.map(e => getBestRelay(e.pubkey) || {url: e.seen_on}))
|
||||
export const getTopEventRelays = (events: Array<MyEvent>, mode = 'all') =>
|
||||
uniqBy(
|
||||
prop('url'),
|
||||
ensurePlural(events)
|
||||
.flatMap(e => [getBestRelay(e.pubkey, mode), {url: e.seen_on}])
|
||||
.filter(identity)
|
||||
)
|
||||
|
||||
export const getStalePubkeys = pubkeys => {
|
||||
// If we're not reloading, only get pubkeys we don't already know about
|
||||
|
@ -2,7 +2,7 @@ import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
|
||||
import {findReply, personKinds, findReplyId, Tags} from 'src/util/nostr'
|
||||
import {chunk} from 'hurdak/lib/hurdak'
|
||||
import {batch} from 'src/util/misc'
|
||||
import {getFollows, getStalePubkeys, getTopRelaysFromEvents} from 'src/agent/helpers'
|
||||
import {getFollows, getStalePubkeys, getTopEventRelays} from 'src/agent/helpers'
|
||||
import pool from 'src/agent/pool'
|
||||
import keys from 'src/agent/keys'
|
||||
import sync from 'src/agent/sync'
|
||||
@ -85,7 +85,7 @@ const loadParents = (relays, notes) => {
|
||||
}
|
||||
|
||||
return load(
|
||||
relays.concat(getTopRelaysFromEvents(notes)),
|
||||
relays.concat(getTopEventRelays(notes, 'read')),
|
||||
{kinds: [1], ids: Array.from(parentIds)}
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type {Relay} from 'nostr-tools'
|
||||
import type {MyEvent} from 'src/util/types'
|
||||
import {relayInit} from 'nostr-tools'
|
||||
import {uniqBy, prop, find, is} from 'ramda'
|
||||
import {uniqBy, without, prop, find, is} from 'ramda'
|
||||
import {ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import {warn} from 'src/util/logger'
|
||||
import {warn, log} from 'src/util/logger'
|
||||
import {isRelay} from 'src/util/nostr'
|
||||
import {sleep} from 'src/util/misc'
|
||||
import database from 'src/agent/database'
|
||||
@ -127,6 +127,8 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
|
||||
const seen = new Set()
|
||||
const eose = new Set()
|
||||
|
||||
log(`Starting subscription ${id} with ${relays.length} relays`, filters)
|
||||
|
||||
// Don't await before returning so we're not blocking on slow connects
|
||||
const promises = relays.map(async relay => {
|
||||
const conn = await connect(relay.url)
|
||||
@ -175,6 +177,8 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
|
||||
|
||||
return {
|
||||
unsub: () => {
|
||||
log(`Closing subscription ${id}`)
|
||||
|
||||
promises.forEach(async promise => {
|
||||
const sub = await promise
|
||||
|
||||
@ -206,7 +210,16 @@ const subscribeUntilEose = async (
|
||||
const eose = new Set()
|
||||
|
||||
const attemptToComplete = () => {
|
||||
if (eose.size === relays.length || Date.now() - now >= timeout) {
|
||||
const isComplete = eose.size === relays.length
|
||||
const isTimeout = Date.now() - now >= timeout
|
||||
|
||||
if (isTimeout) {
|
||||
const timedOutRelays = without(Array.from(eose), relays)
|
||||
|
||||
log(`Timing out ${timedOutRelays.length} relays after ${timeout}ms`, timedOutRelays)
|
||||
}
|
||||
|
||||
if (isComplete || isTimeout) {
|
||||
onClose?.()
|
||||
agg.unsub()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import {Link} from 'svelte-routing'
|
||||
import ImageCircle from 'src/partials/ImageCircle.svelte'
|
||||
import {killEvent} from 'src/util/html'
|
||||
import {displayPerson} from 'src/util/nostr'
|
||||
import {routes} from 'src/app/ui'
|
||||
@ -10,9 +11,7 @@
|
||||
|
||||
{#if inert}
|
||||
<span class="flex gap-2 items-center relative z-10">
|
||||
<div
|
||||
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||
style="background-image: url({person.picture})" />
|
||||
<ImageCircle src={person.picture} />
|
||||
<span class="text-lg font-bold">{displayPerson(person)}</span>
|
||||
</span>
|
||||
{:else}
|
||||
@ -20,9 +19,7 @@
|
||||
to={routes.person(person.pubkey)}
|
||||
class="flex gap-2 items-center relative z-10"
|
||||
on:click={killEvent}>
|
||||
<div
|
||||
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||
style="background-image: url({person.picture})" />
|
||||
<ImageCircle src={person.picture} />
|
||||
<span class="text-lg font-bold">{displayPerson(person)}</span>
|
||||
</Link>
|
||||
{/if}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div
|
||||
on:click
|
||||
in:fly={{y: 20}}
|
||||
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
|
||||
class={cx($$props.class, "card py-2 px-3 text-white", {
|
||||
"cursor-pointer transition-all": interactive,
|
||||
"hover:bg-dark": interactive && !invertColors,
|
||||
"hover:bg-medium": interactive && invertColors,
|
||||
|
10
src/partials/ImageCircle.svelte
Normal file
10
src/partials/ImageCircle.svelte
Normal file
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import cx from 'classnames'
|
||||
|
||||
export let src
|
||||
export let size = 4
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx($$props.class, `overflow-hidden w-${size} h-${size} rounded-full bg-cover bg-center shrink-0 border border-solid border-white`)}
|
||||
style="background-image: url({src})" />
|
@ -1,23 +1,25 @@
|
||||
<script lang="ts">
|
||||
import cx from 'classnames'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {always, whereEq, without, uniq, pluck, reject, propEq, find} from 'ramda'
|
||||
import {last, always, whereEq, without, uniq, pluck, reject, propEq, find} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {tweened} from 'svelte/motion'
|
||||
import {slide} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {quantify} from 'hurdak/lib/hurdak'
|
||||
import {Tags, findReply, findRoot, findReplyId, displayPerson, isLike} from "src/util/nostr"
|
||||
import {extractUrls} from "src/util/html"
|
||||
import ImageCircle from 'src/partials/ImageCircle.svelte'
|
||||
import Preview from 'src/partials/Preview.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {toast, settings, modal, renderNote} from "src/app"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import {user, getEventRelays} from 'src/agent/helpers'
|
||||
import {user, getTopEventRelays, getAllEventRelays} from 'src/agent/helpers'
|
||||
import database from 'src/agent/database'
|
||||
import cmd from 'src/agent/cmd'
|
||||
import {routes} from 'src/app/ui'
|
||||
|
||||
export let note
|
||||
export let anchorId = null
|
||||
@ -37,7 +39,7 @@
|
||||
const interactive = !anchorId || !showEntire
|
||||
const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey))
|
||||
|
||||
let likes, flags, like, flag
|
||||
let likes, flags, like, flag, border, childrenContainer, noteContainer
|
||||
|
||||
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
|
||||
const likesCount = tweened(0, {interpolate})
|
||||
@ -60,30 +62,35 @@
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
|
||||
modal.set({type: 'note/detail', note, relays: getEventRelays(note)})
|
||||
modal.set({type: 'note/detail', note, relays: getTopEventRelays(note)})
|
||||
}
|
||||
}
|
||||
|
||||
const goToParent = async () => {
|
||||
const [id, url] = findReply(note).slice(1)
|
||||
const relays = getEventRelays(note).concat({url})
|
||||
const relays = getTopEventRelays(note).concat({url})
|
||||
|
||||
modal.set({type: 'note/detail', note: {id}, relays})
|
||||
}
|
||||
|
||||
const goToRoot = async () => {
|
||||
const [id, url] = findRoot(note).slice(1)
|
||||
const relays = getEventRelays(note).concat({url})
|
||||
const relays = getTopEventRelays(note).concat({url})
|
||||
|
||||
modal.set({type: 'note/detail', note: {id}, relays})
|
||||
}
|
||||
|
||||
const showActiveRelays = () => {
|
||||
modal.set({type: 'relay/list', relays: [{url: note.seen_on}]})
|
||||
}
|
||||
|
||||
const react = async content => {
|
||||
if (!$user) {
|
||||
return navigate('/login')
|
||||
}
|
||||
|
||||
const event = await cmd.createReaction(getEventRelays(note), note, content)
|
||||
const relays = getTopEventRelays(note)
|
||||
const [event] = cmd.createReaction(relays, note, content)
|
||||
|
||||
if (content === '+') {
|
||||
likes = likes.concat(event)
|
||||
@ -95,7 +102,7 @@
|
||||
}
|
||||
|
||||
const deleteReaction = e => {
|
||||
cmd.deleteEvent(getEventRelays(note), [e.id])
|
||||
cmd.deleteEvent(getAllEventRelays(note), [e.id])
|
||||
|
||||
if (e.content === '+') {
|
||||
likes = reject(propEq('pubkey', $user.pubkey), likes)
|
||||
@ -123,14 +130,14 @@
|
||||
replyMentions = getDefaultReplyMentions()
|
||||
}
|
||||
|
||||
const sendReply = async () => {
|
||||
const sendReply = () => {
|
||||
let {content, mentions, topics} = reply.parse()
|
||||
|
||||
if (content) {
|
||||
mentions = uniq(mentions.concat(replyMentions))
|
||||
|
||||
const relays = getEventRelays(note)
|
||||
const event = await cmd.createReply(relays, note, content, mentions, topics)
|
||||
const relays = getTopEventRelays(note)
|
||||
const [event] = cmd.createReply(relays, note, content, mentions, topics)
|
||||
|
||||
toast.show("info", {
|
||||
text: `Your note has been created!`,
|
||||
@ -154,6 +161,33 @@
|
||||
resetReply()
|
||||
}
|
||||
}
|
||||
|
||||
const setBorderHeight = () => {
|
||||
const getHeight = e => e?.getBoundingClientRect().height || 0
|
||||
|
||||
if (childrenContainer && noteContainer) {
|
||||
const lastChild = last(
|
||||
[].slice.apply(childrenContainer.children)
|
||||
.filter(e => e.matches('.note'))
|
||||
)
|
||||
|
||||
const height = (
|
||||
getHeight(noteContainer)
|
||||
+ getHeight(replyContainer)
|
||||
+ getHeight(childrenContainer)
|
||||
- getHeight(lastChild)
|
||||
- getHeight(lastChild.nextElementSibling)
|
||||
)
|
||||
|
||||
border.style = `height: ${height - 21}px`
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(setBorderHeight, 300)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
@ -166,64 +200,85 @@
|
||||
/>
|
||||
|
||||
{#if $person && shouldDisplay(note)}
|
||||
<Card on:click={onClick} {interactive} {invertColors}>
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<Badge person={$person} />
|
||||
<Anchor
|
||||
href={"/" + nip19.neventEncode({
|
||||
id: note.id,
|
||||
relays: pluck('url', getEventRelays(note).slice(0, 5)),
|
||||
})}
|
||||
class="text-sm text-light"
|
||||
type="unstyled">
|
||||
{formatTimestamp(note.created_at)}
|
||||
<div bind:this={noteContainer} class="note relative">
|
||||
<div class="absolute w-px bg-light z-10 ml-8 mt-12 transition-all h-0" bind:this={border} />
|
||||
<Card class="flex gap-4 relative" on:click={onClick} {interactive} {invertColors}>
|
||||
{#if !showParent}
|
||||
<div class="absolute h-px w-3 bg-light z-10" style="left: 0px; top: 27px;" />
|
||||
{/if}
|
||||
<Anchor class="text-lg font-bold" href={routes.person($person.pubkey)}>
|
||||
<ImageCircle size={10} src={$person.picture} />
|
||||
</Anchor>
|
||||
</div>
|
||||
<div class="ml-6 flex flex-col gap-2">
|
||||
{#if findReply(note) && showParent}
|
||||
<small class="text-light">
|
||||
Reply to <Anchor on:click={goToParent}>{findReplyId(note).slice(0, 8)}</Anchor>
|
||||
</small>
|
||||
{/if}
|
||||
{#if findRoot(note) && findRoot(note) !== findReply(note) && showParent}
|
||||
<small class="text-light">
|
||||
Go to <Anchor on:click={goToRoot}>root</Anchor>
|
||||
</small>
|
||||
{/if}
|
||||
{#if flag}
|
||||
<p class="text-light border-l-2 border-solid border-medium pl-4">
|
||||
You have flagged this content as offensive.
|
||||
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
|
||||
<p>{@html renderNote(note, {showEntire})}</p>
|
||||
{#each links.slice(-2) as link}
|
||||
<button class="inline-block" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex gap-6 text-light" on:click={e => e.stopPropagation()}>
|
||||
<div>
|
||||
<button class="fa fa-reply cursor-pointer" on:click={startReply} />
|
||||
{$repliesCount}
|
||||
<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)}>
|
||||
<span>{displayPerson($person)}</span>
|
||||
{#if $person.verified_as}
|
||||
<i class="fa fa-circle-check text-accent text-sm" />
|
||||
{/if}
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href={"/" + nip19.neventEncode({
|
||||
id: note.id,
|
||||
relays: pluck('url', getTopEventRelays(note).slice(0, 5)),
|
||||
})}
|
||||
class="text-sm text-light"
|
||||
type="unstyled">
|
||||
{formatTimestamp(note.created_at)}
|
||||
</Anchor>
|
||||
</div>
|
||||
<div class={cx({'text-accent': like})}>
|
||||
<button class="fa fa-heart cursor-pointer" on:click={() => like ? deleteReaction(like) : react("+")} />
|
||||
{$likesCount}
|
||||
</div>
|
||||
<div>
|
||||
<button class="fa fa-flag cursor-pointer" on:click={() => react("-")} />
|
||||
{$flagsCount}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if findReply(note) && showParent}
|
||||
<small class="text-light">
|
||||
Reply to <Anchor on:click={goToParent}>{findReplyId(note).slice(0, 8)}</Anchor>
|
||||
</small>
|
||||
{/if}
|
||||
{#if findRoot(note) && findRoot(note) !== findReply(note) && showParent}
|
||||
<small class="text-light">
|
||||
Go to <Anchor on:click={goToRoot}>root</Anchor>
|
||||
</small>
|
||||
{/if}
|
||||
{#if flag}
|
||||
<p class="text-light border-l-2 border-solid border-medium pl-4">
|
||||
You have flagged this content as offensive.
|
||||
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
|
||||
<p>{@html renderNote(note, {showEntire})}</p>
|
||||
{#each links.slice(-2) as link}
|
||||
<button class="inline-block" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex justify-between text-light" on:click={e => e.stopPropagation()}>
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
<button class="fa fa-reply cursor-pointer" on:click={startReply} />
|
||||
{$repliesCount}
|
||||
</div>
|
||||
<div class={cx({'text-accent': like})}>
|
||||
<button class="fa fa-heart cursor-pointer" on:click={() => like ? deleteReaction(like) : react("+")} />
|
||||
{$likesCount}
|
||||
</div>
|
||||
<div>
|
||||
<button class="fa fa-flag cursor-pointer" on:click={() => react("-")} />
|
||||
{$flagsCount}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cursor-pointer text-light" on:click={showActiveRelays}>
|
||||
<i class="fa fa-server" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{#if reply}
|
||||
<div transition:slide class="note-reply" bind:this={replyContainer}>
|
||||
<div transition:slide class="note-reply relative z-10" bind:this={replyContainer}>
|
||||
<div class="bg-medium border-medium border border-solid">
|
||||
<Compose bind:this={reply} onSubmit={sendReply}>
|
||||
<button
|
||||
@ -236,7 +291,7 @@
|
||||
</Compose>
|
||||
</div>
|
||||
{#if replyMentions.length > 0}
|
||||
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium">
|
||||
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium bg-black">
|
||||
<div class="inline-block border-r border-solid border-medium pl-1 pr-3 mr-2">
|
||||
<i class="fa fa-at" />
|
||||
</div>
|
||||
@ -252,7 +307,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="ml-5 border-l border-solid border-medium">
|
||||
{#if note.children.length > 0}
|
||||
<div class="ml-8 note-children" bind:this={childrenContainer}>
|
||||
{#if !showEntire && note.children.length > 3}
|
||||
<button class="ml-5 py-2 text-light cursor-pointer" on:click={onClick}>
|
||||
<i class="fa fa-up-down text-sm pr-2" />
|
||||
@ -264,3 +320,5 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from 'svelte'
|
||||
import {propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
|
||||
import {partition, propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
|
||||
import {slide} from 'svelte/transition'
|
||||
import {quantify} from 'hurdak/lib/hurdak'
|
||||
import {createScroller, now, Cursor} from 'src/util/misc'
|
||||
@ -18,6 +18,7 @@
|
||||
let notes = []
|
||||
let notesBuffer = []
|
||||
|
||||
const since = now()
|
||||
const maxNotes = 300
|
||||
const muffle = getMuffle()
|
||||
const cursor = new Cursor()
|
||||
@ -52,47 +53,40 @@
|
||||
// Drop notes at the end if there are a lot
|
||||
notes = uniqBy(
|
||||
prop('id'),
|
||||
notesBuffer.splice(0).filter(shouldDisplay).concat(notes).slice(0, maxNotes)
|
||||
notesBuffer.filter(shouldDisplay).concat(notes).slice(0, maxNotes)
|
||||
)
|
||||
|
||||
notesBuffer = []
|
||||
}
|
||||
|
||||
const onChunk = async newNotes => {
|
||||
const chunk = sortBy(e => -e.created_at, await processNewNotes(newNotes))
|
||||
const [bottom, top] = partition(e => e.created_at < since, chunk)
|
||||
|
||||
// Slice new notes in case someone leaves the tab open for a long time
|
||||
notes = uniqBy(prop('id'), notes.concat(bottom))
|
||||
notesBuffer = top.concat(notesBuffer).slice(0, maxNotes)
|
||||
|
||||
// Check all notes every time to stay very conservative with moving the window
|
||||
cursor.onChunk(notes)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const sub = network.listen(
|
||||
relays,
|
||||
{...filter, since: now()},
|
||||
async newNotes => {
|
||||
const chunk = await processNewNotes(newNotes)
|
||||
const sub = network.listen(relays, {...filter, since}, onChunk)
|
||||
|
||||
// Slice new notes in case someone leaves the tab open for a long time
|
||||
notesBuffer = chunk.concat(notesBuffer).slice(0, maxNotes)
|
||||
}
|
||||
)
|
||||
|
||||
const scroller = createScroller(async () => {
|
||||
const scroller = createScroller(() => {
|
||||
if ($modal) {
|
||||
return
|
||||
}
|
||||
|
||||
const {limit, until} = cursor
|
||||
|
||||
return network.listenUntilEose(
|
||||
relays,
|
||||
{...filter, limit, until},
|
||||
async newNotes => {
|
||||
cursor.onChunk(newNotes)
|
||||
|
||||
const chunk = await processNewNotes(newNotes)
|
||||
|
||||
notes = sortBy(e => -e.created_at, uniqBy(prop('id'), notes.concat(chunk)))
|
||||
}
|
||||
)
|
||||
return network.listenUntilEose(relays, {...filter, until, limit}, onChunk)
|
||||
})
|
||||
|
||||
return async () => {
|
||||
const {unsub} = await sub
|
||||
|
||||
return () => {
|
||||
scroller.stop()
|
||||
unsub()
|
||||
sub.then(s => s?.unsub())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -10,7 +10,6 @@
|
||||
import {addRelay, removeRelay, setRelayWriteCondition} from "src/app"
|
||||
|
||||
export let relay
|
||||
export let i = 0
|
||||
export let showControls = false
|
||||
|
||||
let status = null
|
||||
@ -35,7 +34,7 @@
|
||||
|
||||
<div
|
||||
class="rounded border border-solid border-medium bg-dark shadow flex flex-col justify-between gap-3 py-3 px-6"
|
||||
in:fly={{y: 20, delay: i * 100}}>
|
||||
in:fly={{y: 20}}>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div class="flex gap-2 items-center text-xl">
|
||||
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
|
||||
|
@ -9,14 +9,14 @@
|
||||
<div class="inline-block">
|
||||
<div class="rounded flex border border-solid border-light cursor-pointer">
|
||||
{#each options as option, i}
|
||||
<button
|
||||
class={cx("px-4 py-2", {
|
||||
<div
|
||||
class={cx("px-4 py-2 transition-all", {
|
||||
"border-l border-solid border-light": i > 0,
|
||||
"bg-accent": value === option,
|
||||
})}
|
||||
on:click={() => { value = option }}>
|
||||
{option}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import NoteDetail from 'src/views/NoteDetail.svelte'
|
||||
import Person from 'src/routes/Person.svelte'
|
||||
import {getUserRelays} from 'src/agent/helpers'
|
||||
|
||||
export let entity
|
||||
|
||||
@ -13,7 +14,7 @@
|
||||
onMount(() => {
|
||||
try {
|
||||
({type, data} = nip19.decode(entity) as {type: string, data: any})
|
||||
relays = (data.relays || []).map(objOf('url'))
|
||||
relays = (data.relays || []).map(objOf('url')).concat(getUserRelays())
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {now} from 'src/util/misc'
|
||||
import Channel from 'src/partials/Channel.svelte'
|
||||
import {getEventRelays, user} from 'src/agent/helpers'
|
||||
import {getTopEventRelays, user} from 'src/agent/helpers'
|
||||
import database from 'src/agent/database'
|
||||
import network from 'src/agent/network'
|
||||
import {modal} from 'src/app'
|
||||
@ -15,7 +15,7 @@
|
||||
const room = database.watch('rooms', rooms => rooms.get(roomId))
|
||||
|
||||
const listenForMessages = async cb => {
|
||||
const relays = getEventRelays($room)
|
||||
const relays = getTopEventRelays($room)
|
||||
|
||||
return network.listen(
|
||||
relays,
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
const loadMessages = async ({until, limit}) => {
|
||||
const relays = getEventRelays($room)
|
||||
const relays = getTopEventRelays($room)
|
||||
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
|
||||
|
||||
if (events.length) {
|
||||
@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
const sendMessage = content =>
|
||||
cmd.createChatMessage(getEventRelays($room), roomId, content)
|
||||
cmd.createChatMessage(getTopEventRelays($room), roomId, content)
|
||||
</script>
|
||||
|
||||
<Channel
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
const sendMessage = async content => {
|
||||
const cyphertext = await crypt.encrypt(pubkey, content)
|
||||
const event = await cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
|
||||
const [event] = cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
|
||||
|
||||
// Return unencrypted content so we can display it immediately
|
||||
return {...event, content}
|
||||
|
@ -46,7 +46,7 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
await cmd.updateUser(getUserRelays('write'), values)
|
||||
cmd.updateUser(getUserRelays('write'), values)
|
||||
|
||||
navigate(routes.person($user.pubkey, 'profile'))
|
||||
|
||||
|
@ -78,8 +78,8 @@
|
||||
<div class="text-center">No relays connected</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{#each relays as relay, i (relay.url)}
|
||||
<RelayCard showControls {relay} {i} />
|
||||
{#each relays as relay (relay.url)}
|
||||
<RelayCard showControls {relay} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
|
||||
|
@ -107,3 +107,4 @@ export const renderContent = content => {
|
||||
|
||||
return content.trim()
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {debounce, throttle} from 'throttle-debounce'
|
||||
import {allPass, prop, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda"
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
import {writable} from 'svelte/store'
|
||||
@ -151,7 +151,16 @@ export class Cursor {
|
||||
this.limit = limit
|
||||
}
|
||||
onChunk(events) {
|
||||
this.until = events.reduce((t, e) => Math.min(t, e.created_at), this.until)
|
||||
if (events.length > 0) {
|
||||
// Don't go straight to the earliest event, since relays often spit
|
||||
// very old stuff at us. Use an overlapping window instead.
|
||||
const sortedEvents = sortBy(prop('created_at'), events)
|
||||
const midpointEvent = sortedEvents[Math.floor(events.length / 2)]
|
||||
|
||||
this.until = Math.min(this.until, midpointEvent.created_at)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,7 +181,7 @@ export const shuffle = sortBy(() => Math.random() > 0.5)
|
||||
|
||||
export const batch = (t, f) => {
|
||||
const xs = []
|
||||
const cb = debounce(t, () => f(xs.splice(0)))
|
||||
const cb = throttle(t, () => f(xs.splice(0)))
|
||||
|
||||
return x => {
|
||||
xs.push(x)
|
||||
|
@ -36,9 +36,9 @@
|
||||
if (!room.name) {
|
||||
toast.show("error", "Please enter a name for your room.")
|
||||
} else {
|
||||
const event = room.id
|
||||
? await cmd.updateRoom(getUserRelays('write'), room)
|
||||
: await cmd.createRoom(getUserRelays('write'), room)
|
||||
const [event] = room.id
|
||||
? cmd.updateRoom(getUserRelays('write'), room)
|
||||
: cmd.createRoom(getUserRelays('write'), room)
|
||||
|
||||
await database.rooms.patch({id: room.id || event.id, joined: true})
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
const {content, mentions, topics} = input.parse()
|
||||
|
||||
if (content) {
|
||||
const event = await cmd.createNote(relays, content, mentions, topics)
|
||||
const [event] = cmd.createNote(relays, content, mentions, topics)
|
||||
|
||||
toast.show("info", {
|
||||
text: `Your note has been created!`,
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {fly} from 'svelte/transition'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {log} from 'src/util/logger'
|
||||
import {getEventRelays, getUserRelays} from 'src/agent/helpers'
|
||||
import {getAllEventRelays} from 'src/agent/helpers'
|
||||
import network from 'src/agent/network'
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
@ -12,11 +12,13 @@
|
||||
import {asDisplayEvent} from 'src/app'
|
||||
|
||||
export let note
|
||||
export let relays = getEventRelays(note)
|
||||
export let relays = []
|
||||
|
||||
let loading = true
|
||||
|
||||
onMount(async () => {
|
||||
relays = relays.concat(getAllEventRelays(note))
|
||||
|
||||
if (!note.pubkey) {
|
||||
note = first(await network.load(relays, {ids: [note.id]}))
|
||||
}
|
||||
@ -25,9 +27,9 @@
|
||||
log('NoteDetail', nip19.noteEncode(note.id), note)
|
||||
|
||||
network.streamContext({
|
||||
relays,
|
||||
depth: 10,
|
||||
notes: [note],
|
||||
relays: getUserRelays().concat(relays),
|
||||
updateNotes: cb => {
|
||||
note = first(cb([note]))
|
||||
},
|
||||
|
@ -15,9 +15,9 @@
|
||||
{#if (person.relays || []).length === 0}
|
||||
<div class="pt-8 text-center">No relays found</div>
|
||||
{:else}
|
||||
{#each person.relays as relay, i (relay.url)}
|
||||
{#each person.relays as relay (relay.url)}
|
||||
{#if relay.write !== '!'}
|
||||
<RelayCard {relay} {i} />
|
||||
<RelayCard {relay} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user