Compare commits

..

13 Commits

Author SHA1 Message Date
Jon Staab 290585e974 perf: task-fix-list-virtualization changes 2026-04-10 11:36:35 -07:00
Jon Staab f1f2083c88 Remove unnecessary snapshots, format 2026-04-10 11:09:26 -07:00
Jon Staab f42889c3c2 Improve performance #182:
increase profile timer and chat search throttle delays
reduce GC pressure in derived stores
use requestIdleCallback for non-critical storage writes
batch repository update processing in feeds
2026-04-10 10:39:38 -07:00
Jon Staab a75e1f96eb Add .claude to gitignore 2026-04-10 10:14:01 -07:00
priyanshu_bharti 85c5293082 Raise message size limit in chat (#186)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-10 16:37:23 +00:00
Jon Staab 37efa6a62c Bump pomade 2026-04-10 09:24:22 -07:00
userAdityaa 1d5f91fb6c fix: realtime updates for room members and admins (#178)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-09 21:05:30 +00:00
userAdityaa ef18655776 make close button / backdrop work on direct invite link page (#177)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-09 20:03:08 +00:00
sakshamjain b786e858d9 correct inverted arrow icon in advanced section toggle (#180)
Co-authored-by: Saksham Jain <reach2saksham2004@gmail.com>
Co-committed-by: Saksham Jain <reach2saksham2004@gmail.com>
2026-04-09 19:57:15 +00:00
mplorentz f4ebc4e99e Video in calls (#135)
#135

This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing.

When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough.

I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc.

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: #135
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
Jon Staab 65ca8a7fd8 Remove follow graph building 2026-04-08 09:46:56 -07:00
nayan9617 7f1e98dcb2 Fix fallback pull race after abort (#167)
Co-authored-by: nayan9617 <nayanp4925@gmail.com>
Co-committed-by: nayan9617 <nayanp4925@gmail.com>
2026-04-08 16:43:04 +00:00
priyanshu_bharti 4c19ee823b 73-video-thumbnails (#142)
This PR implements video thumbnails for `.mov`, `.webm`, and `.mp4` files in the `ContentLinkBlock` component.

Changes:
- Added the `poster` attribute to the `<video>` tag.
- Set the poster source to `{url}#t=1` to capture a clear preview frame at the 1-second mark.
- Verified locally that thumbnails are now correctly displayed instead of a black/empty box.

Closes #73

Reviewed-on: #142
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-08 16:07:11 +00:00
19 changed files with 3108 additions and 249 deletions
+1
View File
@@ -19,5 +19,6 @@ VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=
VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
+2
View File
@@ -28,6 +28,7 @@ node_modules/
.pnpm-store/
build/
.svelte-kit/
.next/
# Rust/Tauri
*target/
@@ -69,6 +70,7 @@ GoogleService-Info.plist
.roo
.idea/
.vscode/
.claude/
# OS generated
.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/sdk": "^5.1.2",
"@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.2",
"@pomade/core": "^0.2.3",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2",
@@ -101,9 +101,9 @@
"onlyBuiltDependencies": [
"sharp",
"nostr-signer-capacitor-plugin"
],
"overrides": {
"sharp": "0.35.0-rc.0"
}
]
},
"overrides": {
"sharp": "0.35.0-rc.0"
}
}
+9 -9
View File
@@ -60,8 +60,8 @@ importers:
specifier: ^1.9.7
version: 1.9.7
'@pomade/core':
specifier: ^0.2.2
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))
specifier: ^0.2.3
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))
'@poppanator/sveltekit-svg':
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))
@@ -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))
'@welshman/app':
specifier: ^0.8.12
version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
version: 0.8.12(2f5bd20a84c8c39e26176b5a5db083ae)
'@welshman/content':
specifier: ^0.8.12
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':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@pomade/core@0.2.2':
resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
version: 0.2.2
'@pomade/core@0.2.3':
resolution: {integrity: sha512-36+abWfMH1Mif9FjBO7xICCkGZE4IqQpy7Csxlauyt0bhYQ9GsB07LqK5Qm3GgEHNwF9rFXdSZawkM+E6BeIfg==}
version: 0.2.3
engines: {node: '>=12.0.0'}
peerDependencies:
'@frostr/bifrost': ^1.0.7
@@ -6610,7 +6610,7 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@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))':
'@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))':
dependencies:
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
'@noble/hashes': 2.0.1
@@ -7276,9 +7276,9 @@ snapshots:
optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6
'@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
'@welshman/app@0.8.12(2f5bd20a84c8c39e26176b5a5db083ae)':
dependencies:
'@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))
'@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))
'@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
'@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)
+4 -4
View File
@@ -1,4 +1,4 @@
@import 'tailwindcss';
@import "tailwindcss";
@config "../tailwind.config.js";
@@ -141,7 +141,7 @@
}
@utility content-padding-y {
@apply pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
@apply pt-4 pb-4 sm:pt-8 sm:pb-8 md:pt-12 md:pb-12;
}
@utility content-sizing {
@@ -149,7 +149,7 @@
}
@utility content {
@apply m-auto w-full max-w-3xl px-4 sm:px-8 md:px-12 pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
@apply m-auto w-full max-w-3xl px-4 pt-4 pb-4 sm:px-8 sm:pt-8 sm:pb-8 md:px-12 md:pt-12 md:pb-12;
}
@utility heading {
@@ -425,7 +425,7 @@ body.keyboard-open .hide-on-keyboard {
/* chat view */
.chat__compose {
@apply relative z-compose mb-14 shrink-0 md:mb-0;
@apply z-compose relative mb-14 shrink-0 md:mb-0;
}
.chat__compose .chat__compose-inner {
+22 -2
View File
@@ -1,12 +1,19 @@
<script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
import {
dufflepud,
PLATFORM_URL,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES,
THUMBNAIL_URL,
} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
@@ -22,6 +29,14 @@
return [url, true]
})
const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
}
return undefined
}
const loadPreview = async () => {
const json = await postJson(dufflepud("link/preview"), {url})
@@ -42,7 +57,12 @@
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
<video
controls
src={url}
poster={getVideoPoster(url)}
preload="metadata"
class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
+1 -2
View File
@@ -62,8 +62,7 @@
{@render children?.()}
<!-- a little extra something for ios -->
<div
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
<div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
</div>
<div
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
+8 -2
View File
@@ -8,16 +8,22 @@
import {getRoomItemPath} from "@app/util/routes"
const props: ComponentProps<typeof NoteContent> = $props()
const MESSAGE_MIN_LENGTH = 5000
const MESSAGE_MAX_LENGTH = 5500
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>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
{#if path && !isMobile}
<Link href={path}>
<NoteContent {...props} />
<NoteContent {...props} {minLength} {maxLength} />
</Link>
{:else}
<NoteContent {...props} />
<NoteContent {...props} {minLength} {maxLength} />
{/if}
</div>
+102 -65
View File
@@ -1,5 +1,6 @@
import {get, writable} from "svelte/store"
import {writable} from "svelte/store"
import {
batch,
call,
uniq,
int,
@@ -25,7 +26,8 @@ import {
sortEventsDesc,
} from "@welshman/util"
import type {TrustedEvent, Filter, List} from "@welshman/util"
import {load, request} from "@welshman/net"
import {load, request, mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {repository, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
@@ -56,57 +58,75 @@ export const makeFeed = ({
let backwardWindow = [at - interval, at]
let forwardWindow = [at, at + interval]
const insertEvent = (event: TrustedEvent) => {
let handled = false
const insertIntoBuffer = (event: TrustedEvent) => {
for (let i = 0; i < buffer.length; i++) {
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
return
}
}
buffer.push(event)
}
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
const $events = get(events)
// Batch-insert events into the visible store with a single update
const insertEvents = (newEvents: TrustedEvent[]) => {
const visible: TrustedEvent[] = []
for (let i = 0; i < $events.length; i++) {
if ($events[i].created_at > event.created_at) {
events.set(insertAt(i, event, $events))
handled = true
break
for (const event of newEvents) {
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
visible.push(event)
} else {
insertIntoBuffer(event)
}
}
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]
}
}
}
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)
}
return $events
})
}
}
const unsubscribers = [
on(repository, "update", ({added, removed}) => {
if (removed.size > 0) {
buffer = buffer.filter(e => !removed.has(e.id))
events.update($events => $events.filter(e => !removed.has(e.id)))
}
on(
repository,
"update",
batch(150, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
for (const event of added) {
if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
insertEvent(event)
if (removed.size > 0) {
buffer = buffer.filter(e => !removed.has(e.id))
events.update($events => $events.filter(e => !removed.has(e.id)))
}
}
}),
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) => {
if (trackerUrl === url) {
const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) {
insertEvent(event)
insertEvents([event])
}
}
}),
@@ -137,9 +157,7 @@ export const makeFeed = ({
backwardWindow = [since - interval, since]
for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
insertEvents(buffer.splice(0, 30))
if (until > now() - int(2, YEAR)) {
loadTimeframe(since, until)
@@ -160,9 +178,7 @@ export const makeFeed = ({
forwardWindow = [until, until + interval]
for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
insertEvents(buffer.splice(0, 30))
if (until < now()) {
loadTimeframe(since, until)
@@ -208,40 +224,61 @@ export const makeCalendarFeed = ({
const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
const insertEvent = (event: TrustedEvent) => {
const start = getStart(event)
const address = getAddress(event)
if (isNaN(start) || isNaN(getEnd(event))) return
// Batch-insert calendar events into the store with a single update
const insertEvents = (newEvents: TrustedEvent[]) => {
const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)))
if (valid.length === 0) return
events.update($events => {
for (let i = 0; i < $events.length; i++) {
if ($events[i].id === event.id) return $events
if (getStart($events[i]) > start) return insertAt(i, event, $events)
}
for (const event of valid) {
const start = getStart(event)
const address = getAddress(event)
return [...$events.filter(e => getAddress(e) !== address), event]
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
})
}
const unsubscribers = [
on(repository, "update", ({added, removed}) => {
if (removed.size > 0) {
events.update($events => $events.filter(e => !removed.has(e.id)))
}
on(
repository,
"update",
batch(150, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
for (const event of added) {
if (matchFilters(filters, event)) {
insertEvent(event)
if (removed.size > 0) {
events.update($events => $events.filter(e => !removed.has(e.id)))
}
}
}),
const matching = added.filter(event => matchFilters(filters, event))
if (matching.length > 0) {
insertEvents(matching)
}
}),
),
on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) {
const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) {
insertEvent(event)
insertEvents([event])
}
}
}),
+121 -79
View File
@@ -8,7 +8,6 @@ import {
on,
gt,
max,
find,
spec,
call,
first,
@@ -210,6 +209,8 @@ export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
[
@@ -553,7 +554,7 @@ export const chatsById = call(() => {
setTimeout(() => {
addEvents(added)
removeEvents(removed)
}, 50)
}, 200)
}),
]
@@ -567,7 +568,7 @@ export const deriveChat = call(() => {
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
})
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => {
return createSearch(
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
{
@@ -606,7 +607,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
})
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
const metaByIdByUrl = new Map<string, Map<string, Room>>()
const result = new Map<string, Room[]>()
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
@@ -618,6 +619,8 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
}
}
const metaById = new Map<string, Room>()
for (const event of metaEvents) {
const meta = tryCatch(() => readRoomMeta(event))
@@ -625,22 +628,14 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
continue
}
let metaById = metaByIdByUrl.get(url)
if (!metaById) {
metaById = new Map()
metaByIdByUrl.set(url, metaById)
}
const id = makeRoomId(url, meta.h)
metaById.set(id, {...meta, url, id})
}
}
const result = new Map<string, Room[]>()
for (const [url, metaById] of metaByIdByUrl.entries()) {
result.set(url, Array.from(metaById.values()))
if (metaById.size > 0) {
result.set(url, Array.from(metaById.values()))
}
}
return result
@@ -813,36 +808,78 @@ export const deriveOtherRooms = (url: string) =>
// Space/room memberships
const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
const members = new Set<string>()
for (const event of sortEventsAsc(events)) {
if (event.kind === RELAY_MEMBERS) {
members.clear()
for (const pubkey of uniq(getTagValues("member", event.tags))) {
members.add(pubkey)
}
continue
}
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === RELAY_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
if (event.kind === RELAY_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
return Array.from(members)
}
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
const members = new Set<string>()
for (const event of sortEventsAsc(events)) {
if (event.kind === ROOM_MEMBERS && getTagValue("d", event.tags) === h) {
members.clear()
for (const pubkey of uniq(getPubkeyTagValues(event.tags))) {
members.add(pubkey)
}
continue
}
if (getTagValue("h", event.tags) !== h) {
continue
}
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === ROOM_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
if (event.kind === ROOM_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
return Array.from(members)
}
export const deriveSpaceMembers = (url: string) =>
derived(
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
$events => {
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
if (membersEvent) {
return uniq(getTagValues("member", membersEvent.tags))
}
const members = new Set<string>()
for (const event of sortBy(e => e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === RELAY_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
if (event.kind === RELAY_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
return Array.from(members)
},
$events => getSpaceMembers(url, $events),
)
export type BannedPubkeyItem = {
@@ -869,33 +906,7 @@ export const deriveRoomMembers = (url: string, h: string) => {
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]
return derived(deriveEventsForUrl(url, filters), $events => {
const membersEvent = find(spec({kind: ROOM_MEMBERS}), $events)
if (membersEvent) {
return uniq(getPubkeyTagValues(membersEvent.tags))
}
const members = new Set<string>()
for (const event of sortEventsAsc($events)) {
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === ROOM_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
if (event.kind === ROOM_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
return Array.from(members)
})
return derived(deriveEventsForUrl(url, filters), $events => getRoomMembers(url, h, $events))
}
export const deriveRoomAdmins = (url: string, h: string) => {
@@ -919,7 +930,7 @@ export const deriveSpaceActionItems = (url: string) =>
derived(
deriveEventsForUrl(url, [
{
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS],
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
},
]),
$events => {
@@ -932,19 +943,50 @@ export const deriveSpaceActionItems = (url: string) =>
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
if (!h) continue
const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
const roomMembersEvent = roomEvents.find(spec({kind: ROOM_MEMBERS}))
const roomMembers = getTagValues("p", roomMembersEvent?.tags ?? [])
const roomJoins: TrustedEvent[] = []
const roomLeaves: TrustedEvent[] = []
const roomMembershipEvents: TrustedEvent[] = []
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))
pendingJoins.push(
...removeUndefined(
Array.from(groupBy(e => e.pubkey, roomJoins).values())
.map(sortEventsDesc)
.map(first),
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events =>
first(sortEventsDesc(events)),
),
).filter(({pubkey, created_at}) => {
if (roomMembers.includes(pubkey)) return false
if (gt(roomMembersEvent?.created_at, created_at)) return false
if (roomMembers.has(pubkey)) return false
if (
roomMembershipEvents.some(event => {
if (event.created_at <= created_at) {
return false
}
if (event.kind === ROOM_MEMBERS) {
return true
}
return getPubkeyTagValues(event.tags).includes(pubkey)
})
) {
return false
}
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false
return true
+90 -67
View File
@@ -1,8 +1,8 @@
import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store"
import {derived, get} from "svelte/store"
import {last, call, ifLet, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
import {PollResponse} from "nostr-tools/kinds"
import {merged} from "@welshman/store"
import {
getListTags,
getRelayTagValues,
@@ -13,6 +13,8 @@ import {
ROOM_MEMBERS,
ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER,
ROOM_JOIN,
ROOM_LEAVE,
ROOM_CREATE_PERMISSION,
RELAY_MEMBERS,
RELAY_ADD_MEMBER,
@@ -21,12 +23,11 @@ import {
unionFilters,
getTagValue,
} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
import {
pubkey,
loadRelay,
userFollowList,
userRelayList,
userMessagingRelayList,
loadRelayList,
@@ -49,7 +50,6 @@ import {
loadGroupList,
userSpaceUrls,
userGroupList,
bootstrapPubkeys,
decodeRelay,
getSpaceUrlsFromGroupList,
getSpaceRoomsFromGroupList,
@@ -74,6 +74,8 @@ const pullOneWithFallback = async (
signal: AbortSignal,
onEvent?: (event: TrustedEvent) => void,
) => {
if (signal.aborted) return
const cachedEvents = repository.query([filter]).filter(isSignedEvent)
const since = last(cachedEvents.slice(10))?.created_at || 0
@@ -86,6 +88,12 @@ const pullOneWithFallback = async (
const shouldFallback =
!hasNegentropy(url) ||
(await new Promise(resolve => {
if (signal.aborted) {
resolve(false)
return
}
// If teardown wins while the diff is opening, skip the fallback path and let cleanup stay in control.
const diff = new Difference({relay: url, filter, events: cachedEvents, signal})
diff.on(DifferenceEvent.Error, () => {
@@ -111,9 +119,7 @@ export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts
if (signal.aborted) return
for (const filter of filters) {
pullOneWithFallback(url, filter, signal, onEvent)
}
await Promise.all(filters.map(filter => pullOneWithFallback(url, filter, signal, onEvent)))
}
const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
@@ -123,6 +129,8 @@ const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
}
const pullAndListen = (options: SyncOpts) => {
if (options.signal.aborted) return
pullWithFallback(options)
listen(options)
}
@@ -197,7 +205,7 @@ const syncUserRoomMembership = (url: string, h: string) => {
const syncUserData = () => {
const unsubscribersByKey = new Map<string, Unsubscriber>()
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => {
const syncGroupList = ($userGroupList: List | undefined) => {
if ($userGroupList) {
const keys = new Set<string>()
@@ -226,43 +234,35 @@ const syncUserData = () => {
}
}
}
}
const syncRelayList = ($userRelayList: PublishedList | undefined) => {
const pubkey = $userRelayList?.event?.pubkey
if (!pubkey) return
loadBlossomServerList(pubkey)
loadBlockedRelayList(pubkey)
loadFollowList(pubkey)
loadGroupList(pubkey)
loadMuteList(pubkey)
loadProfile(pubkey)
loadSettings(pubkey)
loadFeedsForPubkey(pubkey)
}
const unsubscribeGroupList = merged([userGroupList]).subscribe(([$userGroupList]) => {
syncGroupList($userGroupList)
})
const unsubscribeRelayList = userRelayList.subscribe($userRelayList => {
if ($userRelayList) {
loadBlossomServerList($userRelayList.event.pubkey)
loadBlockedRelayList($userRelayList.event.pubkey)
loadFollowList($userRelayList.event.pubkey)
loadGroupList($userRelayList.event.pubkey)
loadMuteList($userRelayList.event.pubkey)
loadProfile($userRelayList.event.pubkey)
loadSettings($userRelayList.event.pubkey)
loadFeedsForPubkey($userRelayList.event.pubkey)
}
})
const unsubscribeFollows = userFollowList.subscribe(async $userFollowList => {
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
await Promise.all(
pubkeys.flatMap(pk => [
loadRelayList(pk),
loadGroupList(pk),
loadProfile(pk),
loadFollowList(pk),
loadMuteList(pk),
]),
)
}
const unsubscribeRelayList = merged([userRelayList]).subscribe(([$userRelayList]) => {
syncRelayList($userRelayList)
})
return () => {
unsubscribersByKey.forEach(call)
unsubscribeGroupList()
unsubscribeRelayList()
unsubscribeFollows()
}
}
@@ -280,8 +280,13 @@ const syncSpace = (url: string, rooms: string[]) => {
url,
signal: controller.signal,
filters: [
{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
{kinds: MESSAGE_KINDS, since, "#h": [room]},
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
{
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
"#h": [room],
},
{kinds: [PollResponse], since},
],
})
@@ -294,7 +299,7 @@ const syncSpace = (url: string, rooms: string[]) => {
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
const roomMemberKinds = [ROOM_DELETE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
const roomMemberKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
pullAndListen({
url,
@@ -321,15 +326,16 @@ const syncSpace = (url: string, rooms: string[]) => {
}
const syncSpaces = () => {
const store = derived([userGroupList, page], identity)
const store = merged([userGroupList, page])
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const roomsByUrl = new Map<string, string>()
const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
const urls = new Set(getSpaceUrlsFromGroupList($userGroupList))
const currentUrl = $page.params.relay ? decodeRelay($page.params.relay) : undefined
if ($page.params.relay) {
urls.add(decodeRelay($page.params.relay))
if (currentUrl) {
urls.add(currentUrl)
}
// Stop syncing removed spaces
@@ -344,6 +350,11 @@ const syncSpaces = () => {
// Start or restart syncing for each space
for (const url of urls) {
const rooms = getSpaceRoomsFromGroupList(url, $userGroupList)
if (currentUrl === url && $page.params.h && !rooms.includes($page.params.h)) {
rooms.push($page.params.h)
}
const roomsKey = rooms.join(",")
if (unsubscribersByUrl.has(url) && roomsByUrl.get(url) === roomsKey) continue
@@ -383,6 +394,7 @@ const syncDMs = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
let currentPubkey: string | undefined
let currentShouldUnwrap = false
const unsubscribeAll = () => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
@@ -391,6 +403,34 @@ const syncDMs = () => {
}
}
const syncPubkey = ($pubkey: string | undefined, $shouldUnwrap: boolean) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => {
if ($l && currentPubkey === $pubkey && currentShouldUnwrap === $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
}
})
}
currentPubkey = $pubkey
currentShouldUnwrap = $shouldUnwrap
}
const syncList = ($userMessagingRelayList: List | undefined) => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
}
const subscribeAll = (pubkey: string, urls: string[]) => {
// Start syncing newly added relays
for (const url of urls) {
@@ -408,33 +448,16 @@ const syncDMs = () => {
}
}
// When pubkey changes, re-sync
const unsubscribePubkey = derived([pubkey, shouldUnwrap], identity).subscribe(
([$pubkey, $shouldUnwrap]) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
// If we have a pubkey, refresh our user's relay list then sync our subscriptions
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
}
currentPubkey = $pubkey
},
)
const unsubscribePubkey = merged([pubkey, shouldUnwrap]).subscribe(([$pubkey, $shouldUnwrap]) => {
syncPubkey($pubkey, $shouldUnwrap)
})
// When user messaging relays change, update synchronization
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
})
const unsubscribeList = merged([userMessagingRelayList]).subscribe(
([$userMessagingRelayList]) => {
syncList($userMessagingRelayList)
},
)
return () => {
unsubscribeAll()
+17 -4
View File
@@ -48,6 +48,18 @@ import {
import type {Unsubscriber} from "svelte/store"
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 = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
@@ -199,14 +211,15 @@ const loadCriticalRelays = async () => {
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
}
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
const syncRelays = () =>
onRelay(batch(FLUSH_INTERVAL, idleWrite(db.table<RelayProfile>("relays").bulkPut)))
const initRelayStats = async () => {
const table = db.table<RelayStats>("relayStats")
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelayStats(batch(1000, table.bulkPut))
return onRelayStats(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
}
const initHandles = async () => {
@@ -214,7 +227,7 @@ const initHandles = async () => {
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(1000, table.bulkPut))
return onHandle(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
}
const initZappers = async () => {
@@ -222,7 +235,7 @@ const initZappers = async () => {
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(3000, table.bulkPut))
return onZapper(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
}
const initPlaintext = async () => {
+1 -2
View File
@@ -6,7 +6,6 @@
import Close from "@assets/icons/close.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {clearModals} from "@app/util/modal"
type Props = {
onClose?: any
@@ -56,7 +55,7 @@
<div class={wrapperClass}>
<div class={innerClass} transition:fly>
{#if !noEscape}
<Button class={buttonClass} onclick={clearModals}>
<Button class={buttonClass} onclick={onClose}>
<Icon icon={Close} size={6} />
</Button>
{/if}
+56
View File
@@ -0,0 +1,56 @@
<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>
+4 -2
View File
@@ -4,13 +4,15 @@
import Dialog from "@lib/components/Dialog.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
const close = () => goto("/home")
const children = {
component: SpaceInviteAccept,
props: {
invite: $page.url.href,
back: () => goto("/home"),
back: close,
},
}
</script>
<Dialog {children} />
<Dialog {children} onClose={close} />
+2 -2
View File
@@ -172,9 +172,9 @@
</strong>
<Button onclick={() => (showAdvanced = !showAdvanced)}>
{#if showAdvanced}
<Icon icon={AltArrowDown} />
{:else}
<Icon icon={AltArrowUp} />
{:else}
<Icon icon={AltArrowDown} />
{/if}
</Button>
</div>
+22 -2
View File
@@ -30,6 +30,7 @@
import SpaceSearch from "@app/components/SpaceSearch.svelte"
import ThunkToast from "@app/components/ThunkToast.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 {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
import {
@@ -105,6 +106,7 @@
const shouldProtect = canEnforceNip70(url)
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const at = $derived(parseInt($page.url.searchParams.get("at")!))
const shouldVirtualize = $derived(isNaN(at))
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
@@ -468,7 +470,7 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p>
{/if}
{#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#each elements as { type, id, value, showPubkey, addSpaceBelow }, i (id)}
{#if type === "new-messages"}
<div
{id}
@@ -480,8 +482,26 @@
</div>
{:else if type === "date"}
<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}
{@const event = $state.snapshot(value as TrustedEvent)}
{@const event = value as TrustedEvent}
{#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
+22 -2
View File
@@ -21,6 +21,7 @@
import SpaceSearch from "@app/components/SpaceSearch.svelte"
import RoomItem from "@app/components/RoomItem.svelte"
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import VirtualItem from "@lib/components/VirtualItem.svelte"
import RoomCompose from "@app/components/RoomCompose.svelte"
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
@@ -37,6 +38,7 @@
const url = decodeRelay($page.params.relay!)
const shouldProtect = canEnforceNip70(url)
const at = $derived(parseInt($page.url.searchParams.get("at")!))
const shouldVirtualize = $derived(isNaN(at))
const replyTo = (event: TrustedEvent) => {
parent = event
@@ -305,7 +307,7 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p>
{/if}
{#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#each elements as { type, id, value, showPubkey, addSpaceBelow }, i (id)}
{#if type === "new-messages"}
<div
{id}
@@ -317,8 +319,26 @@
</div>
{:else if type === "date"}
<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}
{@const event = $state.snapshot(value as TrustedEvent)}
{@const event = value as TrustedEvent}
{#if event.kind === RELAY_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}