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> </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>
+2
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -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 -10
View File
@@ -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" />
+2 -2
View File
@@ -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
+13 -11
View File
@@ -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 -1
View File
@@ -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
+1 -11
View File
@@ -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}
+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 // 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>
+1 -1
View File
@@ -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],
+8 -7
View File
@@ -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>
+8 -6
View File
@@ -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 -2
View File
@@ -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!",
+5
View File
@@ -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>
+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> </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}
+1 -1
View File
@@ -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}
+9 -11
View File
@@ -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>
+5 -5
View File
@@ -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} />
+1 -1
View File
@@ -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()) {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}, },
}) })
} }
+1 -1
View File
@@ -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,
+8 -1
View File
@@ -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>
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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">
+71 -67
View File
@@ -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]})