Compare commits

..

2 Commits

25 changed files with 363 additions and 3004 deletions
-1
View File
@@ -70,7 +70,6 @@ GoogleService-Info.plist
.roo .roo
.idea/ .idea/
.vscode/ .vscode/
.claude/
# OS generated # OS generated
.DS_Store .DS_Store
-2619
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -59,7 +59,7 @@
"@getalby/lightning-tools": "^6.1.0", "@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2", "@getalby/sdk": "^5.1.2",
"@noble/curves": "^1.9.7", "@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.3", "@pomade/core": "^0.2.2",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",
@@ -101,9 +101,9 @@
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"sharp", "sharp",
"nostr-signer-capacitor-plugin" "nostr-signer-capacitor-plugin"
] ],
}, "overrides": {
"overrides": { "sharp": "0.35.0-rc.0"
"sharp": "0.35.0-rc.0" }
} }
} }
+9 -9
View File
@@ -60,8 +60,8 @@ importers:
specifier: ^1.9.7 specifier: ^1.9.7
version: 1.9.7 version: 1.9.7
'@pomade/core': '@pomade/core':
specifier: ^0.2.3 specifier: ^0.2.2
version: 0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
'@poppanator/sveltekit-svg': '@poppanator/sveltekit-svg':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)) version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))
@@ -85,7 +85,7 @@ importers:
version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0)) version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
'@welshman/app': '@welshman/app':
specifier: ^0.8.12 specifier: ^0.8.12
version: 0.8.12(2f5bd20a84c8c39e26176b5a5db083ae) version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
'@welshman/content': '@welshman/content':
specifier: ^0.8.12 specifier: ^0.8.12
version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3))
@@ -1427,9 +1427,9 @@ packages:
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@pomade/core@0.2.3': '@pomade/core@0.2.2':
resolution: {integrity: sha512-36+abWfMH1Mif9FjBO7xICCkGZE4IqQpy7Csxlauyt0bhYQ9GsB07LqK5Qm3GgEHNwF9rFXdSZawkM+E6BeIfg==} resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
version: 0.2.3 version: 0.2.2
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
peerDependencies: peerDependencies:
'@frostr/bifrost': ^1.0.7 '@frostr/bifrost': ^1.0.7
@@ -6610,7 +6610,7 @@ snapshots:
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@pomade/core@0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))': '@pomade/core@0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@frostr/bifrost': 1.0.7(typescript@5.9.3) '@frostr/bifrost': 1.0.7(typescript@5.9.3)
'@noble/hashes': 2.0.1 '@noble/hashes': 2.0.1
@@ -7276,9 +7276,9 @@ snapshots:
optionalDependencies: optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6 '@vite-pwa/assets-generator': 0.2.6
'@welshman/app@0.8.12(2f5bd20a84c8c39e26176b5a5db083ae)': '@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
dependencies: dependencies:
'@pomade/core': 0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3)) '@pomade/core': 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8) '@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
'@welshman/lib': 0.8.12 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
+1 -1
View File
@@ -425,7 +425,7 @@ body.keyboard-open .hide-on-keyboard {
/* chat view */ /* chat view */
.chat__compose { .chat__compose {
@apply z-compose relative mb-14 shrink-0 md:mb-0; @apply relative z-compose mb-14 shrink-0 md:mb-0;
} }
.chat__compose .chat__compose-inner { .chat__compose .chat__compose-inner {
@@ -7,12 +7,13 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
} }
const {url, h}: Props = $props() const {url, h, shareToChat = false}: Props = $props()
</script> </script>
<CalendarEventForm {url} {h}> <CalendarEventForm {url} {h} {shareToChat}>
{#snippet header()} {#snippet header()}
<ModalHeader> <ModalHeader>
<ModalTitle>Create an Event</ModalTitle> <ModalTitle>Create an Event</ModalTitle>
+46 -21
View File
@@ -3,7 +3,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib" import {randomId, HOUR} from "@welshman/lib"
import {makeEvent, EVENT_TIME} from "@welshman/util" import {makeEvent, EVENT_TIME} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl" import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@@ -22,7 +22,7 @@
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
type Values = { type Values = {
d: string d: string
@@ -36,11 +36,12 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
header: Snippet header: Snippet
initialValues?: Values initialValues?: Values
} }
let {url, h, header, initialValues}: Props = $props() let {url, h, shareToChat = false, header, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`)
@@ -57,7 +58,7 @@
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run()) const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const submit = async () => { const submit = async () => {
if ($uploading) return if ($uploading || loading) return
if (!title) { if (!title) {
return pushToast({ return pushToast({
@@ -92,22 +93,42 @@
...ed.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
] ]
if (await shouldProtect) { loading = true
tags.push(PROTECTED)
try {
const protect = await shouldProtect
if (protect) {
tags.push(PROTECTED)
}
if (h) {
tags.push(["h", h])
}
const event = makeEvent(EVENT_TIME, {content, tags})
const calendarThunk = publishThunk({event, relays: [url]})
const error = await waitForThunkError(calendarThunk)
if (error) {
return pushToast({theme: "error", message: error})
}
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: calendarThunk.event, protect})
}
pushToast({message: "Your event has been saved!"})
} finally {
loading = false
} }
if (h) {
tags.push(["h", h])
}
const event = makeEvent(EVENT_TIME, {content, tags})
pushToast({message: "Your event has been saved!"})
publishThunk({event, relays: [url]})
draftKey.clear()
history.back()
} }
let loading = $state(false)
const d = $state(initialValues?.d ?? randomId()) const d = $state(initialValues?.d ?? randomId())
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let location = $state(initialValues?.location ?? "") let location = $state(initialValues?.location ?? "")
@@ -158,7 +179,11 @@
<div class="input-editor grow overflow-hidden"> <div class="input-editor grow overflow-hidden">
<EditorContent {editor} /> <EditorContent {editor} />
</div> </div>
<Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}> <Button
data-tip="Add an image"
class="center btn tooltip"
onclick={selectFiles}
disabled={loading}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
@@ -197,12 +222,12 @@
</Field> </Field>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={$uploading}> <Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
<Spinner loading={$uploading}>Save Event</Spinner> <Spinner loading={$uploading || loading}>Save Event</Spinner>
</Button> </Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+3 -2
View File
@@ -7,12 +7,13 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
} }
const {url, h}: Props = $props() const {url, h, shareToChat = false}: Props = $props()
</script> </script>
<ClassifiedForm {url} {h}> <ClassifiedForm {url} {h} {shareToChat}>
{#snippet header()} {#snippet header()}
<ModalHeader> <ModalHeader>
<ModalTitle>Create a Classified Listing</ModalTitle> <ModalTitle>Create a Classified Listing</ModalTitle>
+18 -5
View File
@@ -2,7 +2,7 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib" import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util" import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -21,7 +21,7 @@
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70, uploadFile} from "@app/core/commands" import {canEnforceNip70, publishRoomQuote, uploadFile} from "@app/core/commands"
type Values = { type Values = {
d: string d: string
@@ -37,11 +37,12 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
header: Snippet header: Snippet
initialValues?: Values initialValues?: Values
} }
let {url, h, header, initialValues}: Props = $props() let {url, h, shareToChat = false, header, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`)
@@ -87,7 +88,9 @@
tags.push(["t", topic]) tags.push(["t", topic])
} }
if (await shouldProtect) { const protect = await shouldProtect
if (protect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
@@ -114,13 +117,23 @@
} }
} }
publishThunk({ const classifiedThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}), event: makeEvent(CLASSIFIED, {content, tags}),
}) })
const error = await waitForThunkError(classifiedThunk)
if (error) {
return pushToast({theme: "error", message: error})
}
draftKey.clear() draftKey.clear()
history.back() history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: classifiedThunk.event, protect})
}
} finally { } finally {
loading = false loading = false
} }
+5 -5
View File
@@ -22,15 +22,15 @@
const {url, h, onClick}: Props = $props() const {url, h, onClick}: Props = $props()
const createGoal = () => pushModal(GoalCreate, {url, h}) const createGoal = () => pushModal(GoalCreate, {url, h, shareToChat: true})
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h}) const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h, shareToChat: true})
const createThread = () => pushModal(ThreadCreate, {url, h}) const createThread = () => pushModal(ThreadCreate, {url, h, shareToChat: true})
const createClassified = () => pushModal(ClassifiedCreate, {url, h}) const createClassified = () => pushModal(ClassifiedCreate, {url, h, shareToChat: true})
const createPoll = () => pushModal(PollCreate, {url, h}) const createPoll = () => pushModal(PollCreate, {url, h, shareToChat: true})
let ul: Element let ul: Element
+46 -21
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, ZAP_GOAL} from "@welshman/util" import {makeEvent, ZAP_GOAL} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
@@ -10,6 +10,7 @@
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
@@ -21,7 +22,7 @@
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
type Values = { type Values = {
title: string title: string
@@ -33,9 +34,10 @@
url: string url: string
h?: string h?: string
initialValues?: Values initialValues?: Values
shareToChat?: boolean
} }
let {url, h, initialValues}: Props = $props() let {url, h, initialValues, shareToChat = false}: Props = $props()
const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`)
@@ -52,7 +54,7 @@
const selectFiles = () => editor.then(ed => ed.commands.selectFiles()) const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
const submit = async () => { const submit = async () => {
if ($uploading) return if ($uploading || loading) return
if (!title) { if (!title) {
return pushToast({ return pushToast({
@@ -78,23 +80,43 @@
["relays", url], ["relays", url],
] ]
if (await shouldProtect) { loading = true
tags.push(PROTECTED)
try {
const protect = await shouldProtect
if (protect) {
tags.push(PROTECTED)
}
if (h) {
tags.push(["h", h])
}
const goalThunk = publishThunk({
relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}),
})
const error = await waitForThunkError(goalThunk)
if (error) {
return pushToast({theme: "error", message: error})
}
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: goalThunk.event, protect})
}
} finally {
loading = false
} }
if (h) {
tags.push(["h", h])
}
publishThunk({
relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}),
})
draftKey.clear()
history.back()
} }
let loading = $state(false)
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let amount = $state(initialValues?.amount ?? 1000) let amount = $state(initialValues?.amount ?? 1000)
let content = $state(initialValues?.content ?? "") let content = $state(initialValues?.content ?? "")
@@ -154,7 +176,8 @@
<Button <Button
data-tip="Add an image" data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2" class="tooltip tooltip-left absolute bottom-1 right-2"
onclick={selectFiles}> onclick={selectFiles}
disabled={loading}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
@@ -188,10 +211,12 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary">Create Goal</Button> <Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
<Spinner {loading}>Create Goal</Spinner>
</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+41 -15
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib" import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent} from "@welshman/util" import {makeEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Poll} from "nostr-tools/kinds" import {Poll} from "nostr-tools/kinds"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -13,6 +13,7 @@
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte" import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
@@ -21,7 +22,7 @@
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import type {PollType} from "@app/util/polls" import type {PollType} from "@app/util/polls"
@@ -40,9 +41,10 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
} }
const {url, h}: Props = $props() const {url, h, shareToChat = false}: Props = $props()
const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`)
const initialValues = draftKey.get() const initialValues = draftKey.get()
@@ -102,6 +104,8 @@
} }
const submit = async () => { const submit = async () => {
if (loading) return
if (!title.trim()) { if (!title.trim()) {
return pushToast({theme: "error", message: "Please provide a title for your poll."}) return pushToast({theme: "error", message: "Please provide a title for your poll."})
} }
@@ -130,19 +134,39 @@
tags.push(["h", h]) tags.push(["h", h])
} }
if (await shouldProtect) { loading = true
tags.push(PROTECTED)
try {
const protect = await shouldProtect
if (protect) {
tags.push(PROTECTED)
}
const pollThunk = publishThunk({
relays: [url],
event: makeEvent(Poll, {content: title.trim(), tags}),
})
const error = await waitForThunkError(pollThunk)
if (error) {
return pushToast({theme: "error", message: error})
}
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: pollThunk.event, protect})
}
} finally {
loading = false
} }
publishThunk({
relays: [url],
event: makeEvent(Poll, {content: title.trim(), tags}),
})
draftKey.clear()
history.back()
} }
let loading = $state(false)
let draggedOptionId = $state<string | undefined>() let draggedOptionId = $state<string | undefined>()
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let pollType = $state<PollType>(initialValues?.pollType ?? "singlechoice") let pollType = $state<PollType>(initialValues?.pollType ?? "singlechoice")
@@ -246,10 +270,12 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary">Create Poll</Button> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Create Poll</Spinner>
</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+3 -3
View File
@@ -5,11 +5,11 @@
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {deriveEventsDesc, deriveEventsById} from "@welshman/store" import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT} from "@welshman/util" import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
import {repository, loadRelayList} from "@welshman/app" import {repository, loadRelayList} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state" import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/core/state"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -36,7 +36,7 @@
load({ load({
filters: [ filters: [
{authors: [pubkey], kinds: [ROOMS]}, {authors: [pubkey], kinds: [ROOMS]},
{authors: [pubkey], limit: 1, kinds: [NOTE, COMMENT, ...MESSAGE_KINDS]}, {authors: [pubkey], limit: 1, kinds: [NOTE, COMMENT, MESSAGE]},
], ],
relays: Router.get().FromPubkeys([pubkey]).getUrls(), relays: Router.get().FromPubkeys([pubkey]).getUrls(),
}) })
+2 -8
View File
@@ -8,22 +8,16 @@
import {getRoomItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
const props: ComponentProps<typeof NoteContent> = $props() const props: ComponentProps<typeof NoteContent> = $props()
const MESSAGE_MIN_LENGTH = 5000
const MESSAGE_MAX_LENGTH = 5500
const path = getRoomItemPath(props.url!, props.event) const path = getRoomItemPath(props.url!, props.event)
const minLength =
props.minLength ?? (props.event.kind === MESSAGE ? MESSAGE_MIN_LENGTH : undefined)
const maxLength =
props.maxLength ?? (props.event.kind === MESSAGE ? MESSAGE_MAX_LENGTH : undefined)
</script> </script>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}> <div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
{#if path && !isMobile} {#if path && !isMobile}
<Link href={path}> <Link href={path}>
<NoteContent {...props} {minLength} {maxLength} /> <NoteContent {...props} />
</Link> </Link>
{:else} {:else}
<NoteContent {...props} {minLength} {maxLength} /> <NoteContent {...props} />
{/if} {/if}
</div> </div>
+46 -21
View File
@@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, THREAD} from "@welshman/util" import {makeEvent, THREAD} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
@@ -19,7 +20,7 @@
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
type Values = { type Values = {
content?: string | object content?: string | object
@@ -29,9 +30,10 @@
type Props = { type Props = {
url: string url: string
h?: string h?: string
shareToChat?: boolean
} }
const {url, h}: Props = $props() const {url, h, shareToChat = false}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const initialValues = draftKey.get() const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -43,7 +45,7 @@
const selectFiles = () => editor.then(ed => ed.commands.selectFiles()) const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
const submit = async () => { const submit = async () => {
if ($uploading) return if ($uploading || loading) return
if (!title) { if (!title) {
return pushToast({ return pushToast({
@@ -64,23 +66,43 @@
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]] const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
if (await shouldProtect) { loading = true
tags.push(PROTECTED)
try {
const protect = await shouldProtect
if (protect) {
tags.push(PROTECTED)
}
if (h) {
tags.push(["h", h])
}
const threadThunk = publishThunk({
relays: [url],
event: makeEvent(THREAD, {content, tags}),
})
const error = await waitForThunkError(threadThunk)
if (error) {
return pushToast({theme: "error", message: error})
}
draftKey.clear()
history.back()
if (shareToChat) {
publishRoomQuote({url, h, parent: threadThunk.event, protect})
}
} finally {
loading = false
} }
if (h) {
tags.push(["h", h])
}
publishThunk({
relays: [url],
event: makeEvent(THREAD, {content, tags}),
})
draftKey.clear()
history.back()
} }
let loading = $state(false)
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let content = $state(initialValues?.content ?? "") let content = $state(initialValues?.content ?? "")
@@ -138,7 +160,8 @@
<Button <Button
data-tip="Add an image" data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2" class="tooltip tooltip-left absolute bottom-1 right-2"
onclick={selectFiles}> onclick={selectFiles}
disabled={loading}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
@@ -148,10 +171,12 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary">Create Thread</Button> <Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
<Spinner {loading}>Create Thread</Spinner>
</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+29
View File
@@ -22,6 +22,7 @@ import {PollResponse} from "nostr-tools/kinds"
import { import {
DELETE, DELETE,
REPORT, REPORT,
MESSAGE,
PROFILE, PROFILE,
MESSAGING_RELAYS, MESSAGING_RELAYS,
RELAYS, RELAYS,
@@ -122,6 +123,34 @@ export const prependParent = (
return {content, tags} return {content, tags}
} }
export const publishRoomQuote = ({
url,
h,
parent,
protect,
delay,
}: {
url: string
h?: string
parent: TrustedEvent
protect: boolean
delay?: number
}) => {
const tags: string[][] = []
if (h) {
tags.push(["h", h])
}
if (protect) {
tags.push(PROTECTED)
}
const event = makeEvent(MESSAGE, prependParent(parent, {content: "", tags}, url))
return publishThunk({relays: [url], event, delay})
}
// Synchronization // Synchronization
export const broadcastUserData = async (relays: string[]) => { export const broadcastUserData = async (relays: string[]) => {
+65 -102
View File
@@ -1,6 +1,5 @@
import {writable} from "svelte/store" import {get, writable} from "svelte/store"
import { import {
batch,
call, call,
uniq, uniq,
int, int,
@@ -26,8 +25,7 @@ import {
sortEventsDesc, sortEventsDesc,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, Filter, List} from "@welshman/util" import type {TrustedEvent, Filter, List} from "@welshman/util"
import {load, request, mergeRepositoryUpdates} from "@welshman/net" import {load, request} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {repository, loadRelay, tracker} from "@welshman/app" import {repository, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
@@ -58,75 +56,57 @@ export const makeFeed = ({
let backwardWindow = [at - interval, at] let backwardWindow = [at - interval, at]
let forwardWindow = [at, at + interval] let forwardWindow = [at, at + interval]
const insertIntoBuffer = (event: TrustedEvent) => { const insertEvent = (event: TrustedEvent) => {
for (let i = 0; i < buffer.length; i++) { let handled = false
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
return
}
}
buffer.push(event)
}
// Batch-insert events into the visible store with a single update if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
const insertEvents = (newEvents: TrustedEvent[]) => { const $events = get(events)
const visible: TrustedEvent[] = []
for (const event of newEvents) { for (let i = 0; i < $events.length; i++) {
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) { if ($events[i].created_at > event.created_at) {
visible.push(event) events.set(insertAt(i, event, $events))
} else { handled = true
insertIntoBuffer(event) break
}
}
if (visible.length > 0) {
events.update($events => {
for (const event of visible) {
let inserted = false
for (let i = 0; i < $events.length; i++) {
if ($events[i].created_at > event.created_at) {
$events = insertAt(i, event, $events)
inserted = true
break
}
}
if (!inserted) {
$events = [...$events, event]
}
} }
return $events }
})
if (!handled) {
events.set([...$events, event])
}
} else {
for (let i = 0; i < buffer.length; i++) {
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
handled = true
break
}
}
if (!handled) {
buffer.push(event)
}
} }
} }
const unsubscribers = [ const unsubscribers = [
on( on(repository, "update", ({added, removed}) => {
repository, if (removed.size > 0) {
"update", buffer = buffer.filter(e => !removed.has(e.id))
batch(150, (updates: RepositoryUpdate[]) => { events.update($events => $events.filter(e => !removed.has(e.id)))
const {added, removed} = mergeRepositoryUpdates(updates) }
if (removed.size > 0) { for (const event of added) {
buffer = buffer.filter(e => !removed.has(e.id)) if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
events.update($events => $events.filter(e => !removed.has(e.id))) insertEvent(event)
} }
}
const matching = added.filter( }),
event => matchFilters(filters, event) && tracker.getRelays(event.id).has(url),
)
if (matching.length > 0) {
insertEvents(matching)
}
}),
),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvents([event]) insertEvent(event)
} }
} }
}), }),
@@ -157,7 +137,9 @@ export const makeFeed = ({
backwardWindow = [since - interval, since] backwardWindow = [since - interval, since]
insertEvents(buffer.splice(0, 30)) for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
if (until > now() - int(2, YEAR)) { if (until > now() - int(2, YEAR)) {
loadTimeframe(since, until) loadTimeframe(since, until)
@@ -178,7 +160,9 @@ export const makeFeed = ({
forwardWindow = [until, until + interval] forwardWindow = [until, until + interval]
insertEvents(buffer.splice(0, 30)) for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
if (until < now()) { if (until < now()) {
loadTimeframe(since, until) loadTimeframe(since, until)
@@ -224,61 +208,40 @@ export const makeCalendarFeed = ({
const events = writable(sortBy(getStart, getEventsForUrl(url, filters))) const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
// Batch-insert calendar events into the store with a single update const insertEvent = (event: TrustedEvent) => {
const insertEvents = (newEvents: TrustedEvent[]) => { const start = getStart(event)
const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e))) const address = getAddress(event)
if (valid.length === 0) return
if (isNaN(start) || isNaN(getEnd(event))) return
events.update($events => { events.update($events => {
for (const event of valid) { for (let i = 0; i < $events.length; i++) {
const start = getStart(event) if ($events[i].id === event.id) return $events
const address = getAddress(event) if (getStart($events[i]) > start) return insertAt(i, event, $events)
let handled = false
for (let i = 0; i < $events.length; i++) {
if ($events[i].id === event.id) {
handled = true
break
}
if (getStart($events[i]) > start) {
$events = insertAt(i, event, $events)
handled = true
break
}
}
if (!handled) {
$events = [...$events.filter(e => getAddress(e) !== address), event]
}
} }
return $events
return [...$events.filter(e => getAddress(e) !== address), event]
}) })
} }
const unsubscribers = [ const unsubscribers = [
on( on(repository, "update", ({added, removed}) => {
repository, if (removed.size > 0) {
"update", events.update($events => $events.filter(e => !removed.has(e.id)))
batch(150, (updates: RepositoryUpdate[]) => { }
const {added, removed} = mergeRepositoryUpdates(updates)
if (removed.size > 0) { for (const event of added) {
events.update($events => $events.filter(e => !removed.has(e.id))) if (matchFilters(filters, event)) {
insertEvent(event)
} }
}
const matching = added.filter(event => matchFilters(filters, event)) }),
if (matching.length > 0) {
insertEvents(matching)
}
}),
),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvents([event]) insertEvent(event)
} }
} }
}), }),
+22 -33
View File
@@ -329,8 +329,6 @@ if (ENABLE_ZAPS) {
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, Poll] export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, Poll]
export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE] export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
// Settings // Settings
@@ -554,7 +552,7 @@ export const chatsById = call(() => {
setTimeout(() => { setTimeout(() => {
addEvents(added) addEvents(added)
removeEvents(removed) removeEvents(removed)
}, 200) }, 50)
}), }),
] ]
@@ -568,7 +566,7 @@ export const deriveChat = call(() => {
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys)) return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
}) })
export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => { export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
return createSearch( return createSearch(
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())), sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
{ {
@@ -607,7 +605,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
}) })
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => { export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
const result = new Map<string, Room[]>() const metaByIdByUrl = new Map<string, Map<string, Room>>()
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) { for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values()) const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
@@ -619,8 +617,6 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
} }
} }
const metaById = new Map<string, Room>()
for (const event of metaEvents) { for (const event of metaEvents) {
const meta = tryCatch(() => readRoomMeta(event)) const meta = tryCatch(() => readRoomMeta(event))
@@ -628,14 +624,22 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
continue continue
} }
let metaById = metaByIdByUrl.get(url)
if (!metaById) {
metaById = new Map()
metaByIdByUrl.set(url, metaById)
}
const id = makeRoomId(url, meta.h) const id = makeRoomId(url, meta.h)
metaById.set(id, {...meta, url, id}) metaById.set(id, {...meta, url, id})
} }
}
if (metaById.size > 0) { const result = new Map<string, Room[]>()
result.set(url, Array.from(metaById.values()))
} for (const [url, metaById] of metaByIdByUrl.entries()) {
result.set(url, Array.from(metaById.values()))
} }
return result return result
@@ -943,33 +947,18 @@ export const deriveSpaceActionItems = (url: string) =>
for (const [h, roomEvents] of groupBy(getRoomId, $events)) { for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
if (!h) continue if (!h) continue
const roomJoins: TrustedEvent[] = [] const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
const roomLeaves: TrustedEvent[] = [] const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
const roomMembershipEvents: TrustedEvent[] = [] const roomMembershipEvents = roomEvents.filter(event =>
[ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER].includes(event.kind),
for (const event of roomEvents) { )
switch (event.kind) {
case ROOM_JOIN:
roomJoins.push(event)
break
case ROOM_LEAVE:
roomLeaves.push(event)
break
case ROOM_MEMBERS:
case ROOM_ADD_MEMBER:
case ROOM_REMOVE_MEMBER:
roomMembershipEvents.push(event)
break
}
}
const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents)) const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents))
pendingJoins.push( pendingJoins.push(
...removeUndefined( ...removeUndefined(
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events => Array.from(groupBy(e => e.pubkey, roomJoins).values())
first(sortEventsDesc(events)), .map(sortEventsDesc)
), .map(first),
).filter(({pubkey, created_at}) => { ).filter(({pubkey, created_at}) => {
if (roomMembers.has(pubkey)) return false if (roomMembers.has(pubkey)) return false
if ( if (
+3 -3
View File
@@ -19,6 +19,7 @@ import {
RELAY_MEMBERS, RELAY_MEMBERS,
RELAY_ADD_MEMBER, RELAY_ADD_MEMBER,
RELAY_REMOVE_MEMBER, RELAY_REMOVE_MEMBER,
MESSAGE,
isSignedEvent, isSignedEvent,
unionFilters, unionFilters,
getTagValue, getTagValue,
@@ -43,7 +44,6 @@ import {
} from "@welshman/app" } from "@welshman/app"
import { import {
REACTION_KINDS, REACTION_KINDS,
MESSAGE_KINDS,
CONTENT_KINDS, CONTENT_KINDS,
INDEXER_RELAYS, INDEXER_RELAYS,
loadSettings, loadSettings,
@@ -281,7 +281,7 @@ const syncSpace = (url: string, rooms: string[]) => {
signal: controller.signal, signal: controller.signal,
filters: [ filters: [
{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]}, {kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
{kinds: MESSAGE_KINDS, since, "#h": [room]}, {kinds: [MESSAGE], since, "#h": [room]},
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}), makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
{ {
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
@@ -305,7 +305,7 @@ const syncSpace = (url: string, rooms: string[]) => {
url, url,
signal: controller.signal, signal: controller.signal,
filters: [ filters: [
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...MESSAGE_KINDS]}, {kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, MESSAGE]},
makeCommentFilter(CONTENT_KINDS, {since}), makeCommentFilter(CONTENT_KINDS, {since}),
{kinds: [PollResponse], since}, {kinds: [PollResponse], since},
], ],
+2 -3
View File
@@ -5,10 +5,9 @@ import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib" import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEventsByIdByUrl} from "@welshman/store" import {deriveEventsByIdByUrl} from "@welshman/store"
import {sortEventsDesc, getTagValue} from "@welshman/util" import {sortEventsDesc, getTagValue, MESSAGE} from "@welshman/util"
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes" import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
import { import {
MESSAGE_KINDS,
notificationSettings, notificationSettings,
chatsById, chatsById,
userGroupList, userGroupList,
@@ -85,7 +84,7 @@ export const allNotifications = derived(
deriveEventsByIdByUrl({ deriveEventsByIdByUrl({
tracker, tracker,
repository, repository,
filters: [{kinds: MESSAGE_KINDS}, makeCommentFilter(MESSAGE_KINDS)], filters: [{kinds: [MESSAGE]}, makeCommentFilter([MESSAGE])],
}), }),
], ],
identity, identity,
+3 -4
View File
@@ -17,13 +17,12 @@ import {
getRelaysFromList, getRelaysFromList,
getTagValue, getTagValue,
matchFilters, matchFilters,
MESSAGE,
type Filter, type Filter,
type TrustedEvent, type TrustedEvent,
} from "@welshman/util" } from "@welshman/util"
import { import {
DM_KINDS, DM_KINDS,
CONTENT_KINDS,
MESSAGE_KINDS,
notificationSettings, notificationSettings,
pushState, pushState,
shouldNotify, shouldNotify,
@@ -45,7 +44,7 @@ export type PushPermissionResult = {
} }
export const onNotification = call(() => { export const onNotification = call(() => {
const allFilters = [{kinds: [...MESSAGE_KINDS, ...DM_KINDS]}, makeCommentFilter(MESSAGE_KINDS)] const allFilters = [{kinds: [MESSAGE, ...DM_KINDS]}, makeCommentFilter([MESSAGE])]
const filters = allFilters.map(assoc("since", now())) const filters = allFilters.map(assoc("since", now()))
const subscribers: Subscriber<TrustedEvent>[] = [] const subscribers: Subscriber<TrustedEvent>[] = []
@@ -158,7 +157,7 @@ export const syncRelaySubscriptions = (
userSettingsValues, userSettingsValues,
]).subscribe( ]).subscribe(
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => { throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)] const baseFilters = [{kinds: [MESSAGE]}, makeCommentFilter([MESSAGE])]
for (const url of $userSpaceUrls) { for (const url of $userSpaceUrls) {
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {} const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
+4 -17
View File
@@ -48,18 +48,6 @@ import {
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {db} from "@app/core/storage" import {db} from "@app/core/storage"
// Shared interval for all non-critical store flushes, so they batch on the same cadence
const FLUSH_INTERVAL = 3000
// Wraps a write callback to run during idle time (non-critical persistence)
const idleWrite = <T>(f: (xs: T[]) => void): ((xs: T[]) => void) => {
if (typeof requestIdleCallback !== "undefined") {
return (xs: T[]) => requestIdleCallback(() => f(xs))
}
return f
}
const kinds = { const kinds = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS], meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
@@ -211,15 +199,14 @@ const loadCriticalRelays = async () => {
relaysByUrl.set(indexBy(r => r.url, await table.getAll())) relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
} }
const syncRelays = () => const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
onRelay(batch(FLUSH_INTERVAL, idleWrite(db.table<RelayProfile>("relays").bulkPut)))
const initRelayStats = async () => { const initRelayStats = async () => {
const table = db.table<RelayStats>("relayStats") const table = db.table<RelayStats>("relayStats")
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll())) relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelayStats(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onRelayStats(batch(1000, table.bulkPut))
} }
const initHandles = async () => { const initHandles = async () => {
@@ -227,7 +214,7 @@ const initHandles = async () => {
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll())) handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onHandle(batch(1000, table.bulkPut))
} }
const initZappers = async () => { const initZappers = async () => {
@@ -235,7 +222,7 @@ const initZappers = async () => {
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll())) zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onZapper(batch(3000, table.bulkPut))
} }
const initPlaintext = async () => { const initPlaintext = async () => {
-56
View File
@@ -1,56 +0,0 @@
<script lang="ts">
import type {Snippet} from "svelte"
let {
children,
root,
initiallyVisible = false,
estimatedHeight = 48,
}: {
children: Snippet
root?: HTMLElement
initiallyVisible?: boolean
estimatedHeight?: number
} = $props()
let visible = $state(initiallyVisible)
let height = $state(estimatedHeight)
let el: HTMLElement | undefined = $state()
let hasMeasured = false
$effect(() => {
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
visible = true
} else {
// Measure actual height before hiding content
if (el) {
const h = el.offsetHeight
if (h > 0) {
height = h
hasMeasured = true
}
}
if (hasMeasured) {
visible = false
}
}
},
{root: root || null, rootMargin: "1000px 0px"},
)
observer.observe(el)
return () => observer.disconnect()
})
</script>
<div bind:this={el}>
{#if visible}
{@render children()}
{:else}
<div style:height="{height}px"></div>
{/if}
</div>
+3 -24
View File
@@ -30,7 +30,6 @@
import SpaceSearch from "@app/components/SpaceSearch.svelte" import SpaceSearch from "@app/components/SpaceSearch.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import VirtualItem from "@lib/components/VirtualItem.svelte"
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte" import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands" import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
import { import {
@@ -38,7 +37,6 @@
deriveRoom, deriveRoom,
deriveUserRoomMembershipStatus, deriveUserRoomMembershipStatus,
getRoomType, getRoomType,
MESSAGE_KINDS,
MembershipStatus, MembershipStatus,
PROTECTED, PROTECTED,
RoomType, RoomType,
@@ -106,7 +104,6 @@
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const membershipStatus = deriveUserRoomMembershipStatus(url, h) const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const at = $derived(parseInt($page.url.searchParams.get("at")!)) const at = $derived(parseInt($page.url.searchParams.get("at")!))
const shouldVirtualize = $derived(isNaN(at))
const showRoomDetail = () => pushModal(RoomDetail, {url, h}) const showRoomDetail = () => pushModal(RoomDetail, {url, h})
@@ -362,7 +359,7 @@
url, url,
at: at || now(), at: at || now(),
element: element!, element: element!,
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER], "#h": [h]}], filters: [{kinds: [MESSAGE, ROOM_ADD_MEMBER], "#h": [h]}],
onBackwardExhausted: () => { onBackwardExhausted: () => {
loadingBackward = false loadingBackward = false
}, },
@@ -470,7 +467,7 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner> <Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p> </p>
{/if} {/if}
{#each elements as { type, id, value, showPubkey, addSpaceBelow }, i (id)} {#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#if type === "new-messages"} {#if type === "new-messages"}
<div <div
{id} {id}
@@ -482,26 +479,8 @@
</div> </div>
{:else if type === "date"} {:else if type === "date"}
<Divider>{value}</Divider> <Divider>{value}</Divider>
{:else if shouldVirtualize}
<VirtualItem root={element} initiallyVisible={i < 25}>
{@const event = value as TrustedEvent}
{#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
<div class="cv">
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
{addSpaceBelow}
canEdit={canEditEvent}
onEdit={onEditEvent} />
</div>
{/if}
</VirtualItem>
{:else} {:else}
{@const event = value as TrustedEvent} {@const event = $state.snapshot(value as TrustedEvent)}
{#if event.kind === ROOM_ADD_MEMBER} {#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} /> <RoomItemAddMember {url} {event} />
{:else} {:else}
+4 -24
View File
@@ -21,12 +21,11 @@
import SpaceSearch from "@app/components/SpaceSearch.svelte" import SpaceSearch from "@app/components/SpaceSearch.svelte"
import RoomItem from "@app/components/RoomItem.svelte" import RoomItem from "@app/components/RoomItem.svelte"
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import VirtualItem from "@lib/components/VirtualItem.svelte"
import RoomCompose from "@app/components/RoomCompose.svelte" import RoomCompose from "@app/components/RoomCompose.svelte"
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte" import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
import RoomComposeParent from "@app/components/RoomComposeParent.svelte" import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
import {userSettingsValues, decodeRelay, PROTECTED, MESSAGE_KINDS} from "@app/core/state" import {userSettingsValues, decodeRelay, PROTECTED} from "@app/core/state"
import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands" import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands"
import {checked} from "@app/util/notifications" import {checked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -38,7 +37,6 @@
const url = decodeRelay($page.params.relay!) const url = decodeRelay($page.params.relay!)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const at = $derived(parseInt($page.url.searchParams.get("at")!)) const at = $derived(parseInt($page.url.searchParams.get("at")!))
const shouldVirtualize = $derived(isNaN(at))
const replyTo = (event: TrustedEvent) => { const replyTo = (event: TrustedEvent) => {
parent = event parent = event
@@ -254,7 +252,7 @@
url, url,
at: at || now(), at: at || now(),
element: element!, element: element!,
filters: [{kinds: [...MESSAGE_KINDS, RELAY_ADD_MEMBER]}], filters: [{kinds: [MESSAGE, RELAY_ADD_MEMBER]}],
onBackwardExhausted: () => { onBackwardExhausted: () => {
loadingBackward = false loadingBackward = false
}, },
@@ -307,7 +305,7 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner> <Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p> </p>
{/if} {/if}
{#each elements as { type, id, value, showPubkey, addSpaceBelow }, i (id)} {#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#if type === "new-messages"} {#if type === "new-messages"}
<div <div
{id} {id}
@@ -319,26 +317,8 @@
</div> </div>
{:else if type === "date"} {:else if type === "date"}
<Divider>{value}</Divider> <Divider>{value}</Divider>
{:else if shouldVirtualize}
<VirtualItem root={element} initiallyVisible={i < 25}>
{@const event = value as TrustedEvent}
{#if event.kind === RELAY_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
<div>
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
canEdit={canEditEvent}
onEdit={onEditEvent}
{addSpaceBelow} />
</div>
{/if}
</VirtualItem>
{:else} {:else}
{@const event = value as TrustedEvent} {@const event = $state.snapshot(value as TrustedEvent)}
{#if event.kind === RELAY_ADD_MEMBER} {#if event.kind === RELAY_ADD_MEMBER}
<RoomItemAddMember {url} {event} /> <RoomItemAddMember {url} {event} />
{:else} {:else}