forked from coracle/flotilla
Add space home page
This commit is contained in:
+4
-1
@@ -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>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint prefer-rest-params: 0 */
|
||||
|
||||
import {page} from "$app/stores"
|
||||
|
||||
const w = window as any
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
|
||||
new Promise(resolve => {
|
||||
sub = subscribe(request)
|
||||
sub.emitter.on("close", resolve)
|
||||
})
|
||||
}),
|
||||
])
|
||||
|
||||
if (!done) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
const submit = () => {
|
||||
onSubmit({
|
||||
content: $editor.getText({blockSeparator: '\n'}),
|
||||
content: $editor.getText({blockSeparator: "\n"}),
|
||||
tags: getEditorTags($editor),
|
||||
})
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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!",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
{#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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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]})
|
||||
|
||||
Reference in New Issue
Block a user