Add space home page

This commit is contained in:
Jon Staab
2024-10-31 12:24:40 -07:00
parent df947e9fcf
commit 74f9531c5f
34 changed files with 401 additions and 155 deletions
+4 -1
View File
@@ -26,6 +26,9 @@
</head>
<body data-sveltekit-preload-data="hover">
<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>
</html>
+2
View File
@@ -1,3 +1,5 @@
/* eslint prefer-rest-params: 0 */
import {page} from "$app/stores"
const w = window as any
+1 -1
View File
@@ -101,7 +101,7 @@ export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
new Promise(resolve => {
sub = subscribe(request)
sub.emitter.on("close", resolve)
})
}),
])
if (!done) {
+1 -1
View File
@@ -17,7 +17,7 @@
const submit = () => {
onSubmit({
content: $editor.getText({blockSeparator: '\n'}),
content: $editor.getText({blockSeparator: "\n"}),
tags: getEditorTags($editor),
})
+5 -10
View File
@@ -5,7 +5,6 @@
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import {isMobile} from "@lib/html"
import {slideAndFade, conditionalTransition} from "@lib/transition"
import LongPress from "@lib/components/LongPress.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Link from "@lib/components/Link.svelte"
@@ -36,16 +35,13 @@
const rootEvent = rootId ? deriveEvent(rootId, rootHints) : readable(null)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const transition = conditionalTransition(thunk, slideAndFade)
const onClick = () => {
const root = $rootEvent || event
pushDrawer(ChannelConversation, {url, room, event: root})
}
const onLongPress = () =>
pushModal(ChannelMessageMenuMobile, {url, event})
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
@@ -66,14 +62,13 @@
<LongPress
on:click={isMobile || inert ? null : onClick}
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">
{#if showPubkey}
<Link external href={pubkeyLink(event.pubkey)} class="flex items-start">
<Avatar
src={$profile?.picture}
class="border border-solid border-base-content"
size={10} />
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
</Link>
{:else}
<div class="w-10 min-w-10 max-w-10" />
+2 -2
View File
@@ -10,7 +10,7 @@
<script lang="ts">
import {onMount} from "svelte"
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 {createEvent, DIRECT_MESSAGE} from "@welshman/util"
import {
@@ -84,7 +84,7 @@
id,
type: "note",
value: event,
showPubkey: ((created_at - previousCreatedAt) > int(15, MINUTE)) || previousPubkey !== pubkey,
showPubkey: created_at - previousCreatedAt > int(15, MINUTE) || previousPubkey !== pubkey,
})
previousDate = date
+13 -11
View File
@@ -81,23 +81,25 @@
</button>
</Tippy>
<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}
<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
src={$profile?.picture}
class="border border-solid border-base-content"
size={4} />
<div class="flex items-center gap-2">
<Link
external
href={pubkeyLink(event.pubkey)}
class="text-sm font-bold"
style="color: {colorValue}">
{$profileDisplay}
</Link>
</div>
<div class="flex items-center gap-2">
<Link
external
href={pubkeyLink(event.pubkey)}
class="text-sm font-bold"
style="color: {colorValue}">
{$profileDisplay}
</Link>
</div>
</Link>
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import Confirm from "@lib/components/Confirm.svelte"
import {publishDelete} from "@app/commands"
import {clearModals} from '@app/modal'
import {clearModals} from "@app/modal"
export let url
export let event
+1 -11
View File
@@ -57,14 +57,6 @@
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 = () => {
warning = null
}
@@ -132,9 +124,7 @@
{/if}
{:else if isEllipsis(parsed) && expandInline}
{@html renderParsed(parsed)}
<button type="button" class="underline text-sm">
Read more
</button>
<button type="button" class="text-sm underline"> Read more </button>
{:else}
{@html renderParsed(parsed)}
{/if}
+1 -1
View File
@@ -32,7 +32,7 @@
}
// 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)
</script>
+1 -1
View File
@@ -41,7 +41,7 @@
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
const event = createEvent(kind, {
content: $editor.getText({blockSeparator: '\n'}),
content: $editor.getText({blockSeparator: "\n"}),
tags: [
["d", randomId()],
["title", title],
+8 -7
View File
@@ -10,14 +10,14 @@
<div slot="title">What is a bunker link?</div>
</ModalHeader>
<p>
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of
passwords to identify users. This allows users to own their social identity instead of
renting it from a tech company, and can bring it with them from app to app.
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of passwords
to identify users. This allows users to own their social identity instead of renting it from a tech
company, and can bring it with them from app to app.
</p>
<p>
A good way to manage your keys is to use a remote signing application. These apps can hold
your keys and log you in remotely to as many applications as you like, without risking
loss or theft of your keys.
A good way to manage your keys is to use a remote signing application. These apps can hold your
keys and log you in remotely to as many applications as you like, without risking loss or theft
of your keys.
</p>
<p>
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!
</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.
</p>
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
+8 -6
View File
@@ -2,7 +2,6 @@
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import {PLATFORM_NAME} from "@app/state"
</script>
<div class="column gap-4">
@@ -10,13 +9,16 @@
<div slot="title">What is nostr?</div>
</ModalHeader>
<p>
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build
social apps that talk to each other. Users own their social identity instead of
renting it from a tech company, and can take it with them.
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
talk to each other. Users own their social identity instead of renting it from a tech company, and
can take it with them.
</p>
<p>
If you'd like to learn more about what other apps exist in the nostr ecosystem, please
visit <Link external class="link" href="https://nostrapps.com/">nostrapps.com</Link>.
If you'd like to learn more about what other apps exist in the nostr ecosystem, please visit <Link
external
class="link"
href="https://nostrapps.com/">nostrapps.com</Link
>.
</p>
<p>
To learn more about how to manage your keys, or to set up an account, try
+1 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import {Nip46Broker} from "@welshman/signer"
import {addSession} from "@welshman/app"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
@@ -28,7 +27,7 @@
loading = true
try {
if (!await loginWithNip46(token, {pubkey, relays})) {
if (!(await loginWithNip46(token, {pubkey, relays}))) {
return pushToast({
theme: "error",
message: "Something went wrong, please try again!",
+5
View File
@@ -125,6 +125,11 @@
</div>
<div in:fly={{delay: getDelay(true)}}>
<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
</SecondaryNavItem>
</div>
+47
View File
@@ -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>
+47
View File
@@ -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>
+52
View File
@@ -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>
+2 -1
View File
@@ -17,7 +17,7 @@
</script>
{#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]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)}
@@ -34,5 +34,6 @@
{/if}
</button>
{/each}
<slot />
</div>
{/if}
+1 -1
View File
@@ -46,7 +46,7 @@
<div class="flex flex-wrap items-center justify-between gap-2">
<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}
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
{/if}
+9 -11
View File
@@ -3,7 +3,6 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {append} from "@welshman/lib"
import {createEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
@@ -26,7 +25,6 @@
const loading = writable(false)
const submit = () => {
if (!title) {
return pushToast({
theme: "error",
@@ -34,7 +32,7 @@
})
}
const content = $editor.getText({blockSeparator: '\n'})
const content = $editor.getText({blockSeparator: "\n"})
if (!content.trim()) {
return pushToast({
@@ -43,11 +41,7 @@
})
}
const tags = [
["title", title],
tagRoom(GENERAL, url),
...getEditorTags($editor),
]
const tags = [["title", title], tagRoom(GENERAL, url), ...getEditorTags($editor)]
publishThunk({
event: createEvent(THREAD, {content, tags}),
@@ -68,7 +62,7 @@
getPubkeyHints,
autofocus: true,
placeholder: "What's on your mind?",
})
}),
)
})
</script>
@@ -78,11 +72,15 @@
<div slot="title">Create a Thread</div>
<div slot="info">Share a link, or start a discussion.</div>
</ModalHeader>
<div class="relative col-8">
<div class="col-8 relative">
<Field>
<p slot="label">Title*</p>
<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>
</Field>
<Field>
+5 -5
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {nthEq} from '@welshman/lib'
import {nthEq} from "@welshman/lib"
import {formatTimestamp} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Content from "@app/components/Content.svelte"
@@ -12,19 +12,19 @@
export let event
export let hideActions = false
const title = event.tags.find(nthEq(0, 'title'))?.[1]
const title = event.tags.find(nthEq(0, "title"))?.[1]
</script>
<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-sm opacity-75">
{formatTimestamp(event.created_at)}
</p>
</div>
<Content {event} expandMode="inline" />
<div class="flex gap-2 items-end justify-between w-full">
<span class="text-sm opacity-75 whitespace-nowrap py-1">
<div class="flex w-full items-end justify-between gap-2">
<span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by
<Link external href={pubkeyLink(event.pubkey)} class="link-content">
@<ProfileName pubkey={event.pubkey} />
+1 -1
View File
@@ -23,7 +23,7 @@
const loading = writable(false)
const submit = () => {
const content = $editor.getText({blockSeparator: '\n'})
const content = $editor.getText({blockSeparator: "\n"})
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
if (!content.trim()) {
+1 -1
View File
@@ -7,7 +7,7 @@
import Tippy from "@lib/components/Tippy.svelte"
import Button from "@lib/components/Button.svelte"
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
import {userSettingValues} from '@app/state'
import {userSettingValues} from "@app/state"
export let thunk: Thunk | MergedThunk
+3 -3
View File
@@ -1,11 +1,11 @@
import type {Page} from "@sveltejs/kit"
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)}`
if (extra) {
path += "/" + encodeURIComponent(extra)
if (extra.length > 0) {
path += "/" + extra.map(s => encodeURIComponent(s)).join("/")
}
return path
+2 -2
View File
@@ -264,7 +264,7 @@ export type Settings = {
values: {
show_media: 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 displayReaction = (content: string) => {
if (content === "+") return "❤️"
if (!content || content === "+") return "❤️"
if (content === "-") return "👎"
return content
}
+1 -1
View File
@@ -6,7 +6,7 @@ export const setupTracking = () => {
dsn: import.meta.env.VITE_GLITCHTIP_API_KEY,
tracesSampleRate: 0.01,
integrations(integrations) {
return integrations.filter(integration => integration.name !== 'Breadcrumbs')
return integrations.filter(integration => integration.name !== "Breadcrumbs")
},
})
}
+1 -1
View File
@@ -150,7 +150,7 @@
"smile-circle": SmileCircle,
server: Server,
settings: Settings,
'settings-minimalistic': SettingsMinimalistic,
"settings-minimalistic": SettingsMinimalistic,
"tag-horizontal": TagHorizontal,
"trash-bin-2": TrashBin2,
"ufo-3": UFO3,
+8 -1
View File
@@ -24,6 +24,13 @@
let timeout: number
</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 />
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
import type {Writable} from "svelte/store"
import {nprofileEncode} from "nostr-tools/nip19"
import {SvelteNodeViewRenderer} from "svelte-tiptap"
import Placeholder from '@tiptap/extension-placeholder'
import Placeholder from "@tiptap/extension-placeholder"
import Code from "@tiptap/extension-code"
import CodeBlock from "@tiptap/extension-code-block"
import Document from "@tiptap/extension-document"
@@ -65,7 +65,7 @@ export const getModifiedHardBreakExtension = () =>
"Shift-Enter": () => this.editor.commands.setHardBreak(),
"Mod-Enter": () => this.editor.commands.setHardBreak(),
Enter: () => {
if (this.editor.getText({blockSeparator: '\n'}).trim()) {
if (this.editor.getText({blockSeparator: "\n"}).trim()) {
uploadFiles(this.editor)
return true
+2 -2
View File
@@ -75,8 +75,8 @@
step="1000"
bind:value={settings.send_delay} />
<p slot="info">
Delay sending chat messages for {settings.send_delay/1000}
{settings.send_delay === 1000 ? 'second' : 'seconds'}.
Delay sending chat messages for {settings.send_delay / 1000}
{settings.send_delay === 1000 ? "second" : "seconds"}.
</p>
</FieldInline>
<div class="mt-4 flex flex-row items-center justify-between gap-4">
+71 -67
View File
@@ -1,91 +1,95 @@
<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 {deriveRelay} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.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"
import ProfileFeed from "@app/components/ProfileFeed.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import {decodeRelay} from "@app/state"
import {pushDrawer} from "@app/modal"
import {makeChatPath} from "@app/routes"
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 relay = deriveRelay(url)
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()
}
})
$: pubkey = $relay?.profile?.pubkey
</script>
<div class="relative flex h-screen flex-col">
<div class="relative flex flex-col">
<PageBar>
<div slot="icon" class="center">
<Icon icon="notes-minimalistic" />
<Icon icon="home-smile" />
</div>
<strong slot="title">Threads</strong>
<strong slot="title">Home</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>
{#if pubkey}
<Link class="btn btn-primary btn-sm" href={makeChatPath([pubkey])}>
<Icon icon="letter" />
Contact Owner
</Link>
{/if}
<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 pubkey}
<div class="col-2 p-2">
<div class="card2 bg-alt col-4 text-left">
<div class="relative flex gap-4">
<div class="relative">
<div class="avatar relative">
<div
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
{#if $relay?.profile?.icon}
<img alt="" src={$relay.profile.icon} />
{:else}
<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}
</Spinner>
</p>
</div>
</div>
<Divider>Recent posts from the relay admin</Divider>
<ProfileFeed {url} {pubkey} />
</div>
{/if}
</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
$: title = $event?.tags.find(nthEq(0, 'title'))?.[1] || ""
$: title = $event?.tags.find(nthEq(0, "title"))?.[1] || ""
onMount(() => {
const sub = subscribe({filters, relays: [url]})