forked from coracle/flotilla
Add space home page
This commit is contained in:
+4
-1
@@ -26,6 +26,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<script defer data-domain="flotilla.social" src="https://plausible.coracle.social/js/script.manual.js"></script>
|
<script
|
||||||
|
defer
|
||||||
|
data-domain="flotilla.social"
|
||||||
|
src="https://plausible.coracle.social/js/script.manual.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint prefer-rest-params: 0 */
|
||||||
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
|
||||||
const w = window as any
|
const w = window as any
|
||||||
|
|||||||
+1
-1
@@ -101,7 +101,7 @@ export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
|
|||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
sub = subscribe(request)
|
sub = subscribe(request)
|
||||||
sub.emitter.on("close", resolve)
|
sub.emitter.on("close", resolve)
|
||||||
})
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
onSubmit({
|
onSubmit({
|
||||||
content: $editor.getText({blockSeparator: '\n'}),
|
content: $editor.getText({blockSeparator: "\n"}),
|
||||||
tags: getEditorTags($editor),
|
tags: getEditorTags($editor),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
|
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import {slideAndFade, conditionalTransition} from "@lib/transition"
|
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import LongPress from "@lib/components/LongPress.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
@@ -36,16 +35,13 @@
|
|||||||
const rootEvent = rootId ? deriveEvent(rootId, rootHints) : readable(null)
|
const rootEvent = rootId ? deriveEvent(rootId, rootHints) : readable(null)
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
|
|
||||||
const transition = conditionalTransition(thunk, slideAndFade)
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
const root = $rootEvent || event
|
const root = $rootEvent || event
|
||||||
|
|
||||||
pushDrawer(ChannelConversation, {url, room, event: root})
|
pushDrawer(ChannelConversation, {url, room, event: root})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLongPress = () =>
|
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
|
||||||
pushModal(ChannelMessageMenuMobile, {url, event})
|
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
@@ -66,14 +62,13 @@
|
|||||||
<LongPress
|
<LongPress
|
||||||
on:click={isMobile || inert ? null : onClick}
|
on:click={isMobile || inert ? null : onClick}
|
||||||
onLongPress={inert ? null : onLongPress}
|
onLongPress={inert ? null : onLongPress}
|
||||||
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors {inert ? 'hover:bg-base-300' : ''}">
|
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors {inert
|
||||||
|
? 'hover:bg-base-300'
|
||||||
|
: ''}">
|
||||||
<div class="flex w-full gap-3">
|
<div class="flex w-full gap-3">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="flex items-start">
|
<Link external href={pubkeyLink(event.pubkey)} class="flex items-start">
|
||||||
<Avatar
|
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
|
||||||
src={$profile?.picture}
|
|
||||||
class="border border-solid border-base-content"
|
|
||||||
size={10} />
|
|
||||||
</Link>
|
</Link>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-10 min-w-10 max-w-10" />
|
<div class="w-10 min-w-10 max-w-10" />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived, writable} from "svelte/store"
|
import {derived, writable} from "svelte/store"
|
||||||
import {int, assoc, MINUTE, now, sortBy, remove} from "@welshman/lib"
|
import {int, assoc, MINUTE, sortBy, remove} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {createEvent, DIRECT_MESSAGE} from "@welshman/util"
|
import {createEvent, DIRECT_MESSAGE} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
id,
|
id,
|
||||||
type: "note",
|
type: "note",
|
||||||
value: event,
|
value: event,
|
||||||
showPubkey: ((created_at - previousCreatedAt) > int(15, MINUTE)) || previousPubkey !== pubkey,
|
showPubkey: created_at - previousCreatedAt > int(15, MINUTE) || previousPubkey !== pubkey,
|
||||||
})
|
})
|
||||||
|
|
||||||
previousDate = date
|
previousDate = date
|
||||||
|
|||||||
@@ -81,23 +81,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<LongPress class="chat-bubble mx-1 max-w-sm text-left flex flex-col gap-1" onLongPress={showMobileMenu}>
|
<LongPress
|
||||||
|
class="chat-bubble mx-1 flex max-w-sm flex-col gap-1 text-left"
|
||||||
|
onLongPress={showMobileMenu}>
|
||||||
{#if showPubkey && event.pubkey !== $pubkey}
|
{#if showPubkey && event.pubkey !== $pubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="flex gap-1 items-center">
|
<Link external href={pubkeyLink(event.pubkey)} class="flex items-center gap-1">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={$profile?.picture}
|
src={$profile?.picture}
|
||||||
class="border border-solid border-base-content"
|
class="border border-solid border-base-content"
|
||||||
size={4} />
|
size={4} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
external
|
external
|
||||||
href={pubkeyLink(event.pubkey)}
|
href={pubkeyLink(event.pubkey)}
|
||||||
class="text-sm font-bold"
|
class="text-sm font-bold"
|
||||||
style="color: {colorValue}">
|
style="color: {colorValue}">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete} from "@app/commands"
|
||||||
import {clearModals} from '@app/modal'
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let event
|
export let event
|
||||||
|
|||||||
@@ -57,14 +57,6 @@
|
|||||||
|
|
||||||
const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
||||||
|
|
||||||
const isBlock = (i: number) => {
|
|
||||||
const parsed = fullContent[i]
|
|
||||||
|
|
||||||
return isEvent(parsed) || isAddress(parsed) || isLink(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNextToBlock = (i: number) => isBlock(i - 1) || isBlock(i + 1)
|
|
||||||
|
|
||||||
const ignoreWarning = () => {
|
const ignoreWarning = () => {
|
||||||
warning = null
|
warning = null
|
||||||
}
|
}
|
||||||
@@ -132,9 +124,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if isEllipsis(parsed) && expandInline}
|
{:else if isEllipsis(parsed) && expandInline}
|
||||||
{@html renderParsed(parsed)}
|
{@html renderParsed(parsed)}
|
||||||
<button type="button" class="underline text-sm">
|
<button type="button" class="text-sm underline"> Read more </button>
|
||||||
Read more
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
{@html renderParsed(parsed)}
|
{@html renderParsed(parsed)}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we found this event on a relay that the user is a member of, redirect internally
|
// If we found this event on a relay that the user is a member of, redirect internally
|
||||||
$: localHref = getLocalHref($event)
|
$: localHref = $event ? getLocalHref($event) : null
|
||||||
$: href = localHref || entityLink(entity)
|
$: href = localHref || entityLink(entity)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
|
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
|
||||||
const event = createEvent(kind, {
|
const event = createEvent(kind, {
|
||||||
content: $editor.getText({blockSeparator: '\n'}),
|
content: $editor.getText({blockSeparator: "\n"}),
|
||||||
tags: [
|
tags: [
|
||||||
["d", randomId()],
|
["d", randomId()],
|
||||||
["title", title],
|
["title", title],
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
<div slot="title">What is a bunker link?</div>
|
<div slot="title">What is a bunker link?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of
|
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of passwords
|
||||||
passwords to identify users. This allows users to own their social identity instead of
|
to identify users. This allows users to own their social identity instead of renting it from a tech
|
||||||
renting it from a tech company, and can bring it with them from app to app.
|
company, and can bring it with them from app to app.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A good way to manage your keys is to use a remote signing application. These apps can hold
|
A good way to manage your keys is to use a remote signing application. These apps can hold your
|
||||||
your keys and log you in remotely to as many applications as you like, without risking
|
keys and log you in remotely to as many applications as you like, without risking loss or theft
|
||||||
loss or theft of your keys.
|
of your keys.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
One way to log in with a remote signer is using a "bunker link" which is more secure and
|
One way to log in with a remote signer is using a "bunker link" which is more secure and
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
copy it into {PLATFORM_NAME}, and you should be good to go!
|
copy it into {PLATFORM_NAME}, and you should be good to go!
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you don't have a signer yet, <Link external class="link" href="https://nsec.app/">nsec.app</Link>
|
If you don't have a signer yet, <Link external class="link" href="https://nsec.app/"
|
||||||
|
>nsec.app</Link>
|
||||||
is a great way to get started.
|
is a great way to get started.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
@@ -10,13 +9,16 @@
|
|||||||
<div slot="title">What is nostr?</div>
|
<div slot="title">What is nostr?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build
|
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
||||||
social apps that talk to each other. Users own their social identity instead of
|
talk to each other. Users own their social identity instead of renting it from a tech company, and
|
||||||
renting it from a tech company, and can take it with them.
|
can take it with them.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you'd like to learn more about what other apps exist in the nostr ecosystem, please
|
If you'd like to learn more about what other apps exist in the nostr ecosystem, please visit <Link
|
||||||
visit <Link external class="link" href="https://nostrapps.com/">nostrapps.com</Link>.
|
external
|
||||||
|
class="link"
|
||||||
|
href="https://nostrapps.com/">nostrapps.com</Link
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To learn more about how to manage your keys, or to set up an account, try
|
To learn more about how to manage your keys, or to set up an account, try
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Nip46Broker} from "@welshman/signer"
|
import {Nip46Broker} from "@welshman/signer"
|
||||||
import {addSession} from "@welshman/app"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await loginWithNip46(token, {pubkey, relays})) {
|
if (!(await loginWithNip46(token, {pubkey, relays}))) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Something went wrong, please try again!",
|
message: "Something went wrong, please try again!",
|
||||||
|
|||||||
@@ -125,6 +125,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div in:fly={{delay: getDelay(true)}}>
|
<div in:fly={{delay: getDelay(true)}}>
|
||||||
<SecondaryNavItem href={makeSpacePath(url)}>
|
<SecondaryNavItem href={makeSpacePath(url)}>
|
||||||
|
<Icon icon="home-smile" /> Home
|
||||||
|
</SecondaryNavItem>
|
||||||
|
</div>
|
||||||
|
<div in:fly={{delay: getDelay()}}>
|
||||||
|
<SecondaryNavItem href={makeSpacePath(url, "threads")}>
|
||||||
<Icon icon="notes-minimalistic" /> Threads
|
<Icon icon="notes-minimalistic" /> Threads
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {REACTION} from "@welshman/util"
|
||||||
|
import {pubkey, load, formatTimestamp} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
|
import Content from "@app/components/Content.svelte"
|
||||||
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||||
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
|
|
||||||
|
if (reaction) {
|
||||||
|
publishDelete({relays: [url], event: reaction})
|
||||||
|
} else {
|
||||||
|
publishReaction({event, content, relays: [url]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
|
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load({filters: [{kinds: [REACTION], "#e": [event.id]}]})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NoteCard {event} class="card2 bg-alt">
|
||||||
|
<Content {event} expandMode="inline" />
|
||||||
|
<div class="flex w-full justify-between gap-2">
|
||||||
|
<ReactionSummary {event} {onReactionClick}>
|
||||||
|
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||||
|
<Icon icon="smile-circle" size={4} />
|
||||||
|
</EmojiButton>
|
||||||
|
</ReactionSummary>
|
||||||
|
<p class="whitespace-nowrap text-sm opacity-75">
|
||||||
|
{formatTimestamp(event.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</NoteCard>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {sortBy, flatten} from "@welshman/lib"
|
||||||
|
import {feedFromFilter} from "@welshman/feeds"
|
||||||
|
import {NOTE, getAncestorTags} from "@welshman/util"
|
||||||
|
import {deriveEvents} from "@welshman/store"
|
||||||
|
import {repository, feedLoader} from "@welshman/app"
|
||||||
|
import {createScroller} from "@lib/html"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let pubkey
|
||||||
|
|
||||||
|
const filter = {kinds: [NOTE], authors: [pubkey]}
|
||||||
|
const events = deriveEvents(repository, {filters: [filter]})
|
||||||
|
const loader = feedLoader.getLoader(feedFromFilter(filter), {})
|
||||||
|
|
||||||
|
let element: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const scroller = createScroller({
|
||||||
|
element,
|
||||||
|
onScroll: async () => {
|
||||||
|
const $loader = await loader
|
||||||
|
|
||||||
|
$loader(5)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => scroller.stop()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex max-w-full flex-col gap-4 p-4" bind:this={element}>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each sortBy(e => -e.created_at, $events) as event (event.id)}
|
||||||
|
{#if flatten(Object.values(getAncestorTags(event.tags))).length === 0}
|
||||||
|
<NoteItem {url} {event} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="flex center my-12">
|
||||||
|
<Spinner loading />
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {sortBy, flatten} from "@welshman/lib"
|
||||||
|
import {feedFromFilter} from "@welshman/feeds"
|
||||||
|
import {NOTE, getAncestorTags} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {feedLoader} from "@welshman/app"
|
||||||
|
import {createScroller} from "@lib/html"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let pubkey
|
||||||
|
|
||||||
|
const filter = {kinds: [NOTE], authors: [pubkey]}
|
||||||
|
const loader = feedLoader.getLoader(feedFromFilter(filter), {
|
||||||
|
onEvent: (e: TrustedEvent) => {
|
||||||
|
events = sortBy(e => -e.created_at, [...events, e])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let element: Element
|
||||||
|
let events: TrustedEvent[] = []
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const scroller = createScroller({
|
||||||
|
element,
|
||||||
|
delay: 300,
|
||||||
|
threshold: 3000,
|
||||||
|
onScroll: async () => {
|
||||||
|
const $loader = await loader
|
||||||
|
|
||||||
|
$loader(5)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => scroller.stop()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="col-4" bind:this={element}>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each events as event (event.id)}
|
||||||
|
{#if flatten(Object.values(getAncestorTags(event.tags))).length === 0}
|
||||||
|
<NoteItem {url} {event} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="center my-12 flex">
|
||||||
|
<Spinner loading />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $reactions.length > 0}
|
{#if $reactions.length > 0}
|
||||||
<div class="flex gap-2">
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
{#each groupedReactions.entries() as [content, events]}
|
{#each groupedReactions.entries() as [content, events]}
|
||||||
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
|
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
|
||||||
{@const onClick = () => onReactionClick(content, events)}
|
{@const onClick = () => onReactionClick(content, events)}
|
||||||
@@ -34,5 +34,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<ReactionSummary {event} {onReactionClick} />
|
<ReactionSummary {event} {onReactionClick} />
|
||||||
<div class="flex flex-wrap flex-grow justify-end gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
{#if $deleted}
|
{#if $deleted}
|
||||||
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {append} from "@welshman/lib"
|
|
||||||
import {createEvent} from "@welshman/util"
|
import {createEvent} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -26,7 +25,6 @@
|
|||||||
const loading = writable(false)
|
const loading = writable(false)
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -34,7 +32,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = $editor.getText({blockSeparator: '\n'})
|
const content = $editor.getText({blockSeparator: "\n"})
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -43,11 +41,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = [
|
const tags = [["title", title], tagRoom(GENERAL, url), ...getEditorTags($editor)]
|
||||||
["title", title],
|
|
||||||
tagRoom(GENERAL, url),
|
|
||||||
...getEditorTags($editor),
|
|
||||||
]
|
|
||||||
|
|
||||||
publishThunk({
|
publishThunk({
|
||||||
event: createEvent(THREAD, {content, tags}),
|
event: createEvent(THREAD, {content, tags}),
|
||||||
@@ -68,7 +62,7 @@
|
|||||||
getPubkeyHints,
|
getPubkeyHints,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: "What's on your mind?",
|
placeholder: "What's on your mind?",
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -78,11 +72,15 @@
|
|||||||
<div slot="title">Create a Thread</div>
|
<div slot="title">Create a Thread</div>
|
||||||
<div slot="info">Share a link, or start a discussion.</div>
|
<div slot="info">Share a link, or start a discussion.</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="relative col-8">
|
<div class="col-8 relative">
|
||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Title*</p>
|
<p slot="label">Title*</p>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<input bind:value={title} class="grow" type="text" placeholder="What is this thread about?" />
|
<input
|
||||||
|
bind:value={title}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="What is this thread about?" />
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nthEq} from '@welshman/lib'
|
import {nthEq} from "@welshman/lib"
|
||||||
import {formatTimestamp} from "@welshman/app"
|
import {formatTimestamp} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
@@ -12,19 +12,19 @@
|
|||||||
export let event
|
export let event
|
||||||
export let hideActions = false
|
export let hideActions = false
|
||||||
|
|
||||||
const title = event.tags.find(nthEq(0, 'title'))?.[1]
|
const title = event.tags.find(nthEq(0, "title"))?.[1]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||||
<div class="flex w-full justify-between items-center gap-2">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
<p class="text-xl">{title}</p>
|
<p class="text-xl">{title}</p>
|
||||||
<p class="text-sm opacity-75">
|
<p class="text-sm opacity-75">
|
||||||
{formatTimestamp(event.created_at)}
|
{formatTimestamp(event.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Content {event} expandMode="inline" />
|
<Content {event} expandMode="inline" />
|
||||||
<div class="flex gap-2 items-end justify-between w-full">
|
<div class="flex w-full items-end justify-between gap-2">
|
||||||
<span class="text-sm opacity-75 whitespace-nowrap py-1">
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
Posted by
|
Posted by
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="link-content">
|
<Link external href={pubkeyLink(event.pubkey)} class="link-content">
|
||||||
@<ProfileName pubkey={event.pubkey} />
|
@<ProfileName pubkey={event.pubkey} />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
const loading = writable(false)
|
const loading = writable(false)
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
const content = $editor.getText({blockSeparator: '\n'})
|
const content = $editor.getText({blockSeparator: "\n"})
|
||||||
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
|
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
|
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
|
||||||
import {userSettingValues} from '@app/state'
|
import {userSettingValues} from "@app/state"
|
||||||
|
|
||||||
export let thunk: Thunk | MergedThunk
|
export let thunk: Thunk | MergedThunk
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
import type {Page} from "@sveltejs/kit"
|
import type {Page} from "@sveltejs/kit"
|
||||||
import {userMembership, makeChatId, decodeRelay, encodeRelay, getMembershipUrls} from "@app/state"
|
import {userMembership, makeChatId, decodeRelay, encodeRelay, getMembershipUrls} from "@app/state"
|
||||||
|
|
||||||
export const makeSpacePath = (url: string, extra = "") => {
|
export const makeSpacePath = (url: string, ...extra: string[]) => {
|
||||||
let path = `/spaces/${encodeRelay(url)}`
|
let path = `/spaces/${encodeRelay(url)}`
|
||||||
|
|
||||||
if (extra) {
|
if (extra.length > 0) {
|
||||||
path += "/" + encodeURIComponent(extra)
|
path += "/" + extra.map(s => encodeURIComponent(s)).join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|||||||
+2
-2
@@ -264,7 +264,7 @@ export type Settings = {
|
|||||||
values: {
|
values: {
|
||||||
show_media: boolean
|
show_media: boolean
|
||||||
hide_sensitive: boolean
|
hide_sensitive: boolean
|
||||||
send_delay: number,
|
send_delay: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ export const encodeRelay = (url: string) => encodeURIComponent(normalizeRelayUrl
|
|||||||
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))
|
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))
|
||||||
|
|
||||||
export const displayReaction = (content: string) => {
|
export const displayReaction = (content: string) => {
|
||||||
if (content === "+") return "❤️"
|
if (!content || content === "+") return "❤️"
|
||||||
if (content === "-") return "👎"
|
if (content === "-") return "👎"
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export const setupTracking = () => {
|
|||||||
dsn: import.meta.env.VITE_GLITCHTIP_API_KEY,
|
dsn: import.meta.env.VITE_GLITCHTIP_API_KEY,
|
||||||
tracesSampleRate: 0.01,
|
tracesSampleRate: 0.01,
|
||||||
integrations(integrations) {
|
integrations(integrations) {
|
||||||
return integrations.filter(integration => integration.name !== 'Breadcrumbs')
|
return integrations.filter(integration => integration.name !== "Breadcrumbs")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
"smile-circle": SmileCircle,
|
"smile-circle": SmileCircle,
|
||||||
server: Server,
|
server: Server,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
'settings-minimalistic': SettingsMinimalistic,
|
"settings-minimalistic": SettingsMinimalistic,
|
||||||
"tag-horizontal": TagHorizontal,
|
"tag-horizontal": TagHorizontal,
|
||||||
"trash-bin-2": TrashBin2,
|
"trash-bin-2": TrashBin2,
|
||||||
"ufo-3": UFO3,
|
"ufo-3": UFO3,
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
let timeout: number
|
let timeout: number
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div role="button" tabindex="0" on:click on:touchstart={onTouchStart} on:touchmove={onTouchMove} on:touchend={onTouchEnd} {...$$props}>
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:click
|
||||||
|
on:touchstart={onTouchStart}
|
||||||
|
on:touchmove={onTouchMove}
|
||||||
|
on:touchend={onTouchEnd}
|
||||||
|
{...$$props}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {nprofileEncode} from "nostr-tools/nip19"
|
import {nprofileEncode} from "nostr-tools/nip19"
|
||||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from "@tiptap/extension-placeholder"
|
||||||
import Code from "@tiptap/extension-code"
|
import Code from "@tiptap/extension-code"
|
||||||
import CodeBlock from "@tiptap/extension-code-block"
|
import CodeBlock from "@tiptap/extension-code-block"
|
||||||
import Document from "@tiptap/extension-document"
|
import Document from "@tiptap/extension-document"
|
||||||
@@ -65,7 +65,7 @@ export const getModifiedHardBreakExtension = () =>
|
|||||||
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
||||||
"Mod-Enter": () => this.editor.commands.setHardBreak(),
|
"Mod-Enter": () => this.editor.commands.setHardBreak(),
|
||||||
Enter: () => {
|
Enter: () => {
|
||||||
if (this.editor.getText({blockSeparator: '\n'}).trim()) {
|
if (this.editor.getText({blockSeparator: "\n"}).trim()) {
|
||||||
uploadFiles(this.editor)
|
uploadFiles(this.editor)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
step="1000"
|
step="1000"
|
||||||
bind:value={settings.send_delay} />
|
bind:value={settings.send_delay} />
|
||||||
<p slot="info">
|
<p slot="info">
|
||||||
Delay sending chat messages for {settings.send_delay/1000}
|
Delay sending chat messages for {settings.send_delay / 1000}
|
||||||
{settings.send_delay === 1000 ? 'second' : 'seconds'}.
|
{settings.send_delay === 1000 ? "second" : "seconds"}.
|
||||||
</p>
|
</p>
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -1,91 +1,95 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import type {Filter} from "@welshman/util"
|
|
||||||
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
|
||||||
import {nthEq} from "@welshman/lib"
|
|
||||||
import {feedLoader, userMutes} from "@welshman/app"
|
|
||||||
import {createScroller} from "@lib/html"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
import ProfileFeed from "@app/components/ProfileFeed.svelte"
|
||||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import {THREAD, COMMENT, deriveEventsForUrl, decodeRelay} from "@app/state"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {pushModal, pushDrawer} from "@app/modal"
|
import {decodeRelay} from "@app/state"
|
||||||
|
import {pushDrawer} from "@app/modal"
|
||||||
|
import {makeChatPath} from "@app/routes"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay)
|
const url = decodeRelay($page.params.relay)
|
||||||
const events = deriveEventsForUrl(url, [{kinds: [THREAD]}])
|
const relay = deriveRelay(url)
|
||||||
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
|
||||||
const filters: Filter[] = [{kinds: [THREAD]}, {kinds: [COMMENT], "#k": [String(THREAD)]}]
|
|
||||||
const feed = makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(filters))
|
|
||||||
const loader = feedLoader.getLoader(feed, {
|
|
||||||
onExhausted: () => {
|
|
||||||
loading = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(MenuSpace, {url})
|
const openMenu = () => pushDrawer(MenuSpace, {url})
|
||||||
|
|
||||||
const createThread = () => pushModal(ThreadCreate, {url})
|
$: pubkey = $relay?.profile?.pubkey
|
||||||
|
|
||||||
let limit = 5
|
|
||||||
let loading = true
|
|
||||||
let element: Element
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Why is element not defined sometimes? SVELTEKIT
|
|
||||||
if (element) {
|
|
||||||
const scroller = createScroller({
|
|
||||||
element,
|
|
||||||
delay: 300,
|
|
||||||
threshold: 3000,
|
|
||||||
onScroll: async () => {
|
|
||||||
const $loader = await loader
|
|
||||||
|
|
||||||
await $loader(5)
|
|
||||||
limit += 5
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => scroller.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex h-screen flex-col">
|
<div class="relative flex flex-col">
|
||||||
<PageBar>
|
<PageBar>
|
||||||
<div slot="icon" class="center">
|
<div slot="icon" class="center">
|
||||||
<Icon icon="notes-minimalistic" />
|
<Icon icon="home-smile" />
|
||||||
</div>
|
</div>
|
||||||
<strong slot="title">Threads</strong>
|
<strong slot="title">Home</strong>
|
||||||
<div slot="action" class="row-2">
|
<div slot="action" class="row-2">
|
||||||
<Button class="btn btn-primary btn-sm" on:click={createThread}>
|
{#if pubkey}
|
||||||
<Icon icon="notes-minimalistic" />
|
<Link class="btn btn-primary btn-sm" href={makeChatPath([pubkey])}>
|
||||||
Create a Thread
|
<Icon icon="letter" />
|
||||||
</Button>
|
Contact Owner
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
<Button on:click={openMenu} class="btn btn-neutral btn-sm md:hidden">
|
<Button on:click={openMenu} class="btn btn-neutral btn-sm md:hidden">
|
||||||
<Icon icon="menu-dots" />
|
<Icon icon="menu-dots" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</PageBar>
|
</PageBar>
|
||||||
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2" bind:this={element}>
|
{#if pubkey}
|
||||||
{#each $events.slice(0, limit) as event (event.id)}
|
<div class="col-2 p-2">
|
||||||
{#if !event.tags.some(nthEq(0, "e")) && !mutedPubkeys.includes(event.pubkey)}
|
<div class="card2 bg-alt col-4 text-left">
|
||||||
<ThreadItem {url} {event} />
|
<div class="relative flex gap-4">
|
||||||
{/if}
|
<div class="relative">
|
||||||
{/each}
|
<div class="avatar relative">
|
||||||
<p class="flex h-10 items-center justify-center py-20">
|
<div
|
||||||
<Spinner {loading}>
|
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||||
{#if loading}
|
{#if $relay?.profile?.icon}
|
||||||
Looking for threads...
|
<img alt="" src={$relay.profile.icon} />
|
||||||
{:else if $events.length === 0}
|
{:else}
|
||||||
No threads found.
|
<Icon icon="ghost" size={5} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="ellipsize whitespace-nowrap text-xl">
|
||||||
|
<RelayName {url} />
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm opacity-75">{url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<RelayDescription {url} />
|
||||||
|
{#if $relay?.profile}
|
||||||
|
{@const {software, version, supported_nips, limitation} = $relay.profile}
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#if limitation?.auth_required}
|
||||||
|
<p class="badge badge-neutral">Authentication Required</p>
|
||||||
|
{/if}
|
||||||
|
{#if limitation?.payment_required}
|
||||||
|
<p class="badge badge-neutral">Payment Required</p>
|
||||||
|
{/if}
|
||||||
|
{#if limitation?.min_pow_difficulty}
|
||||||
|
<p class="badge badge-neutral">Requires PoW {limitation?.min_pow_difficulty}</p>
|
||||||
|
{/if}
|
||||||
|
{#if supported_nips}
|
||||||
|
<p class="badge badge-neutral">NIPs: {supported_nips.join(", ")}</p>
|
||||||
|
{/if}
|
||||||
|
{#if software}
|
||||||
|
<p class="badge badge-neutral">Software: {software}</p>
|
||||||
|
{/if}
|
||||||
|
{#if version}
|
||||||
|
<p class="badge badge-neutral">Version: {version}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Spinner>
|
</div>
|
||||||
</p>
|
<Divider>Recent posts from the relay admin</Divider>
|
||||||
</div>
|
<ProfileFeed {url} {pubkey} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
|
import type {Filter} from "@welshman/util"
|
||||||
|
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||||
|
import {nthEq} from "@welshman/lib"
|
||||||
|
import {feedLoader, userMutes} from "@welshman/app"
|
||||||
|
import {createScroller} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
|
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||||
|
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||||
|
import {THREAD, COMMENT, deriveEventsForUrl, decodeRelay} from "@app/state"
|
||||||
|
import {pushModal, pushDrawer} from "@app/modal"
|
||||||
|
|
||||||
|
const url = decodeRelay($page.params.relay)
|
||||||
|
const events = deriveEventsForUrl(url, [{kinds: [THREAD]}])
|
||||||
|
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||||
|
const filters: Filter[] = [{kinds: [THREAD]}, {kinds: [COMMENT], "#k": [String(THREAD)]}]
|
||||||
|
const feed = makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(filters))
|
||||||
|
const loader = feedLoader.getLoader(feed, {
|
||||||
|
onExhausted: () => {
|
||||||
|
loading = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const openMenu = () => pushDrawer(MenuSpace, {url})
|
||||||
|
|
||||||
|
const createThread = () => pushModal(ThreadCreate, {url})
|
||||||
|
|
||||||
|
let limit = 5
|
||||||
|
let loading = true
|
||||||
|
let element: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Why is element not defined sometimes? SVELTEKIT
|
||||||
|
if (element) {
|
||||||
|
const scroller = createScroller({
|
||||||
|
element,
|
||||||
|
delay: 300,
|
||||||
|
threshold: 3000,
|
||||||
|
onScroll: async () => {
|
||||||
|
const $loader = await loader
|
||||||
|
|
||||||
|
await $loader(5)
|
||||||
|
limit += 5
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => scroller.stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative flex h-screen flex-col">
|
||||||
|
<PageBar>
|
||||||
|
<div slot="icon" class="center">
|
||||||
|
<Icon icon="notes-minimalistic" />
|
||||||
|
</div>
|
||||||
|
<strong slot="title">Threads</strong>
|
||||||
|
<div slot="action" class="row-2">
|
||||||
|
<Button class="btn btn-primary btn-sm" on:click={createThread}>
|
||||||
|
<Icon icon="notes-minimalistic" />
|
||||||
|
Create a Thread
|
||||||
|
</Button>
|
||||||
|
<Button on:click={openMenu} class="btn btn-neutral btn-sm md:hidden">
|
||||||
|
<Icon icon="menu-dots" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PageBar>
|
||||||
|
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2" bind:this={element}>
|
||||||
|
{#each $events.slice(0, limit) as event (event.id)}
|
||||||
|
{#if !event.tags.some(nthEq(0, "e")) && !mutedPubkeys.includes(event.pubkey)}
|
||||||
|
<ThreadItem {url} {event} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="flex h-10 items-center justify-center py-20">
|
||||||
|
<Spinner {loading}>
|
||||||
|
{#if loading}
|
||||||
|
Looking for threads...
|
||||||
|
{:else if $events.length === 0}
|
||||||
|
No threads found.
|
||||||
|
{/if}
|
||||||
|
</Spinner>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
let showReply = false
|
let showReply = false
|
||||||
|
|
||||||
$: title = $event?.tags.find(nthEq(0, 'title'))?.[1] || ""
|
$: title = $event?.tags.find(nthEq(0, "title"))?.[1] || ""
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const sub = subscribe({filters, relays: [url]})
|
const sub = subscribe({filters, relays: [url]})
|
||||||
|
|||||||
Reference in New Issue
Block a user