Migrate more stuff

This commit is contained in:
Jon Staab
2025-02-03 16:37:14 -08:00
parent 0f705c459a
commit 8d3433b167
150 changed files with 2001 additions and 1205 deletions
+31 -14
View File
@@ -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>
+8 -3
View File
@@ -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>
+32 -24
View File
@@ -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>
+29 -24
View File
@@ -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>
+27 -9
View File
@@ -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>
+24 -20
View File
@@ -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>
+6 -1
View File
@@ -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>
+76 -52
View File
@@ -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>
+50 -34
View File
@@ -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>
+38 -26
View File
@@ -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}>
+6 -1
View File
@@ -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}
+10 -3
View File
@@ -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>
+26 -20
View File
@@ -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">
+46 -39
View File
@@ -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"}
+28 -20
View File
@@ -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)}
+28 -29
View File
@@ -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}