Try opening feeds filtered by relay in a modal

This commit is contained in:
Jonathan Staab 2023-04-06 17:56:02 -05:00
parent 5d6d3b1b6f
commit dbbbec4d07
18 changed files with 230 additions and 107 deletions

View File

@ -1,13 +1,14 @@
# Current
- [ ] Fix scrolling with embedded modals by registering open modals in the component
- [ ] Relays bounty
- [x] Ability to click into a relay's global feed
- [ ] Ability to filter feeds by relay
- Global Mutes? Global Whitelist?
- Open in modal with "here's what this feed would look like with only this relay"
- [x] Ability to filter feeds by relay
- [-] Global Mutes? Global Whitelist?
- [x] Open in modal with "here's what this feed would look like with only this relay"
- [ ] Fix scrolling with embedded modals by registering open modals in the component
- [ ] Ability to create custom feeds
- [ ] Fix tag-style event mentions. Probably transform all mentions into entities in parse
- [ ] Some lnurls aren't working npub1y3k2nheva29y9ej8a22e07epuxrn04rvgy28wvs54y57j7vsxxuq0gvp4j
- [ ] Fix performance issues
- [ ] https://github.com/techfort/LokiJS
- Use indexed adapter github.com/techfort/LokiJS/blob/master/tutorials/Persistence%20Adapters.md and partitioning adapter

View File

@ -24,7 +24,7 @@
import user from "src/agent/user"
import {loadAppData} from "src/app"
import {theme, getThemeVariables} from "src/app/ui"
import {modal, routes, menuIsOpen, logUsage} from "src/app/ui"
import {modal, openModals, routes, menuIsOpen, logUsage} from "src/app/ui"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
@ -97,11 +97,16 @@
}
onMount(() => {
// Keep scroll position on body, but don't allow scrolling
// Log modals
const unsubModal = modal.subscribe($modal => {
if ($modal) {
logUsage(btoa(["modal", $modal.type].join(":")))
}
})
// Keep scroll position on body, but don't allow scrolling
const unsubOpenModals = openModals.subscribe(n => {
if (n > 0) {
// This is not idempotent, so don't duplicate it
if (document.body.style.position !== "fixed") {
scrollY = window.scrollY
@ -133,6 +138,7 @@
return () => {
unsubHistory()
unsubModal()
unsubOpenModals()
}
})

View File

@ -118,7 +118,7 @@ export const getRelaysForEventParent = event => {
// to read from the current user's network's read relays instead.
export const getRelaysForEventChildren = event => {
return uniqByUrl(
getPubkeyReadRelays(event.pubkey).concat(event.seen_on.map(url => ({url, score: 1})))
getPubkeyReadRelays(event.pubkey).concat((event.seen_on || []).map(url => ({url, score: 1})))
)
}

View File

@ -36,6 +36,7 @@ export const routes = new Table("routes", "id", {
listener.connect()
export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
export const getRelayWithFallback = url => relays.get(url) || {url}
const ready = derived(pluck("ready", Object.values(registry)), all(identity))

View File

@ -6,7 +6,7 @@ import {navigate} from "svelte-routing"
import {nip19} from "nostr-tools"
import {writable, get} from "svelte/store"
import {globalHistory} from "svelte-routing/src/history"
import {sleep, WritableList, synced, hash} from "src/util/misc"
import {sleep, synced, hash} from "src/util/misc"
import {warn} from "src/util/logger"
import user from "src/agent/user"
@ -48,6 +48,8 @@ export const menuIsOpen = writable(false)
// Modals
export const openModals = writable(0)
export const modal = {
history: [],
set: data => {

View File

@ -1,9 +1,19 @@
<script>
import {onMount, onDestroy} from "svelte"
import {fly, fade} from "svelte/transition"
import {openModals} from "src/app/ui"
export let onEscape = null
let root, content
onMount(() => {
openModals.update(n => n + 1)
})
onDestroy(() => {
openModals.update(n => n - 1)
})
</script>
<svelte:body

View File

@ -9,6 +9,7 @@
export let triggerType = "click"
export let placement = "top"
export let interactive = true
export let arrow = false
let trigger
let tooltip
@ -17,6 +18,7 @@
onMount(() => {
instance = tippy(trigger, {
theme,
arrow,
placement: placement as Placement,
appendTo: () => document.body,
allowHTML: true,

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {objOf} from "ramda"
import {objOf, last} from "ramda"
import {onMount} from "svelte"
import {nip19} from "nostr-tools"
import {warn} from "src/util/logger"
@ -10,6 +10,8 @@
export let entity
entity = last(entity.split(":"))
let type, data, relays
onMount(() => {

View File

@ -1,10 +1,9 @@
<script lang="ts">
import {displayRelay} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/views/feed/Feed.svelte"
import RelayTitle from "src/views/relays/RelayTitle.svelte"
import RelayJoin from "src/views/relays/RelayJoin.svelte"
import RelayActions from "src/views/relays/RelayActions.svelte"
import {relays} from "src/agent/tables"
export let url
@ -17,7 +16,7 @@
<Content>
<div class="flex items-center justify-between gap-2">
<RelayTitle {relay} />
<RelayJoin {relay} />
<RelayActions {relay} />
</div>
{#if relay.description}
<p>{relay.description}</p>

View File

@ -125,7 +125,7 @@ export const poll = (t, cb) => {
}
}
export const createScroller = (loadMore, {reverse = false} = {}) => {
export const createScroller = (loadMore, {reverse = false, element = document.body} = {}) => {
const THRESHOLD = 2000
// NOTE TO FUTURE SELF
@ -136,10 +136,11 @@ export const createScroller = (loadMore, {reverse = false} = {}) => {
const check = async () => {
// While we have empty space, fill it
const {scrollY, innerHeight} = window
const {scrollHeight} = document.body
const {scrollHeight, scrollTop} = element
const offset = scrollTop || scrollY
const shouldLoad = reverse
? scrollY < THRESHOLD
: scrollY + innerHeight + THRESHOLD > scrollHeight
? offset < THRESHOLD
: offset + innerHeight + THRESHOLD > scrollHeight
// Only trigger loading the first time we reach the threshold
if (shouldLoad) {

View File

@ -1,15 +1,14 @@
<script lang="ts">
import {onMount} from "svelte"
import {find, partition, always, propEq, uniqBy, sortBy, prop} from "ramda"
import {partition, always, propEq, uniqBy, sortBy, prop} from "ramda"
import {fly} from "svelte/transition"
import {quantify} from "hurdak/lib/hurdak"
import {createScroller, now, timedelta, Cursor} from "src/util/misc"
import {asDisplayEvent, mergeFilter, displayRelay} from "src/util/nostr"
import {asDisplayEvent, mergeFilter} from "src/util/nostr"
import Spinner from "src/partials/Spinner.svelte"
import Modal from "src/partials/Modal.svelte"
import Content from "src/partials/Content.svelte"
import RelayTitle from "src/views/relays/RelayTitle.svelte"
import RelayJoin from "src/views/relays/RelayJoin.svelte"
import RelayFeed from "src/views/feed/RelayFeed.svelte"
import Note from "src/views/notes/Note.svelte"
import user from "src/agent/user"
import network from "src/agent/network"
@ -25,6 +24,7 @@
let notes = []
let notesBuffer = []
let feedRelay = null
let feedScroller = null
// Add a short buffer so we can get the most possible results for recent notes
const since = now()
@ -34,6 +34,15 @@
const setFeedRelay = relay => {
feedRelay = relay
setTimeout(() => {
feedScroller?.stop()
feedScroller = !relay
? null
: createScroller(loadMore, {
element: document.querySelector(".modal-content"),
})
}, 300)
}
const loadBufferedNotes = () => {
@ -95,6 +104,22 @@
notes = uniqBy(prop("id"), notes.concat(bottom))
}
const loadMore = async () => {
if ($modal) {
return
}
// Wait for this page to load before trying again
await network.load({
relays: feedRelay ? [feedRelay] : relays,
filter: mergeFilter(filter, cursor.getFilter()),
onChunk,
})
// Update our cursor
cursor.update(notes)
}
onMount(() => {
const sub = network.listen({
relays,
@ -102,24 +127,11 @@
onChunk,
})
const scroller = createScroller(async () => {
if ($modal) {
return
}
// Wait for this page to load before trying again
await network.load({
relays: feedRelay ? [feedRelay] : relays,
filter: mergeFilter(filter, cursor.getFilter()),
onChunk,
})
// Update our cursor
cursor.update(notes)
})
const scroller = createScroller(loadMore)
return () => {
scroller.stop()
feedScroller?.stop()
sub.then(s => s?.unsub())
}
})
@ -141,7 +153,7 @@
<div class="flex flex-col gap-4">
{#each notes as note (note.id)}
<Note depth={2} {note} setFeedRelay={setFeedRelay} />
<Note depth={2} {note} {feedRelay} {setFeedRelay} />
{/each}
</div>
@ -149,26 +161,7 @@
</Content>
{#if feedRelay}
<Modal onEscape={() => setFeedRelay(null)}>
<Content>
<div class="flex items-center justify-between gap-2">
<RelayTitle relay={feedRelay} />
<RelayJoin relay={feedRelay} />
</div>
{#if feedRelay.description}
<p>{feedRelay.description}</p>
{/if}
<p class="text-gray-4">
<i class="fa fa-info-circle" />
Below is your current feed including only notes seen on {displayRelay(feedRelay)}
</p>
<div class="flex flex-col gap-4">
{#each notes as note (note.id)}
{#if note.seen_on.includes(feedRelay.url)}
<Note depth={2} {note} />
{/if}
{/each}
</div>
</Content>
</Modal>
<Modal onEscape={() => setFeedRelay(null)}>
<RelayFeed {feedRelay} {notes} depth={2} />
</Modal>
{/if}

View File

@ -0,0 +1,39 @@
<script lang="ts">
import {displayRelay} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import RelayTitle from "src/views/relays/RelayTitle.svelte"
import RelayActions from "src/views/relays/RelayActions.svelte"
import Note from "src/views/notes/Note.svelte"
export let depth
export let showContext = false
export let feedRelay
export let notes
$: filteredNotes = notes.filter(n => n.seen_on.includes(feedRelay.url))
</script>
<Content>
<div class="flex items-center justify-between gap-2">
<RelayTitle relay={feedRelay} />
<RelayActions relay={feedRelay} />
</div>
{#if feedRelay.description}
<p>{feedRelay.description}</p>
{/if}
<p class="text-gray-4">
<i class="fa fa-info-circle" />
Below is your current feed including only notes seen on {displayRelay(feedRelay)}
</p>
<div class="flex flex-col gap-4">
<!-- If someone clicks on a child note that was seen on a relay the parent was not
seen on, we get nothing, so just show everything - but pass down the filter -->
{#each filteredNotes.length > 0 ? filteredNotes : notes as note (note.id)}
<Note invertColors {depth} {note} {feedRelay} {showContext} />
{/each}
</div>
<Spinner />
</Content>

View File

@ -1,5 +1,4 @@
<script lang="ts">
import {fly} from "svelte/transition"
import user from "src/agent/user"
import {modal} from "src/app/ui"

View File

@ -50,7 +50,8 @@
import NoteContent from "src/views/notes/NoteContent.svelte"
export let note
export let setFeedRelay
export let feedRelay
export let setFeedRelay = null
export let depth = 0
export let anchorId = null
export let showParent = true
@ -134,8 +135,15 @@
$: $likesCount = likes.length
$: $zapsTotal = sum(zaps.map(zap => zap.invoiceAmount)) / 1000
$: $repliesCount = note.replies.length
$: visibleNotes = note.replies.filter(r => (showContext ? true : !r.isContext))
$: canZap = $person?.zapper && $person?.pubkey !== user.getPubkey()
$: visibleNotes = note.replies.filter(r => {
if (feedRelay && !r.seen_on.includes(feedRelay.url)) {
return false
}
return showContext ? true : !r.isContext
})
$: {
actions = []
@ -393,7 +401,7 @@
<div slot="trigger">
<Anchor
type="unstyled"
class="flex items-center gap-2 text-lg font-bold"
class="flex items-center gap-2 pr-16 text-lg font-bold sm:pr-0"
href={isMobile ? null : routes.person($person.pubkey)}>
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
@ -465,18 +473,31 @@
</div>
<div on:click|stopPropagation class="flex items-center">
{#if pool.forceUrls.length === 0}
<!-- Mobile version -->
<div
class={cx("absolute top-0 right-0 m-3 sm:relative sm:m-0", {
"hidden group-hover:flex": !showEntire,
flex: showEntire,
style="transform: scale(-1, 1)"
class="absolute top-0 right-0 m-3 grid grid-cols-3 gap-2 sm:hidden">
{#each note.seen_on as url, i}
<div class={`cursor-pointer order-${3 - (i % 3)}`}>
<div
class="h-3 w-3 rounded-full border border-solid border-gray-6"
style={`background: ${hsl(stringToHue(url))}`}
on:click={() => setFeedRelay?.({url})} />
</div>
{/each}
</div>
<!-- Desktop version -->
<div
class={cx("hidden sm:flex transition-opacity", {
"opacity-0 group-hover:opacity-100": !showEntire,
})}>
{#each note.seen_on as url}
{#each note.seen_on as url, i}
<Popover triggerType="mouseenter" interactive={false}>
<div slot="trigger" class="cursor-pointer p-1">
<div
class="h-3 w-3 rounded-full border border-solid border-gray-6"
style={`background: ${hsl(stringToHue(url))}`}
on:click={() => setFeedRelay({url})} />
on:click={() => setFeedRelay?.({url})} />
</div>
<div slot="tooltip">{displayRelay({url})}</div>
</Popover>
@ -596,6 +617,8 @@
showParent={false}
note={r}
depth={depth - 1}
{feedRelay}
{setFeedRelay}
{invertColors}
{anchorId}
{showContext} />

View File

@ -6,6 +6,8 @@
import {log} from "src/util/logger"
import {asDisplayEvent} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
import RelayFeed from "src/views/feed/RelayFeed.svelte"
import Modal from "src/partials/Modal.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/views/notes/Note.svelte"
import user from "src/agent/user"
@ -18,6 +20,11 @@
let sub = null
let loading = true
let feedRelay = null
const setFeedRelay = relay => {
feedRelay = relay
}
onMount(async () => {
if (!note.pubkey) {
@ -56,10 +63,23 @@
</div>
{:else if note.pubkey}
<div in:fly={{y: 20}} class="m-auto flex w-full max-w-2xl flex-col gap-4 p-4">
<Note showContext depth={6} anchorId={note.id} note={asDisplayEvent(note)} {invertColors} />
<Note
showContext
depth={6}
anchorId={note.id}
note={asDisplayEvent(note)}
{invertColors}
{feedRelay}
{setFeedRelay} />
</div>
{/if}
{#if loading}
<Spinner />
{/if}
{#if feedRelay}
<Modal onEscape={() => setFeedRelay(null)}>
<RelayFeed {feedRelay} notes={[note]} depth={6} showContext />
</Modal>
{/if}

View File

@ -0,0 +1,56 @@
<script lang="ts">
import {find, last, propEq} from "ramda"
import Anchor from "src/partials/Anchor.svelte"
import Popover from "src/partials/Popover.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import user from "src/agent/user"
import {getRelayWithFallback} from "src/agent/tables"
export let relay
relay = getRelayWithFallback(relay.url)
const {relays: userRelays} = user
let actions = []
$: joined = find(propEq("url", relay.url), $userRelays)
$: {
actions = []
if (!joined) {
actions.push({
onClick: () => user.addRelay(relay.url),
label: "Join",
icon: "right-to-bracket",
})
} else if ($userRelays.length > 1) {
actions.push({
onClick: () => user.removeRelay(relay.url),
label: "Leave",
icon: "right-from-bracket",
})
}
if (relay.contact) {
actions.push({
onClick: () => window.open("mailto:" + last(relay.contact.split(":"))),
label: "Contact",
icon: "envelope",
})
}
}
</script>
{#if actions.length > 0}
{#if actions.length === 1}
<Popover triggerType="mouseenter">
<Anchor slot="trigger" type="button-circle" on:click={actions[0].onClick}>
<i class={`fa fa-${actions[0].icon}`} />
</Anchor>
<p slot="tooltip">{actions[0].label}</p>
</Popover>
{:else}
<OverflowMenu {actions} />
{/if}
{/if}

View File

@ -1,36 +0,0 @@
<script lang="ts">
import {find, propEq} from "ramda"
import Anchor from 'src/partials/Anchor.svelte'
import user from "src/agent/user"
export let relay
const {relays: userRelays} = user
$: joined = find(propEq("url", relay.url), $userRelays)
</script>
<div class="flex flex-wrap items-center gap-3 whitespace-nowrap">
{#if relay.contact}
<Anchor type="button-circle" href={`mailto:${relay.contact}`}>
<i class="fa fa-envelope" />
</Anchor>
{/if}
{#if joined}
{#if $userRelays.length > 1}
<Anchor
type="button"
class="flex items-center gap-2 rounded-full"
on:click={() => user.removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</Anchor>
{/if}
{:else}
<Anchor
type="button"
class="flex items-center gap-2 rounded-full"
on:click={() => user.addRelay(relay.url)}>
<i class="fa fa-right-to-bracket" /> Join
</Anchor>
{/if}
</div>

View File

@ -3,7 +3,8 @@
import {between} from "hurdak/lib/hurdak"
import {displayRelay} from "src/util/nostr"
import {poll, stringToHue, hsl} from "src/util/misc"
import pool from 'src/agent/pool'
import Anchor from "src/partials/Anchor.svelte"
import pool from "src/agent/pool"
export let relay
@ -20,9 +21,13 @@
<div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("wss") ? "fa fa-lock" : "fa fa-unlock"} />
<span class="border-b border-solid" style={`border-color: ${hsl(stringToHue(relay.url))}`}>
<Anchor
type="unstyled"
href={`/relays/${btoa(relay.url)}`}
class="border-b border-solid"
style={`border-color: ${hsl(stringToHue(relay.url))}`}>
{displayRelay(relay)}
</span>
</Anchor>
<span
on:mouseout={() => {
showStatus = false