forked from coracle/flotilla
Migrate more stuff
This commit is contained in:
+31
-14
@@ -7,7 +7,19 @@
|
||||
import {dev} from "$app/environment"
|
||||
import {goto} from "$app/navigation"
|
||||
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
||||
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
|
||||
import {
|
||||
identity,
|
||||
sleep,
|
||||
take,
|
||||
sortBy,
|
||||
defer,
|
||||
ago,
|
||||
now,
|
||||
HOUR,
|
||||
WEEK,
|
||||
MONTH,
|
||||
Worker,
|
||||
} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
MESSAGE,
|
||||
@@ -60,14 +72,16 @@
|
||||
import * as commands from "@app/commands"
|
||||
import * as requests from "@app/requests"
|
||||
import * as notifications from "@app/notifications"
|
||||
import * as state from "@app/state"
|
||||
import * as appState from "@app/state"
|
||||
|
||||
// Migration: old nostrtalk instance used different sessions
|
||||
if ($session && !$signer) {
|
||||
dropSession($session.pubkey)
|
||||
}
|
||||
|
||||
let ready: Promise<unknown> = Promise.resolve()
|
||||
let {children} = $props()
|
||||
|
||||
let ready = $state(defer<void>())
|
||||
|
||||
onMount(async () => {
|
||||
Object.assign(window, {
|
||||
@@ -80,7 +94,7 @@
|
||||
...util,
|
||||
...net,
|
||||
...app,
|
||||
...state,
|
||||
...appState,
|
||||
...commands,
|
||||
...requests,
|
||||
...notifications,
|
||||
@@ -126,7 +140,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
ready = initStorage("flotilla", 5, {
|
||||
initStorage("flotilla", 5, {
|
||||
relays: storageAdapters.fromCollectionStore("url", relays, {throttle: 3000}),
|
||||
handles: storageAdapters.fromCollectionStore("nip05", handles, {throttle: 3000}),
|
||||
freshness: storageAdapters.fromObjectStore(freshness, {
|
||||
@@ -152,12 +166,12 @@
|
||||
const ALWAYS_KEEP = Infinity
|
||||
const reactionKinds = [REACTION, ZAP_RESPONSE, DELETE]
|
||||
const metaKinds = [PROFILE, FOLLOWS, RELAYS, INBOX_RELAYS]
|
||||
const $sessionKeys = new Set(Object.keys(app.sessions.get()))
|
||||
const $userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows))))
|
||||
const $maxWot = get(app.maxWot)
|
||||
const sessionKeys = new Set(Object.keys(app.sessions.get()))
|
||||
const userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows))))
|
||||
const maxWot = get(app.maxWot)
|
||||
|
||||
const scoreEvent = (e: TrustedEvent) => {
|
||||
const isFollowing = $userFollows.has(e.pubkey)
|
||||
const isFollowing = userFollows.has(e.pubkey)
|
||||
|
||||
// No need to keep a record of everyone who follows the current user
|
||||
if (e.kind === FOLLOWS && !isFollowing) return NEVER_KEEP
|
||||
@@ -166,15 +180,15 @@
|
||||
if (e.kind === MESSAGE && e.created_at < ago(MONTH)) return NEVER_KEEP
|
||||
|
||||
// Always keep stuff by or tagging a signed in user
|
||||
if ($sessionKeys.has(e.pubkey)) return ALWAYS_KEEP
|
||||
if (e.tags.some(t => $sessionKeys.has(t[1]))) return ALWAYS_KEEP
|
||||
if (sessionKeys.has(e.pubkey)) return ALWAYS_KEEP
|
||||
if (e.tags.some(t => sessionKeys.has(t[1]))) return ALWAYS_KEEP
|
||||
|
||||
// Get rid of irrelevant messages, reactions, and likes
|
||||
if (e.wrap || e.kind === 4 || e.kind === WRAP) return NEVER_KEEP
|
||||
if (reactionKinds.includes(e.kind)) return NEVER_KEEP
|
||||
|
||||
// If the user follows this person, use max wot score
|
||||
let score = isFollowing ? $maxWot : app.getUserWotScore(e.pubkey)
|
||||
let score = isFollowing ? maxWot : app.getUserWotScore(e.pubkey)
|
||||
|
||||
// Inflate the score for profiles/relays/follows to avoid redundant fetches
|
||||
// Demote non-metadata type events, and introduce recency bias
|
||||
@@ -189,7 +203,10 @@
|
||||
)
|
||||
},
|
||||
}),
|
||||
}).then(() => sleep(300))
|
||||
}).then(async () => {
|
||||
await sleep(300)
|
||||
ready.resolve()
|
||||
})
|
||||
|
||||
// Unwrap gift wraps as they come in, but throttled
|
||||
const unwrapper = new Worker<TrustedEvent>({chunkSize: 10})
|
||||
@@ -259,7 +276,7 @@
|
||||
{:then}
|
||||
<div data-theme={$theme}>
|
||||
<AppContainer>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</AppContainer>
|
||||
<ModalContainer />
|
||||
<div class="tippy-target"></div>
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
import {chatSearch} from "@app/state"
|
||||
import {pullConservatively} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
let {children}: Props = $props()
|
||||
|
||||
const startChat = () => pushModal(ChatStart)
|
||||
|
||||
@@ -23,9 +28,9 @@
|
||||
relays: ctx.app.router.UserInbox().getUrls(),
|
||||
})
|
||||
|
||||
let term = ""
|
||||
let term = $state("")
|
||||
|
||||
$: chats = $chatSearch.searchOptions(term)
|
||||
let chats = $derived($chatSearch.searchOptions(term))
|
||||
</script>
|
||||
|
||||
<SecondaryNav>
|
||||
@@ -54,6 +59,6 @@
|
||||
</SecondaryNav>
|
||||
<Page>
|
||||
{#key $page.url.pathname}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Page>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
import {pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
|
||||
let term = ""
|
||||
let term = $state("")
|
||||
|
||||
const startChat = () => pushModal(ChatStart)
|
||||
|
||||
const openMenu = () => pushModal(ChatMenuMobile)
|
||||
|
||||
$: chats = $chatSearch.searchOptions(term)
|
||||
let chats = $derived($chatSearch.searchOptions(term))
|
||||
|
||||
onDestroy(() => {
|
||||
setChecked($page.url.pathname)
|
||||
@@ -39,26 +39,34 @@
|
||||
</div>
|
||||
|
||||
<ContentSearch class="md:hidden">
|
||||
<div slot="input" class="row-2 min-w-0 flex-grow items-center">
|
||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="magnifer" />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search for conversations..." />
|
||||
</label>
|
||||
<Button class="btn btn-primary" on:click={openMenu}>
|
||||
<Icon icon="menu-dots" />
|
||||
</Button>
|
||||
</div>
|
||||
<div slot="content" class="col-2">
|
||||
{#each chats as { id, pubkeys, messages } (id)}
|
||||
<ChatItem {id} {pubkeys} {messages} class="bg-alt card2" />
|
||||
{:else}
|
||||
<div class="py-20 max-w-sm col-4 items-center m-auto text-center">
|
||||
<p>No chats found! Try starting one up.</p>
|
||||
<Button class="btn btn-primary" on:click={startChat}>
|
||||
<Icon icon="add-circle" />
|
||||
Start a Chat
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#snippet input()}
|
||||
<div class="row-2 min-w-0 flex-grow items-center">
|
||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="magnifer" />
|
||||
<input
|
||||
bind:value={term}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="Search for conversations..." />
|
||||
</label>
|
||||
<Button class="btn btn-primary" on:click={openMenu}>
|
||||
<Icon icon="menu-dots" />
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="col-2">
|
||||
{#each chats as { id, pubkeys, messages } (id)}
|
||||
<ChatItem {id} {pubkeys} {messages} class="bg-alt card2" />
|
||||
{:else}
|
||||
<div class="py-20 max-w-sm col-4 items-center m-auto text-center">
|
||||
<p>No chats found! Try starting one up.</p>
|
||||
<Button class="btn btn-primary" on:click={startChat}>
|
||||
<Icon icon="add-circle" />
|
||||
Start a Chat
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ContentSearch>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {addToMapKey, dec, gt} from "@welshman/lib"
|
||||
import type {Relay} from "@welshman/app"
|
||||
import {relays, createSearch} from "@welshman/app"
|
||||
@@ -24,11 +23,11 @@
|
||||
import {discoverRelays} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const wotGraph = derived(membershipByPubkey, $m => {
|
||||
const wotGraph = $derived.by(() => {
|
||||
const scores = new Map<string, Set<string>>()
|
||||
|
||||
for (const pubkey of getDefaultPubkeys()) {
|
||||
for (const url of getMembershipUrls($m.get(pubkey))) {
|
||||
for (const url of getMembershipUrls($membershipByPubkey.get(pubkey))) {
|
||||
addToMapKey(scores, url, pubkey)
|
||||
}
|
||||
}
|
||||
@@ -36,27 +35,29 @@
|
||||
return scores
|
||||
})
|
||||
|
||||
const relaySearch = $derived(
|
||||
createSearch($relays, {
|
||||
getValue: (relay: Relay) => relay.url,
|
||||
sortFn: ({score, item}) => {
|
||||
if (score && score > 0.1) return -score!
|
||||
|
||||
const wotScore = wotGraph.get(item.url)?.size || 0
|
||||
|
||||
return score ? dec(score) * wotScore : -wotScore
|
||||
},
|
||||
fuseOptions: {
|
||||
keys: ["url", "name", {name: "description", weight: 0.3}],
|
||||
shouldSort: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
||||
|
||||
let term = ""
|
||||
let limit = 20
|
||||
let term = $state("")
|
||||
let limit = $state(20)
|
||||
let element: Element
|
||||
|
||||
$: relaySearch = createSearch($relays, {
|
||||
getValue: (relay: Relay) => relay.url,
|
||||
sortFn: ({score, item}) => {
|
||||
if (score && score > 0.1) return -score!
|
||||
|
||||
const wotScore = $wotGraph.get(item.url)?.size || 0
|
||||
|
||||
return score ? dec(score) * wotScore : -wotScore
|
||||
},
|
||||
fuseOptions: {
|
||||
keys: ["url", "name", {name: "description", weight: 0.3}],
|
||||
shouldSort: false,
|
||||
},
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element,
|
||||
@@ -74,8 +75,12 @@
|
||||
<Page>
|
||||
<div class="content column gap-4" bind:this={element}>
|
||||
<PageHeader>
|
||||
<div slot="title">Discover Spaces</div>
|
||||
<div slot="info">Find communities all across the nostr network</div>
|
||||
{#snippet title()}
|
||||
Discover Spaces
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
Find communities all across the nostr network
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="magnifer" />
|
||||
@@ -115,10 +120,10 @@
|
||||
</div>
|
||||
<RelayDescription url={relay.url} />
|
||||
</div>
|
||||
{#if gt($wotGraph.get(relay.url)?.size, 0)}
|
||||
{#if gt(wotGraph.get(relay.url)?.size, 0)}
|
||||
<div class="row-2 card2 card2-sm bg-alt">
|
||||
Members:
|
||||
<ProfileCircles pubkeys={Array.from($wotGraph.get(relay.url) || [])} />
|
||||
<ProfileCircles pubkeys={Array.from(wotGraph.get(relay.url) || [])} />
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
@@ -31,23 +31,41 @@
|
||||
<div class="col-3">
|
||||
<Button on:click={addSpace}>
|
||||
<CardButton>
|
||||
<div slot="icon"><Icon icon="add-circle" size={7} /></div>
|
||||
<div slot="title">Add a space</div>
|
||||
<div slot="info">Use an invite link, or create your own space.</div>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="add-circle" size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Add a space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Use an invite link, or create your own space.</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
<Link href="/people">
|
||||
<CardButton>
|
||||
<div slot="icon"><Icon icon="compass" size={7} /></div>
|
||||
<div slot="title">Browse the network</div>
|
||||
<div slot="info">Find your people on the nostr network.</div>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="compass" size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Browse the network</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Find your people on the nostr network.</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Button on:click={startChat}>
|
||||
<CardButton>
|
||||
<div slot="icon"><Icon icon="chat-round" size={7} /></div>
|
||||
<div slot="title">Start a conversation</div>
|
||||
<div slot="info">Use nostr's encrypted group chats to stay in touch.</div>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="chat-round" size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Start a conversation</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Use nostr's encrypted group chats to stay in touch.</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
|
||||
const defaultPubkeys = getDefaultPubkeys()
|
||||
|
||||
let term = ""
|
||||
let limit = 10
|
||||
let element: Element
|
||||
let term = $state("")
|
||||
let limit = $state(10)
|
||||
let element: Element | undefined = $state()
|
||||
|
||||
$: pubkeys = term ? $profileSearch.searchValues(term) : defaultPubkeys
|
||||
let pubkeys = $derived(term ? $profileSearch.searchValues(term) : defaultPubkeys)
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element,
|
||||
element: element!,
|
||||
onScroll: () => {
|
||||
limit += 10
|
||||
},
|
||||
@@ -30,20 +30,24 @@
|
||||
|
||||
<Page>
|
||||
<ContentSearch>
|
||||
<label slot="input" class="row-2 input input-bordered">
|
||||
<Icon icon="magnifer" />
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus={!isMobile}
|
||||
bind:value={term}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="Search for people..." />
|
||||
</label>
|
||||
<div slot="content" class="col-2" bind:this={element}>
|
||||
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
||||
<PeopleItem {pubkey} />
|
||||
{/each}
|
||||
</div>
|
||||
{#snippet input()}
|
||||
<label class="row-2 input input-bordered">
|
||||
<Icon icon="magnifer" />
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus={!isMobile}
|
||||
bind:value={term}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="Search for people..." />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="col-2" bind:this={element}>
|
||||
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
||||
<PeopleItem {pubkey} />
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ContentSearch>
|
||||
</Page>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import LogOut from "@app/components/LogOut.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
let {children}: Props = $props()
|
||||
|
||||
const logout = () => pushModal(LogOut)
|
||||
</script>
|
||||
@@ -42,5 +47,5 @@
|
||||
</SecondaryNav>
|
||||
|
||||
<Page>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</Page>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "svelte/legacy"
|
||||
|
||||
import {ctx} from "@welshman/lib"
|
||||
import {getListTags, createEvent, getPubkeyTagValues, MUTES} from "@welshman/util"
|
||||
import {pubkey, signer, userMutes, tagPubkey, publishThunk} from "@welshman/app"
|
||||
@@ -31,72 +33,94 @@
|
||||
pushToast({message: "Your settings have been saved!"})
|
||||
}
|
||||
|
||||
let settings = {...$userSettingValues}
|
||||
let mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||
let settings = $state({...$userSettingValues})
|
||||
let mutedPubkeys = $state(getPubkeyTagValues(getListTags($userMutes)))
|
||||
</script>
|
||||
|
||||
<form class="content column gap-4" on:submit|preventDefault={onSubmit}>
|
||||
<form class="content column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<div class="card2 bg-alt col-4 shadow-xl">
|
||||
<p class="text-lg">Content Settings</p>
|
||||
<FieldInline>
|
||||
<p slot="label">Hide sensitive content?</p>
|
||||
<input
|
||||
slot="input"
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={settings.hide_sensitive} />
|
||||
<p slot="info">
|
||||
If content is marked by the author as sensitive, {PLATFORM_NAME} will hide it by default.
|
||||
</p>
|
||||
{#snippet label()}
|
||||
<p>Hide sensitive content?</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={settings.hide_sensitive} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
If content is marked by the author as sensitive, {PLATFORM_NAME} will hide it by default.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
<p slot="label">Show media?</p>
|
||||
<input
|
||||
slot="input"
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={settings.show_media} />
|
||||
<p slot="info">Use this to disable link previews and image rendering.</p>
|
||||
{#snippet label()}
|
||||
<p>Show media?</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.show_media} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>Use this to disable link previews and image rendering.</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<Field>
|
||||
<p slot="label">Muted Accounts</p>
|
||||
<div slot="input">
|
||||
<ProfileMultiSelect bind:value={mutedPubkeys} />
|
||||
</div>
|
||||
{#snippet label()}
|
||||
<p>Muted Accounts</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div>
|
||||
<ProfileMultiSelect bind:value={mutedPubkeys} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<p class="text-lg">Editor Settings</p>
|
||||
<FieldInline>
|
||||
<p slot="label">Send Delay</p>
|
||||
<input
|
||||
class="range range-primary"
|
||||
slot="input"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10000"
|
||||
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"}.
|
||||
</p>
|
||||
{#snippet label()}
|
||||
<p>Send Delay</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input
|
||||
class="range range-primary"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10000"
|
||||
step="1000"
|
||||
bind:value={settings.send_delay} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Delay sending chat messages for {settings.send_delay / 1000}
|
||||
{settings.send_delay === 1000 ? "second" : "seconds"}.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<Field>
|
||||
<p slot="label">Media Server</p>
|
||||
<div slot="input" class="flex flex-col gap-2 sm:flex-row">
|
||||
<select bind:value={settings.upload_type} class="select select-bordered">
|
||||
<option value="nip96">NIP 96 (default)</option>
|
||||
<option value="blossom">Blossom</option>
|
||||
</select>
|
||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="link-round" />
|
||||
{#if settings.upload_type === "nip96"}
|
||||
<input class="grow" bind:value={settings.nip96_urls[0]} />
|
||||
{:else}
|
||||
<input class="grow" bind:value={settings.blossom_urls[0]} />
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
<p slot="info">Choose a media server type and url for files you upload to flotilla.</p>
|
||||
{#snippet label()}
|
||||
<p>Media Server</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<select bind:value={settings.upload_type} class="select select-bordered">
|
||||
<option value="nip96">NIP 96 (default)</option>
|
||||
<option value="blossom">Blossom</option>
|
||||
</select>
|
||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="link-round" />
|
||||
{#if settings.upload_type === "nip96"}
|
||||
<input class="grow" bind:value={settings.nip96_urls[0]} />
|
||||
{:else}
|
||||
<input class="grow" bind:value={settings.blossom_urls[0]} />
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>Choose a media server type and url for files you upload to flotilla.</p>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||
<Button class="btn btn-neutral" on:click={reset}>Discard Changes</Button>
|
||||
|
||||
@@ -56,48 +56,64 @@
|
||||
{#if $session?.email}
|
||||
<div class="card2 bg-alt col-4 shadow-xl">
|
||||
<FieldInline>
|
||||
<p slot="label">Email Address</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="user-rounded" />
|
||||
<input readonly value={$session.email} class="grow" />
|
||||
</label>
|
||||
<p slot="info">
|
||||
Your email and password can only be used to log into {PLATFORM_NAME}.
|
||||
<Button class="link" on:click={startEject}>Start holding your own keys</Button>
|
||||
</p>
|
||||
{#snippet label()}
|
||||
<p>Email Address</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-rounded" />
|
||||
<input readonly value={$session.email} class="grow" />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Your email and password can only be used to log into {PLATFORM_NAME}.
|
||||
<Button class="link" on:click={startEject}>Start holding your own keys</Button>
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card2 bg-alt col-4 shadow-xl">
|
||||
<FieldInline>
|
||||
<p slot="label">Public Key</p>
|
||||
<label
|
||||
class="input input-bordered flex w-full items-center justify-between gap-2"
|
||||
slot="input">
|
||||
<div class="row-2 flex-grow items-center">
|
||||
<Icon icon="link-round" />
|
||||
<input readonly class="ellipsize flex-grow" value={$session?.pubkey} />
|
||||
</div>
|
||||
<Button class="flex items-center" on:click={copyNpub}>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
</label>
|
||||
<p slot="info">
|
||||
Your public key is your nostr user identifier. It also allows other people to authenticate
|
||||
your messages.
|
||||
</p>
|
||||
</FieldInline>
|
||||
{#if $session?.method === "nip01"}
|
||||
<FieldInline>
|
||||
<p slot="label">Private Key</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="link-round" />
|
||||
<input readonly value={$session.secret} class="grow" type="password" />
|
||||
<Button class="flex items-center" on:click={copyNsec}>
|
||||
{#snippet label()}
|
||||
<p>Public Key</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center justify-between gap-2">
|
||||
<div class="row-2 flex-grow items-center">
|
||||
<Icon icon="link-round" />
|
||||
<input readonly class="ellipsize flex-grow" value={$session?.pubkey} />
|
||||
</div>
|
||||
<Button class="flex items-center" on:click={copyNpub}>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
</label>
|
||||
<p slot="info">Your private key is your nostr password. Keep this somewhere safe!</p>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Your public key is your nostr user identifier. It also allows other people to authenticate
|
||||
your messages.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{#if $session?.method === "nip01"}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Private Key</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="link-round" />
|
||||
<input readonly value={$session.secret} class="grow" type="password" />
|
||||
<Button class="flex items-center" on:click={copyNsec}>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>Your private key is your nostr password. Keep this somewhere safe!</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -53,15 +53,19 @@
|
||||
|
||||
<div class="content column gap-4">
|
||||
<Collapse class="card2 bg-alt column gap-4">
|
||||
<h2 slot="title" class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="earth" />
|
||||
Outbox Relays
|
||||
</h2>
|
||||
<p slot="description" class="text-sm">
|
||||
These relays will be advertised on your profile as places where you send your public notes. Be
|
||||
sure to select relays that will accept your notes, and which will let people who follow you
|
||||
read them.
|
||||
</p>
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="earth" />
|
||||
Outbox Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places where you send your public notes.
|
||||
Be sure to select relays that will accept your notes, and which will let people who follow
|
||||
you read them.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $writeRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
@@ -82,14 +86,18 @@
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse class="card2 bg-alt column gap-4">
|
||||
<h2 slot="title" class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="inbox" />
|
||||
Inbox Relays
|
||||
</h2>
|
||||
<p slot="description" class="text-sm">
|
||||
These relays will be advertised on your profile as places where other people should send notes
|
||||
intended for you. Be sure to select relays that will accept notes that tag you.
|
||||
</p>
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="inbox" />
|
||||
Inbox Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places where other people should send
|
||||
notes intended for you. Be sure to select relays that will accept notes that tag you.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $readRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
@@ -110,15 +118,19 @@
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse class="card2 bg-alt column gap-4">
|
||||
<h2 slot="title" class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="mailbox" />
|
||||
Messaging Relays
|
||||
</h2>
|
||||
<p slot="description" class="text-sm">
|
||||
These relays will be advertised on your profile as places you use to send and receive direct
|
||||
messages. Be sure to select relays that will accept your messages and messages from people
|
||||
you'd like to be in contact with.
|
||||
</p>
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon="mailbox" />
|
||||
Messaging Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places you use to send and receive direct
|
||||
messages. Be sure to select relays that will accept your messages and messages from people
|
||||
you'd like to be in contact with.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $inboxRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
let {children}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#key $page.params.relay}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {run} from "svelte/legacy"
|
||||
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {ago, MONTH} from "@welshman/lib"
|
||||
@@ -15,6 +17,11 @@
|
||||
import {decodeRelay, userRoomsByUrl} from "@app/state"
|
||||
import {pullConservatively} from "@app/requests"
|
||||
import {notifications} from "@app/notifications"
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
let {children}: Props = $props()
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
|
||||
@@ -37,11 +44,11 @@
|
||||
}
|
||||
|
||||
// We have to watch this one, since on mobile the badge will be visible when active
|
||||
$: {
|
||||
run(() => {
|
||||
if ($notifications.has($page.url.pathname)) {
|
||||
setChecked($page.url.pathname)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
checkConnection()
|
||||
@@ -77,6 +84,6 @@
|
||||
</SecondaryNav>
|
||||
<Page>
|
||||
{#key $page.url.pathname}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Page>
|
||||
|
||||
@@ -39,31 +39,37 @@
|
||||
|
||||
const addRoom = () => pushModal(RoomCreate, {url})
|
||||
|
||||
let relayAdminEvents: TrustedEvent[] = []
|
||||
let relayAdminEvents: TrustedEvent[] = $state([])
|
||||
|
||||
$: pubkey = $relay?.profile?.pubkey
|
||||
let pubkey = $derived($relay?.profile?.pubkey)
|
||||
</script>
|
||||
|
||||
<div class="relative flex flex-col">
|
||||
<PageBar>
|
||||
<div slot="icon" class="center">
|
||||
<Icon icon="home-smile" />
|
||||
</div>
|
||||
<strong slot="title">Home</strong>
|
||||
<div slot="action" class="row-2">
|
||||
{#if !$userRoomsByUrl.has(url)}
|
||||
<Button class="btn btn-primary btn-sm" on:click={joinSpace}>
|
||||
<Icon icon="login-2" />
|
||||
Join Space
|
||||
</Button>
|
||||
{:else if pubkey}
|
||||
<Link class="btn btn-primary btn-sm" href={makeChatPath([pubkey])}>
|
||||
<Icon icon="letter" />
|
||||
Contact Owner
|
||||
</Link>
|
||||
{/if}
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon="home-smile" />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Home</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
{#if !$userRoomsByUrl.has(url)}
|
||||
<Button class="btn btn-primary btn-sm" on:click={joinSpace}>
|
||||
<Icon icon="login-2" />
|
||||
Join Space
|
||||
</Button>
|
||||
{:else if pubkey}
|
||||
<Link class="btn btn-primary btn-sm" href={makeChatPath([pubkey])}>
|
||||
<Icon icon="letter" />
|
||||
Contact Owner
|
||||
</Link>
|
||||
{/if}
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<div class="col-2 p-2">
|
||||
<div class="card2 bg-alt col-4 text-left">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {readable} from "svelte/store"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import type {Readable} from "svelte/store"
|
||||
@@ -70,7 +71,7 @@
|
||||
|
||||
const replyTo = (event: TrustedEvent) => {
|
||||
parent = event
|
||||
compose.focus()
|
||||
compose?.focus()
|
||||
}
|
||||
|
||||
const clearParent = () => {
|
||||
@@ -107,25 +108,23 @@
|
||||
}
|
||||
|
||||
const scrollToNewMessages = () =>
|
||||
newMessages.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
newMessages?.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
|
||||
const scrollToBottom = () => element.scrollTo({top: 0, behavior: "smooth"})
|
||||
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
|
||||
let parent: TrustedEvent | undefined
|
||||
let loading = true
|
||||
let element: HTMLElement
|
||||
let newMessages: HTMLElement
|
||||
let loading = $state(true)
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let element: HTMLElement | undefined = $state()
|
||||
let newMessages: HTMLElement | undefined = $state()
|
||||
let newMessagesSeen = false
|
||||
let showFixedNewMessages = false
|
||||
let showScrollButton = false
|
||||
let showFixedNewMessages = $state(false)
|
||||
let showScrollButton = $state(false)
|
||||
let cleanup: () => void
|
||||
let events: Readable<TrustedEvent[]>
|
||||
let compose: ChannelCompose
|
||||
let elements: any[] = []
|
||||
|
||||
$: {
|
||||
elements = []
|
||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||
let compose: ChannelCompose | undefined = $state()
|
||||
|
||||
const elements = $derived.by(() => {
|
||||
const elements = []
|
||||
const seen = new Set()
|
||||
|
||||
let previousDate
|
||||
@@ -170,11 +169,13 @@
|
||||
elements.reverse()
|
||||
|
||||
setTimeout(onScroll, 100)
|
||||
}
|
||||
|
||||
return elements
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
;({events, cleanup} = makeFeed({
|
||||
element,
|
||||
element: element!,
|
||||
relays: [url],
|
||||
feedFilters: [filter],
|
||||
subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
|
||||
@@ -193,32 +194,38 @@
|
||||
|
||||
<div class="saib relative flex h-full flex-col">
|
||||
<PageBar>
|
||||
<div slot="icon" class="center">
|
||||
<Icon icon="hashtag" />
|
||||
</div>
|
||||
<strong slot="title">
|
||||
<ChannelName {url} {room} />
|
||||
</strong>
|
||||
<div slot="action" class="row-2">
|
||||
{#if room !== GENERAL}
|
||||
{#if $userRoomsByUrl.get(url)?.has(room)}
|
||||
<Button class="btn btn-neutral btn-sm" on:click={leaveRoom}>
|
||||
<Icon icon="arrows-a-logout-2" />
|
||||
Leave Room
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" on:click={joinRoom}>
|
||||
<Icon icon="login-2" />
|
||||
Join Room
|
||||
</Button>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon="hashtag" />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>
|
||||
<ChannelName {url} {room} />
|
||||
</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
{#if room !== GENERAL}
|
||||
{#if $userRoomsByUrl.get(url)?.has(room)}
|
||||
<Button class="btn btn-neutral btn-sm" on:click={leaveRoom}>
|
||||
<Icon icon="arrows-a-logout-2" />
|
||||
Leave Room
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" on:click={joinRoom}>
|
||||
<Icon icon="login-2" />
|
||||
Join Room
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<div
|
||||
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-y-auto overflow-x-hidden py-2"
|
||||
on:scroll={onScroll}
|
||||
onscroll={onScroll}
|
||||
bind:this={element}>
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "new-messages"}
|
||||
|
||||
@@ -30,28 +30,30 @@
|
||||
parseInt(event.tags.find(t => t[0] === "start")?.[1] || "")
|
||||
|
||||
const limit = 5
|
||||
let loading = true
|
||||
let loading = $state(true)
|
||||
|
||||
type Item = {
|
||||
event: TrustedEvent
|
||||
dateDisplay?: string
|
||||
}
|
||||
|
||||
$: items = sortBy(e => -getStart(e), $events)
|
||||
.reduce<Item[]>((r, event) => {
|
||||
const end = getEnd(event)
|
||||
const start = getStart(event)
|
||||
let items = $derived(
|
||||
sortBy(e => -getStart(e), $events)
|
||||
.reduce<Item[]>((r, event) => {
|
||||
const end = getEnd(event)
|
||||
const start = getStart(event)
|
||||
|
||||
if (isNaN(start) || isNaN(end)) return r
|
||||
if (isNaN(start) || isNaN(end)) return r
|
||||
|
||||
const prevDateDisplay =
|
||||
r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
|
||||
const newDateDisplay = formatTimestampAsDate(start)
|
||||
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
|
||||
const prevDateDisplay =
|
||||
r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
|
||||
const newDateDisplay = formatTimestampAsDate(start)
|
||||
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
|
||||
|
||||
return [...r, {event, dateDisplay}]
|
||||
}, [])
|
||||
.slice(0, limit)
|
||||
return [...r, {event, dateDisplay}]
|
||||
}, [])
|
||||
.slice(0, limit),
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
const sub = subscribe({filters: [{kinds, since: ago(30)}]})
|
||||
@@ -72,13 +74,19 @@
|
||||
|
||||
<div class="relative flex h-screen flex-col">
|
||||
<PageBar>
|
||||
<div slot="icon" class="center">
|
||||
<Icon icon="calendar-minimalistic" />
|
||||
</div>
|
||||
<strong slot="title">Calendar</strong>
|
||||
<div slot="action" class="md:hidden">
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon="calendar-minimalistic" />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Calendar</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="md:hidden">
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
|
||||
{#each items as { event, dateDisplay }, i (event.id)}
|
||||
|
||||
@@ -59,32 +59,25 @@
|
||||
})
|
||||
|
||||
let limit = 10
|
||||
let loading = true
|
||||
let unmounted = false
|
||||
let element: Element
|
||||
let loading = $state(true)
|
||||
let element: Element | undefined = $state()
|
||||
let scroller: Scroller
|
||||
|
||||
onMount(() => {
|
||||
// Element is frequently not defined. I don't know why
|
||||
sleep(1000).then(() => {
|
||||
if (!unmounted) {
|
||||
scroller = createScroller({
|
||||
element,
|
||||
delay: 300,
|
||||
threshold: 3000,
|
||||
onScroll: () => {
|
||||
limit += 10
|
||||
scroller = createScroller({
|
||||
element: element!,
|
||||
delay: 300,
|
||||
threshold: 3000,
|
||||
onScroll: () => {
|
||||
limit += 10
|
||||
|
||||
if ($events.length - limit < 10) {
|
||||
ctrl.load(50)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
if ($events.length - limit < 10) {
|
||||
ctrl.load(50)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
unmounted = true
|
||||
scroller?.stop()
|
||||
setChecked($page.url.pathname)
|
||||
}
|
||||
@@ -93,17 +86,23 @@
|
||||
|
||||
<div class="relative flex h-screen flex-col" bind:this={element}>
|
||||
<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}>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon="notes-minimalistic" />
|
||||
Create a Thread
|
||||
</Button>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Threads</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" on:click={createThread}>
|
||||
<Icon icon="notes-minimalistic" />
|
||||
Create a Thread
|
||||
</Button>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
|
||||
{#each $events as event (event.id)}
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
showAll = true
|
||||
}
|
||||
|
||||
let showAll = false
|
||||
let showReply = false
|
||||
let showAll = $state(false)
|
||||
let showReply = $state(false)
|
||||
|
||||
$: title = $event?.tags.find(nthEq(0, "title"))?.[1] || ""
|
||||
let title = $derived($event?.tags.find(nthEq(0, "title"))?.[1] || "")
|
||||
|
||||
onMount(() => {
|
||||
const sub = subscribe({relays: [url], filters})
|
||||
@@ -93,16 +93,22 @@
|
||||
{/await}
|
||||
{/if}
|
||||
<PageBar class="mx-0">
|
||||
<div slot="icon">
|
||||
<Button class="btn btn-neutral btn-sm" on:click={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
<h1 slot="title" class="text-xl">{title}</h1>
|
||||
<div slot="action">
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm" on:click={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{title}</h1>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
</div>
|
||||
{#if showReply}
|
||||
|
||||
Reference in New Issue
Block a user