Compare commits

...

7 Commits

Author SHA1 Message Date
Jon Staab 7334cd26f8 Bump version 2025-10-13 15:17:46 -07:00
Jon Staab 44555215cf Track shards separately, upgrade deps 2025-10-13 13:41:27 -07:00
Jon Staab 0cc25913c0 Optimize event storage 2025-10-13 12:46:56 -07:00
Jon Staab 004b30b737 Update caniuse 2025-10-13 11:48:22 -07:00
Jon Staab 632f330b4c Re-work storage to optimize file access 2025-10-06 17:01:25 -07:00
Jon Staab 666433912f Only show send toast in chat if send_delay is set 2025-10-06 11:27:07 -07:00
Jon Staab db98ce8db7 Bump welshman 2025-10-06 11:26:27 -07:00
25 changed files with 2296 additions and 2047 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
# 1.3.1
* Fix memory leak in storage adapter
* Show fewer annoying toast messages
# 1.3.0
* Add optional badge and sound for notifications
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 27
versionName "1.3.0"
versionCode 28
versionName "1.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+10 -10
View File
@@ -1,30 +1,30 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/android/capacitor')
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/android/capacitor')
include ':capacitor-community-safe-area'
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area/android')
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.2.0/node_modules/@capacitor/filesystem/android')
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences/android')
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences/android')
include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications/android')
include ':capawesome-capacitor-android-dark-mode-support'
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge/android')
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin/android')
+2 -2
View File
@@ -365,7 +365,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.3.0;
MARKETING_VERSION = 1.3.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -391,7 +391,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.3.0;
MARKETING_VERSION = 1.3.1;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
+11 -11
View File
@@ -1,4 +1,4 @@
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios/scripts/pods_helpers'
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
@@ -9,16 +9,16 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.2.0/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin'
end
target 'Flotilla Chat' do
+53 -53
View File
@@ -1,6 +1,6 @@
{
"name": "flotilla",
"version": "1.3.0",
"version": "1.3.1",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -15,72 +15,72 @@
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.26.0",
"@sentry/cli": "^2.40.0",
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.19",
"@eslint/js": "^9.37.0",
"@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.21",
"classnames": "^2.5.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"globals": "^15.0.0",
"postcss": "^8.4.40",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.4"
"eslint": "^9.37.0",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.39.12",
"svelte-check": "^4.3.3",
"tailwindcss": "^3.4.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^5.4.20"
},
"type": "module",
"dependencies": {
"@capacitor-community/safe-area": "7.0.0-alpha.1",
"@capacitor/android": "^7.0.0",
"@capacitor/app": "^7.0.0",
"@capacitor/cli": "^7.0.0",
"@capacitor/core": "^7.0.1",
"@capacitor/filesystem": "^7.0.0",
"@capacitor/ios": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
"@capacitor/android": "^7.4.3",
"@capacitor/app": "^7.1.0",
"@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.4.3",
"@capacitor/filesystem": "^7.1.4",
"@capacitor/ios": "^7.4.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.2",
"@capacitor/push-notifications": "^7.0.1",
"@capacitor/push-notifications": "^7.0.3",
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1",
"@getalby/sdk": "^5.1.0",
"@getalby/sdk": "^5.1.2",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0",
"@sveltejs/adapter-static": "^3.0.4",
"@tiptap/core": "^2.12.0",
"@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5",
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "^0.5.1",
"@welshman/content": "^0.5.1",
"@welshman/editor": "^0.5.1",
"@welshman/feeds": "^0.5.1",
"@welshman/lib": "^0.5.1",
"@welshman/net": "^0.5.1",
"@welshman/relay": "^0.5.1",
"@welshman/router": "^0.5.1",
"@welshman/signer": "^0.5.1",
"@welshman/store": "^0.5.1",
"@welshman/util": "^0.5.1",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.5.3",
"@welshman/content": "^0.5.3",
"@welshman/editor": "^0.5.3",
"@welshman/feeds": "^0.5.3",
"@welshman/lib": "^0.5.3",
"@welshman/net": "^0.5.3",
"@welshman/relay": "^0.5.3",
"@welshman/router": "^0.5.3",
"@welshman/signer": "^0.5.3",
"@welshman/store": "^0.5.3",
"@welshman/util": "^0.5.3",
"compressorjs": "^1.2.1",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5",
"emoji-picker-element": "^1.22.8",
"fuse.js": "^7.0.0",
"husky": "^9.1.6",
"idb": "^8.0.0",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0",
"dotenv": "^16.6.1",
"emoji-picker-element": "^1.27.0",
"fuse.js": "^7.1.0",
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"nostr-tools": "^2.17.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
+1978 -1824
View File
File diff suppressed because it is too large Load Diff
@@ -56,7 +56,7 @@
const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([decryptedData]))
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
}
} else {
src = url
+3 -3
View File
@@ -110,7 +110,7 @@ import {
} from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push"
import {preferencesStorageProvider, collectionStorageProvider} from "@src/lib/storage"
import {preferencesStorageProvider, Collection} from "@src/lib/storage"
// Utils
@@ -156,7 +156,7 @@ export const logout = async () => {
localStorage.clear()
await preferencesStorageProvider.clear()
await collectionStorageProvider.clear()
await Collection.clearAll()
}
// Synchronization
@@ -744,7 +744,7 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
["encryption-algorithm", algorithm],
)
file = new File([new Blob([ciphertext])], name, {
file = new File([new Uint8Array(ciphertext)], name, {
type: "application/octet-stream",
})
}
+1 -1
View File
@@ -64,7 +64,7 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
case "discover":
return urls.length + 2
case "spaces": {
const routeUrl = decodeRelay($page.params.relay)
const routeUrl = decodeRelay($page.params.relay || "")
return urls.findIndex(url => url === routeUrl) + 1
}
+100 -38
View File
@@ -1,4 +1,15 @@
import {on, throttle, fromPairs, batch, sortBy, concat} from "@welshman/lib"
import {
always,
on,
hash,
last,
groupBy,
throttle,
fromPairs,
batch,
sortBy,
concat,
} from "@welshman/lib"
import {throttled, freshness} from "@welshman/store"
import {
PROFILE,
@@ -19,6 +30,7 @@ import {
MESSAGE,
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
verifiedSymbol,
} from "@welshman/util"
import type {Zapper, TrustedEvent} from "@welshman/util"
import type {RepositoryUpdate} from "@welshman/relay"
@@ -33,10 +45,23 @@ import {
onZapper,
onHandle,
} from "@welshman/app"
import {collectionStorageProvider} from "@lib/storage"
import {Collection} from "@lib/storage"
const syncEvents = async () => {
repository.load(await collectionStorageProvider.get<TrustedEvent>("events"))
const collection = new Collection<TrustedEvent>({
table: "events",
shards: Array.from("0123456789abcdef"),
getShard: (event: TrustedEvent) => last(event.id),
})
const initialEvents = await collection.get()
// Mark events verified to avoid re-verification of signatures
for (const event of initialEvents) {
event[verifiedSymbol] = true
}
repository.load(initialEvents)
const rankEvent = (event: TrustedEvent) => {
switch (event.kind) {
@@ -97,29 +122,46 @@ const syncEvents = async () => {
}
for (const id of update.removed) {
added = added.filter(event => !update.removed.has(event.id))
removed.add(id)
}
}
if (added.length > 0) {
let events = concat(await collectionStorageProvider.get<TrustedEvent>("events"), added)
if (removed.size > 0) {
added = added.filter(e => !removed.has(e.id))
// If we're well above our retention limit, drop lowest-ranked events
if (events.length > 15_000) {
events = sortBy(e => -rankEvent(e), events).slice(10_000)
const removedByShard = groupBy(id => last(id), removed)
const addedByShard = groupBy(e => last(e.id), added)
const shards = new Set([...removedByShard.keys(), ...addedByShard.keys()])
for (const shard of shards) {
const removedInShard = removedByShard.get(shard)
const addedInShard = addedByShard.get(shard) || []
const current = await collection.getShard(shard)
const filtered = current.filter(e => !removedInShard?.includes(e.id))
const sorted = sortBy(e => -rankEvent(e), concat(filtered, addedInShard))
const pruned = sorted.slice(0, 10_000)
await collection.setShard(shard, pruned)
}
await collectionStorageProvider.set("events", events)
} else if (added.length > 0) {
await collection.add(added)
}
}),
)
}
type TrackerItem = [string, string[]]
const syncTracker = async () => {
const collection = new Collection<TrackerItem>({
table: "tracker",
shards: Array.from("0123456789abcdef"),
getShard: (item: TrackerItem) => last(item[0]),
})
const relaysById = new Map<string, Set<string>>()
for (const [id, relays] of await collectionStorageProvider.get<[string, string[]]>("tracker")) {
for (const [id, relays] of await collection.get()) {
relaysById.set(id, new Set(relays))
}
@@ -129,17 +171,13 @@ const syncTracker = async () => {
const updateOne = batch(3000, (ids: string[]) => {
p = p.then(() => {
collectionStorageProvider.add(
"tracker",
ids.map(id => [id, Array.from(tracker.getRelays(id))]),
)
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
})
})
const updateAll = throttle(3000, () => {
p = p.then(() => {
collectionStorageProvider.set(
"tracker",
collection.set(
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
)
})
@@ -159,46 +197,70 @@ const syncTracker = async () => {
}
const syncRelays = async () => {
relays.set(await collectionStorageProvider.get<Relay>("relays"))
return throttled(3000, relays).subscribe($relays => {
collectionStorageProvider.set("relays", $relays)
const collection = new Collection<Relay>({
table: "relays",
shards: Array.from("0123456789"),
getShard: (item: Relay) => last(hash(item.url)),
})
relays.set(await collection.get())
return throttled(3000, relays).subscribe(collection.set)
}
const syncHandles = async () => {
handles.set(await collectionStorageProvider.get<Handle>("handles"))
const collection = new Collection<Handle>({
table: "handles",
shards: Array.from("0123456789"),
getShard: (item: Handle) => last(hash(item.nip05)),
})
return onHandle(
batch(3000, async $handles => {
await collectionStorageProvider.add("handles", $handles)
}),
)
handles.set(await collection.get())
return onHandle(batch(3000, collection.add))
}
const syncZappers = async () => {
zappers.set(await collectionStorageProvider.get<Zapper>("zappers"))
const collection = new Collection<Zapper>({
table: "zappers",
shards: Array.from("0123456789"),
getShard: (item: Zapper) => last(hash(item.lnurl)),
})
return onZapper(
batch(3000, async $zappers => {
await collectionStorageProvider.add("zappers", $zappers)
}),
)
zappers.set(await collection.get())
return onZapper(batch(3000, collection.add))
}
type FreshnessItem = [string, number]
const syncFreshness = async () => {
freshness.set(fromPairs(await collectionStorageProvider.get<[string, number]>("freshness")))
const collection = new Collection<FreshnessItem>({
table: "freshness",
shards: ["0"],
getShard: always("0"),
})
freshness.set(fromPairs(await collection.get()))
return throttled(3000, freshness).subscribe($freshness => {
collectionStorageProvider.set("freshness", Object.entries($freshness))
collection.set(Object.entries($freshness))
})
}
type PlaintextItem = [string, string]
const syncPlaintext = async () => {
plaintext.set(fromPairs(await collectionStorageProvider.get<[string, string]>("plaintext")))
const collection = new Collection<PlaintextItem>({
table: "plaintext",
shards: ["0"],
getShard: always("0"),
})
plaintext.set(fromPairs(await collection.get()))
return throttled(3000, plaintext).subscribe($plaintext => {
collectionStorageProvider.set("plaintext", Object.entries($plaintext))
collection.set(Object.entries($plaintext))
})
}
+1 -1
View File
@@ -13,7 +13,7 @@
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2">
<div
class="flex min-h-12 items-center justify-between gap-4 rounded-xl rounded-xl bg-base-100 px-4 shadow-xl">
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
{@render props.icon?.()}
{@render props.title?.()}
+90 -74
View File
@@ -1,3 +1,4 @@
import {flatten, identity, groupBy} from "@welshman/lib"
import {type StorageProvider} from "@welshman/store"
import {Preferences} from "@capacitor/preferences"
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
@@ -30,79 +31,94 @@ export class PreferencesStorageProvider implements StorageProvider {
export const preferencesStorageProvider = new PreferencesStorageProvider()
export class CollectionStorageProvider implements StorageProvider {
p = Promise.resolve()
get = async <T>(key: string): Promise<T[]> => {
try {
const file = await Filesystem.readFile({
path: key + ".json",
directory: Directory.Data,
encoding: Encoding.UTF8,
})
const items: T[] = []
for (const line of file.data.toString().split("\n")) {
try {
items.push(JSON.parse(line))
} catch (e) {
// pass
}
}
return items
} catch (err) {
// file doesn't exist, or isn't valid json
return []
}
}
set = async <T>(key: string, value: T[]): Promise<void> => {
this.p = this.p.then(async () => {
await Filesystem.writeFile({
path: key + ".json",
directory: Directory.Data,
encoding: Encoding.UTF8,
data: value.map(v => JSON.stringify(v)).join("\n"),
})
})
await this.p
}
add = async <T>(key: string, value: T[]): Promise<void> => {
this.p = this.p.then(async () => {
await Filesystem.appendFile({
path: key + ".json",
directory: Directory.Data,
encoding: Encoding.UTF8,
data: "\n" + value.map(v => JSON.stringify(v)).join("\n"),
})
})
await this.p
}
clear = async (): Promise<void> => {
this.p = this.p.then(async () => {
try {
const res = await Filesystem.readdir({path: "./", directory: Directory.Data})
await Promise.all(
res.files.map(file =>
Filesystem.deleteFile({
path: file.name + ".json",
directory: Directory.Data,
}),
),
)
} catch (e) {
// Directory might not have been created yet
}
})
await this.p
}
export type CollectionOptions<T> = {
table: string
shards: string[]
getShard: (item: T) => string
}
export const collectionStorageProvider = new CollectionStorageProvider()
export class Collection<T> {
#promises = new Map<string, Promise<any>>()
constructor(readonly options: CollectionOptions<T>) {}
static clearAll = async (): Promise<void> => {
const res = await Filesystem.readdir({
path: "",
directory: Directory.Data,
})
await Promise.all(
res.files.map(file =>
Filesystem.deleteFile({
path: file.name,
directory: Directory.Data,
}),
),
)
}
#then = <R>(shard: string, f: () => Promise<R>) => {
const oldPromise = this.#promises.get(shard) || Promise.resolve()
const newPromise = oldPromise.then(f)
this.#promises.set(shard, newPromise)
return newPromise
}
#path = (shard: string) => `collection_${this.options.table}_${shard}.json`
getShard = (shard: string): Promise<T[]> =>
this.#then(shard, async () => {
try {
const file = await Filesystem.readFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
})
// Speed things up by parsing only once
return JSON.parse("[" + file.data.toString().split("\n").filter(identity).join(",") + "]")
} catch (err) {
// file doesn't exist, or isn't valid json
return []
}
})
get = async (): Promise<T[]> => flatten(await Promise.all(this.options.shards.map(this.getShard)))
setShard = (shard: string, items: T[]) =>
this.#then(shard, async () => {
await Filesystem.writeFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
data: items.map(v => JSON.stringify(v)).join("\n"),
})
})
set = (items: T[]) =>
Promise.all(
Array.from(groupBy(this.options.getShard, items)).map(([shard, chunk]) =>
this.setShard(shard, chunk),
),
)
addToShard = (shard: string, items: T[]) =>
this.#then(shard, async () => {
await Filesystem.appendFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
data: "\n" + items.map(v => JSON.stringify(v)).join("\n"),
})
})
add = (items: T[]) =>
Promise.all(
Array.from(groupBy(this.options.getShard, items)).map(([shard, chunk]) =>
this.addToShard(shard, chunk),
),
)
}
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import type {MakeNonOptional} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {Address, getIdFilters} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay"
@@ -10,7 +11,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import {goToEvent} from "@app/util/routes"
const {bech32} = $page.params
const {bech32} = $page.params as MakeNonOptional<typeof $page.params>
const attemptToNavigate = async () => {
const {type, data} = nip19.decode(bech32) as any
+4 -1
View File
@@ -1,8 +1,11 @@
<script lang="ts">
import {page} from "$app/stores"
import type {MakeNonOptional} from "@welshman/lib"
import Chat from "@app/components/Chat.svelte"
import {notifications, setChecked} from "@app/util/notifications"
const {chat} = $page.params as MakeNonOptional<typeof $page.params>
// We have to watch this one, since on mobile the badge will be visible when active
$effect(() => {
if ($notifications.has($page.url.pathname)) {
@@ -11,4 +14,4 @@
})
</script>
<Chat id={$page.params.chat} />
<Chat id={chat} />
+1 -1
View File
@@ -31,7 +31,7 @@
const {children}: Props = $props()
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const rooms = Array.from($userRoomsByUrl.get(url) || [])
+1 -1
View File
@@ -26,7 +26,7 @@
import {makeChatPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const relay = deriveRelay(url)
const joinSpace = () => pushModal(SpaceJoin, {url})
+12 -9
View File
@@ -5,6 +5,7 @@
import {page} from "$app/stores"
import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {
@@ -57,10 +58,10 @@
import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast"
const {room} = $page.params
const {room, relay} = $page.params as MakeNonOptional<typeof $page.params>
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay)
const url = decodeRelay(relay)
const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [room]}
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
@@ -137,13 +138,15 @@
delay: $userSettingsValues.send_delay,
})
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
if ($userSettingsValues.send_delay) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
}
clearParent()
clearShare()
@@ -23,7 +23,7 @@
import {makeCalendarFeed} from "@app/core/requests"
import {setChecked} from "@app/util/notifications"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const makeEvent = () => pushModal(CalendarEventCreate, {url})
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {request} from "@welshman/net"
import {repository} from "@welshman/app"
@@ -25,7 +26,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]
+10 -8
View File
@@ -36,7 +36,7 @@
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const filter = {kinds: [MESSAGE]}
const shouldProtect = canEnforceNip70(url)
@@ -74,13 +74,15 @@
delay: $userSettingsValues.send_delay,
})
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
if ($userSettingsValues.send_delay) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
}
clearParent()
clearShare()
+1 -1
View File
@@ -20,7 +20,7 @@
import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const goals: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
import {request} from "@welshman/net"
@@ -24,7 +25,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]
@@ -21,7 +21,7 @@
import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const threads: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
import {request} from "@welshman/net"
@@ -23,7 +24,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]