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 # Current
- [ ] Fix scrolling with embedded modals by registering open modals in the component
- [ ] Relays bounty - [ ] Relays bounty
- [x] Ability to click into a relay's global feed - [x] Ability to click into a relay's global feed
- [ ] Ability to filter feeds by relay - [x] Ability to filter feeds by relay
- Global Mutes? Global Whitelist? - [-] Global Mutes? Global Whitelist?
- Open in modal with "here's what this feed would look like with only this relay" - [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 - [ ] Ability to create custom feeds
- [ ] Fix tag-style event mentions. Probably transform all mentions into entities in parse - [ ] Fix tag-style event mentions. Probably transform all mentions into entities in parse
- [ ] Some lnurls aren't working npub1y3k2nheva29y9ej8a22e07epuxrn04rvgy28wvs54y57j7vsxxuq0gvp4j
- [ ] Fix performance issues - [ ] Fix performance issues
- [ ] https://github.com/techfort/LokiJS - [ ] https://github.com/techfort/LokiJS
- Use indexed adapter github.com/techfort/LokiJS/blob/master/tutorials/Persistence%20Adapters.md and partitioning adapter - 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 user from "src/agent/user"
import {loadAppData} from "src/app" import {loadAppData} from "src/app"
import {theme, getThemeVariables} from "src/app/ui" 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 Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte" import Modal from "src/partials/Modal.svelte"
@ -97,11 +97,16 @@
} }
onMount(() => { onMount(() => {
// Keep scroll position on body, but don't allow scrolling // Log modals
const unsubModal = modal.subscribe($modal => { const unsubModal = modal.subscribe($modal => {
if ($modal) { if ($modal) {
logUsage(btoa(["modal", $modal.type].join(":"))) 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 // This is not idempotent, so don't duplicate it
if (document.body.style.position !== "fixed") { if (document.body.style.position !== "fixed") {
scrollY = window.scrollY scrollY = window.scrollY
@ -133,6 +138,7 @@
return () => { return () => {
unsubHistory() unsubHistory()
unsubModal() unsubModal()
unsubOpenModals()
} }
}) })

View File

@ -118,7 +118,7 @@ export const getRelaysForEventParent = event => {
// to read from the current user's network's read relays instead. // to read from the current user's network's read relays instead.
export const getRelaysForEventChildren = event => { export const getRelaysForEventChildren = event => {
return uniqByUrl( 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() listener.connect()
export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey} 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)) 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 {nip19} from "nostr-tools"
import {writable, get} from "svelte/store" import {writable, get} from "svelte/store"
import {globalHistory} from "svelte-routing/src/history" 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 {warn} from "src/util/logger"
import user from "src/agent/user" import user from "src/agent/user"
@ -48,6 +48,8 @@ export const menuIsOpen = writable(false)
// Modals // Modals
export const openModals = writable(0)
export const modal = { export const modal = {
history: [], history: [],
set: data => { set: data => {

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import {displayRelay} from "src/util/nostr" import {displayRelay} from "src/util/nostr"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/views/feed/Feed.svelte" import Feed from "src/views/feed/Feed.svelte"
import RelayTitle from "src/views/relays/RelayTitle.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" import {relays} from "src/agent/tables"
export let url export let url
@ -17,7 +16,7 @@
<Content> <Content>
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<RelayTitle {relay} /> <RelayTitle {relay} />
<RelayJoin {relay} /> <RelayActions {relay} />
</div> </div>
{#if relay.description} {#if relay.description}
<p>{relay.description}</p> <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 const THRESHOLD = 2000
// NOTE TO FUTURE SELF // NOTE TO FUTURE SELF
@ -136,10 +136,11 @@ export const createScroller = (loadMore, {reverse = false} = {}) => {
const check = async () => { const check = async () => {
// While we have empty space, fill it // While we have empty space, fill it
const {scrollY, innerHeight} = window const {scrollY, innerHeight} = window
const {scrollHeight} = document.body const {scrollHeight, scrollTop} = element
const offset = scrollTop || scrollY
const shouldLoad = reverse const shouldLoad = reverse
? scrollY < THRESHOLD ? offset < THRESHOLD
: scrollY + innerHeight + THRESHOLD > scrollHeight : offset + innerHeight + THRESHOLD > scrollHeight
// Only trigger loading the first time we reach the threshold // Only trigger loading the first time we reach the threshold
if (shouldLoad) { if (shouldLoad) {

View File

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" 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 {fly} from "svelte/transition"
import {quantify} from "hurdak/lib/hurdak" import {quantify} from "hurdak/lib/hurdak"
import {createScroller, now, timedelta, Cursor} from "src/util/misc" 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 Spinner from "src/partials/Spinner.svelte"
import Modal from "src/partials/Modal.svelte" import Modal from "src/partials/Modal.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import RelayTitle from "src/views/relays/RelayTitle.svelte" import RelayFeed from "src/views/feed/RelayFeed.svelte"
import RelayJoin from "src/views/relays/RelayJoin.svelte"
import Note from "src/views/notes/Note.svelte" import Note from "src/views/notes/Note.svelte"
import user from "src/agent/user" import user from "src/agent/user"
import network from "src/agent/network" import network from "src/agent/network"
@ -25,6 +24,7 @@
let notes = [] let notes = []
let notesBuffer = [] let notesBuffer = []
let feedRelay = null let feedRelay = null
let feedScroller = null
// Add a short buffer so we can get the most possible results for recent notes // Add a short buffer so we can get the most possible results for recent notes
const since = now() const since = now()
@ -34,6 +34,15 @@
const setFeedRelay = relay => { const setFeedRelay = relay => {
feedRelay = relay feedRelay = relay
setTimeout(() => {
feedScroller?.stop()
feedScroller = !relay
? null
: createScroller(loadMore, {
element: document.querySelector(".modal-content"),
})
}, 300)
} }
const loadBufferedNotes = () => { const loadBufferedNotes = () => {
@ -95,6 +104,22 @@
notes = uniqBy(prop("id"), notes.concat(bottom)) 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(() => { onMount(() => {
const sub = network.listen({ const sub = network.listen({
relays, relays,
@ -102,24 +127,11 @@
onChunk, onChunk,
}) })
const scroller = createScroller(async () => { const scroller = createScroller(loadMore)
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)
})
return () => { return () => {
scroller.stop() scroller.stop()
feedScroller?.stop()
sub.then(s => s?.unsub()) sub.then(s => s?.unsub())
} }
}) })
@ -141,7 +153,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{#each notes as note (note.id)} {#each notes as note (note.id)}
<Note depth={2} {note} setFeedRelay={setFeedRelay} /> <Note depth={2} {note} {feedRelay} {setFeedRelay} />
{/each} {/each}
</div> </div>
@ -149,26 +161,7 @@
</Content> </Content>
{#if feedRelay} {#if feedRelay}
<Modal onEscape={() => setFeedRelay(null)}> <Modal onEscape={() => setFeedRelay(null)}>
<Content> <RelayFeed {feedRelay} {notes} depth={2} />
<div class="flex items-center justify-between gap-2"> </Modal>
<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>
{/if} {/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"> <script lang="ts">
import {fly} from "svelte/transition"
import user from "src/agent/user" import user from "src/agent/user"
import {modal} from "src/app/ui" import {modal} from "src/app/ui"

View File

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

View File

@ -6,6 +6,8 @@
import {log} from "src/util/logger" import {log} from "src/util/logger"
import {asDisplayEvent} from "src/util/nostr" import {asDisplayEvent} from "src/util/nostr"
import Content from "src/partials/Content.svelte" 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 Spinner from "src/partials/Spinner.svelte"
import Note from "src/views/notes/Note.svelte" import Note from "src/views/notes/Note.svelte"
import user from "src/agent/user" import user from "src/agent/user"
@ -18,6 +20,11 @@
let sub = null let sub = null
let loading = true let loading = true
let feedRelay = null
const setFeedRelay = relay => {
feedRelay = relay
}
onMount(async () => { onMount(async () => {
if (!note.pubkey) { if (!note.pubkey) {
@ -56,10 +63,23 @@
</div> </div>
{:else if note.pubkey} {:else if note.pubkey}
<div in:fly={{y: 20}} class="m-auto flex w-full max-w-2xl flex-col gap-4 p-4"> <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> </div>
{/if} {/if}
{#if loading} {#if loading}
<Spinner /> <Spinner />
{/if} {/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 {between} from "hurdak/lib/hurdak"
import {displayRelay} from "src/util/nostr" import {displayRelay} from "src/util/nostr"
import {poll, stringToHue, hsl} from "src/util/misc" 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 export let relay
@ -20,9 +21,13 @@
<div class="flex items-center gap-2 text-xl"> <div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("wss") ? "fa fa-lock" : "fa fa-unlock"} /> <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)} {displayRelay(relay)}
</span> </Anchor>
<span <span
on:mouseout={() => { on:mouseout={() => {
showStatus = false showStatus = false