Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e832af3e4 | |||
| 84b8650fa4 | |||
| 83abb5aa94 | |||
| a12eddb47b | |||
| c87166247c | |||
| 037c8cb41b | |||
| 79de2e1176 | |||
| d4b026a3ad | |||
| 00f383ff2e | |||
| 6f6bb508db | |||
| e2a0672ca5 | |||
| e2a5fe7a79 | |||
| 5d02ae75dc | |||
| 2460bbbc83 | |||
| 084d8d931b | |||
| 6ee4ac1a89 | |||
| 1d07097350 | |||
| 63d6b362c7 | |||
| bfed277ea9 | |||
| 9e8aa2ef3a | |||
| 4bbc0878f7 | |||
| 16a3ba2a9b | |||
| 7c11eb8947 | |||
| 6bdc8d4d9f | |||
| b9048936ba | |||
| b9620f4443 | |||
| f2249fe592 | |||
| fd42a0e8d4 | |||
| 37d52ba35f | |||
| 3037323dc0 | |||
| 5301ef876d | |||
| aa054d8b1a | |||
| 3655790e5f | |||
| 6cca823ed4 | |||
| 18a383edab | |||
| 43da7d628e | |||
| 2fae3ca248 | |||
| d99ada44f5 | |||
| cb0119b9b8 | |||
| dac9ef8e4e | |||
| 528917b90e | |||
| a22db78967 | |||
| 5718510779 | |||
| f877dc7fbe | |||
| df03fb1116 | |||
| 7455b49f8d |
@@ -13,5 +13,6 @@ VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.
|
||||
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
||||
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,5 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
# 1.2.2
|
||||
|
||||
* Fix phantom chat notifications
|
||||
* Fix zaps on mobile
|
||||
|
||||
# 1.2.1
|
||||
|
||||
* Add zaps to chat, threads, and events
|
||||
* Add funding goals
|
||||
* Add NWC support
|
||||
* Add wallet settings page
|
||||
* Handle invalid bunker url
|
||||
* Fix sidebar overflow
|
||||
* Fix profile npub display
|
||||
|
||||
# 1.2.0
|
||||
|
||||
* Fix sort order of thread comments
|
||||
* Fix link display when no title is available
|
||||
* Fix making profiles non-protected
|
||||
* Replace bunker url with relay claims for notifier auth
|
||||
* Add push notifications on all platforms
|
||||
* Add "mark all as read" on desktop
|
||||
* Re-design space dashboard
|
||||
|
||||
# 1.1.1
|
||||
|
||||
* Add chat quick link
|
||||
|
||||
# 1.1.0
|
||||
|
||||
* Add better theming support
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 18
|
||||
versionName "1.0.4"
|
||||
versionCode 23
|
||||
versionName "1.2.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -12,6 +12,8 @@ dependencies {
|
||||
implementation project(':capacitor-community-safe-area')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-keyboard')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capawesome-capacitor-badge')
|
||||
implementation project(':nostr-signer-capacitor-plugin')
|
||||
|
||||
}
|
||||
|
||||
@@ -34,4 +34,5 @@
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
</manifest>
|
||||
|
||||
@@ -11,5 +11,11 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Fetch tags and set to env vars
|
||||
git fetch --prune --unshallow --tags
|
||||
git describe --tags --abbrev=0
|
||||
git fetch --prune --unshallow --tags || true
|
||||
git describe --tags --abbrev=0 || true
|
||||
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
||||
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ const config: CapacitorConfig = {
|
||||
style: "DARK",
|
||||
resizeOnFullScreen: true,
|
||||
},
|
||||
Badge: {
|
||||
persist: true,
|
||||
autoClear: true
|
||||
},
|
||||
},
|
||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||
// server: {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
|
||||
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
@@ -57,6 +58,7 @@
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
@@ -349,16 +351,17 @@
|
||||
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.2.2;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -374,16 +377,17 @@
|
||||
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,9 @@
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -14,6 +14,8 @@ def capacitor_pods
|
||||
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 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
|
||||
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'
|
||||
end
|
||||
|
||||
|
||||
+18
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "1.0.4",
|
||||
"version": "1.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -45,25 +45,28 @@
|
||||
"@capacitor/core": "^7.0.1",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/push-notifications": "^7.0.1",
|
||||
"@capawesome/capacitor-badge": "^7.0.1",
|
||||
"@getalby/sdk": "^5.1.0",
|
||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||
"@sentry/browser": "^8.35.0",
|
||||
"@sveltejs/adapter-static": "^3.0.4",
|
||||
"@tiptap/core": "^2.12.0",
|
||||
"@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.3.4",
|
||||
"@welshman/content": "^0.3.4",
|
||||
"@welshman/dvm": "^0.3.4",
|
||||
"@welshman/editor": "^0.3.4",
|
||||
"@welshman/feeds": "^0.3.4",
|
||||
"@welshman/lib": "^0.3.4",
|
||||
"@welshman/net": "^0.3.4",
|
||||
"@welshman/relay": "^0.3.4",
|
||||
"@welshman/router": "^0.3.4",
|
||||
"@welshman/signer": "^0.3.4",
|
||||
"@welshman/store": "^0.3.4",
|
||||
"@welshman/util": "^0.3.4",
|
||||
"@welshman/app": "^0.4.0",
|
||||
"@welshman/content": "^0.4.0",
|
||||
"@welshman/editor": "^0.4.0",
|
||||
"@welshman/feeds": "^0.4.0",
|
||||
"@welshman/lib": "^0.4.0",
|
||||
"@welshman/net": "^0.4.0",
|
||||
"@welshman/relay": "^0.4.0",
|
||||
"@welshman/router": "^0.4.0",
|
||||
"@welshman/signer": "^0.4.0",
|
||||
"@welshman/store": "^0.4.0",
|
||||
"@welshman/util": "^0.4.0",
|
||||
"compressorjs": "^1.2.1",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
@@ -75,7 +78,9 @@
|
||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||
"nostr-tools": "^2.14.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
Generated
+237
-169
@@ -29,6 +29,15 @@ importers:
|
||||
'@capacitor/keyboard':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1(@capacitor/core@7.2.0)
|
||||
'@capacitor/push-notifications':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(@capacitor/core@7.2.0)
|
||||
'@capawesome/capacitor-badge':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(@capacitor/core@7.2.0)
|
||||
'@getalby/sdk':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(typescript@5.8.3)
|
||||
'@poppanator/sveltekit-svg':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(rollup@2.79.2)(svelte@5.25.10)(svgo@3.3.2)(vite@5.4.17(@types/node@22.14.0)(terser@5.39.0))
|
||||
@@ -44,6 +53,9 @@ importers:
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5
|
||||
'@types/throttle-debounce':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@vite-pwa/assets-generator':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6
|
||||
@@ -51,41 +63,38 @@ importers:
|
||||
specifier: ^0.6.6
|
||||
version: 0.6.8(@sveltejs/kit@2.20.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.25.10)(vite@5.4.17(@types/node@22.14.0)(terser@5.39.0)))(svelte@5.25.10)(vite@5.4.17(@types/node@22.14.0)(terser@5.39.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.17(@types/node@22.14.0)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||
'@welshman/app':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/content':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)
|
||||
'@welshman/dvm':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)
|
||||
'@welshman/editor':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(@tiptap/extension-image@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(linkifyjs@4.3.1)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(typescript@5.8.3)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(@tiptap/extension-image@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(linkifyjs@4.3.1)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.39.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(typescript@5.8.3)
|
||||
'@welshman/feeds':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/lib':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
'@welshman/net':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)(ws@8.18.2)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/relay':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)
|
||||
'@welshman/router':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)
|
||||
'@welshman/signer':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/store':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)
|
||||
'@welshman/util':
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(typescript@5.8.3)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.8.3)
|
||||
compressorjs:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@@ -119,9 +128,15 @@ importers:
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.6.5
|
||||
version: 0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.25.10))(prettier@3.5.3)
|
||||
qr-scanner:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
throttle-debounce:
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
tippy.js:
|
||||
specifier: ^6.3.7
|
||||
version: 6.3.7
|
||||
@@ -743,6 +758,16 @@ packages:
|
||||
peerDependencies:
|
||||
'@capacitor/core': '>=7.0.0'
|
||||
|
||||
'@capacitor/push-notifications@7.0.1':
|
||||
resolution: {integrity: sha512-nSHsMSrTHX5pOkX1Khse75/uvSx/JTcXG+9aT6a66CvzalH6MCs0ha8Jv+xu0k9xW8caO+qSUMjfj5Oy82Uxmw==}
|
||||
peerDependencies:
|
||||
'@capacitor/core': '>=7.0.0'
|
||||
|
||||
'@capawesome/capacitor-badge@7.0.1':
|
||||
resolution: {integrity: sha512-jhVieRRVLgGO1NU7PW8uWZmf3WD4IsYUlkrJ82KuoRgLFx1tbJGwHU1ro0sUJmEwfLO9vldhBnJJ/J5nHrjbQQ==}
|
||||
peerDependencies:
|
||||
'@capacitor/core': '>=7.0.0'
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -931,6 +956,14 @@ packages:
|
||||
resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@getalby/lightning-tools@5.2.0':
|
||||
resolution: {integrity: sha512-8kBvENBTMh541VjGKhw3I29+549/C02gLSh3AQaMfoMNSZaMxfQW+7dcMcc7vbFaCKEcEe18ST5bUveTRBuXCQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@getalby/sdk@5.1.0':
|
||||
resolution: {integrity: sha512-0ijo4enzoxZinyhOMFlR4h3qTQ9I0Se+dBkefk0ja5zOcpi61ZqT86n0T+7u94l8SH6/poysFBObdtN61u+6tQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -1370,77 +1403,77 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-code-block@2.14.0':
|
||||
resolution: {integrity: sha512-LRYYZeh8U2XgfTsJ4houB9s9cVRt7PRfVa4MaCeOYKfowVOKQh67yV5oom8Azk9XrMPkPxDmMmdPAEPxeVYFvw==}
|
||||
'@tiptap/extension-code-block@2.26.1':
|
||||
resolution: {integrity: sha512-/TDDOwONl0qEUc4+B6V9NnWtSjz95eg7/8uCb8Y8iRbGvI9vT4/znRKofFxstvKmW4URu/H74/g0ywV57h0B+A==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-code@2.14.0':
|
||||
resolution: {integrity: sha512-kyo02mnzqgwXayMcyRA/fHQgb+nMmQQpIt1irZwjtEoFZshA7NnY/6b5SJmRcxQ4/X4r2Y2Ha2sWmOcEkLmt4A==}
|
||||
'@tiptap/extension-code@2.26.1':
|
||||
resolution: {integrity: sha512-GU9deB1A/Tr4FMPu71CvlcjGKwRhGYz60wQ8m4aM+ELZcVIcZRa1ebR8bExRIEWnvRztQuyRiCQzw2N0xQJ1QQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-document@2.14.0':
|
||||
resolution: {integrity: sha512-qwEgpPIJ3AgXdEtRTr88hODbXRdt14VAwLj27PTSqexB5V7Ra1Jy7iQDhqRwBCoUomVywBsWYxkSuDisSRG+9w==}
|
||||
'@tiptap/extension-document@2.26.1':
|
||||
resolution: {integrity: sha512-2P2IZp1NRAE+21mRuFBiP3X2WKfZ6kUC23NJKpn8bcOamY3obYqCt0ltGPhE4eR8n8QAl2fI/3jIgjR07dC8ow==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-dropcursor@2.14.0':
|
||||
resolution: {integrity: sha512-FIh5cdPuoPKvZ0GqSKhzMZGixm05ac3hSgqhMNCBZmXX459qBUI9CvDl/uzSnY9koBDeLVV3HYMthWQQLSXl9A==}
|
||||
'@tiptap/extension-dropcursor@2.26.1':
|
||||
resolution: {integrity: sha512-JkDQU2ZYFOuT5mNYb8OiWGwD1HcjbtmX8tLNugQbToECmz9WvVPqJmn7V/q8VGpP81iEECz/IsyRmuf2kSD4uA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-gapcursor@2.14.0':
|
||||
resolution: {integrity: sha512-as+SqC39FRshw4Fm1XVlrdSXveiusf5xiC4nuefLmXsUxO7Yx67x8jS0/VQbxWTLHZ6R1YEW8prLtnxGmVLCAQ==}
|
||||
'@tiptap/extension-gapcursor@2.26.1':
|
||||
resolution: {integrity: sha512-KOiMZc3PwJS3hR0nSq5d0TJi2jkNZkLZElcT6pCEnhRHzPH6dRMu9GM5Jj798ZRUy0T9UFcKJalFZaDxnmRnpg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-hard-break@2.14.0':
|
||||
resolution: {integrity: sha512-A8c8n8881iBq3AusNqibh6Hloybr+FgYdg4Lg4jNxbbEaL0WhyLFge1bWlGVpbHXFqdv5YldMUAu6Rop3FhNvw==}
|
||||
'@tiptap/extension-hard-break@2.26.1':
|
||||
resolution: {integrity: sha512-d6uStdNKi8kjPlHAyO59M6KGWATNwhLCD7dng0NXfwGndc22fthzIk/6j9F6ltQx30huy5qQram6j3JXwNACoA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-history@2.14.0':
|
||||
resolution: {integrity: sha512-/qnOHQFCEPfkb3caykqd+sqzEC2gx30EQB/mM7+5kIG7CQy7XXaGjFAEaqzE1xJ783Q2E7GVk4JxWM+3NhYSLw==}
|
||||
'@tiptap/extension-history@2.26.1':
|
||||
resolution: {integrity: sha512-m6YR1gkkauIDo3PRl0gP+7Oc4n5OqDzcjVh6LvWREmZP8nmi94hfseYbqOXUb6RPHIc0JKF02eiRifT4MSd2nw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-image@2.14.0':
|
||||
resolution: {integrity: sha512-pYCUzZBgsxIvVGTzuW03cPz6PIrAo26xpoxqq4W090uMVoK0SgY5W5y0IqCdw4QyLkJ2/oNSFNc2EP9jVi1CcQ==}
|
||||
'@tiptap/extension-image@2.26.1':
|
||||
resolution: {integrity: sha512-96+MaYBJebQlR/ik5W72GLUfXdEoxFs+6jsoERxbM5qEdhb7TEnodBFtWZOwgDO27kFd6rSNZuW9r5KJNtljEg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-link@2.14.0':
|
||||
resolution: {integrity: sha512-fsqW7eRD2xoD6xy7eFrNPAdIuZ3eicA4jKC45Vcft/Xky0DJoIehlVBLxsPbfmv3f27EBrtPkg5+msLXkLyzJA==}
|
||||
'@tiptap/extension-link@2.26.1':
|
||||
resolution: {integrity: sha512-7yfum5Jymkue/uOSTQPt2SmkZIdZx7t3QhZLqBU7R9ettkdSCBgEGok6N+scJM1R1Zes+maSckLm0JZw5BKYNA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-paragraph@2.14.0':
|
||||
resolution: {integrity: sha512-bsQesVpgvDS2e+wr2fp59QO7rWRp2FqcJvBafwXS3Br9U5Mx3eFYryx4wC7cUnhlhUwX5pmaoA7zISgV9dZDgg==}
|
||||
'@tiptap/extension-paragraph@2.26.1':
|
||||
resolution: {integrity: sha512-UezvM9VDRAVJlX1tykgHWSD1g3MKfVMWWZ+Tg+PE4+kizOwoYkRWznVPgCAxjmyHajxpCKRXgqTZkOxjJ9Kjzg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-placeholder@2.14.0':
|
||||
resolution: {integrity: sha512-xzfjHvuukbch4i5O/5uyS2K2QgNEaMKi6e6GExTTgVwnFjKfJmgTqee33tt5JCqSItBvtSZlU3SX/vpiaIof+w==}
|
||||
'@tiptap/extension-placeholder@2.26.1':
|
||||
resolution: {integrity: sha512-MBlqbkd+63btY7Qu+SqrXvWjPwooGZDsLTtl7jp52BczBl61cq9yygglt9XpM11TFMBdySgdLHBrLtQ0B7fBlw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-text@2.14.0':
|
||||
resolution: {integrity: sha512-rHny566nGZHq61zRLwQ9BPG55W/O+eDKwUJl+LhrLiVWwzpvAl9QQYixtoxJKOY48VK41PKwxe3bgDYgNs/Fhg==}
|
||||
'@tiptap/extension-text@2.26.1':
|
||||
resolution: {integrity: sha512-p2n8WVMd/2vckdJlol24acaTDIZAhI7qle5cM75bn01sOEZoFlSw6SwINOULrUCzNJsYb43qrLEibZb4j2LeQw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/pm@2.12.0':
|
||||
resolution: {integrity: sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==}
|
||||
|
||||
'@tiptap/suggestion@2.14.0':
|
||||
resolution: {integrity: sha512-AXzEw0KYIyg5id8gz5geIffnBtkZqan5MWe29rGo3gXTfKH+Ik8tWbZdnlMVheycsUCllrymDRei4zw9DqVqkQ==}
|
||||
'@tiptap/suggestion@2.26.1':
|
||||
resolution: {integrity: sha512-iNWJdQN7h01keNoVwyCsdI7ZX11YkrexZjCnutWK17Dd72s3NYVTmQXu7saftwddT4nDdlczNxAFosrt0zMhcg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
@@ -1515,6 +1548,9 @@ packages:
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
'@types/offscreencanvas@2019.7.3':
|
||||
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
|
||||
|
||||
'@types/qrcode@1.5.5':
|
||||
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
|
||||
|
||||
@@ -1596,44 +1632,41 @@ packages:
|
||||
'@vite-pwa/assets-generator':
|
||||
optional: true
|
||||
|
||||
'@welshman/app@0.3.4':
|
||||
resolution: {integrity: sha512-pb1I53hWog6plLVyOx6biN0uiB7zdheDqN2hWfb/uUKKzD484u+Yk3p5H2S6VaJnSbgNHnPuuJnjVstts1rhLg==}
|
||||
'@welshman/app@0.4.0':
|
||||
resolution: {integrity: sha512-LTlqbuiRFYAdwXIUYPOxaAusjhlj2ZgZlAuyEpQoBwNTyD7TUaTXj0kA5pbQZLFXWYuqDmrDB14Nl1zzBJBESQ==}
|
||||
|
||||
'@welshman/content@0.3.4':
|
||||
resolution: {integrity: sha512-kuyykt5SK4vHUciJBooqfLDI/HBxekZPK3qVgmKlpVf8cMRmaSOGTjsKljINgFsT/3J5z7wZI5AzQR/2FBo2ZQ==}
|
||||
'@welshman/content@0.4.0':
|
||||
resolution: {integrity: sha512-3pWxr0Byc/Asmvlnq5UchkT0yeaGg63xTEk9fVJyzIrphIxn5bboaIixEw7y2w2lggFaqHgx+DFrulmhdJ9dXQ==}
|
||||
|
||||
'@welshman/dvm@0.3.4':
|
||||
resolution: {integrity: sha512-39uSVco5VZmYEE+BZ3Lx/hGVM/HzGfnlEMR0fysh4BqfMm3Po9KXfOLy4YDMoHt7Ai/rC4z/HdJ81F3jHhJZjw==}
|
||||
'@welshman/editor@0.4.0':
|
||||
resolution: {integrity: sha512-aIt/t+pMs2XKWZ6wN58jdPWlN9MXVdK1rccKk6Z54ckarCzB4B7usSZvstwMMkmZra/HPLOaWw5KXqhDR1YiUA==}
|
||||
|
||||
'@welshman/editor@0.3.4':
|
||||
resolution: {integrity: sha512-fpUnacyZvbtytadtVwV4CEm1rGHgp8xWHQB7pZ0m/5bxlAHT1p/cL5a83CjdrVbu3+HMzBfgV4k6ldhH1XnJaQ==}
|
||||
'@welshman/feeds@0.4.0':
|
||||
resolution: {integrity: sha512-fwQ4eDzEtcSxFj2LKps6XYFXuZv6lFXKDTq+Nvs5tNYYJUbv/Cz4x3aLQo2ivInz9gAMOLmgpIgNCxkzMqCnoQ==}
|
||||
|
||||
'@welshman/feeds@0.3.4':
|
||||
resolution: {integrity: sha512-w4GLcWOxTUlx9MBbqHfee0ePWW8oyMGaf+Hey35FNsKcBvxiO58uW9j6BBvlu9yacthw0RGa/91giJn3zZzx1Q==}
|
||||
|
||||
'@welshman/lib@0.3.4':
|
||||
resolution: {integrity: sha512-37WDPZHsUFBLKdlkQXKAVuEDZB3lzQOkfIG0MMy7uZsg8+zR4VoA28g9B1vAKGky9lmWequHMPNigPRjC7RHQA==}
|
||||
'@welshman/lib@0.4.0':
|
||||
resolution: {integrity: sha512-1GPQ2X1FT2R55KWPKhDs+ZK/EkVpeMkVwWdSXC88w+YCoUop00keFm7P452kQKgA/lixNURJSyeWgfI2tUdpkQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
'@welshman/net@0.3.4':
|
||||
resolution: {integrity: sha512-btC7eXwEA8wfzaCsq8mdFIfbX61JeoyI3am53+L23ujbJQ9aqCciTA6EYEa2JzYB2HdydmCzdZrP/DTSjHN6bw==}
|
||||
'@welshman/net@0.4.0':
|
||||
resolution: {integrity: sha512-QBU5dsALCr9V51lIyNseUDIfvjCJo6VFWe6G1gkJ1PQGh5rgJNZJWCaD430PDpCKsufv2JIkCVYZG5xYZgxzMg==}
|
||||
|
||||
'@welshman/relay@0.3.4':
|
||||
resolution: {integrity: sha512-XjZY6XQXvNVFr3KC4O2Mlj+LHg/xJuQnhAHUYemvtJ9BcxvY01fq4ESadyiJklCjqNabsCPz76o+qFweZmym7Q==}
|
||||
'@welshman/relay@0.4.0':
|
||||
resolution: {integrity: sha512-5zTPSDPhMR2v55hotQf4JO3XgBXEws4k/xChAbYZDfUwxG7HQxmDM2n56aFrKqgI3w+qp8l1lx/1KeksKvBiWw==}
|
||||
|
||||
'@welshman/router@0.3.4':
|
||||
resolution: {integrity: sha512-ej4EjXWNj6srj6xnzcmFqhGXg6asTByPHPxQCkPG6HDOMVmG8GejlcatvjkzB4ppfdwYCz+jyJvjq30JBHmlbQ==}
|
||||
'@welshman/router@0.4.0':
|
||||
resolution: {integrity: sha512-ccpx9QrJ7Uq3CI7r/PyBOwO0G/2xknKXN0xLW995hta1Z1bUzYWhz+C9YvoceDPHCUPaZotBCgdmpY9oOiYqHg==}
|
||||
|
||||
'@welshman/signer@0.3.4':
|
||||
resolution: {integrity: sha512-kg2d4dQutRYvuuiCFC2I0UZfEHu+71t6o2g+jMtJ6d55dSa6+xUMCQ0/8Ua0AMOc/D7tJ1HLvtbmOjsehKkkKQ==}
|
||||
'@welshman/signer@0.4.0':
|
||||
resolution: {integrity: sha512-I+4l1gmSBVQkFtu6Bm5aAxsFXlE5oXeCsUX+GSsTb0Pg1e4FnTMgeaI3xM8tcCLma4EK+3mD7Yi9MaZMSYX8YQ==}
|
||||
peerDependencies:
|
||||
nostr-signer-capacitor-plugin: ~0.0.4
|
||||
|
||||
'@welshman/store@0.3.4':
|
||||
resolution: {integrity: sha512-AYDLTA7+ptbJYfAVRQ5lC/l65FtkAj7lKSadUguSf+R10JClNeYPWqQ6RTRpvyZ9qn9QetuYx8ecLfm8BxC7Ow==}
|
||||
'@welshman/store@0.4.0':
|
||||
resolution: {integrity: sha512-/gIX1hTTGPkhFlMm91oY7khqriIZwnNIFs3leWIbJGWXLd4pd4fMFM7bKuOTuEsnHkB9thkeXurAdYMquhlHOw==}
|
||||
|
||||
'@welshman/util@0.3.4':
|
||||
resolution: {integrity: sha512-sBznEqAPlNu9CZQSNFPcL5kNOIoWYfUTY/lyXQXLiYhMorXQmVGXS4TT7knS41oKiLgkUK98T0DvjMr4iJd3/g==}
|
||||
'@welshman/util@0.4.0':
|
||||
resolution: {integrity: sha512-UiJyqXeWEx0s83M0AD/bN5ylvpCfYUSjLepb0QxxZpBPnZgMj07oO1OS4967QFuePSvmjTQNcxy3ABm8aagxGg==}
|
||||
|
||||
'@xml-tools/parser@1.0.11':
|
||||
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
|
||||
@@ -3381,8 +3414,8 @@ packages:
|
||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
nostr-editor-coracle-workaround@0.0.4-pre.18:
|
||||
resolution: {integrity: sha512-QAoySZ9uOsR7C4nnVbcEpVgT0vLxwZxlYhE2NsJzmeoK7nTgkcHFaZkn/QMlUem3qjT8AolW8X5TrRdnZ5eIZQ==}
|
||||
nostr-editor@1.0.0:
|
||||
resolution: {integrity: sha512-+TL3G0m7WsXeEAitxzQhun7hyARxqRANjGIS2z9CBbniCGvT/Wz6YLgUnUysnBg3tmSgMZg5FWhaDPwfvdvbSw==}
|
||||
engines: {node: '>=18.16.1'}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.6.6
|
||||
@@ -3390,10 +3423,11 @@ packages:
|
||||
'@tiptap/extension-link': ^2.6.6
|
||||
'@tiptap/pm': ^2.6.6
|
||||
linkifyjs: ^4.1.3
|
||||
nostr-tools: ^2.14.2
|
||||
nostr-tools: ~2.14.2
|
||||
prosemirror-markdown: ^1.13.0
|
||||
prosemirror-model: ^1.22.3
|
||||
prosemirror-state: ^1.4.3
|
||||
prosemirror-view: ^1.39.3
|
||||
tiptap-markdown: ^0.8.10
|
||||
|
||||
nostr-signer-capacitor-plugin@0.0.4:
|
||||
@@ -3401,6 +3435,14 @@ packages:
|
||||
peerDependencies:
|
||||
'@capacitor/core': ^7.0.0
|
||||
|
||||
nostr-tools@2.12.0:
|
||||
resolution: {integrity: sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
nostr-tools@2.14.2:
|
||||
resolution: {integrity: sha512-YOIOn5EdJ2Kq5sQW5Zh4wOcqzR6kUyrCDHG4+mVD2szzthsyOTpiWX0yrwaRZGlHJG6q83vkhg95qc2W201XTQ==}
|
||||
peerDependencies:
|
||||
@@ -3824,6 +3866,9 @@ packages:
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
qr-scanner@1.4.2:
|
||||
resolution: {integrity: sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==}
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -4687,8 +4732,8 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@8.18.2:
|
||||
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@@ -5547,6 +5592,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@capacitor/core': 7.2.0
|
||||
|
||||
'@capacitor/push-notifications@7.0.1(@capacitor/core@7.2.0)':
|
||||
dependencies:
|
||||
'@capacitor/core': 7.2.0
|
||||
|
||||
'@capawesome/capacitor-badge@7.0.1(@capacitor/core@7.2.0)':
|
||||
dependencies:
|
||||
'@capacitor/core': 7.2.0
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
@@ -5670,6 +5723,15 @@ snapshots:
|
||||
'@eslint/core': 0.13.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@getalby/lightning-tools@5.2.0': {}
|
||||
|
||||
'@getalby/sdk@5.1.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@getalby/lightning-tools': 5.2.0
|
||||
nostr-tools: 2.12.0(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@@ -6150,58 +6212,58 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-code-block@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-code-block@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-code@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-code@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
'@tiptap/extension-document@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-document@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
'@tiptap/extension-dropcursor@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-dropcursor@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-gapcursor@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-gapcursor@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-hard-break@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-hard-break@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
'@tiptap/extension-history@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-history@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-image@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-image@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
'@tiptap/extension-link@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-link@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
linkifyjs: 4.3.1
|
||||
|
||||
'@tiptap/extension-paragraph@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-paragraph@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
'@tiptap/extension-placeholder@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/extension-placeholder@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
|
||||
'@tiptap/extension-text@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
'@tiptap/extension-text@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
|
||||
@@ -6226,7 +6288,7 @@ snapshots:
|
||||
prosemirror-transform: 1.10.4
|
||||
prosemirror-view: 1.39.3
|
||||
|
||||
'@tiptap/suggestion@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
'@tiptap/suggestion@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
@@ -6322,6 +6384,8 @@ snapshots:
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/offscreencanvas@2019.7.3': {}
|
||||
|
||||
'@types/qrcode@1.5.5':
|
||||
dependencies:
|
||||
'@types/node': 22.14.0
|
||||
@@ -6435,18 +6499,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@vite-pwa/assets-generator': 0.2.6
|
||||
|
||||
'@welshman/app@0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)':
|
||||
'@welshman/app@0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@types/throttle-debounce': 5.0.2
|
||||
'@welshman/dvm': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/feeds': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/relay': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/router': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/signer': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/store': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/feeds': 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/net': 0.4.0(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/relay': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/router': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/signer': 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/store': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
fuse.js: 7.1.0
|
||||
idb: 8.0.2
|
||||
svelte: 4.2.20
|
||||
@@ -6456,43 +6519,31 @@ snapshots:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/content@0.3.4(typescript@5.8.3)':
|
||||
'@welshman/content@0.4.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.1.1
|
||||
nostr-tools: 2.14.2(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/dvm@0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/signer': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- nostr-signer-capacitor-plugin
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/editor@0.3.4(@tiptap/extension-image@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(linkifyjs@4.3.1)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(typescript@5.8.3)':
|
||||
'@welshman/editor@0.4.0(@tiptap/extension-image@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(linkifyjs@4.3.1)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.39.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-code': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-code-block': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-document': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-dropcursor': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-gapcursor': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-hard-break': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-history': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-paragraph': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-placeholder': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-text': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-code': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-code-block': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-document': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-dropcursor': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-gapcursor': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-hard-break': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-history': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-paragraph': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-placeholder': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-text': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/pm': 2.12.0
|
||||
'@tiptap/suggestion': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
nostr-editor-coracle-workaround: 0.0.4-pre.18(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-image@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(linkifyjs@4.3.1)(nostr-tools@2.14.2(typescript@5.8.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))
|
||||
'@tiptap/suggestion': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
nostr-editor: 1.0.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-image@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(linkifyjs@4.3.1)(nostr-tools@2.14.2(typescript@5.8.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.39.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))
|
||||
nostr-tools: 2.14.2(typescript@5.8.3)
|
||||
tippy.js: 6.3.7
|
||||
transitivePeerDependencies:
|
||||
@@ -6502,82 +6553,82 @@ snapshots:
|
||||
- prosemirror-markdown
|
||||
- prosemirror-model
|
||||
- prosemirror-state
|
||||
- prosemirror-view
|
||||
- tiptap-markdown
|
||||
- typescript
|
||||
|
||||
'@welshman/feeds@0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)':
|
||||
'@welshman/feeds@0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/dvm': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/relay': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/router': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/signer': 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/net': 0.4.0(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/relay': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/router': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/signer': 0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
trava: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- nostr-signer-capacitor-plugin
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/lib@0.3.4':
|
||||
'@welshman/lib@0.4.0':
|
||||
dependencies:
|
||||
'@scure/base': 1.2.6
|
||||
'@types/events': 3.0.3
|
||||
events: 3.3.0
|
||||
|
||||
'@welshman/net@0.3.4(typescript@5.8.3)(ws@8.18.2)':
|
||||
'@welshman/net@0.4.0(typescript@5.8.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/relay': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/relay': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
events: 3.3.0
|
||||
isomorphic-ws: 5.0.0(ws@8.18.2)
|
||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/relay@0.3.4(typescript@5.8.3)':
|
||||
'@welshman/relay@0.4.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/router@0.3.4(typescript@5.8.3)':
|
||||
'@welshman/router@0.4.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/relay': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/relay': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/signer@0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2)':
|
||||
'@welshman/signer@0.4.0(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.2
|
||||
'@noble/hashes': 1.8.0
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/net': 0.4.0(typescript@5.8.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.2.0)
|
||||
nostr-tools: 2.14.2(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/store@0.3.4(typescript@5.8.3)':
|
||||
'@welshman/store@0.4.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/relay': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/util': 0.3.4(typescript@5.8.3)
|
||||
'@welshman/lib': 0.4.0
|
||||
'@welshman/relay': 0.4.0(typescript@5.8.3)
|
||||
'@welshman/util': 0.4.0(typescript@5.8.3)
|
||||
svelte: 4.2.20
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/util@0.3.4(typescript@5.8.3)':
|
||||
'@welshman/util@0.4.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@types/ws': 8.18.1
|
||||
'@welshman/lib': 0.3.4
|
||||
'@welshman/lib': 0.4.0
|
||||
js-base64: 3.7.7
|
||||
nostr-tools: 2.14.2(typescript@5.8.3)
|
||||
nostr-wasm: 0.1.0
|
||||
@@ -8075,9 +8126,9 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isomorphic-ws@5.0.0(ws@8.18.2):
|
||||
isomorphic-ws@5.0.0(ws@8.18.3):
|
||||
dependencies:
|
||||
ws: 8.18.2
|
||||
ws: 8.18.3
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
@@ -8409,11 +8460,11 @@ snapshots:
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
nostr-editor-coracle-workaround@0.0.4-pre.18(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-image@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(linkifyjs@4.3.1)(nostr-tools@2.14.2(typescript@5.8.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))):
|
||||
nostr-editor@1.0.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-image@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(linkifyjs@4.3.1)(nostr-tools@2.14.2(typescript@5.8.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.39.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-image': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-link': 2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/extension-image': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
'@tiptap/extension-link': 2.26.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
|
||||
'@tiptap/pm': 2.12.0
|
||||
js-base64: 3.7.7
|
||||
light-bolt11-decoder: 3.2.0
|
||||
@@ -8422,12 +8473,25 @@ snapshots:
|
||||
prosemirror-markdown: 1.13.2
|
||||
prosemirror-model: 1.25.1
|
||||
prosemirror-state: 1.4.3
|
||||
prosemirror-view: 1.39.3
|
||||
tiptap-markdown: 0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
|
||||
|
||||
nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0):
|
||||
dependencies:
|
||||
'@capacitor/core': 7.2.0
|
||||
|
||||
nostr-tools@2.12.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@noble/ciphers': 0.5.3
|
||||
'@noble/curves': 1.2.0
|
||||
'@noble/hashes': 1.3.1
|
||||
'@scure/base': 1.1.1
|
||||
'@scure/bip32': 1.3.1
|
||||
'@scure/bip39': 1.2.1
|
||||
optionalDependencies:
|
||||
nostr-wasm: 0.1.0
|
||||
typescript: 5.8.3
|
||||
|
||||
nostr-tools@2.14.2(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@noble/ciphers': 0.5.3
|
||||
@@ -8811,6 +8875,10 @@ snapshots:
|
||||
|
||||
q@1.5.1: {}
|
||||
|
||||
qr-scanner@1.4.2:
|
||||
dependencies:
|
||||
'@types/offscreencanvas': 2019.7.3
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
@@ -9924,7 +9992,7 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.18.2: {}
|
||||
ws@8.18.3: {}
|
||||
|
||||
xcode@3.0.1:
|
||||
dependencies:
|
||||
|
||||
+100
-75
@@ -1,6 +1,7 @@
|
||||
import {nwc} from "@getalby/sdk"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {get} from "svelte/store"
|
||||
import {randomId, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
||||
import {randomId, flatten, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
||||
import type {Feed} from "@welshman/feeds"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {
|
||||
@@ -12,14 +13,14 @@ import {
|
||||
FOLLOWS,
|
||||
REACTION,
|
||||
AUTH_JOIN,
|
||||
GROUP_JOIN,
|
||||
GROUP_LEAVE,
|
||||
GROUP_CREATE,
|
||||
GROUP_EDIT_META,
|
||||
GROUPS,
|
||||
ROOMS,
|
||||
COMMENT,
|
||||
ALERT_EMAIL,
|
||||
ALERT_WEB,
|
||||
ALERT_IOS,
|
||||
ALERT_ANDROID,
|
||||
isSignedEvent,
|
||||
createEvent,
|
||||
makeEvent,
|
||||
displayProfile,
|
||||
normalizeRelayUrl,
|
||||
makeList,
|
||||
@@ -55,11 +56,11 @@ import {
|
||||
getThunkError,
|
||||
} from "@welshman/app"
|
||||
import {
|
||||
tagRoom,
|
||||
wallet,
|
||||
getWebLn,
|
||||
PROTECTED,
|
||||
userMembership,
|
||||
INDEXER_RELAYS,
|
||||
ALERT,
|
||||
NOTIFIER_PUBKEY,
|
||||
NOTIFIER_RELAY,
|
||||
userRoomsByUrl,
|
||||
@@ -126,38 +127,10 @@ export const broadcastUserData = async (relays: string[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
// NIP 29 stuff
|
||||
|
||||
export const createRoom = (url: string, room: string) => {
|
||||
const event = createEvent(GROUP_CREATE, {tags: [tagRoom(room, url)]})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
}
|
||||
|
||||
export const editRoom = (url: string, room: string, meta: Record<string, string>) => {
|
||||
const event = createEvent(GROUP_EDIT_META, {
|
||||
tags: [tagRoom(room, url), ...Object.entries(meta)],
|
||||
})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
}
|
||||
|
||||
export const joinRoom = (url: string, room: string) => {
|
||||
const event = createEvent(GROUP_JOIN, {tags: [tagRoom(room, url)]})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
}
|
||||
|
||||
export const leaveRoom = (url: string, room: string) => {
|
||||
const event = createEvent(GROUP_LEAVE, {tags: [tagRoom(room, url)]})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
}
|
||||
|
||||
// List updates
|
||||
|
||||
export const addSpaceMembership = async (url: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const list = get(userMembership) || makeList({kind: ROOMS})
|
||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
@@ -165,7 +138,7 @@ export const addSpaceMembership = async (url: string) => {
|
||||
}
|
||||
|
||||
export const removeSpaceMembership = async (url: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const list = get(userMembership) || makeList({kind: ROOMS})
|
||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
@@ -174,7 +147,7 @@ export const removeSpaceMembership = async (url: string) => {
|
||||
}
|
||||
|
||||
export const addRoomMembership = async (url: string, room: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const list = get(userMembership) || makeList({kind: ROOMS})
|
||||
const newTags = [
|
||||
["r", url],
|
||||
["group", room, url],
|
||||
@@ -186,7 +159,7 @@ export const addRoomMembership = async (url: string, room: string) => {
|
||||
}
|
||||
|
||||
export const removeRoomMembership = async (url: string, room: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const list = get(userMembership) || makeList({kind: ROOMS})
|
||||
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
@@ -207,7 +180,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
||||
}
|
||||
|
||||
return publishThunk({
|
||||
event: createEvent(list.kind, {tags}),
|
||||
event: makeEvent(list.kind, {tags}),
|
||||
relays: [
|
||||
url,
|
||||
...INDEXER_RELAYS,
|
||||
@@ -229,7 +202,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
}
|
||||
|
||||
return publishThunk({
|
||||
event: createEvent(list.kind, {tags}),
|
||||
event: makeEvent(list.kind, {tags}),
|
||||
relays: [
|
||||
...INDEXER_RELAYS,
|
||||
...Router.get().FromUser().getUrls(),
|
||||
@@ -241,13 +214,18 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
|
||||
// Relay access
|
||||
|
||||
export const attemptAuth = (url: string) =>
|
||||
Pool.get()
|
||||
.get(url)
|
||||
.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
|
||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
await attemptAuth(url)
|
||||
|
||||
const thunk = publishThunk({
|
||||
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
||||
event: makeEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
||||
relays: [url],
|
||||
})
|
||||
|
||||
@@ -266,6 +244,9 @@ export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
// Ignore messages about the relay ignoring ours
|
||||
if (error?.startsWith("mute: ")) return
|
||||
|
||||
// Ignore rejected empty claims
|
||||
if (!claim && error?.includes("invite code")) return
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
@@ -297,7 +278,7 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
||||
const socket = Pool.get().get(url)
|
||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
await attemptAuth(url)
|
||||
|
||||
// Only raise an error if it's not a timeout.
|
||||
// If it is, odds are the problem is with our signer, not the relay
|
||||
@@ -324,20 +305,26 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
|
||||
// Actions
|
||||
|
||||
export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
||||
const tags = [["k", String(event.kind)], ...tagEvent(event)]
|
||||
export const makeDelete = ({event, tags = []}: {event: TrustedEvent; tags?: string[][]}) => {
|
||||
const thisTags = [["k", String(event.kind)], ...tagEvent(event), ...tags]
|
||||
const groupTag = getTag("h", event.tags)
|
||||
|
||||
if (groupTag) {
|
||||
tags.push(PROTECTED)
|
||||
tags.push(groupTag)
|
||||
thisTags.push(PROTECTED, groupTag)
|
||||
}
|
||||
|
||||
return createEvent(DELETE, {tags})
|
||||
return makeEvent(DELETE, {tags: thisTags})
|
||||
}
|
||||
|
||||
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
||||
publishThunk({event: makeDelete({event}), relays})
|
||||
export const publishDelete = ({
|
||||
relays,
|
||||
event,
|
||||
tags = [],
|
||||
}: {
|
||||
relays: string[]
|
||||
event: TrustedEvent
|
||||
tags?: string[][]
|
||||
}) => publishThunk({event: makeDelete({event, tags}), relays})
|
||||
|
||||
export type ReportParams = {
|
||||
event: TrustedEvent
|
||||
@@ -351,7 +338,7 @@ export const makeReport = ({event, reason, content}: ReportParams) => {
|
||||
["e", event.id, reason],
|
||||
]
|
||||
|
||||
return createEvent(REPORT, {content, tags})
|
||||
return makeEvent(REPORT, {content, tags})
|
||||
}
|
||||
|
||||
export const publishReport = ({
|
||||
@@ -378,7 +365,7 @@ export const makeReaction = ({content, event, tags: paramTags = []}: ReactionPar
|
||||
tags.push(groupTag)
|
||||
}
|
||||
|
||||
return createEvent(REACTION, {content, tags})
|
||||
return makeEvent(REACTION, {content, tags})
|
||||
}
|
||||
|
||||
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
||||
@@ -391,42 +378,64 @@ export type CommentParams = {
|
||||
}
|
||||
|
||||
export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
||||
createEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
|
||||
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
|
||||
|
||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||
publishThunk({event: makeComment(params), relays})
|
||||
|
||||
export type AlertParams = {
|
||||
feed: Feed
|
||||
cron: string
|
||||
email: string
|
||||
bunker: string
|
||||
secret: string
|
||||
description: string
|
||||
claims: Record<string, string>
|
||||
email?: {
|
||||
cron: string
|
||||
email: string
|
||||
handler: string[]
|
||||
}
|
||||
web?: {
|
||||
endpoint: string
|
||||
p256dh: string
|
||||
auth: string
|
||||
}
|
||||
ios?: {
|
||||
device_token: string
|
||||
bundle_identifier: string
|
||||
}
|
||||
android?: {
|
||||
device_token: string
|
||||
}
|
||||
}
|
||||
|
||||
export const makeAlert = async ({cron, email, feed, bunker, secret, description}: AlertParams) => {
|
||||
export const makeAlert = async (params: AlertParams) => {
|
||||
const tags = [
|
||||
["feed", JSON.stringify(feed)],
|
||||
["cron", cron],
|
||||
["email", email],
|
||||
["feed", JSON.stringify(params.feed)],
|
||||
["locale", LOCALE],
|
||||
["timezone", TIMEZONE],
|
||||
["description", description],
|
||||
["channel", "email"],
|
||||
[
|
||||
"handler",
|
||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||
"wss://relay.nostr.band/",
|
||||
"web",
|
||||
],
|
||||
["description", params.description],
|
||||
]
|
||||
|
||||
if (bunker) {
|
||||
tags.push(["nip46", secret, bunker])
|
||||
for (const [relay, claim] of Object.entries(params.claims)) {
|
||||
tags.push(["claim", relay, claim])
|
||||
}
|
||||
|
||||
return createEvent(ALERT, {
|
||||
let kind: number
|
||||
if (params.email) {
|
||||
kind = ALERT_EMAIL
|
||||
tags.push(...Object.entries(params.email).map(flatten))
|
||||
} else if (params.web) {
|
||||
kind = ALERT_WEB
|
||||
tags.push(...Object.entries(params.web).map(flatten))
|
||||
} else if (params.ios) {
|
||||
kind = ALERT_IOS
|
||||
tags.push(...Object.entries(params.ios).map(flatten))
|
||||
} else if (params.android) {
|
||||
kind = ALERT_ANDROID
|
||||
tags.push(...Object.entries(params.android).map(flatten))
|
||||
} else {
|
||||
throw new Error("Alert has invalid params")
|
||||
}
|
||||
|
||||
return makeEvent(kind, {
|
||||
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
|
||||
tags: [
|
||||
["d", randomId()],
|
||||
@@ -437,3 +446,19 @@ export const makeAlert = async ({cron, email, feed, bunker, secret, description}
|
||||
|
||||
export const publishAlert = async (params: AlertParams) =>
|
||||
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||
|
||||
export const payInvoice = async (invoice: string) => {
|
||||
const $wallet = get(wallet)
|
||||
|
||||
if (!$wallet) {
|
||||
throw new Error("No wallet is connected")
|
||||
}
|
||||
|
||||
if ($wallet.type === "nwc") {
|
||||
return new nwc.NWCClient($wallet.info).payInvoice({invoice})
|
||||
} else if ($wallet.type === "webln") {
|
||||
return getWebLn()
|
||||
.enable()
|
||||
.then(() => getWebLn().sendPayment(invoice))
|
||||
}
|
||||
}
|
||||
|
||||
+172
-106
@@ -1,24 +1,56 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||
import {
|
||||
displayRelayUrl,
|
||||
getTagValue,
|
||||
getAddress,
|
||||
THREAD,
|
||||
MESSAGE,
|
||||
EVENT_TIME,
|
||||
COMMENT,
|
||||
} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {pubkey, signer, getThunkError} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
|
||||
import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
|
||||
import {loadAlertStatuses} from "@app/requests"
|
||||
import {publishAlert} from "@app/commands"
|
||||
import {
|
||||
alerts,
|
||||
getMembershipUrls,
|
||||
userMembership,
|
||||
NOTIFIER_PUBKEY,
|
||||
NOTIFIER_RELAY,
|
||||
} from "@app/state"
|
||||
import {loadAlertStatuses, requestRelayClaim} from "@app/requests"
|
||||
import {publishAlert, attemptAuth} from "@app/commands"
|
||||
import type {AlertParams} from "@app/commands"
|
||||
import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/push"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
channel?: string
|
||||
notifyChat?: boolean
|
||||
notifyThreads?: boolean
|
||||
notifyCalendar?: boolean
|
||||
hideSpaceField?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
url = "",
|
||||
channel = "email",
|
||||
notifyChat = true,
|
||||
notifyThreads = true,
|
||||
notifyCalendar = true,
|
||||
hideSpaceField = false,
|
||||
}: Props = $props()
|
||||
|
||||
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
|
||||
const minute = randomInt(0, 59)
|
||||
@@ -26,49 +58,22 @@
|
||||
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||
const DAILY = `0 ${minute} ${hour} * * *`
|
||||
|
||||
let loading = false
|
||||
let cron = WEEKLY
|
||||
let email = $alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || ""
|
||||
let relay = ""
|
||||
let bunker = ""
|
||||
let secret = ""
|
||||
let notifyThreads = true
|
||||
let notifyCalendar = true
|
||||
let notifyChat = false
|
||||
let showBunker = false
|
||||
let loading = $state(false)
|
||||
let cron = $state(WEEKLY)
|
||||
let claim = $state("")
|
||||
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const controller = new BunkerConnectController({
|
||||
onNostrConnect: (response: Nip46ResponseWithResult) => {
|
||||
bunker = controller.broker.getBunkerUrl()
|
||||
secret = controller.broker.params.clientSecret
|
||||
showBunker = false
|
||||
},
|
||||
})
|
||||
|
||||
const connectBunker = () => {
|
||||
showBunker = true
|
||||
}
|
||||
|
||||
const hideBunker = () => {
|
||||
showBunker = false
|
||||
}
|
||||
|
||||
const clearBunker = () => {
|
||||
bunker = ""
|
||||
secret = ""
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!email.includes("@")) {
|
||||
if (channel === "email" && !email.includes("@")) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide an email address",
|
||||
})
|
||||
}
|
||||
|
||||
if (!relay) {
|
||||
if (!url) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please select a space",
|
||||
@@ -99,22 +104,69 @@
|
||||
|
||||
if (notifyChat) {
|
||||
display.push("chat")
|
||||
filters.push({
|
||||
kinds: [MESSAGE],
|
||||
"#h": getMembershipRoomsByUrl(relay, $userMembership),
|
||||
})
|
||||
filters.push({kinds: [MESSAGE]})
|
||||
}
|
||||
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
|
||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
||||
const thunk = await publishAlert({cron, email, feed, bunker, secret, description})
|
||||
const claims = claim ? {[url]: claim} : {}
|
||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
|
||||
const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
|
||||
const params: AlertParams = {feed, claims, description}
|
||||
|
||||
await thunk.result
|
||||
await loadAlertStatuses($pubkey!)
|
||||
if (channel === "email") {
|
||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||
|
||||
params.description = `${cadence} alert ${description}, sent via email.`
|
||||
params.email = {
|
||||
cron,
|
||||
email,
|
||||
handler: [
|
||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||
"wss://relay.nostr.band/",
|
||||
"web",
|
||||
],
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
params[platform] = await getPushInfo()
|
||||
params.description = `${platformName} push notification ${description}.`
|
||||
} catch (e: any) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: String(e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't do this we'll get an event rejection
|
||||
await attemptAuth(NOTIFIER_RELAY)
|
||||
|
||||
const thunk = await publishAlert(params)
|
||||
const error = await getThunkError(thunk)
|
||||
|
||||
if (error) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to send your alert to the notification server (${error}).`,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch our new status to make sure it's active
|
||||
const address = getAddress(thunk.event)
|
||||
const statusEvents = await loadAlertStatuses($pubkey!)
|
||||
const statusEvent = statusEvents.find(event => getTagValue("d", event.tags) === address)
|
||||
const statusTags = statusEvent
|
||||
? parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, statusEvent.content))
|
||||
: []
|
||||
const {status = "error", message = "Your alert was not activated"}: Record<string, string> =
|
||||
fromPairs(statusTags)
|
||||
|
||||
if (status === "error") {
|
||||
return pushToast({theme: "error", message})
|
||||
}
|
||||
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
back()
|
||||
@@ -122,6 +174,20 @@
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!canSendPushNotifications()) {
|
||||
channel = "email"
|
||||
}
|
||||
|
||||
if (url) {
|
||||
requestRelayClaim(url).then(code => {
|
||||
if (code) {
|
||||
claim = code
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
@@ -130,13 +196,20 @@
|
||||
Add an Alert
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if showBunker}
|
||||
<div class="card2 flex flex-col items-center gap-4 bg-base-300">
|
||||
<p>Scan using a nostr signer, or click to copy.</p>
|
||||
<BunkerConnect {controller} />
|
||||
<Button class="btn btn-neutral btn-sm" onclick={hideBunker}>Cancel</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if canSendPushNotifications()}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Alert Type*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={channel} class="select select-bordered">
|
||||
<option value="email">Email Digest</option>
|
||||
<option value="push">Push Notification</option>
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
{#if channel === "email"}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email Address*</p>
|
||||
@@ -158,12 +231,14 @@
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
{#if !hideSpaceField}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Space*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={relay} class="select select-bordered">
|
||||
<select bind:value={url} class="select select-bordered">
|
||||
<option value="" disabled selected>Choose a space URL</option>
|
||||
{#each getMembershipUrls($userMembership) as url (url)}
|
||||
<option value={url}>{displayRelayUrl(url)}</option>
|
||||
@@ -171,59 +246,50 @@
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Notifications*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||
Threads
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||
Calendar
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||
Chat
|
||||
</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<div class="card2 flex flex-col gap-3 bg-base-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong>Connect a Bunker</strong>
|
||||
<span class="flex items-center gap-2 text-sm" class:text-primary={bunker}>
|
||||
{#if bunker}
|
||||
<Icon icon="check-circle" size={5} />
|
||||
Connected
|
||||
{:else}
|
||||
<Icon icon="close-circle" size={5} />
|
||||
Not Connected
|
||||
{/if}
|
||||
{/if}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Notifications*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||
Threads
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||
Calendar
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||
Chat
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
Required for receiving alerts about spaces with access controls. You can get one from your
|
||||
<Button class="text-primary" onclick={() => pushModal(InfoBunker)}>remote signer app</Button
|
||||
>.
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Invite Code</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={claim} />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
To get notifications from private spaces, please provide an invite code which grants access
|
||||
to the space.
|
||||
</p>
|
||||
{#if bunker}
|
||||
<Button class="btn btn-neutral btn-sm flex-grow" onclick={clearBunker}>Disconnect</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-primary btn-sm w-full flex-grow" onclick={connectBunker}
|
||||
>Connect</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || showBunker}>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Confirm</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import type {Alert} from "@app/state"
|
||||
import {NOTIFIER_RELAY} from "@app/state"
|
||||
import {NOTIFIER_RELAY, NOTIFIER_PUBKEY} from "@app/state"
|
||||
import {publishDelete} from "@app/commands"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const confirm = () => {
|
||||
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY]})
|
||||
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY], tags: [["p", NOTIFIER_PUBKEY]]})
|
||||
pushToast({message: "Your alert has been deleted!"})
|
||||
history.back()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {parseJson, nthEq} from "@welshman/lib"
|
||||
import {parseJson} from "@welshman/lib"
|
||||
import {displayFeeds} from "@welshman/feeds"
|
||||
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||
import type {Alert} from "@app/state"
|
||||
import {alertStatuses} from "@app/state"
|
||||
import {deriveAlertStatus} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
@@ -15,8 +15,7 @@
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const address = $derived(getAddress(alert.event))
|
||||
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
||||
const status = deriveAlertStatus(getAddress(alert.event))
|
||||
const cron = $derived(getTagValue("cron", alert.tags))
|
||||
const channel = $derived(getTagValue("channel", alert.tags))
|
||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||
@@ -39,24 +38,24 @@
|
||||
</Button>
|
||||
<div class="flex-inline gap-1">{description}</div>
|
||||
</div>
|
||||
{#if status}
|
||||
{@const statusText = getTagValue("status", status.tags) || "error"}
|
||||
{#if $status}
|
||||
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||
{#if statusText === "ok"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||
data-tip={getTagValue("message", status.tags)}>
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Active
|
||||
</span>
|
||||
{:else if statusText === "pending"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||
data-tip={getTagValue("message", status.tags)}>
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Pending
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip={getTagValue("message", status.tags)}>
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import AlertItem from "@app/components/AlertItem.svelte"
|
||||
import {loadAlertStatuses, loadAlerts} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {alerts} from "@app/state"
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd)
|
||||
type Props = {
|
||||
url?: string
|
||||
channel?: string
|
||||
hideSpaceField?: boolean
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadAlertStatuses($pubkey!)
|
||||
loadAlerts($pubkey!)
|
||||
})
|
||||
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||
|
||||
const filteredAlerts = $derived(
|
||||
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||
@@ -29,10 +34,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#each $alerts as alert (alert.event.id)}
|
||||
{#each filteredAlerts as alert (alert.event.id)}
|
||||
<AlertItem {alert} />
|
||||
{:else}
|
||||
<p class="text-center opacity-75 py-12">No alerts found</p>
|
||||
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {Snippet} from "svelte"
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId, HOUR} from "@welshman/lib"
|
||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
||||
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
|
||||
const ed = await editor
|
||||
const event = createEvent(EVENT_TIME, {
|
||||
const event = makeEvent(EVENT_TIME, {
|
||||
content: ed.getText({blockSeparator: "\n"}).trim(),
|
||||
tags: [
|
||||
["d", initialValues?.d || randomId()],
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
|
||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
||||
import {colors} from "@app/state"
|
||||
import {colors, ENABLE_ZAPS} from "@app/state"
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
@@ -94,6 +95,9 @@
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ChannelMessageZapButton {url} {event} />
|
||||
{/if}
|
||||
<ChannelMessageEmojiButton {url} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {ENABLE_ZAPS} from "@app/state"
|
||||
import {publishReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
@@ -40,6 +42,12 @@
|
||||
<Icon size={4} icon="smile-circle" />
|
||||
Send Reaction
|
||||
</Button>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ZapButton replaceState {url} {event} class="btn btn-secondary w-full">
|
||||
<Icon size={4} icon="bolt" />
|
||||
Send Zap
|
||||
</ZapButton>
|
||||
{/if}
|
||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
Send Reply
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
|
||||
const {url, event} = $props()
|
||||
</script>
|
||||
|
||||
<ZapButton {url} {event} class="btn join-item btn-xs">
|
||||
<Icon icon="bolt" size={4} />
|
||||
</ZapButton>
|
||||
@@ -16,7 +16,7 @@
|
||||
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||
import {parse, isLink} from "@welshman/content"
|
||||
import {
|
||||
createEvent,
|
||||
makeEvent,
|
||||
tagsFromIMeta,
|
||||
getTags,
|
||||
DIRECT_MESSAGE,
|
||||
@@ -97,7 +97,7 @@
|
||||
content = content.trim()
|
||||
|
||||
if (content) {
|
||||
templates.push(createEvent(kind, {content, tags: [...tags, ...ptags]}))
|
||||
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
import {makeThreadPath} from "@app/routes"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
event: any
|
||||
showActivity?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showActivity = false}: Props = $props()
|
||||
|
||||
const path = makeThreadPath(url, event.id)
|
||||
|
||||
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
|
||||
|
||||
const createReaction = (template: EventContent) =>
|
||||
publishReaction({...template, event, relays: [url]})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {ellipsize, postJson} from "@welshman/lib"
|
||||
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||
import {dufflepud, imgproxy} from "@app/state"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
@@ -52,15 +52,13 @@
|
||||
alt="Link preview"
|
||||
onerror={onError}
|
||||
src={imgproxy(preview.image)}
|
||||
class="bg-alt max-h-72 object-contain object-center" />
|
||||
{/if}
|
||||
{#if preview.title}
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>{preview.title}</strong>
|
||||
<p>{ellipsize(preview.description, 140)}</p>
|
||||
</div>
|
||||
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>{preview.title || displayUrl(url)}</strong>
|
||||
<p>{ellipsize(preview.description, 140)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:catch}
|
||||
<p class="bg-alt p-12 text-center leading-normal">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import type {ProfilePointer} from "@welshman/content"
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -14,11 +13,11 @@
|
||||
|
||||
const {value, url}: Props = $props()
|
||||
|
||||
const profile = deriveProfile(value.pubkey, removeNil([url]))
|
||||
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||
</script>
|
||||
|
||||
<Button onclick={openProfile} class="link-content">
|
||||
@{displayProfile($profile)}
|
||||
@{$display}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {goToEvent} from "@app/routes"
|
||||
import {displayChannel} from "@app/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
events: TrustedEvent[]
|
||||
latest: TrustedEvent
|
||||
earliest: TrustedEvent
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
const {url, room, events, latest, earliest, participants}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
{#if room}
|
||||
<span class="font-medium text-blue-400">
|
||||
#{displayChannel(url, room)}
|
||||
</span>
|
||||
<span class="opacity-50">•</span>
|
||||
{/if}
|
||||
<span>{formatTimestamp(earliest.created_at)}</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-13 flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<span class="text-sm opacity-70">
|
||||
{events.length}
|
||||
{events.length === 1 ? "message" : "messages"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ProfileCircles pubkeys={participants} size={6} />
|
||||
<span class="text-sm opacity-70">
|
||||
{participants.length}
|
||||
{participants.length === 1 ? "participant" : "participants"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if latest !== earliest}
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(latest)}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
<ProfileCircle pubkey={latest.pubkey} size={5} />
|
||||
<span class="font-medium">Latest reply:</span>
|
||||
</div>
|
||||
<span class="text-xs opacity-50">
|
||||
{formatTimestamp(latest.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -6,18 +6,21 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import EventMenu from "@app/components/EventMenu.svelte"
|
||||
import {ENABLE_ZAPS} from "@app/state"
|
||||
import {publishReaction} from "@app/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
noun: string
|
||||
event: TrustedEvent
|
||||
hideZap?: boolean
|
||||
customActions?: Snippet
|
||||
}
|
||||
|
||||
const {url, noun, event, customActions}: Props = $props()
|
||||
const {url, noun, event, hideZap, customActions}: Props = $props()
|
||||
|
||||
const showPopover = () => popover?.show()
|
||||
|
||||
@@ -30,6 +33,11 @@
|
||||
</script>
|
||||
|
||||
<Button class="join rounded-full">
|
||||
{#if ENABLE_ZAPS && !hideZap}
|
||||
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon="bolt" size={4} />
|
||||
</ZapButton>
|
||||
{/if}
|
||||
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</EmojiButton>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
import {makeGoalPath} from "@app/routes"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
event: any
|
||||
showActivity?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showActivity = false}: Props = $props()
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
|
||||
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
|
||||
|
||||
const createReaction = (template: EventContent) =>
|
||||
publishReaction({...template, event, relays: [url]})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {PROTECTED} from "@app/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
|
||||
if (!content) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide a title for your funding goal.",
|
||||
})
|
||||
}
|
||||
|
||||
const ed = await editor
|
||||
const summary = ed.getText({blockSeparator: "\n"}).trim()
|
||||
|
||||
if (!summary.trim()) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide details about your funding goal.",
|
||||
})
|
||||
}
|
||||
|
||||
const tags = [
|
||||
...ed.storage.nostr.getEditorTags(),
|
||||
["summary", summary],
|
||||
["amount", String(amount)],
|
||||
["relays", url],
|
||||
PROTECTED,
|
||||
]
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(ZAP_GOAL, {content, tags}),
|
||||
})
|
||||
|
||||
history.back()
|
||||
}
|
||||
|
||||
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
|
||||
|
||||
let content = $state("")
|
||||
let amount = $state(1000)
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Funding Goal</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Request contributions for your fundraiser.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="col-8 relative">
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Title*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus={!isMobile}
|
||||
bind:value={content}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="What do funds go towards?" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<div class="relative">
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Details*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="note-editor flex-grow overflow-hidden">
|
||||
<EditorContent {editor} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||
onclick={selectFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="paperclip" size={3} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
Goal Amount (sats)*
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex flex-grow justify-end">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<Icon icon="bolt" />
|
||||
<input bind:value={amount} type="number" class="w-28" />
|
||||
<p class="opacity-50">sats</p>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<input
|
||||
class="range range-primary -mt-2"
|
||||
type="range"
|
||||
min="1000"
|
||||
max="100000"
|
||||
step="1000"
|
||||
bind:value={amount} />
|
||||
</div>
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">Create Goal</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import {makeGoalPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const summary = getTagValue("summary", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||
<p class="text-2xl">{event.content}</p>
|
||||
<Content
|
||||
event={{content: summary, tags: event.tags}}
|
||||
{url}
|
||||
expandMode="inline"
|
||||
minLength={50}
|
||||
maxLength={300} />
|
||||
<GoalSummary {url} {event} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} />
|
||||
</div>
|
||||
</Link>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import {now, DAY, uniq, sum} from "@welshman/lib"
|
||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
import {repository, getValidZap} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||
itemToEvent: item => item.response,
|
||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||
})
|
||||
|
||||
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
||||
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
||||
const contributorsCount = $derived(uniq($zaps.map(zap => zap.request.pubkey)).length)
|
||||
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-8">
|
||||
<div class="flex gap-8">
|
||||
<div>
|
||||
<p class="text-xl text-primary">{zapAmount} sats</p>
|
||||
<p class="text-sm opacity-75">funded of {goalAmount} sats</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xl">{contributorsCount}</p>
|
||||
<p class="text-sm opacity-75">{contributorsCount === 1 ? "contributor" : "contributors"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xl">{daysOld}</p>
|
||||
<p class="text-sm opacity-75">{daysOld === 1 ? "day" : "days"} old</p>
|
||||
</div>
|
||||
</div>
|
||||
<progress class="progress progress-primary" value={zapAmount} max={goalAmount}></progress>
|
||||
<ZapButton {url} {event} class="btn btn-primary lg:m-auto lg:px-20">
|
||||
<Icon icon="bolt" />
|
||||
Contribute to this goal
|
||||
</ZapButton>
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import {PLATFORM_NAME} from "@app/state"
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Where did my rooms go?</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
You might have noticed that old rooms have disappeared from navigation. {PLATFORM_NAME} is still
|
||||
under heavy development, which means that we occasionally have to make breaking changes. In this
|
||||
case, we've changed how rooms work in {PLATFORM_NAME} to be more fully compatible with other NIP
|
||||
29 clients, like <Link external class="link" href="https://chachi.chat">Chachi</Link> and
|
||||
<Link external class="link" href="https://0xchat.com">0xChat</Link>.
|
||||
</p>
|
||||
<p>
|
||||
If you run a relay, please upgrade to a version that supports NIP 29. {PLATFORM_NAME} works best
|
||||
with the latest version of <Link
|
||||
external
|
||||
class="link"
|
||||
href="https://github.com/coracle-social/frith">Frith</Link
|
||||
>, which will automatically migrate your rooms. In the meantime, your messages are all still
|
||||
available under the "Chat" tab (all conversations have been temporarily merged together).
|
||||
</p>
|
||||
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import {deriveZapperForPubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
||||
const {pubkey} = $props()
|
||||
|
||||
const zapper = deriveZapperForPubkey(pubkey)
|
||||
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Unable to Zap</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
Zapping <ProfileLink {pubkey} class="!text-primary" /> isn't possible right now because
|
||||
{#if $zapper}
|
||||
their zap receiver isn't correctly set up.
|
||||
{:else}
|
||||
they don't currently have a zap receiver set up.
|
||||
{/if}
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
@@ -33,18 +33,20 @@
|
||||
const onSubmit = async () => {
|
||||
if (controller.loading) return
|
||||
|
||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(controller.bunker)
|
||||
|
||||
if (!signerPubkey || relays.length === 0) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, it looks like that's an invalid bunker link.",
|
||||
})
|
||||
}
|
||||
|
||||
controller.loading = true
|
||||
|
||||
try {
|
||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(controller.bunker)
|
||||
|
||||
console.log({signerPubkey, connectSecret, relays})
|
||||
|
||||
if (!signerPubkey || relays.length === 0) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, it looks like that's an invalid bunker link.",
|
||||
})
|
||||
}
|
||||
|
||||
controller.loading = true
|
||||
|
||||
const {clientSecret} = controller
|
||||
const broker = new Nip46Broker({relays, clientSecret, signerPubkey})
|
||||
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
||||
@@ -64,6 +66,13 @@
|
||||
message: "Something went wrong, please try again!",
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Something went wrong, please try again!",
|
||||
})
|
||||
} finally {
|
||||
controller.loading = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {displayRelayUrl, getTagValue} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -13,16 +13,19 @@
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import ProfileList from "@app/components/ProfileList.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import Alerts from "@app/components/Alerts.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
|
||||
import {
|
||||
ENABLE_ZAPS,
|
||||
userRoomsByUrl,
|
||||
hasMembershipUrl,
|
||||
memberships,
|
||||
deriveUserRooms,
|
||||
deriveOtherRooms,
|
||||
hasNip29,
|
||||
alerts,
|
||||
} from "@app/state"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -32,10 +35,12 @@
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
const chatPath = makeSpacePath(url, "chat")
|
||||
const goalsPath = makeSpacePath(url, "goals")
|
||||
const threadsPath = makeSpacePath(url, "threads")
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||
|
||||
const openMenu = () => {
|
||||
showMenu = true
|
||||
@@ -45,8 +50,6 @@
|
||||
showMenu = !showMenu
|
||||
}
|
||||
|
||||
const showMissingRooms = () => pushModal(InfoMissingRooms)
|
||||
|
||||
const showMembers = () =>
|
||||
pushModal(
|
||||
ProfileList,
|
||||
@@ -62,6 +65,13 @@
|
||||
|
||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||
|
||||
const manageAlerts = () => {
|
||||
const component = hasAlerts ? Alerts : AlertAdd
|
||||
const params = {url, channel: "push", hideSpaceField: true}
|
||||
|
||||
pushModal(component, params, {replaceState})
|
||||
}
|
||||
|
||||
let showMenu = $state(false)
|
||||
let replaceState = $state(false)
|
||||
let element: Element | undefined = $state()
|
||||
@@ -75,18 +85,20 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={element}>
|
||||
<SecondaryNavSection class="max-h-screen">
|
||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||
<SecondaryNavSection>
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
||||
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
|
||||
<strong class="ellipsize flex items-center gap-3">
|
||||
{displayRelayUrl(url)}
|
||||
</strong>
|
||||
<Icon icon="alt-arrow-down" />
|
||||
</SecondaryNavItem>
|
||||
{#if showMenu}
|
||||
<Popover hideOnClick onClose={toggleMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
||||
<li>
|
||||
<Button onclick={showMembers}>
|
||||
<Icon icon="user-rounded" />
|
||||
@@ -116,10 +128,18 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-col gap-1 overflow-auto">
|
||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
||||
<Icon icon="home-smile" /> Home
|
||||
</SecondaryNavItem>
|
||||
{#if ENABLE_ZAPS}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={goalsPath}
|
||||
notification={$notifications.has(goalsPath)}>
|
||||
<Icon icon="star-fall-minimalistic-2" /> Goals
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
@@ -164,11 +184,13 @@
|
||||
notification={$notifications.has(chatPath)}>
|
||||
<Icon icon="chat-round" /> Chat
|
||||
</SecondaryNavItem>
|
||||
<Button class="link flex items-center gap-2 py-2 pl-4 text-sm" onclick={showMissingRooms}>
|
||||
<Icon icon="info-circle" size={4} />
|
||||
Where did my rooms go?
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
<div class="p-4">
|
||||
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
|
||||
<Icon icon="bell" />
|
||||
Manage Alerts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import {formatTimestampRelative} from "@welshman/lib"
|
||||
import {NOTE, GROUPS, MESSAGE, THREAD, COMMENT, getRelayTags, getListTags} from "@welshman/util"
|
||||
import {NOTE, ROOMS, MESSAGE, THREAD, COMMENT, getRelayTags, getListTags} from "@welshman/util"
|
||||
import {repository, loadRelaySelections} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
||||
@@ -35,7 +35,7 @@
|
||||
// Load groups and at least one note, regardless of time frame
|
||||
load({
|
||||
filters: [
|
||||
{authors: [pubkey], kinds: [GROUPS]},
|
||||
{authors: [pubkey], kinds: [ROOMS]},
|
||||
{authors: [pubkey], limit: 1, kinds: [NOTE, MESSAGE, THREAD, COMMENT]},
|
||||
],
|
||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {chunk, sleep, uniq} from "@welshman/lib"
|
||||
import {
|
||||
createEvent,
|
||||
makeEvent,
|
||||
createProfile,
|
||||
PROFILE,
|
||||
DELETE,
|
||||
@@ -36,8 +36,8 @@
|
||||
}
|
||||
|
||||
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
|
||||
const profileEvent = createEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||
const vanishEvent = createEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||
const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||
const denominator = chunks.length + 2
|
||||
const relays = uniq([
|
||||
...INDEXER_RELAYS,
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
await publishThunk({relays, event: createEvent(DELETE, {tags})})
|
||||
await publishThunk({relays, event: makeEvent(DELETE, {tags})})
|
||||
|
||||
await incrementProgress()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {nthNe} from "@welshman/lib"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {
|
||||
getTag,
|
||||
createEvent,
|
||||
makeEvent,
|
||||
makeProfile,
|
||||
editProfile,
|
||||
createProfile,
|
||||
@@ -24,16 +25,19 @@
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
|
||||
const router = Router.get()
|
||||
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||
const relays = [...getMembershipUrls($userMembership)]
|
||||
const scenarios = [router.FromRelays(getMembershipUrls($userMembership))]
|
||||
|
||||
if (shouldBroadcast) {
|
||||
relays.push(...Router.get().FromUser().getUrls())
|
||||
scenarios.push(router.FromUser(), router.Index())
|
||||
template.tags = template.tags.filter(nthNe(0, "-"))
|
||||
} else {
|
||||
template.tags = uniqTags([...template.tags, PROTECTED])
|
||||
}
|
||||
|
||||
const event = createEvent(template.kind, template)
|
||||
const event = makeEvent(template.kind, template)
|
||||
const relays = router.merge(scenarios).getUrls()
|
||||
|
||||
publishThunk({event, relays})
|
||||
pushToast({message: "Your profile has been updated!"})
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
type Props = {
|
||||
pubkey: string
|
||||
url?: string
|
||||
class?: string
|
||||
unstyled?: boolean
|
||||
}
|
||||
|
||||
const {pubkey, url, unstyled}: Props = $props()
|
||||
const {pubkey, url, unstyled, ...props}: Props = $props()
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
||||
<Button onclick={preventDefault(openProfile)} class={cx({"link-content": !unstyled})}>
|
||||
<Button onclick={preventDefault(openProfile)} class={cx(props.class, {"link-content": !unstyled})}>
|
||||
@<ProfileName {pubkey} {url} />
|
||||
</Button>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Snippet} from "svelte"
|
||||
import {groupBy, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
||||
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
||||
import {
|
||||
REACTION,
|
||||
ZAP_RESPONSE,
|
||||
getReplyFilters,
|
||||
getEmojiTags,
|
||||
getEmojiTag,
|
||||
fromMsats,
|
||||
getTag,
|
||||
REPORT,
|
||||
DELETE,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
||||
import {deriveEvents, deriveEventsMapped} from "@welshman/store"
|
||||
import {load} from "@welshman/net"
|
||||
import {pubkey, repository, displayProfileByPubkey} from "@welshman/app"
|
||||
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
|
||||
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Reaction from "@app/components/Reaction.svelte"
|
||||
import EventReportDetails from "@app/components/EventReportDetails.svelte"
|
||||
import {REACTION_KINDS} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
interface Props {
|
||||
@@ -49,6 +52,12 @@
|
||||
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
||||
})
|
||||
|
||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||
itemToEvent: item => item.response,
|
||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||
})
|
||||
|
||||
const onReactionClick = (events: TrustedEvent[]) => {
|
||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||
|
||||
@@ -77,6 +86,8 @@
|
||||
),
|
||||
)
|
||||
|
||||
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps))
|
||||
|
||||
onMount(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -84,7 +95,7 @@
|
||||
load({
|
||||
relays: [url],
|
||||
signal: controller.signal,
|
||||
filters: getReplyFilters([event], {kinds: [REACTION, REPORT, DELETE]}),
|
||||
filters: getReplyFilters([event], {kinds: [REPORT, DELETE, ...REACTION_KINDS]}),
|
||||
onEvent: batch(300, (events: TrustedEvent[]) => {
|
||||
load({
|
||||
relays: [url],
|
||||
@@ -100,7 +111,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $reactions.length > 0 || $reports.length > 0}
|
||||
{#if $reactions.length > 0 || $zaps.length || $reports.length > 0}
|
||||
<div class="flex min-w-0 flex-wrap gap-2">
|
||||
{#if url && $reports.length > 0}
|
||||
<button
|
||||
@@ -113,6 +124,24 @@
|
||||
<span>{$reports.length}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#each groupedZaps.entries() as [key, zaps]}
|
||||
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
|
||||
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
|
||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||
{@const tooltip = `${info} zapped`}
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:border={isOwn}
|
||||
class:border-solid={isOwn}
|
||||
class:border-primary={isOwn}>
|
||||
<Reaction event={zaps[0].request} />
|
||||
<span>{amount}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{#each groupedReactions.entries() as [key, events]}
|
||||
{@const pubkeys = events.map(e => e.pubkey)}
|
||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay, getThunkError} from "@welshman/app"
|
||||
import {uniqBy, nth} from "@welshman/lib"
|
||||
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||
import {deriveRelay, getThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -11,25 +11,26 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {hasNip29, loadChannel} from "@app/state"
|
||||
import {createRoom, editRoom, joinRoom} from "@app/commands"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const room = randomId()
|
||||
const room = makeRoomMeta()
|
||||
const relay = deriveRelay(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const tryCreate = async () => {
|
||||
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
|
||||
|
||||
const createMessage = await getThunkError(createRoom(url, room))
|
||||
|
||||
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editMessage = await getThunkError(editRoom(url, room, {name}))
|
||||
const editMessage = await getThunkError(editRoom(url, room))
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
@@ -41,9 +42,9 @@
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
await loadChannel(url, room)
|
||||
await loadChannel(url, room.id)
|
||||
|
||||
goto(makeSpacePath(url, room))
|
||||
goto(makeSpacePath(url, room.id))
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {PROFILE, createProfile, makeProfile, createEvent} from "@welshman/util"
|
||||
import {PROFILE, createProfile, makeProfile, makeEvent} from "@welshman/util"
|
||||
import {loginWithNip01, publishThunk} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
|
||||
const event = createEvent(PROFILE, createProfile(profile))
|
||||
const event = makeEvent(PROFILE, createProfile(profile))
|
||||
const relays = shouldBroadcast ? INDEXER_RELAYS : []
|
||||
|
||||
loginWithNip01(secret)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {clearModals} from "@app/modal"
|
||||
import {addSpaceMembership} from "@app/commands"
|
||||
import {addSpaceMembership, broadcastUserData} from "@app/commands"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
const tryJoin = async () => {
|
||||
await addSpaceMembership(url)
|
||||
|
||||
broadcastUserData([url])
|
||||
clearModals()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
<script module lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {ROOM_META} from "@welshman/util"
|
||||
import {load} from "@welshman/net"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {addSpaceMembership} from "@app/commands"
|
||||
import {addSpaceMembership, broadcastUserData} from "@app/commands"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
export const confirmSpaceJoin = async (url: string) => {
|
||||
await addSpaceMembership(url)
|
||||
|
||||
goto(makeSpacePath(url), {replaceState: true})
|
||||
const path = makeSpacePath(url)
|
||||
|
||||
if (window.location.pathname === path) {
|
||||
load({
|
||||
relays: [url],
|
||||
filters: [{kinds: [ROOM_META]}],
|
||||
})
|
||||
}
|
||||
|
||||
broadcastUserData([url])
|
||||
|
||||
goto(path, {replaceState: true})
|
||||
|
||||
pushToast({
|
||||
message: "Welcome to the space!",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
|
||||
import {makeThreadPath, makeCalendarPath, makeRoomPath, makeSpacePath} from "@app/routes"
|
||||
import {
|
||||
hasNip29,
|
||||
deriveUserRooms,
|
||||
@@ -25,6 +25,7 @@
|
||||
const relay = deriveRelay(url)
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const chatPath = makeSpacePath(url, "chat")
|
||||
const threadsPath = makeThreadPath(url)
|
||||
const calendarPath = makeCalendarPath(url)
|
||||
|
||||
@@ -76,34 +77,46 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
{#if $userRooms.length + $otherRooms.length > 10}
|
||||
<label class="input input-sm input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="magnifer" size={4} />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search rooms..." />
|
||||
</label>
|
||||
{/if}
|
||||
{#each filteredRooms() as room (room)}
|
||||
{@const roomPath = makeRoomPath(url, room)}
|
||||
{@const channel = $channelsById.get(makeChannelId(url, room))}
|
||||
<Link href={roomPath} class="btn btn-neutral btn-sm relative w-full justify-start">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
|
||||
{#if channel?.closed || channel?.private}
|
||||
<Icon icon="lock" size={4} />
|
||||
{:else}
|
||||
<Icon icon="hashtag" />
|
||||
{/if}
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
{#if $notifications.has(roomPath)}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||
{/if}
|
||||
</Link>
|
||||
{/each}
|
||||
{#if hasNip29($relay)}
|
||||
{#if $userRooms.length + $otherRooms.length > 10}
|
||||
<label class="input input-sm input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon="magnifer" size={4} />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search rooms..." />
|
||||
</label>
|
||||
{/if}
|
||||
{#each filteredRooms() as room (room)}
|
||||
{@const roomPath = makeRoomPath(url, room)}
|
||||
{@const channel = $channelsById.get(makeChannelId(url, room))}
|
||||
<Link href={roomPath} class="btn btn-neutral btn-sm relative w-full justify-start">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
|
||||
{#if channel?.closed || channel?.private}
|
||||
<Icon icon="lock" size={4} />
|
||||
{:else}
|
||||
<Icon icon="hashtag" />
|
||||
{/if}
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
{#if $notifications.has(roomPath)}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</Link>
|
||||
{/each}
|
||||
<Button onclick={addRoom} class="btn btn-neutral btn-sm w-full justify-start">
|
||||
<Icon icon="add-circle" />
|
||||
Create Room
|
||||
</Button>
|
||||
{:else}
|
||||
<Link href={chatPath} class="btn btn-neutral w-full justify-start">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<Icon icon="chat-round" />
|
||||
Chat
|
||||
{#if $notifications.has(chatPath)}
|
||||
<div class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-primary" transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {groupBy, ago, MONTH, first, last, uniq, avg, overlappingPairs} from "@welshman/lib"
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
import {MESSAGE, getTagValue} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import ConversationCard from "@app/components/ConversationCard.svelte"
|
||||
import {deriveEventsForUrl} from "@app/state"
|
||||
import {goToEvent} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -76,54 +72,25 @@
|
||||
</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if $conversations.length === 0}
|
||||
<div class="py-8 text-center opacity-70">
|
||||
<p>No recent conversations</p>
|
||||
</div>
|
||||
{#if $messages.length > 0}
|
||||
{@const events = $messages.slice(0, 1)}
|
||||
{@const event = events[0]}
|
||||
{@const room = getTagValue("h", event.tags)}
|
||||
<ConversationCard
|
||||
{url}
|
||||
{room}
|
||||
{events}
|
||||
latest={event}
|
||||
earliest={event}
|
||||
participants={[event.pubkey]} />
|
||||
{:else}
|
||||
<div class="py-8 text-center opacity-70">
|
||||
<p>No recent conversations</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)}
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
<span class="font-medium text-blue-400">#{room}</span>
|
||||
<span class="opacity-50">•</span>
|
||||
<span>{formatTimestamp(earliest.created_at)}</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-13 flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<span class="text-sm opacity-70">
|
||||
{events.length} messages
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ProfileCircles pubkeys={participants} size={6} />
|
||||
<span class="text-sm opacity-70">
|
||||
{participants.length} participants
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(latest)}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
<ProfileCircle pubkey={latest.pubkey} size={5} />
|
||||
<span class="font-medium">Latest reply:</span>
|
||||
</div>
|
||||
<span class="text-xs opacity-50">
|
||||
{formatTimestamp(latest.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Button>
|
||||
<ConversationCard {url} {room} {events} {latest} {earliest} {participants} />
|
||||
{/each}
|
||||
{#if $conversations.length > limit}
|
||||
<Button class="btn btn-primary" onclick={viewMore}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import {createEvent, THREAD} from "@welshman/util"
|
||||
import {makeEvent, THREAD} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: createEvent(THREAD, {content, tags}),
|
||||
event: makeEvent(THREAD, {content, tags}),
|
||||
})
|
||||
|
||||
history.back()
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {nwc} from "@getalby/sdk"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Scanner from "@lib/components/Scanner.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import type {NWCInfo} from "@app/state"
|
||||
import {wallet, getWebLn} from "@app/state"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const connectWithWebLn = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await Promise.all([sleep(800), getWebLn().enable()])
|
||||
const info = await getWebLn().getInfo()
|
||||
|
||||
if (!info?.supports?.includes("lightning")) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Your extension does not support lightning payments",
|
||||
})
|
||||
} else {
|
||||
wallet.set({type: "webln", info})
|
||||
pushToast({message: "Wallet successfully connected!"})
|
||||
|
||||
await sleep(400)
|
||||
|
||||
back()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Wallet failed to connect",
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const connectWithNWC = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const client = new nwc.NWCClient({nostrWalletConnectUrl})
|
||||
const [_, info] = await Promise.all([sleep(800), client.getInfo()])
|
||||
|
||||
if (!info) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Wallet failed to connect",
|
||||
})
|
||||
} else {
|
||||
wallet.set({type: "nwc", info: client.options as unknown as NWCInfo})
|
||||
pushToast({message: "Wallet successfully connected!"})
|
||||
|
||||
await sleep(400)
|
||||
|
||||
back()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Wallet failed to connect",
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleScanner = () => {
|
||||
showScanner = !showScanner
|
||||
}
|
||||
|
||||
const onScan = debounce(1000, async (data: string) => {
|
||||
showScanner = false
|
||||
nostrWalletConnectUrl = data
|
||||
await connectWithNWC()
|
||||
})
|
||||
|
||||
let nostrWalletConnectUrl = $state("")
|
||||
let showScanner = $state(false)
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Connect a Wallet</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
Use Nostr Wallet Connect to send Bitcoin payments over Lightning.
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if getWebLn()}
|
||||
<Button
|
||||
class="btn btn-primary"
|
||||
disabled={Boolean(nostrWalletConnectUrl || loading)}
|
||||
onclick={connectWithWebLn}>
|
||||
<Spinner loading={!nostrWalletConnectUrl && loading}>
|
||||
{#if !nostrWalletConnectUrl && loading}
|
||||
Connecting...
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="cpu" />
|
||||
Connect with WebLN
|
||||
</div>
|
||||
{/if}
|
||||
</Spinner>
|
||||
</Button>
|
||||
<Divider>Or</Divider>
|
||||
{/if}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
Connection Secret*
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="lock" />
|
||||
<input
|
||||
bind:value={nostrWalletConnectUrl}
|
||||
autocomplete="off"
|
||||
name="flotilla-nwc"
|
||||
class="grow"
|
||||
type="password" />
|
||||
<Button onclick={toggleScanner}>
|
||||
<Icon icon="qr-code" />
|
||||
</Button>
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
You can find this in any wallet that supports
|
||||
<Link external href="https://nwc.getalby.com/about" class="text-primary"
|
||||
>Nostr Wallet Connect</Link
|
||||
>.
|
||||
{/snippet}
|
||||
</Field>
|
||||
{#if showScanner}
|
||||
<Scanner onscan={onScan} />
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button
|
||||
class="btn btn-primary"
|
||||
disabled={!nostrWalletConnectUrl || loading}
|
||||
onclick={connectWithNWC}>
|
||||
<Spinner loading={Boolean(nostrWalletConnectUrl && loading)}>
|
||||
{#if nostrWalletConnectUrl && loading}
|
||||
Connecting...
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
Connect Wallet
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</div>
|
||||
{/if}
|
||||
</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import {wallet} from "@app/state"
|
||||
import {clearModals} from "@app/modal"
|
||||
|
||||
const confirm = async () => {
|
||||
wallet.set(undefined)
|
||||
|
||||
clearModals()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm
|
||||
{confirm}
|
||||
title="Disconnect Wallet"
|
||||
message="Are you sure you want to disconnect your bitcoin wallet?" />
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import {signer, deriveZapperForPubkey} from "@welshman/app"
|
||||
import {load} from "@welshman/net"
|
||||
import {Router} from "@welshman/router"
|
||||
import {requestZap, makeZapRequest, getZapResponseFilter} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import {payInvoice} from "@app/commands"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
pubkey: string
|
||||
eventId?: string
|
||||
}
|
||||
|
||||
const {url, pubkey, eventId}: Props = $props()
|
||||
|
||||
const minPos = 1
|
||||
const maxPos = 1000
|
||||
const minVal = 21
|
||||
const maxVal = 1000000
|
||||
const zapperStore = deriveZapperForPubkey(pubkey)
|
||||
|
||||
const posToAmount = (pos: number) => {
|
||||
const normalizedPos = (pos - minPos) / (maxPos - minPos)
|
||||
const logMin = Math.log(minVal)
|
||||
const logMax = Math.log(maxVal)
|
||||
const logValue = logMin + normalizedPos * (logMax - logMin)
|
||||
return Math.round(Math.exp(logValue))
|
||||
}
|
||||
|
||||
const amountToPos = (amount: number) => {
|
||||
const clampedAmount = Math.max(minVal, Math.min(maxVal, amount))
|
||||
const logMin = Math.log(minVal)
|
||||
const logMax = Math.log(maxVal)
|
||||
const logValue = Math.log(clampedAmount)
|
||||
const normalizedPos = (logValue - logMin) / (logMax - logMin)
|
||||
return Math.round(minPos + normalizedPos * (maxPos - minPos))
|
||||
}
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onEmoji = (emoji: NativeEmoji) => {
|
||||
content = emoji.unicode
|
||||
}
|
||||
|
||||
const sendZap = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const zapper = $zapperStore!
|
||||
const msats = amount * 1000
|
||||
const relays = url ? [url] : Router.get().ForPubkey(pubkey).getUrls()
|
||||
const filters = [getZapResponseFilter({zapper, pubkey, eventId})]
|
||||
const params = {pubkey, content, eventId, msats, relays, zapper}
|
||||
const event = await $signer!.sign(makeZapRequest(params))
|
||||
const res = await requestZap({zapper, event})
|
||||
|
||||
if (!res.invoice) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to zap: ${res.error || "no error given"}`,
|
||||
})
|
||||
}
|
||||
|
||||
await payInvoice(res.invoice)
|
||||
await load({relays, filters})
|
||||
|
||||
pushToast({message: "Zap successfully sent!"})
|
||||
back()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
const message = String(e).replace(/^.*Error: /, "")
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to zap: ${message}`,
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let pos = $state(minPos)
|
||||
let amount = $state(minVal)
|
||||
let content = $state("⚡️")
|
||||
let loading = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
amount = posToAmount(pos)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const newPos = amountToPos(amount)
|
||||
if (newPos !== pos) {
|
||||
pos = newPos
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Send a Zap</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>To <ProfileLink {pubkey} class="!text-primary" /></div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline class="!grid-cols-3">
|
||||
{#snippet label()}
|
||||
Emoji Reaction
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex flex-grow items-center justify-end gap-4">
|
||||
<EmojiButton {onEmoji} class="btn btn-neutral">
|
||||
{content}
|
||||
</EmojiButton>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline class="!grid-cols-3">
|
||||
{#snippet label()}
|
||||
Amount
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex flex-grow justify-end">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<Icon icon="bolt" />
|
||||
<input bind:value={amount} type="number" class="w-24" />
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<input
|
||||
class="range range-primary -mt-2"
|
||||
type="range"
|
||||
min={minPos}
|
||||
max={maxPos}
|
||||
bind:value={pos} />
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={sendZap} disabled={loading}>
|
||||
<Spinner {loading}>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !loading}
|
||||
<Icon icon="bolt" />
|
||||
{/if}
|
||||
Send Zap
|
||||
</div>
|
||||
</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {deriveZapperForPubkey} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Zap from "@app/components/Zap.svelte"
|
||||
import InfoZapperError from "@app/components/InfoZapperError.svelte"
|
||||
import WalletConnect from "@app/components/WalletConnect.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {wallet} from "@app/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
children: Snippet
|
||||
replaceState?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, event, children, replaceState, ...props}: Props = $props()
|
||||
|
||||
const zapper = deriveZapperForPubkey(event.pubkey)
|
||||
|
||||
const onClick = () => {
|
||||
if (!$zapper?.allowsNostr) {
|
||||
pushModal(InfoZapperError, {url, pubkey: event.pubkey, eventId: event.id}, {replaceState})
|
||||
} else if ($wallet) {
|
||||
pushModal(Zap, {url, pubkey: event.pubkey, eventId: event.id}, {replaceState})
|
||||
} else {
|
||||
pushModal(WalletConnect, {}, {replaceState})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button onclick={onClick} {...props}>
|
||||
{@render children?.()}
|
||||
</Button>
|
||||
+32
-14
@@ -1,6 +1,6 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {synced, throttled} from "@welshman/store"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {synced, localStorageProvider, throttled} from "@welshman/store"
|
||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
||||
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
||||
@@ -9,13 +9,18 @@ import {
|
||||
makeChatPath,
|
||||
makeThreadPath,
|
||||
makeCalendarPath,
|
||||
makeSpaceChatPath,
|
||||
makeRoomPath,
|
||||
} from "@app/routes"
|
||||
import {chats, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/state"
|
||||
import {chats, hasNip29, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/state"
|
||||
|
||||
// Checked state
|
||||
|
||||
export const checked = synced<Record<string, number>>("checked", {})
|
||||
export const checked = synced<Record<string, number>>({
|
||||
key: "checked",
|
||||
defaultValue: {},
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
||||
|
||||
@@ -26,9 +31,12 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [
|
||||
export const notifications = derived(
|
||||
throttled(
|
||||
1000,
|
||||
derived([pubkey, checked, chats, userRoomsByUrl, repositoryStore, getUrlsForEvent], identity),
|
||||
derived(
|
||||
[pubkey, checked, chats, userRoomsByUrl, repositoryStore, getUrlsForEvent, relaysByUrl],
|
||||
identity,
|
||||
),
|
||||
),
|
||||
([$pubkey, $checked, $chats, $userRoomsByUrl, $repository, $getUrlsForEvent]) => {
|
||||
([$pubkey, $checked, $chats, $userRoomsByUrl, $repository, $getUrlsForEvent, $relaysByUrl]) => {
|
||||
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
|
||||
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
||||
return false
|
||||
@@ -75,8 +83,10 @@ export const notifications = derived(
|
||||
const spacePath = makeSpacePath(url)
|
||||
const threadPath = makeThreadPath(url)
|
||||
const calendarPath = makeCalendarPath(url)
|
||||
const messagesPath = makeSpaceChatPath(url)
|
||||
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||
const calendarEvents = allCalendarEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||
const messagesEvents = allMessageEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||
|
||||
if (hasNotification(threadPath, threadEvents[0])) {
|
||||
paths.add(spacePath)
|
||||
@@ -114,16 +124,24 @@ export const notifications = derived(
|
||||
}
|
||||
}
|
||||
|
||||
for (const room of rooms) {
|
||||
const roomPath = makeRoomPath(url, room)
|
||||
const latestEvent = allMessageEvents.find(
|
||||
e =>
|
||||
$getUrlsForEvent(e.id).includes(url) && e.tags.find(t => t[0] === "h" && t[1] === room),
|
||||
)
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
for (const room of rooms) {
|
||||
const roomPath = makeRoomPath(url, room)
|
||||
const latestEvent = allMessageEvents.find(
|
||||
e =>
|
||||
$getUrlsForEvent(e.id).includes(url) &&
|
||||
e.tags.find(t => t[0] === "h" && t[1] === room),
|
||||
)
|
||||
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
paths.add(spacePath)
|
||||
paths.add(roomPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (hasNotification(messagesPath, messagesEvents[0])) {
|
||||
paths.add(spacePath)
|
||||
paths.add(roomPath)
|
||||
paths.add(messagesPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
||||
import {PushNotifications} from "@capacitor/push-notifications"
|
||||
import {parseJson, poll} from "@welshman/lib"
|
||||
import {isSignedEvent} from "@welshman/util"
|
||||
import {goto} from "$app/navigation"
|
||||
import {ucFirst} from "@lib/util"
|
||||
import {VAPID_PUBLIC_KEY} from "@app/state"
|
||||
|
||||
export const platform = Capacitor.getPlatform()
|
||||
|
||||
export const platformName = platform === "ios" ? "iOS" : ucFirst(platform)
|
||||
|
||||
export const initializePushNotifications = () => {
|
||||
if (platform === "web") return
|
||||
|
||||
PushNotifications.addListener("pushNotificationActionPerformed", (action: ActionPerformed) => {
|
||||
const event = parseJson(action.notification.data.event)
|
||||
const parsedRelays = parseJson(action.notification.data.relays)
|
||||
const relays = Array.isArray(parsedRelays) ? parsedRelays : []
|
||||
|
||||
if (isSignedEvent(event)) {
|
||||
goto("/" + nip19.neventEncode({id: event.id, relays}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const canSendPushNotifications = () => ["web", "android", "ios"].includes(platform)
|
||||
|
||||
export const getWebPushInfo = async () => {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
throw new Error("Service Worker not supported")
|
||||
}
|
||||
|
||||
if (!("PushManager" in window)) {
|
||||
throw new Error("Push notifications are not supported")
|
||||
}
|
||||
|
||||
if (Notification.permission === "denied") {
|
||||
throw new Error("Push notifications are blocked")
|
||||
}
|
||||
|
||||
if (Notification.permission !== "granted") {
|
||||
const permission = await Notification.requestPermission()
|
||||
|
||||
if (permission !== "granted") {
|
||||
throw new Error("Push notification permission denied")
|
||||
}
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
let subscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (!subscription) {
|
||||
subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: VAPID_PUBLIC_KEY,
|
||||
})
|
||||
}
|
||||
|
||||
const {keys} = subscription.toJSON()
|
||||
|
||||
if (!keys) {
|
||||
throw new Error(`Failed to get push info: no keys were returned`)
|
||||
}
|
||||
|
||||
return {
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
}
|
||||
}
|
||||
|
||||
export type PushInfo = {
|
||||
device_token: string
|
||||
bundle_identifier?: string
|
||||
}
|
||||
|
||||
export const getCapacitorPushInfo = async () => {
|
||||
let status = await PushNotifications.checkPermissions()
|
||||
|
||||
if (status.receive === "prompt") {
|
||||
status = await PushNotifications.requestPermissions()
|
||||
}
|
||||
|
||||
if (status.receive !== "granted") {
|
||||
throw new Error("Failed to register for push notifications")
|
||||
}
|
||||
|
||||
let device_token = ""
|
||||
let error = "Failed to register for push notifications"
|
||||
|
||||
PushNotifications.addListener("registration", (token: Token) => {
|
||||
device_token = token.value
|
||||
})
|
||||
|
||||
PushNotifications.addListener("registrationError", (_error: RegistrationError) => {
|
||||
error = _error.error
|
||||
})
|
||||
|
||||
await PushNotifications.register()
|
||||
await poll({
|
||||
condition: () => Boolean(device_token),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!device_token) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
const info: PushInfo = {device_token}
|
||||
|
||||
if (platform === "ios") {
|
||||
info.bundle_identifier = "social.flotilla"
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
export const getPushInfo = (): Promise<Record<string, string>> => {
|
||||
switch (platform) {
|
||||
case "web":
|
||||
return getWebPushInfo()
|
||||
case "ios":
|
||||
case "android":
|
||||
return getCapacitorPushInfo()
|
||||
default:
|
||||
throw new Error(`Invalid push platform: ${platform}`)
|
||||
}
|
||||
}
|
||||
+28
-4
@@ -13,13 +13,22 @@ import {
|
||||
sortBy,
|
||||
assoc,
|
||||
now,
|
||||
isNotNil,
|
||||
filterVals,
|
||||
fromPairs,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
MESSAGE,
|
||||
DELETE,
|
||||
THREAD,
|
||||
EVENT_TIME,
|
||||
AUTH_INVITE,
|
||||
COMMENT,
|
||||
ALERT_EMAIL,
|
||||
ALERT_WEB,
|
||||
ALERT_IOS,
|
||||
ALERT_ANDROID,
|
||||
ALERT_STATUS,
|
||||
matchFilters,
|
||||
getTagValues,
|
||||
getTagValue,
|
||||
@@ -48,8 +57,6 @@ import {
|
||||
import {createScroller} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import {
|
||||
ALERT,
|
||||
ALERT_STATUS,
|
||||
NOTIFIER_RELAY,
|
||||
INDEXER_RELAYS,
|
||||
getDefaultPubkeys,
|
||||
@@ -343,7 +350,7 @@ export const makeCalendarFeed = ({
|
||||
export const loadAlerts = (pubkey: string) =>
|
||||
load({
|
||||
relays: [NOTIFIER_RELAY],
|
||||
filters: [{kinds: [ALERT], authors: [pubkey]}],
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}],
|
||||
})
|
||||
|
||||
export const loadAlertStatuses = (pubkey: string) =>
|
||||
@@ -359,7 +366,7 @@ export const listenForNotifications = () => {
|
||||
|
||||
for (const [url, allRooms] of userRoomsByUrl.get()) {
|
||||
// Limit how many rooms we load at a time, since we have to send a separate filter
|
||||
// for each one due to nip 29 breaking postel's law
|
||||
// for each one due to relay29 being picky
|
||||
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
|
||||
|
||||
load({
|
||||
@@ -367,6 +374,7 @@ export const listenForNotifications = () => {
|
||||
relays: [url],
|
||||
filters: [
|
||||
{kinds: [THREAD], limit: 1},
|
||||
{kinds: [MESSAGE], limit: 1},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||
],
|
||||
@@ -377,6 +385,7 @@ export const listenForNotifications = () => {
|
||||
relays: [url],
|
||||
filters: [
|
||||
{kinds: [THREAD], since: now()},
|
||||
{kinds: [MESSAGE], since: now()},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||
],
|
||||
@@ -430,3 +439,18 @@ export const discoverRelays = (lists: List[]) =>
|
||||
.filter(isShareableRelayUrl)
|
||||
.map(url => loadRelay(url)),
|
||||
)
|
||||
|
||||
export const requestRelayClaim = async (url: string) => {
|
||||
const filters = [{kinds: [AUTH_INVITE], limit: 1}]
|
||||
const events = await load({filters, relays: [url]})
|
||||
|
||||
if (events.length > 0) {
|
||||
return getTagValue("claim", events[0].tags)
|
||||
}
|
||||
}
|
||||
|
||||
export const requestRelayClaims = async (urls: string[]) =>
|
||||
filterVals(
|
||||
isNotNil,
|
||||
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
|
||||
)
|
||||
|
||||
+35
-16
@@ -12,6 +12,7 @@ import {
|
||||
DIRECT_MESSAGE_FILE,
|
||||
MESSAGE,
|
||||
THREAD,
|
||||
ZAP_GOAL,
|
||||
EVENT_TIME,
|
||||
} from "@welshman/util"
|
||||
import {makeChatId, entityLink, decodeRelay, encodeRelay, userRoomsByUrl, ROOM} from "@app/state"
|
||||
@@ -35,6 +36,10 @@ export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}
|
||||
|
||||
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
|
||||
|
||||
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
||||
|
||||
export const makeGoalPath = (url: string, eventId?: string) => makeSpacePath(url, "goals", eventId)
|
||||
|
||||
export const makeThreadPath = (url: string, eventId?: string) =>
|
||||
makeSpacePath(url, "threads", eventId)
|
||||
|
||||
@@ -61,53 +66,67 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const goToMessage = async (url: string, room: string | undefined, id: string) => {
|
||||
await goto(room ? makeRoomPath(url, room) : makeSpacePath(url, "chat"))
|
||||
await sleep(300)
|
||||
|
||||
return scrollToEvent(id)
|
||||
}
|
||||
|
||||
export const goToEvent = async (event: TrustedEvent) => {
|
||||
export const goToEvent = async (event: TrustedEvent, options: Record<string, any> = {}) => {
|
||||
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) {
|
||||
return await scrollToEvent(event.id)
|
||||
await scrollToEvent(event.id)
|
||||
}
|
||||
|
||||
const urls = Array.from(tracker.getRelays(event.id))
|
||||
const path = await getEventPath(event, urls)
|
||||
|
||||
if (path.includes("://")) {
|
||||
window.open(path)
|
||||
} else {
|
||||
goto(path, options)
|
||||
|
||||
await sleep(300)
|
||||
await scrollToEvent(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
const room = getTagValue(ROOM, event.tags)
|
||||
|
||||
if (urls.length > 0) {
|
||||
const url = urls[0]
|
||||
|
||||
if (event.kind === ZAP_GOAL) {
|
||||
return makeGoalPath(url, event.id)
|
||||
}
|
||||
|
||||
if (event.kind === THREAD) {
|
||||
return goto(makeThreadPath(url, event.id))
|
||||
return makeThreadPath(url, event.id)
|
||||
}
|
||||
|
||||
if (event.kind === EVENT_TIME) {
|
||||
return goto(makeCalendarPath(url, event.id))
|
||||
return makeCalendarPath(url, event.id)
|
||||
}
|
||||
|
||||
if (event.kind === MESSAGE) {
|
||||
return goToMessage(url, room, event.id)
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
}
|
||||
|
||||
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
||||
const id = event.tags.find(nthEq(0, "E"))?.[1]
|
||||
|
||||
if (id && kind) {
|
||||
if (parseInt(kind) === ZAP_GOAL) {
|
||||
return makeGoalPath(url, id)
|
||||
}
|
||||
|
||||
if (parseInt(kind) === THREAD) {
|
||||
return goto(makeThreadPath(url, id))
|
||||
return makeThreadPath(url, id)
|
||||
}
|
||||
|
||||
if (parseInt(kind) === EVENT_TIME) {
|
||||
return goto(makeCalendarPath(url, id))
|
||||
return makeCalendarPath(url, id)
|
||||
}
|
||||
|
||||
if (parseInt(kind) === MESSAGE) {
|
||||
return goToMessage(url, room, id)
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.open(entityLink(nip19.neventEncode({id: event.id, relays: urls})))
|
||||
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||
}
|
||||
|
||||
+100
-39
@@ -1,4 +1,5 @@
|
||||
import twColors from "tailwindcss/colors"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {get, derived} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {
|
||||
@@ -23,24 +24,38 @@ import {
|
||||
} from "@welshman/lib"
|
||||
import type {Socket} from "@welshman/net"
|
||||
import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net"
|
||||
import {collection, custom} from "@welshman/store"
|
||||
import {
|
||||
collection,
|
||||
custom,
|
||||
deriveEvents,
|
||||
deriveEventsMapped,
|
||||
withGetter,
|
||||
synced,
|
||||
localStorageProvider,
|
||||
} from "@welshman/store"
|
||||
import {
|
||||
getIdFilters,
|
||||
WRAP,
|
||||
CLIENT_AUTH,
|
||||
AUTH_JOIN,
|
||||
REACTION,
|
||||
ZAP_REQUEST,
|
||||
ZAP_RESPONSE,
|
||||
DIRECT_MESSAGE,
|
||||
DIRECT_MESSAGE_FILE,
|
||||
GROUP_META,
|
||||
ROOM_META,
|
||||
MESSAGE,
|
||||
GROUPS,
|
||||
ROOMS,
|
||||
THREAD,
|
||||
COMMENT,
|
||||
GROUP_JOIN,
|
||||
GROUP_ADD_USER,
|
||||
GROUP_REMOVE_USER,
|
||||
ROOM_JOIN,
|
||||
ROOM_ADD_USER,
|
||||
ROOM_REMOVE_USER,
|
||||
ALERT_EMAIL,
|
||||
ALERT_WEB,
|
||||
ALERT_IOS,
|
||||
ALERT_ANDROID,
|
||||
ALERT_STATUS,
|
||||
getGroupTags,
|
||||
getRelayTagValues,
|
||||
getPubkeyTagValues,
|
||||
@@ -51,6 +66,7 @@ import {
|
||||
asDecryptedEvent,
|
||||
normalizeRelayUrl,
|
||||
getTag,
|
||||
getTagValue,
|
||||
getTagValues,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
||||
@@ -76,7 +92,6 @@ import {
|
||||
appContext,
|
||||
} from "@welshman/app"
|
||||
import type {Thunk, Relay} from "@welshman/app"
|
||||
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
|
||||
|
||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||
|
||||
@@ -84,14 +99,16 @@ export const ROOM = "h"
|
||||
|
||||
export const PROTECTED = ["-"]
|
||||
|
||||
export const ALERT = 32830
|
||||
export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios"
|
||||
|
||||
export const ALERT_STATUS = 32831
|
||||
export const REACTION_KINDS = ENABLE_ZAPS ? [REACTION, ZAP_RESPONSE] : [REACTION]
|
||||
|
||||
export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
|
||||
|
||||
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
|
||||
|
||||
export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY
|
||||
|
||||
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
|
||||
|
||||
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
|
||||
@@ -120,11 +137,9 @@ export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
export const IMGPROXY_URL = "https://imgproxy.coracle.social"
|
||||
|
||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||
|
||||
export const NIP46_PERMS =
|
||||
"nip44_encrypt,nip44_decrypt," +
|
||||
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION]
|
||||
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST]
|
||||
.map(k => `sign_event:${k}`)
|
||||
.join(",")
|
||||
|
||||
@@ -171,8 +186,6 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}`
|
||||
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
|
||||
entityLink(nip19.nprofileEncode({pubkey, relays}))
|
||||
|
||||
export const tagRoom = (room: string, url: string) => [ROOM, room]
|
||||
|
||||
export const getDefaultPubkeys = () => {
|
||||
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
||||
const userPubkeys = shuffle(getPubkeyTagValues(getListTags(get(userFollows))))
|
||||
@@ -293,7 +306,11 @@ routerContext.getIndexerRelays = always(INDEXER_RELAYS)
|
||||
|
||||
// Settings
|
||||
|
||||
export const canDecrypt = synced("canDecrypt", false)
|
||||
export const canDecrypt = synced({
|
||||
key: "canDecrypt",
|
||||
defaultValue: false,
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
export const SETTINGS = 38489
|
||||
|
||||
@@ -338,6 +355,43 @@ export const {
|
||||
load: makeOutboxLoader(SETTINGS),
|
||||
})
|
||||
|
||||
// Wallets
|
||||
|
||||
export type WebLNInfo = {
|
||||
methods?: string[]
|
||||
supports?: string[]
|
||||
version?: string
|
||||
node?: {
|
||||
alias: string
|
||||
}
|
||||
}
|
||||
|
||||
export type NWCInfo = {
|
||||
lud16: string
|
||||
secret: string
|
||||
relayUrl: string
|
||||
walletPubkey: string
|
||||
nostrWalletConnectUrl: string
|
||||
}
|
||||
|
||||
export type Wallet =
|
||||
| {
|
||||
type: "webln"
|
||||
info: WebLNInfo
|
||||
}
|
||||
| {
|
||||
type: "nwc"
|
||||
info: NWCInfo
|
||||
}
|
||||
|
||||
export const wallet = synced<Wallet | undefined>({
|
||||
key: "wallet",
|
||||
defaultValue: undefined,
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
export const getWebLn = () => (window as any).webln
|
||||
|
||||
// Alerts
|
||||
|
||||
export type Alert = {
|
||||
@@ -345,15 +399,17 @@ export type Alert = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||
export const alerts = withGetter(
|
||||
deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
},
|
||||
})
|
||||
return {event, tags}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// Alert Statuses
|
||||
|
||||
@@ -362,15 +418,20 @@ export type AlertStatus = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||
export const alertStatuses = withGetter(
|
||||
deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
},
|
||||
})
|
||||
return {event, tags}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export const deriveAlertStatus = (address: string) =>
|
||||
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
||||
|
||||
// Membership
|
||||
|
||||
@@ -399,7 +460,7 @@ export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
||||
sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1)))
|
||||
|
||||
export const memberships = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [GROUPS]}],
|
||||
filters: [{kinds: [ROOMS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
})
|
||||
@@ -412,7 +473,7 @@ export const {
|
||||
name: "memberships",
|
||||
store: memberships,
|
||||
getKey: list => list.event.pubkey,
|
||||
load: makeOutboxLoader(GROUPS),
|
||||
load: makeOutboxLoader(ROOMS),
|
||||
})
|
||||
|
||||
// Chats
|
||||
@@ -510,7 +571,7 @@ export const splitChannelId = (id: string) => id.split("'")
|
||||
export const hasNip29 = (relay?: Relay) =>
|
||||
relay?.profile?.supported_nips?.map?.(String)?.includes?.("29")
|
||||
|
||||
export const channelEvents = deriveEvents(repository, {filters: [{kinds: [GROUP_META]}]})
|
||||
export const channelEvents = deriveEvents(repository, {filters: [{kinds: [ROOM_META]}]})
|
||||
|
||||
export const channels = derived(
|
||||
[channelEvents, getUrlsForEvent],
|
||||
@@ -559,7 +620,7 @@ export const {
|
||||
|
||||
await load({
|
||||
relays: [url],
|
||||
filters: [{kinds: [GROUP_META], "#d": [room]}],
|
||||
filters: [{kinds: [ROOM_META], "#d": [room]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -645,22 +706,22 @@ export const deriveUserMembershipStatus = (url: string, room: string) =>
|
||||
[
|
||||
pubkey,
|
||||
deriveEventsForUrl(url, [
|
||||
{kinds: [GROUP_JOIN, GROUP_ADD_USER, GROUP_REMOVE_USER], "#h": [room]},
|
||||
{kinds: [ROOM_JOIN, ROOM_ADD_USER, ROOM_REMOVE_USER], "#h": [room]},
|
||||
]),
|
||||
],
|
||||
([$pubkey, $events]) => {
|
||||
let status = MembershipStatus.Initial
|
||||
|
||||
for (const event of $events) {
|
||||
if (event.kind === GROUP_JOIN && event.pubkey === $pubkey) {
|
||||
if (event.kind === ROOM_JOIN && event.pubkey === $pubkey) {
|
||||
status = MembershipStatus.Pending
|
||||
}
|
||||
|
||||
if (event.kind === GROUP_REMOVE_USER && getTagValues("p", event.tags).includes($pubkey!)) {
|
||||
if (event.kind === ROOM_REMOVE_USER && getTagValues("p", event.tags).includes($pubkey!)) {
|
||||
break
|
||||
}
|
||||
|
||||
if (event.kind === GROUP_ADD_USER && getTagValues("p", event.tags).includes($pubkey!)) {
|
||||
if (event.kind === ROOM_ADD_USER && getTagValues("p", event.tags).includes($pubkey!)) {
|
||||
return MembershipStatus.Granted
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,3 +1,7 @@
|
||||
import {synced} from "@welshman/store"
|
||||
import {synced, localStorageProvider} from "@welshman/store"
|
||||
|
||||
export const theme = synced<string>("theme", "dark")
|
||||
export const theme = synced({
|
||||
key: "theme",
|
||||
defaultValue: "dark",
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.7491 9.70957V9.00497C18.7491 5.13623 15.7274 2 12 2C8.27256 2 5.25087 5.13623 5.25087 9.00497V9.70957C5.25087 10.5552 5.00972 11.3818 4.5578 12.0854L3.45036 13.8095C2.43882 15.3843 3.21105 17.5249 4.97036 18.0229C9.57274 19.3257 14.4273 19.3257 19.0296 18.0229C20.789 17.5249 21.5612 15.3843 20.5496 13.8095L19.4422 12.0854C18.9903 11.3818 18.7491 10.5552 18.7491 9.70957Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M7.5 19C8.15503 20.7478 9.92246 22 12 22C14.0775 22 15.845 20.7478 16.5 19" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 674 B |
@@ -0,0 +1,11 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 16.9C2 15.5906 2 14.9359 2.29472 14.455C2.45963 14.1859 2.68589 13.9596 2.955 13.7947C3.43594 13.5 4.09063 13.5 5.4 13.5H6.5C8.38562 13.5 9.32843 13.5 9.91421 14.0858C10.5 14.6716 10.5 15.6144 10.5 17.5V18.6C10.5 19.9094 10.5 20.5641 10.2053 21.045C10.0404 21.3141 9.81411 21.5404 9.545 21.7053C9.06406 22 8.40937 22 7.1 22C5.13594 22 4.15391 22 3.4325 21.5579C3.02884 21.3106 2.68945 20.9712 2.44208 20.5675C2 19.8461 2 18.8641 2 16.9Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M13.5 5.4C13.5 4.09063 13.5 3.43594 13.7947 2.955C13.9596 2.68589 14.1859 2.45963 14.455 2.29472C14.9359 2 15.5906 2 16.9 2C18.8641 2 19.8461 2 20.5675 2.44208C20.9712 2.68945 21.3106 3.02884 21.5579 3.4325C22 4.15391 22 5.13594 22 7.1C22 8.40937 22 9.06406 21.7053 9.545C21.5404 9.81411 21.3141 10.0404 21.045 10.2053C20.5641 10.5 19.9094 10.5 18.6 10.5H17.5C15.6144 10.5 14.6716 10.5 14.0858 9.91421C13.5 9.32843 13.5 8.38562 13.5 6.5V5.4Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M16.5 6.25C16.5 5.73459 16.5 5.47689 16.6291 5.29493C16.6747 5.23072 16.7307 5.17466 16.7949 5.12911C16.9769 5 17.2346 5 17.75 5C18.2654 5 18.5231 5 18.7051 5.12911C18.7693 5.17466 18.8253 5.23072 18.8709 5.29493C19 5.47689 19 5.73459 19 6.25C19 6.76541 19 7.02311 18.8709 7.20507C18.8253 7.26928 18.7693 7.32534 18.7051 7.37089C18.5231 7.5 18.2654 7.5 17.75 7.5C17.2346 7.5 16.9769 7.5 16.7949 7.37089C16.7307 7.32534 16.6747 7.26928 16.6291 7.20507C16.5 7.02311 16.5 6.76541 16.5 6.25Z" fill="#1C274C"/>
|
||||
<path d="M12.75 22C12.75 22.4142 13.0858 22.75 13.5 22.75C13.9142 22.75 14.25 22.4142 14.25 22H12.75ZM14.3889 13.8371L14.8055 14.4607L14.8055 14.4607L14.3889 13.8371ZM13.8371 14.3889L13.2135 13.9722L13.2135 13.9722L13.8371 14.3889ZM19 12.75H17V14.25H19V12.75ZM12.75 19V22H14.25V19H12.75ZM17 12.75C16.3134 12.75 15.742 12.7491 15.281 12.796C14.8075 12.8441 14.3682 12.9489 13.9722 13.2135L14.8055 14.4607C14.914 14.3882 15.078 14.3244 15.4328 14.2883C15.8002 14.2509 16.2822 14.25 17 14.25V12.75ZM14.25 17C14.25 16.2822 14.2509 15.8002 14.2883 15.4328C14.3244 15.078 14.3882 14.914 14.4607 14.8055L13.2135 13.9722C12.9489 14.3682 12.8441 14.8075 12.796 15.281C12.7491 15.742 12.75 16.3134 12.75 17H14.25ZM13.9722 13.2135C13.6719 13.4141 13.4141 13.6719 13.2135 13.9722L14.4607 14.8055C14.5519 14.669 14.669 14.5519 14.8055 14.4607L13.9722 13.2135Z" fill="#1C274C"/>
|
||||
<path d="M22.75 13.5C22.75 13.0858 22.4142 12.75 22 12.75C21.5858 12.75 21.25 13.0858 21.25 13.5H22.75ZM20.7654 21.8478L21.0524 22.5407L21.0524 22.5407L20.7654 21.8478ZM21.8478 20.7654L21.1548 20.4784V20.4784L21.8478 20.7654ZM17 22.75H19V21.25H17V22.75ZM22.75 17V13.5H21.25V17H22.75ZM19 22.75C19.4557 22.75 19.835 22.7504 20.1454 22.7292C20.4625 22.7076 20.762 22.661 21.0524 22.5407L20.4784 21.1548C20.4012 21.1868 20.284 21.2163 20.0433 21.2327C19.7958 21.2496 19.4762 21.25 19 21.25V22.75ZM21.25 19C21.25 19.4762 21.2496 19.7958 21.2327 20.0433C21.2163 20.284 21.1868 20.4012 21.1548 20.4784L22.5407 21.0524C22.661 20.762 22.7076 20.4625 22.7292 20.1454C22.7504 19.835 22.75 19.4557 22.75 19H21.25ZM21.0524 22.5407C21.7262 22.2616 22.2616 21.7262 22.5407 21.0524L21.1548 20.4784C21.028 20.7846 20.7846 21.028 20.4784 21.1549L21.0524 22.5407Z" fill="#1C274C"/>
|
||||
<path d="M2 7.1C2 5.13594 2 4.15391 2.44208 3.4325C2.68945 3.02884 3.02884 2.68945 3.4325 2.44208C4.15391 2 5.13594 2 7.1 2C8.40937 2 9.06406 2 9.545 2.29472C9.81411 2.45963 10.0404 2.68589 10.2053 2.955C10.5 3.43594 10.5 4.09063 10.5 5.4V6.5C10.5 8.38562 10.5 9.32843 9.91421 9.91421C9.32843 10.5 8.38562 10.5 6.5 10.5H5.4C4.09063 10.5 3.43594 10.5 2.955 10.2053C2.68589 10.0404 2.45963 9.81411 2.29472 9.545C2 9.06406 2 8.40937 2 7.1Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M5 6.25C5 5.73459 5 5.47689 5.12911 5.29493C5.17466 5.23072 5.23072 5.17466 5.29493 5.12911C5.47689 5 5.73459 5 6.25 5C6.76541 5 7.02311 5 7.20507 5.12911C7.26928 5.17466 7.32534 5.23072 7.37089 5.29493C7.5 5.47689 7.5 5.73459 7.5 6.25C7.5 6.76541 7.5 7.02311 7.37089 7.20507C7.32534 7.26928 7.26928 7.32534 7.20507 7.37089C7.02311 7.5 6.76541 7.5 6.25 7.5C5.73459 7.5 5.47689 7.5 5.29493 7.37089C5.23072 7.32534 5.17466 7.26928 5.12911 7.20507C5 7.02311 5 6.76541 5 6.25Z" fill="#1C274C"/>
|
||||
<path d="M5 17.75C5 17.2346 5 16.9769 5.12911 16.7949C5.17466 16.7307 5.23072 16.6747 5.29493 16.6291C5.47689 16.5 5.73459 16.5 6.25 16.5C6.76541 16.5 7.02311 16.5 7.20507 16.6291C7.26928 16.6747 7.32534 16.7307 7.37089 16.7949C7.5 16.9769 7.5 17.2346 7.5 17.75C7.5 18.2654 7.5 18.5231 7.37089 18.7051C7.32534 18.7693 7.26928 18.8253 7.20507 18.8709C7.02311 19 6.76541 19 6.25 19C5.73459 19 5.47689 19 5.29493 18.8709C5.23072 18.8253 5.17466 18.7693 5.12911 18.7051C5 18.5231 5 18.2654 5 17.75Z" fill="#1C274C"/>
|
||||
<path d="M16 17.75C16 17.0478 16 16.6967 16.1685 16.4444C16.2415 16.3352 16.3352 16.2415 16.4444 16.1685C16.6967 16 17.0478 16 17.75 16C18.4522 16 18.8033 16 19.0556 16.1685C19.1648 16.2415 19.2585 16.3352 19.3315 16.4444C19.5 16.6967 19.5 17.0478 19.5 17.75C19.5 18.4522 19.5 18.8033 19.3315 19.0556C19.2585 19.1648 19.1648 19.2585 19.0556 19.3315C18.8033 19.5 18.4522 19.5 17.75 19.5C17.0478 19.5 16.6967 19.5 16.4444 19.3315C16.3352 19.2585 16.2415 19.1648 16.1685 19.0556C16 18.8033 16 18.4522 16 17.75Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.8114 6.7267C12.8247 4.9089 13.3314 4 14.0889 4C14.8464 4 15.353 4.9089 16.3663 6.7267L16.6285 7.19699C16.9164 7.71355 17.0604 7.97183 17.2849 8.14225C17.5094 8.31266 17.789 8.37592 18.3482 8.50244L18.8572 8.61762C20.825 9.06284 21.8089 9.28545 22.0429 10.0382C22.277 10.7909 21.6063 11.5753 20.2648 13.1439L19.9177 13.5498C19.5365 13.9955 19.3459 14.2184 19.2602 14.4942C19.1744 14.7699 19.2032 15.0673 19.2609 15.662L19.3134 16.2035C19.5162 18.2965 19.6176 19.343 19.0047 19.8082C18.3919 20.2734 17.4707 19.8492 15.6283 19.0009L15.1517 18.7815C14.6281 18.5404 14.3664 18.4199 14.0889 18.4199C13.8114 18.4199 13.5496 18.5404 13.0261 18.7815L12.5494 19.0009C10.707 19.8492 9.78581 20.2734 9.17299 19.8082C8.56016 19.343 8.66157 18.2965 8.86438 16.2035L8.91685 15.662C8.97449 15.0673 9.0033 14.7699 8.91756 14.4942C8.83181 14.2184 8.64121 13.9955 8.26 13.5498L7.91295 13.1439C6.57147 11.5753 5.90073 10.7909 6.1348 10.0382C6.36888 9.28545 7.35275 9.06284 9.3205 8.61762L9.82958 8.50244C10.3887 8.37592 10.6683 8.31266 10.8928 8.14225C11.1173 7.97183 11.2613 7.71355 11.5492 7.19699L11.8114 6.7267Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M2.08887 16C3.20445 15.121 4.68639 14.7971 6.08887 15.1257" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M2.08887 10.5C3.08887 10 3.37862 10.0605 4.08887 10" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M2 5.60867L2.20816 5.48676C4.41383 4.19506 6.75032 3.84687 8.95304 4.48161L9.16092 4.54152" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8H10" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.8333 9H18.2308C16.4465 9 15 10.3431 15 12C15 13.6569 16.4465 15 18.2308 15H20.8333C20.9167 15 20.9583 15 20.9935 14.9979C21.5328 14.965 21.9623 14.5662 21.9977 14.0654C22 14.0327 22 13.994 22 13.9167V10.0833C22 10.006 22 9.96726 21.9977 9.9346C21.9623 9.43384 21.5328 9.03496 20.9935 9.00214C20.9583 9 20.9167 9 20.8333 9Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M20.965 9C20.8873 7.1277 20.6366 5.97975 19.8284 5.17157C18.6569 4 16.7712 4 13 4L10 4C6.22876 4 4.34315 4 3.17157 5.17157C2 6.34315 2 8.22876 2 12C2 15.7712 2 17.6569 3.17157 18.8284C4.34315 20 6.22876 20 10 20H13C16.7712 20 18.6569 20 19.8284 18.8284C20.6366 18.0203 20.8873 16.8723 20.965 15" stroke="#1C274C" stroke-width="1.5"/>
|
||||
<path d="M17.9912 12H18.0002" stroke="#1C274C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -20,7 +20,7 @@
|
||||
if (popover) {
|
||||
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
||||
|
||||
if (!between([x, x + width], clientX) || !between([y - 30, y + height + 30], clientY)) {
|
||||
if (!between([x, x + width], clientX) || !between([y - 100, y + height + 100], clientY)) {
|
||||
popover.hide()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {switcher} from "@welshman/lib"
|
||||
import AddSquare from "@assets/icons/Add Square.svg?dataurl"
|
||||
import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.svg?dataurl"
|
||||
import Bell from "@assets/icons/Bell.svg?dataurl"
|
||||
import Bookmark from "@assets/icons/Bookmark.svg?dataurl"
|
||||
import BillList from "@assets/icons/Bill List.svg?dataurl"
|
||||
import Code2 from "@assets/icons/Code 2.svg?dataurl"
|
||||
@@ -67,6 +68,7 @@
|
||||
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
|
||||
import Paperclip from "@assets/icons/Paperclip.svg?dataurl"
|
||||
import Plain from "@assets/icons/Plain.svg?dataurl"
|
||||
import QRCode from "@assets/icons/QR Code.svg?dataurl"
|
||||
import QuestionSquare from "@assets/icons/Question Square.svg?dataurl"
|
||||
import RemoteControllerMinimalistic from "@assets/icons/Remote Controller Minimalistic.svg?dataurl"
|
||||
import Rocket2 from "@assets/icons/Rocket 2.svg?dataurl"
|
||||
@@ -84,11 +86,13 @@
|
||||
import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl"
|
||||
import SortVertical from "@assets/icons/Sort Vertical.svg?dataurl"
|
||||
import Star from "@assets/icons/Star.svg?dataurl"
|
||||
import StarFallMinimalistic2 from "@assets/icons/Star Fall Minimalistic 2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/Trash Bin 2.svg?dataurl"
|
||||
import UFO3 from "@assets/icons/UFO 3.svg?dataurl"
|
||||
import UserHeart from "@assets/icons/User Heart.svg?dataurl"
|
||||
import UserCircle from "@assets/icons/User Circle.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/User Rounded.svg?dataurl"
|
||||
import Wallet from "@assets/icons/Wallet.svg?dataurl"
|
||||
import Widget from "@assets/icons/Widget.svg?dataurl"
|
||||
import WidgetAdd from "@assets/icons/Widget Add.svg?dataurl"
|
||||
import WiFiRouterRound from "@assets/icons/Wi-Fi Router Round.svg?dataurl"
|
||||
@@ -108,6 +112,7 @@
|
||||
const data = switcher(icon, {
|
||||
"add-square": AddSquare,
|
||||
"arrows-a-logout-2": ArrowsALogout2,
|
||||
bell: Bell,
|
||||
bookmark: Bookmark,
|
||||
"bill-list": BillList,
|
||||
"code-2": Code2,
|
||||
@@ -166,6 +171,7 @@
|
||||
"pallete-2": Pallete2,
|
||||
paperclip: Paperclip,
|
||||
plain: Plain,
|
||||
"qr-code": QRCode,
|
||||
"question-square": QuestionSquare,
|
||||
reply: Reply,
|
||||
"remote-controller-minimalistic": RemoteControllerMinimalistic,
|
||||
@@ -185,9 +191,11 @@
|
||||
"square-share-line": SquareShareLine,
|
||||
"sort-vertical": SortVertical,
|
||||
star: Star,
|
||||
"star-fall-minimalistic-2": StarFallMinimalistic2,
|
||||
"user-heart": UserHeart,
|
||||
"user-circle": UserCircle,
|
||||
"user-rounded": UserRounded,
|
||||
wallet: Wallet,
|
||||
widget: Widget,
|
||||
"widget-add": WidgetAdd,
|
||||
"wifi-router-round": WiFiRouterRound,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import QrScanner from "qr-scanner"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
|
||||
const {onscan} = $props()
|
||||
|
||||
let video: HTMLVideoElement
|
||||
let scanner: QrScanner
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(() => {
|
||||
scanner = new QrScanner(video, r => onscan(r.data), {
|
||||
returnDetailedScanResult: true,
|
||||
})
|
||||
|
||||
scanner.start().then(() => {
|
||||
loading = false
|
||||
})
|
||||
|
||||
return () => scanner.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="bg-alt flex min-h-48 w-full flex-col items-center justify-center rounded p-px">
|
||||
{#if loading}
|
||||
<p class="py-20">
|
||||
<Spinner loading>Loading your camera...</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
<video class="m-auto rounded" class:h-0={loading} bind:this={video}></video>
|
||||
</div>
|
||||
+5
-2
@@ -102,6 +102,7 @@ export const isIntersecting = async (element: Element) =>
|
||||
|
||||
export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => {
|
||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||
const elements = Array.from(document.querySelectorAll("[data-event]"))
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
@@ -116,8 +117,8 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
|
||||
}, 800 + 400)
|
||||
|
||||
return true
|
||||
} else {
|
||||
const lastElement = last(Array.from(document.querySelectorAll("[data-event]")))
|
||||
} else if (elements.length > 0) {
|
||||
const lastElement = last(elements)
|
||||
|
||||
if (lastElement && !isIntersecting(lastElement)) {
|
||||
lastElement.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
@@ -131,4 +132,6 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import {App} from "@capacitor/app"
|
||||
import {dev} from "$app/environment"
|
||||
import {goto} from "$app/navigation"
|
||||
import {sync, localStorageProvider} from "@welshman/store"
|
||||
import {identity, memoize, sleep, defer, ago, WEEK, TaskQueue} from "@welshman/lib"
|
||||
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
||||
import {
|
||||
@@ -33,17 +34,21 @@
|
||||
initStorage,
|
||||
repository,
|
||||
pubkey,
|
||||
defaultStorageAdapters,
|
||||
session,
|
||||
sessions,
|
||||
signer,
|
||||
dropSession,
|
||||
defaultStorageAdapters,
|
||||
userInboxRelaySelections,
|
||||
loginWithNip01,
|
||||
loginWithNip46,
|
||||
EventsStorageAdapter,
|
||||
loadRelaySelections,
|
||||
} from "@welshman/app"
|
||||
import * as lib from "@welshman/lib"
|
||||
import * as util from "@welshman/util"
|
||||
import * as feeds from "@welshman/feeds"
|
||||
import * as router from "@welshman/router"
|
||||
import * as welshmanSigner from "@welshman/signer"
|
||||
import * as net from "@welshman/net"
|
||||
import * as app from "@welshman/app"
|
||||
@@ -61,6 +66,7 @@
|
||||
} from "@app/state"
|
||||
import {loadUserData, listenForNotifications} from "@app/requests"
|
||||
import {theme} from "@app/theme"
|
||||
import {initializePushNotifications} from "@app/push"
|
||||
import * as commands from "@app/commands"
|
||||
import * as requests from "@app/requests"
|
||||
import * as notifications from "@app/notifications"
|
||||
@@ -71,6 +77,9 @@
|
||||
dropSession($session.pubkey)
|
||||
}
|
||||
|
||||
// Initialize push notification handler asap
|
||||
initializePushNotifications()
|
||||
|
||||
const {children} = $props()
|
||||
|
||||
const ready = $state(defer<void>())
|
||||
@@ -81,7 +90,9 @@
|
||||
nip19,
|
||||
...lib,
|
||||
...welshmanSigner,
|
||||
...router,
|
||||
...util,
|
||||
...feeds,
|
||||
...net,
|
||||
...app,
|
||||
...appState,
|
||||
@@ -90,6 +101,13 @@
|
||||
...notifications,
|
||||
})
|
||||
|
||||
// Listen for navigation messages from service worker
|
||||
navigator.serviceWorker?.addEventListener("message", event => {
|
||||
if (event.data && event.data.type === "NAVIGATE") {
|
||||
goto(event.data.url)
|
||||
}
|
||||
})
|
||||
|
||||
// Nstart login
|
||||
if (window.location.hash?.startsWith("#nostr-login")) {
|
||||
const params = new URLSearchParams(window.location.hash.slice(1))
|
||||
@@ -153,17 +171,29 @@
|
||||
const unwrapper = new TaskQueue<TrustedEvent>({batchSize: 10, processItem: ensureUnwrapped})
|
||||
|
||||
repository.on("update", ({added}) => {
|
||||
if (!$canDecrypt) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const event of added) {
|
||||
if (event.kind === WRAP) {
|
||||
loadRelaySelections(event.pubkey)
|
||||
|
||||
if ($canDecrypt && event.kind === WRAP) {
|
||||
unwrapper.push(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Sync current pubkey
|
||||
sync({
|
||||
key: "pubkey",
|
||||
store: pubkey,
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
// Sync user sessions
|
||||
sync({
|
||||
key: "sessions",
|
||||
store: sessions,
|
||||
storage: localStorageProvider,
|
||||
})
|
||||
|
||||
await initStorage("flotilla", 8, {
|
||||
...defaultStorageAdapters,
|
||||
events: new EventsStorageAdapter({
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import {onMount} from "svelte"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Address, getIdFilters, getTagValue} from "@welshman/util"
|
||||
import {Address, getIdFilters} from "@welshman/util"
|
||||
import {LOCAL_RELAY_URL} from "@welshman/relay"
|
||||
import {load} from "@welshman/net"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {scrollToEvent} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import {makeRoomPath, makeThreadPath} from "@app/routes"
|
||||
import {goToEvent} from "@app/routes"
|
||||
|
||||
const {bech32} = $page.params
|
||||
|
||||
@@ -22,19 +22,11 @@
|
||||
let found = false
|
||||
|
||||
load({
|
||||
relays: data.relays,
|
||||
relays: [LOCAL_RELAY_URL, ...data.relays],
|
||||
filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
found = true
|
||||
|
||||
if (event.kind === 9) {
|
||||
goto(makeRoomPath(data.relays[0], getTagValue("h", event.tags)!), {replaceState: true})
|
||||
scrollToEvent(event.id)
|
||||
} else if (event.kind === 11) {
|
||||
goto(makeThreadPath(data.relays[0], event.id), {replaceState: true})
|
||||
} else {
|
||||
goto("/", {replaceState: true})
|
||||
}
|
||||
goToEvent(event, {replaceState: true})
|
||||
},
|
||||
onClose: () => {
|
||||
if (!found) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {WRAP} from "@welshman/util"
|
||||
import {Router} from "@welshman/router"
|
||||
@@ -10,18 +11,19 @@
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import ChatStart from "@app/components/ChatStart.svelte"
|
||||
import ChatMenu from "@app/components/ChatMenu.svelte"
|
||||
import ChatItem from "@app/components/ChatItem.svelte"
|
||||
import {chatSearch} from "@app/state"
|
||||
import {pullConservatively} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet
|
||||
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
const startChat = () => pushModal(ChatStart)
|
||||
const openMenu = () => pushModal(ChatMenu)
|
||||
|
||||
const promise = pullConservatively({
|
||||
filters: [{kinds: [WRAP], "#p": [$pubkey!]}],
|
||||
@@ -37,8 +39,8 @@
|
||||
<SecondaryNavSection>
|
||||
<SecondaryNavHeader>
|
||||
Chats
|
||||
<Button onclick={startChat}>
|
||||
<Icon icon="add-circle" />
|
||||
<Button onclick={openMenu}>
|
||||
<Icon icon="menu-dots" />
|
||||
</Button>
|
||||
</SecondaryNavHeader>
|
||||
</SecondaryNavSection>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
||||
import ChatItem from "@app/components/ChatItem.svelte"
|
||||
import ChatStart from "@app/components/ChatStart.svelte"
|
||||
import ChatMenuMobile from "@app/components/ChatMenuMobile.svelte"
|
||||
import ChatMenu from "@app/components/ChatMenu.svelte"
|
||||
import {chatSearch} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
const startChat = () => pushModal(ChatStart)
|
||||
|
||||
const openMenu = () => pushModal(ChatMenuMobile)
|
||||
const openMenu = () => pushModal(ChatMenu)
|
||||
|
||||
const chats = $derived($chatSearch.searchOptions(term))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {addToMapKey, dec, gt} from "@welshman/lib"
|
||||
import {GROUPS} from "@welshman/util"
|
||||
import {ROOMS} from "@welshman/util"
|
||||
import {Router} from "@welshman/router"
|
||||
import {load} from "@welshman/net"
|
||||
import type {Relay} from "@welshman/app"
|
||||
@@ -29,7 +29,7 @@
|
||||
const discoverRelays = () =>
|
||||
Promise.all([
|
||||
load({
|
||||
filters: [{kinds: [GROUPS]}],
|
||||
filters: [{kinds: [ROOMS]}],
|
||||
relays: Router.get().Index().getUrls(),
|
||||
}),
|
||||
...getDefaultPubkeys().map(async pubkey => {
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<Icon icon="user-circle" /> Profile
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local>
|
||||
<SecondaryNavItem href="/settings/wallet">
|
||||
<Icon icon="wallet" /> Wallet
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local={{delay: 50}}>
|
||||
<SecondaryNavItem href="/settings/relays">
|
||||
<Icon icon="server" /> Relays
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
getListTags,
|
||||
tagger,
|
||||
createEvent,
|
||||
makeEvent,
|
||||
getPubkeyTagValues,
|
||||
getTagValues,
|
||||
MUTES,
|
||||
@@ -38,17 +38,17 @@
|
||||
const relays = Router.get().FromUser().getUrls()
|
||||
|
||||
publishThunk({
|
||||
event: createEvent(SETTINGS, {content}),
|
||||
event: makeEvent(SETTINGS, {content}),
|
||||
relays,
|
||||
})
|
||||
|
||||
publishThunk({
|
||||
event: createEvent(MUTES, {tags: mutedPubkeys.map(tagPubkey)}),
|
||||
event: makeEvent(MUTES, {tags: mutedPubkeys.map(tagPubkey)}),
|
||||
relays,
|
||||
})
|
||||
|
||||
publishThunk({
|
||||
event: createEvent(BLOSSOM_SERVERS, {tags: blossomServers.map(tagger("server"))}),
|
||||
event: makeEvent(BLOSSOM_SERVERS, {tags: blossomServers.map(tagger("server"))}),
|
||||
relays,
|
||||
})
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import {pushModal} from "@app/modal"
|
||||
import {clip} from "@app/toast"
|
||||
|
||||
const npub = nip19.npubEncode($pubkey!)
|
||||
const profile = deriveProfile($pubkey!)
|
||||
|
||||
const pubkeyDisplay = displayPubkey($pubkey!)
|
||||
|
||||
const copyNpub = () => clip(nip19.npubEncode($session!.pubkey))
|
||||
const copyNpub = () => clip(npub)
|
||||
|
||||
const copyNsec = () => clip(nip19.nsecEncode(hexToBytes($session!.secret!)))
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<label class="input input-bordered flex w-full items-center justify-between gap-2">
|
||||
<div class="row-2 flex-grow items-center">
|
||||
<Icon icon="link-round" />
|
||||
<input readonly class="ellipsize flex-grow" value={$session?.pubkey} />
|
||||
<input readonly class="ellipsize flex-grow" value={npub} />
|
||||
</div>
|
||||
<Button class="flex items-center" onclick={copyNpub}>
|
||||
<Icon icon="copy" />
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import {nwc} from "@getalby/sdk"
|
||||
import {LOCALE} from "@welshman/lib"
|
||||
import {displayRelayUrl, fromMsats} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import WalletConnect from "@app/components/WalletConnect.svelte"
|
||||
import WalletDisconnect from "@app/components/WalletDisconnect.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {wallet, getWebLn} from "@app/state"
|
||||
|
||||
const connect = () => pushModal(WalletConnect)
|
||||
|
||||
const disconnect = () => pushModal(WalletDisconnect)
|
||||
</script>
|
||||
|
||||
<div class="content column gap-4">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon="wallet" />
|
||||
Wallet
|
||||
</strong>
|
||||
{#if $wallet}
|
||||
<div class="flex items-center gap-2 text-sm text-success">
|
||||
<Icon icon="check-circle" size={4} />
|
||||
Connected
|
||||
</div>
|
||||
{:else}
|
||||
<Button class="btn btn-primary btn-sm" onclick={connect}>
|
||||
<Icon icon="add-circle" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#if $wallet}
|
||||
{#if $wallet?.type === "webln"}
|
||||
{@const {node, version} = $wallet.info}
|
||||
<div class="flex flex-col justify-between gap-2 lg:flex-row">
|
||||
<p>
|
||||
Connected to <strong>{node?.alias || version || "unknown wallet"}</strong>
|
||||
via <strong>{$wallet.type}</strong>
|
||||
</p>
|
||||
<p class="flex gap-2 whitespace-nowrap">
|
||||
Balance:
|
||||
{#await getWebLn()
|
||||
?.enable()
|
||||
.then(() => getWebLn().getBalance())}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:then res}
|
||||
{new Intl.NumberFormat(LOCALE).format(res?.balance || 0)}
|
||||
{:catch}
|
||||
[unknown]
|
||||
{/await}
|
||||
sats
|
||||
</p>
|
||||
</div>
|
||||
{:else if $wallet.type === "nwc"}
|
||||
{@const {lud16, relayUrl, nostrWalletConnectUrl} = $wallet.info}
|
||||
<div class="flex flex-col justify-between gap-2 lg:flex-row">
|
||||
<p>
|
||||
Connected to <strong>{lud16}</strong> via <strong>{displayRelayUrl(relayUrl)}</strong>
|
||||
</p>
|
||||
<p class="flex gap-2 whitespace-nowrap">
|
||||
Balance:
|
||||
{#await new nwc.NWCClient({nostrWalletConnectUrl}).getBalance()}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:then res}
|
||||
{new Intl.NumberFormat(LOCALE).format(fromMsats(res?.balance || 0))}
|
||||
{:catch}
|
||||
[unknown]
|
||||
{/await}
|
||||
sats
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<Button class="btn btn-neutral btn-sm" onclick={disconnect}>
|
||||
<Icon icon="close-circle" />
|
||||
Disconnect Wallet
|
||||
</Button>
|
||||
{:else}
|
||||
<p class="py-12 text-center opacity-75">No wallet connected</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {ago, MONTH} from "@welshman/lib"
|
||||
import {GROUP_META, EVENT_TIME, THREAD, COMMENT, MESSAGE} from "@welshman/util"
|
||||
import {ROOM_META, EVENT_TIME, THREAD, COMMENT, MESSAGE} from "@welshman/util"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||
@@ -61,8 +61,8 @@
|
||||
pullConservatively({
|
||||
relays,
|
||||
filters: [
|
||||
{kinds: [GROUP_META]},
|
||||
{kinds: [THREAD, EVENT_TIME], since},
|
||||
{kinds: [ROOM_META]},
|
||||
{kinds: [THREAD, EVENT_TIME, MESSAGE], since},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD), String(EVENT_TIME)], since},
|
||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since})),
|
||||
],
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
import {request} from "@welshman/net"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {
|
||||
createEvent,
|
||||
makeEvent,
|
||||
makeRoomMeta,
|
||||
MESSAGE,
|
||||
DELETE,
|
||||
REACTION,
|
||||
GROUP_ADD_USER,
|
||||
GROUP_REMOVE_USER,
|
||||
ROOM_ADD_USER,
|
||||
ROOM_REMOVE_USER,
|
||||
} from "@welshman/util"
|
||||
import {pubkey, publishThunk, getThunkError} from "@welshman/app"
|
||||
import {pubkey, publishThunk, getThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
||||
import {slide, fade, fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -32,20 +32,14 @@
|
||||
userRoomsByUrl,
|
||||
userSettingValues,
|
||||
decodeRelay,
|
||||
tagRoom,
|
||||
getEventsForUrl,
|
||||
deriveUserMembershipStatus,
|
||||
deriveChannel,
|
||||
MembershipStatus,
|
||||
REACTION_KINDS,
|
||||
} from "@app/state"
|
||||
import {setChecked, checked} from "@app/notifications"
|
||||
import {
|
||||
joinRoom,
|
||||
leaveRoom,
|
||||
addRoomMembership,
|
||||
removeRoomMembership,
|
||||
prependParent,
|
||||
} from "@app/commands"
|
||||
import {addRoomMembership, removeRoomMembership, prependParent} from "@app/commands"
|
||||
import {PROTECTED} from "@app/state"
|
||||
import {makeFeed} from "@app/requests"
|
||||
import {popKey} from "@app/implicit"
|
||||
@@ -68,7 +62,7 @@
|
||||
joining = true
|
||||
|
||||
try {
|
||||
const message = await getThunkError(joinRoom(url, room))
|
||||
const message = await getThunkError(joinRoom(url, makeRoomMeta({id: room})))
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
return pushToast({theme: "error", message})
|
||||
@@ -84,7 +78,7 @@
|
||||
const leave = async () => {
|
||||
leaving = true
|
||||
try {
|
||||
const message = await getThunkError(leaveRoom(url, room))
|
||||
const message = await getThunkError(leaveRoom(url, makeRoomMeta({id: room})))
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
pushToast({theme: "error", message})
|
||||
@@ -108,7 +102,7 @@
|
||||
}
|
||||
|
||||
const onSubmit = ({content, tags}: EventContent) => {
|
||||
tags.push(tagRoom(room, url))
|
||||
tags.push(["h", room])
|
||||
tags.push(PROTECTED)
|
||||
|
||||
let template = {content, tags}
|
||||
@@ -123,7 +117,7 @@
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: createEvent(MESSAGE, template),
|
||||
event: makeEvent(MESSAGE, template),
|
||||
delay: $userSettingValues.send_delay,
|
||||
})
|
||||
|
||||
@@ -232,7 +226,9 @@
|
||||
element: element!,
|
||||
relays: [url],
|
||||
feedFilters: [filter],
|
||||
subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
|
||||
subscriptionFilters: [
|
||||
{kinds: [DELETE, MESSAGE, ...REACTION_KINDS], "#h": [room], since: now()},
|
||||
],
|
||||
initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]),
|
||||
onExhausted: () => {
|
||||
loadingEvents = false
|
||||
@@ -251,7 +247,7 @@
|
||||
relays: [url],
|
||||
filters: [
|
||||
{
|
||||
kinds: [GROUP_ADD_USER, GROUP_REMOVE_USER],
|
||||
kinds: [ROOM_ADD_USER, ROOM_REMOVE_USER],
|
||||
"#p": [$pubkey!],
|
||||
"#h": [room],
|
||||
limit: 10,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {page} from "$app/stores"
|
||||
import {now, last, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {REACTION, DELETE, EVENT_TIME, getTagValue} from "@welshman/util"
|
||||
import {DELETE, EVENT_TIME, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -17,13 +17,13 @@
|
||||
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
|
||||
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {getEventsForUrl, decodeRelay} from "@app/state"
|
||||
import {getEventsForUrl, decodeRelay, REACTION_KINDS} from "@app/state"
|
||||
import {makeCalendarFeed} from "@app/requests"
|
||||
import {setChecked} from "@app/notifications"
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
|
||||
const createEvent = () => pushModal(CalendarEventCreate, {url})
|
||||
const makeEvent = () => pushModal(CalendarEventCreate, {url})
|
||||
|
||||
const getStart = (event: TrustedEvent) => parseInt(getTagValue("start", event.tags) || "")
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
onMount(() => {
|
||||
const feedFilters = [{kinds: [EVENT_TIME]}]
|
||||
const subscriptionFilters = [{kinds: [DELETE, REACTION, EVENT_TIME], since: now()}]
|
||||
const subscriptionFilters = [{kinds: [DELETE, EVENT_TIME, ...REACTION_KINDS], since: now()}]
|
||||
|
||||
;({events, cleanup} = makeCalendarFeed({
|
||||
element: element!,
|
||||
@@ -124,7 +124,7 @@
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createEvent}>
|
||||
<Button class="btn btn-primary btn-sm" onclick={makeEvent}>
|
||||
<Icon icon="calendar-add" />
|
||||
Create an Event
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type {Readable} from "svelte/store"
|
||||
import {now, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
|
||||
import {makeEvent, MESSAGE, DELETE} from "@welshman/util"
|
||||
import {pubkey, publishThunk} from "@welshman/app"
|
||||
import {slide, fade, fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -21,7 +21,7 @@
|
||||
import {userSettingValues, decodeRelay, getEventsForUrl} from "@app/state"
|
||||
import {setChecked, checked} from "@app/notifications"
|
||||
import {prependParent} from "@app/commands"
|
||||
import {PROTECTED} from "@app/state"
|
||||
import {PROTECTED, REACTION_KINDS} from "@app/state"
|
||||
import {makeFeed} from "@app/requests"
|
||||
import {popKey} from "@app/implicit"
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: createEvent(MESSAGE, template),
|
||||
event: makeEvent(MESSAGE, template),
|
||||
delay: $userSettingValues.send_delay,
|
||||
})
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
element: element!,
|
||||
relays: [url],
|
||||
feedFilters: [filter],
|
||||
subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], since: now()}],
|
||||
subscriptionFilters: [{kinds: [DELETE, MESSAGE, ...REACTION_KINDS], since: now()}],
|
||||
initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]),
|
||||
onExhausted: () => {
|
||||
loadingEvents = false
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, max, nthEq} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {ZAP_GOAL, DELETE, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {userMutes} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import GoalItem from "@app/components/GoalItem.svelte"
|
||||
import GoalCreate from "@app/components/GoalCreate.svelte"
|
||||
import {decodeRelay, getEventsForUrl, REACTION_KINDS} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {makeFeed} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||
const goals: TrustedEvent[] = $state([])
|
||||
const comments: TrustedEvent[] = $state([])
|
||||
|
||||
let loading = $state(true)
|
||||
let element: HTMLElement | undefined = $state()
|
||||
|
||||
const createGoal = () => pushModal(GoalCreate, {url})
|
||||
|
||||
const events = $derived.by(() => {
|
||||
const scores = new Map<string, number>()
|
||||
|
||||
for (const comment of comments) {
|
||||
const id = comment.tags.find(nthEq(0, "E"))?.[1]
|
||||
|
||||
if (id) {
|
||||
scores.set(id, max([scores.get(id), comment.created_at]))
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(e => -max([scores.get(e.id), e.created_at]), goals)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const {cleanup} = makeFeed({
|
||||
element: element!,
|
||||
relays: [url],
|
||||
feedFilters: [{kinds: [ZAP_GOAL, COMMENT]}],
|
||||
subscriptionFilters: [
|
||||
{kinds: [ZAP_GOAL, DELETE, ...REACTION_KINDS]},
|
||||
{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]},
|
||||
],
|
||||
initialEvents: getEventsForUrl(url, [{kinds: [ZAP_GOAL, COMMENT], limit: 10}]),
|
||||
onEvent: event => {
|
||||
if (event.kind === ZAP_GOAL && !mutedPubkeys.includes(event.pubkey)) {
|
||||
goals.push(event)
|
||||
}
|
||||
|
||||
if (event.kind === COMMENT) {
|
||||
comments.push(event)
|
||||
}
|
||||
},
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
setChecked($page.url.pathname)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon="notes-minimalistic" />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Goals</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createGoal}>
|
||||
<Icon icon="notes-minimalistic" />
|
||||
Create a Goal
|
||||
</Button>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 pt-4">
|
||||
{#each events as event (event.id)}
|
||||
<div in:fly>
|
||||
<GoalItem {url} event={$state.snapshot(event)} />
|
||||
</div>
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for goals...
|
||||
{:else if events.length === 0}
|
||||
No goals found.
|
||||
{:else}
|
||||
That's all!
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
</PageContent>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, sleep} from "@welshman/lib"
|
||||
import {COMMENT, getTagValue} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {request} from "@welshman/net"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
import {deriveEvent, decodeRelay} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
|
||||
const {relay, id} = $page.params
|
||||
const url = decodeRelay(relay)
|
||||
const event = deriveEvent(id)
|
||||
const filters = [{kinds: [COMMENT], "#E": [id]}]
|
||||
const replies = deriveEvents(repository, {filters})
|
||||
const summary = getTagValue("summary", $event.tags)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const openReply = () => {
|
||||
showReply = true
|
||||
}
|
||||
|
||||
const closeReply = () => {
|
||||
showReply = false
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
showAll = true
|
||||
}
|
||||
|
||||
let showAll = $state(false)
|
||||
let showReply = $state(false)
|
||||
|
||||
onMount(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
request({relays: [url], filters, signal: controller.signal})
|
||||
|
||||
return () => {
|
||||
controller.abort()
|
||||
setChecked($page.url.pathname)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{$event.content}</h1>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col p-2 pt-4">
|
||||
{#if $event}
|
||||
<div class="flex flex-col gap-3">
|
||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||
<div class="col-3 ml-12">
|
||||
<Content showEntire event={{...$event, content: summary}} {url} />
|
||||
<GoalSummary event={$event} {url} />
|
||||
<GoalActions event={$event} {url} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{#if !showAll && $replies.length > 4}
|
||||
<div class="flex justify-center">
|
||||
<Button class="btn btn-link" onclick={expand}>
|
||||
<Icon icon="sort-vertical" />
|
||||
Show all {$replies.length} replies
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#each sortBy(e => e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||
<div class="col-3 ml-12">
|
||||
<Content showEntire event={reply} {url} />
|
||||
<CommentActions event={reply} {url} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showReply}
|
||||
<EventReply {url} event={$event} onClose={closeReply} onSubmit={closeReply} />
|
||||
{:else}
|
||||
<div class="flex justify-end p-2">
|
||||
<Button class="btn btn-primary" onclick={openReply}>
|
||||
<Icon icon="reply" />
|
||||
Comment on this goal
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#await sleep(5000)}
|
||||
<Spinner loading>Loading funding goal...</Spinner>
|
||||
{:then}
|
||||
<p>Failed to load funding goal.</p>
|
||||
{/await}
|
||||
{/if}
|
||||
</PageContent>
|
||||
@@ -3,7 +3,7 @@
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, max, nthEq} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {THREAD, REACTION, DELETE, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {THREAD, DELETE, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {userMutes} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -16,6 +16,7 @@
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {decodeRelay, getEventsForUrl} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {REACTION_KINDS} from "@app/state"
|
||||
import {makeFeed} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
@@ -49,7 +50,7 @@
|
||||
relays: [url],
|
||||
feedFilters: [{kinds: [THREAD, COMMENT]}],
|
||||
subscriptionFilters: [
|
||||
{kinds: [THREAD, REACTION, DELETE]},
|
||||
{kinds: [THREAD, DELETE, ...REACTION_KINDS]},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)]},
|
||||
],
|
||||
initialEvents: getEventsForUrl(url, [{kinds: [THREAD, COMMENT], limit: 10}]),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
import {deriveEvent, decodeRelay} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
@@ -90,11 +91,11 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#each sortBy(e => -e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||
{#each sortBy(e => e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||
<div class="col-3 ml-12">
|
||||
<Content showEntire event={reply} {url} />
|
||||
<ThreadActions event={reply} {url} />
|
||||
<CommentActions event={reply} {url} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* global clients */
|
||||
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
|
||||
self.addEventListener("install", event => {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener("activate", event => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener("push", e => {
|
||||
console.log("Service Worker: Push event received", e)
|
||||
|
||||
let url = "/"
|
||||
let title = "New activity"
|
||||
let body = "You have a new message"
|
||||
|
||||
try {
|
||||
const data = e.data?.json()
|
||||
|
||||
if (data?.event) {
|
||||
url += nip19.neventEncode({
|
||||
id: data.event.id,
|
||||
relays: data.relays || [],
|
||||
})
|
||||
}
|
||||
|
||||
if (data?.title) {
|
||||
title = data.title
|
||||
}
|
||||
|
||||
if (data?.body) {
|
||||
body = data.body
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Service Worker: Failed to parse push data", e)
|
||||
}
|
||||
|
||||
e.waitUntil(
|
||||
self.registration.showNotification(title, {
|
||||
body,
|
||||
data: {url},
|
||||
icon: "/pwa-192x192.png",
|
||||
badge: "/pwa-64x64.png",
|
||||
tag: "flotilla-notification",
|
||||
requireInteraction: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener("notificationclick", e => {
|
||||
console.log("Service Worker: Notification click event", e)
|
||||
|
||||
e.notification.close()
|
||||
|
||||
if (e.action === "close") {
|
||||
return
|
||||
}
|
||||
|
||||
// Default action or 'open' action
|
||||
const url = e.notification.data?.url
|
||||
|
||||
e.waitUntil(
|
||||
clients
|
||||
.matchAll({
|
||||
type: "window",
|
||||
includeUncontrolled: true,
|
||||
})
|
||||
.then(clientList => {
|
||||
// Check if app is already open and send navigation message
|
||||
for (const client of clientList) {
|
||||
if (client.url.includes(location.origin)) {
|
||||
client.postMessage({
|
||||
type: "NAVIGATE",
|
||||
url: url,
|
||||
})
|
||||
|
||||
return client.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Open new window if app is not open
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -25,4 +25,9 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
compilerOptions: {
|
||||
warningFilter: (warning) => {
|
||||
return !['a11y_media_has_caption'].includes(warning.code)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+12
-8
@@ -1,9 +1,13 @@
|
||||
flotilla:
|
||||
android:
|
||||
identifier: social.flotilla
|
||||
name: Flotilla
|
||||
description: Self-hosted community chat and threads built on the nostr protocol.
|
||||
repository: https://github.com/coracle-social/flotilla
|
||||
artifacts:
|
||||
- app-release-signed.apk
|
||||
identifier: social.flotilla
|
||||
name: Flotilla
|
||||
tags: nostr nip29 community chat group
|
||||
changelog: CHANGELOG.md
|
||||
homepage: https://flotilla.social
|
||||
description: Self-hosted community chat and threads built on the nostr protocol.
|
||||
repository: https://github.com/coracle-social/flotilla
|
||||
blossom_servers:
|
||||
- https://cdn.zapstore.dev
|
||||
- https://hbr.coracle.social
|
||||
assets:
|
||||
- app-release-signed.apk
|
||||
|
||||
|
||||
Reference in New Issue
Block a user