Compare commits

...

25 Commits

Author SHA1 Message Date
Jon Staab 16a3ba2a9b Bump version 2025-06-30 11:08:42 -07:00
Jon Staab 7c11eb8947 Allow mark all as read on desktop 2025-06-30 11:03:02 -07:00
Jon Staab 6bdc8d4d9f Space alerts dialog 2025-06-30 10:41:42 -07:00
Jon Staab b9048936ba Tweak alerts button layout 2025-06-30 09:38:43 -07:00
Jon Staab b9620f4443 Add claim to alert add 2025-06-27 14:36:09 -07:00
Jon Staab f2249fe592 Handle conversations with no room 2025-06-27 09:44:01 -07:00
Jon Staab fd42a0e8d4 Clear badge when opening app 2025-06-27 09:41:30 -07:00
Jon Staab 37d52ba35f Show latest note as conversation 2025-06-27 09:00:43 -07:00
Jon Staab 3037323dc0 Add support for ios push notifications 2025-06-27 08:33:31 -07:00
Jon Staab 5301ef876d Fix notification badge for global chat 2025-06-24 17:36:14 -07:00
Jon Staab aa054d8b1a Fix ContentMention display 2025-06-24 17:23:07 -07:00
Jon Staab 3655790e5f Add fcm push notifications 2025-06-24 14:27:16 -07:00
Jon Staab 6cca823ed4 Get web push working 2025-06-23 11:16:25 -07:00
Jon Staab 18a383edab Update alert form to include push notifications 2025-06-19 10:01:16 -07:00
Jon Staab 43da7d628e Replace bunker with claim on alerts page 2025-06-18 17:02:32 -07:00
Jon Staab 2fae3ca248 Fix broadcasting user profiles when protected 2025-06-16 16:56:59 -07:00
Jon Staab d99ada44f5 Show link url if no title is available 2025-06-16 11:45:05 -07:00
Jon Staab cb0119b9b8 Update welshman 2025-06-16 10:12:24 -07:00
Jon Staab dac9ef8e4e Move some stuff to welshman, broadcast profile updates 2025-06-13 15:17:20 -07:00
Jon Staab 528917b90e Fix sort order of thread comments 2025-06-13 10:14:12 -07:00
Jon Staab a22db78967 rename createEvent to makeEvent 2025-06-10 13:35:57 -07:00
Jon Staab 5718510779 Bump versions 2025-06-09 15:29:13 -07:00
Jon Staab f877dc7fbe Add chat quick link 2025-06-09 15:27:00 -07:00
Jon Staab df03fb1116 Remove old docker workflow 2025-06-09 15:20:09 -07:00
Jon Staab 7455b49f8d Bump version
Build and Publish Docker Image / build-and-push (push) Failing after 9s
2025-06-09 15:15:58 -07:00
58 changed files with 1177 additions and 670 deletions
+1
View File
@@ -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_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/ VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_VAPID_PUBLIC_KEY=
VITE_GLITCHTIP_API_KEY= VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN= GLITCHTIP_AUTH_TOKEN=
-47
View File
@@ -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 }}
+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
# 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 # 1.1.0
* Add better theming support * Add better theming support
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 18 versionCode 21
versionName "1.0.4" versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+2
View File
@@ -12,6 +12,8 @@ dependencies {
implementation project(':capacitor-community-safe-area') implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-keyboard') implementation project(':capacitor-keyboard')
implementation project(':capacitor-push-notifications')
implementation project(':capawesome-capacitor-badge')
implementation project(':nostr-signer-capacitor-plugin') implementation project(':nostr-signer-capacitor-plugin')
} }
+1
View File
@@ -34,4 +34,5 @@
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest> </manifest>
+6
View File
@@ -11,5 +11,11 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
include ':capacitor-keyboard' include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android') project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.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' include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android') project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
+4
View File
@@ -15,6 +15,10 @@ const config: CapacitorConfig = {
style: "DARK", style: "DARK",
resizeOnFullScreen: true, resizeOnFullScreen: true,
}, },
Badge: {
persist: true,
autoClear: true
},
}, },
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload // Use this for live reload https://capacitorjs.com/docs/guides/live-reload
// server: { // server: {
+8 -4
View File
@@ -18,6 +18,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
@@ -57,6 +58,7 @@
504EC2FB1FED79650016851F = { 504EC2FB1FED79650016851F = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */, 7F8756D8B27F46E3366F6CEA /* Pods */,
@@ -349,16 +351,17 @@
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */; baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0.4; MARKETING_VERSION = 1.2.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -374,16 +377,17 @@
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */; baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0.4; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+9
View File
@@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) 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)
}
} }
+4
View File
@@ -49,5 +49,9 @@
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
</dict> </dict>
</plist> </plist>
+8
View File
@@ -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>
+2
View File
@@ -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 '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 '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 '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' 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 end
+14 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.0.4", "version": "1.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -45,6 +45,8 @@
"@capacitor/core": "^7.0.1", "@capacitor/core": "^7.0.1",
"@capacitor/ios": "^7.0.0", "@capacitor/ios": "^7.0.0",
"@capacitor/keyboard": "^7.0.0", "@capacitor/keyboard": "^7.0.0",
"@capacitor/push-notifications": "^7.0.1",
"@capawesome/capacitor-badge": "^7.0.1",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0", "@sentry/browser": "^8.35.0",
"@sveltejs/adapter-static": "^3.0.4", "@sveltejs/adapter-static": "^3.0.4",
@@ -52,18 +54,17 @@
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6", "@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "^0.3.4", "@welshman/app": "^0.3.8",
"@welshman/content": "^0.3.4", "@welshman/content": "^0.3.8",
"@welshman/dvm": "^0.3.4", "@welshman/editor": "^0.3.8",
"@welshman/editor": "^0.3.4", "@welshman/feeds": "^0.3.8",
"@welshman/feeds": "^0.3.4", "@welshman/lib": "^0.3.8",
"@welshman/lib": "^0.3.4", "@welshman/net": "^0.3.8",
"@welshman/net": "^0.3.4", "@welshman/relay": "^0.3.8",
"@welshman/relay": "^0.3.4", "@welshman/router": "^0.3.8",
"@welshman/router": "^0.3.4", "@welshman/signer": "^0.3.8",
"@welshman/signer": "^0.3.4", "@welshman/store": "^0.3.8",
"@welshman/store": "^0.3.4", "@welshman/util": "^0.3.8",
"@welshman/util": "^0.3.4",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.13.0",
+176 -169
View File
@@ -29,6 +29,12 @@ importers:
'@capacitor/keyboard': '@capacitor/keyboard':
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.1(@capacitor/core@7.2.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)
'@poppanator/sveltekit-svg': '@poppanator/sveltekit-svg':
specifier: ^4.2.1 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)) 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))
@@ -51,41 +57,38 @@ importers:
specifier: ^0.6.6 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)) 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': '@welshman/app':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2) version: 0.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
'@welshman/content': '@welshman/content':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3) version: 0.3.8(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)
'@welshman/editor': '@welshman/editor':
specifier: ^0.3.4 specifier: ^0.3.8
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) version: 0.3.8(@tiptap/extension-image@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.23.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)(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': '@welshman/feeds':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2) version: 0.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
'@welshman/lib': '@welshman/lib':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4 version: 0.3.8
'@welshman/net': '@welshman/net':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3)(ws@8.18.2) version: 0.3.8(typescript@5.8.3)(ws@8.18.3)
'@welshman/relay': '@welshman/relay':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3) version: 0.3.8(typescript@5.8.3)
'@welshman/router': '@welshman/router':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3) version: 0.3.8(typescript@5.8.3)
'@welshman/signer': '@welshman/signer':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.2) version: 0.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
'@welshman/store': '@welshman/store':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3) version: 0.3.8(typescript@5.8.3)
'@welshman/util': '@welshman/util':
specifier: ^0.3.4 specifier: ^0.3.8
version: 0.3.4(typescript@5.8.3) version: 0.3.8(typescript@5.8.3)
compressorjs: compressorjs:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@@ -743,6 +746,16 @@ packages:
peerDependencies: peerDependencies:
'@capacitor/core': '>=7.0.0' '@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': '@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1370,77 +1383,77 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code-block@2.14.0': '@tiptap/extension-code-block@2.23.0':
resolution: {integrity: sha512-LRYYZeh8U2XgfTsJ4houB9s9cVRt7PRfVa4MaCeOYKfowVOKQh67yV5oom8Azk9XrMPkPxDmMmdPAEPxeVYFvw==} resolution: {integrity: sha512-p8iizp5nQBBhYPrIgBVwEqcDnc2fFRAZCXy/xjmAk2kKOhB7NYe3+1yrbFcQKVAdaUFxG+BRj2WxNDeeRt5tJA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.14.0': '@tiptap/extension-code@2.23.0':
resolution: {integrity: sha512-kyo02mnzqgwXayMcyRA/fHQgb+nMmQQpIt1irZwjtEoFZshA7NnY/6b5SJmRcxQ4/X4r2Y2Ha2sWmOcEkLmt4A==} resolution: {integrity: sha512-Ip/5+kNoqrxYPHLnZMf7i6wfjjRuR5QgfC3IR3Mk1WQM1JGXCLL+uUjTUxKXFUj28hjSJfsmVbTUhoVvgZEWfw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.14.0': '@tiptap/extension-document@2.23.0':
resolution: {integrity: sha512-qwEgpPIJ3AgXdEtRTr88hODbXRdt14VAwLj27PTSqexB5V7Ra1Jy7iQDhqRwBCoUomVywBsWYxkSuDisSRG+9w==} resolution: {integrity: sha512-kuRPqH0UdjZ4RcnpPELsu1N8LqeixEin+mv5eaQJI/aZ6rFq+kcY4UZF3C7q56Rat5r9CgHBiZbD0t5l6E3gdA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.14.0': '@tiptap/extension-dropcursor@2.23.0':
resolution: {integrity: sha512-FIh5cdPuoPKvZ0GqSKhzMZGixm05ac3hSgqhMNCBZmXX459qBUI9CvDl/uzSnY9koBDeLVV3HYMthWQQLSXl9A==} resolution: {integrity: sha512-m2LzkJpipHLPEllD3MXZQMssu7Xng7YJOJ8ZNDkF0uUkXljwh7G0ROjGNKUlV8/dqoCVmJIZIyF6t9saQwTTbA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.14.0': '@tiptap/extension-gapcursor@2.23.0':
resolution: {integrity: sha512-as+SqC39FRshw4Fm1XVlrdSXveiusf5xiC4nuefLmXsUxO7Yx67x8jS0/VQbxWTLHZ6R1YEW8prLtnxGmVLCAQ==} resolution: {integrity: sha512-SpYsDtMiVwqcSB84g714PrnHo985R5UiIaGngef6iMNy/0xjKcO0tj/feu0WwJDuSj22Opzlnb/Ld/D4Va27Ng==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.14.0': '@tiptap/extension-hard-break@2.23.0':
resolution: {integrity: sha512-A8c8n8881iBq3AusNqibh6Hloybr+FgYdg4Lg4jNxbbEaL0WhyLFge1bWlGVpbHXFqdv5YldMUAu6Rop3FhNvw==} resolution: {integrity: sha512-OpNBEYv9HDUPo8SgvmI5oPd0b+xmdadtFyL7t4lxhYar8n5NDYubaXYgbKcdJfXvUxEeGwdc3ePnTFpsF0mrYw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.14.0': '@tiptap/extension-history@2.23.0':
resolution: {integrity: sha512-/qnOHQFCEPfkb3caykqd+sqzEC2gx30EQB/mM7+5kIG7CQy7XXaGjFAEaqzE1xJ783Q2E7GVk4JxWM+3NhYSLw==} resolution: {integrity: sha512-W+2bZ/02nm56g/wmEaSx9QcdZ8mHjoFyc8MKf54Mrzi+nIdNjsNreKrn1yCp683CGEPd8DLadDFkz0o13N+rxA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-image@2.14.0': '@tiptap/extension-image@2.23.0':
resolution: {integrity: sha512-pYCUzZBgsxIvVGTzuW03cPz6PIrAo26xpoxqq4W090uMVoK0SgY5W5y0IqCdw4QyLkJ2/oNSFNc2EP9jVi1CcQ==} resolution: {integrity: sha512-/rW2+a21VBGBv5c/78CVW8XA7bThSqE3FqcBtWyq8IxZoe8Hj9+Jac7FcB2YR3aY0BeHwso474e1RuVr1iYBKQ==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.14.0': '@tiptap/extension-link@2.23.0':
resolution: {integrity: sha512-fsqW7eRD2xoD6xy7eFrNPAdIuZ3eicA4jKC45Vcft/Xky0DJoIehlVBLxsPbfmv3f27EBrtPkg5+msLXkLyzJA==} resolution: {integrity: sha512-D+ethAE8+2f7RH7kqS+//EsC2wNblhmssJYVE0hCXM5BKIBixjs8eCOAvLbJsw0u/5LqFYjsyAimTqa4hD5uvg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-paragraph@2.14.0': '@tiptap/extension-paragraph@2.23.0':
resolution: {integrity: sha512-bsQesVpgvDS2e+wr2fp59QO7rWRp2FqcJvBafwXS3Br9U5Mx3eFYryx4wC7cUnhlhUwX5pmaoA7zISgV9dZDgg==} resolution: {integrity: sha512-MXhRkb741UOcJp2evG/H0MY3WJQnX7z8PsejmJbJXOHBrS/Esxq0AlrDAjuFhbfAnJwYiWQ1lk6ucvKV6DhFuQ==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-placeholder@2.14.0': '@tiptap/extension-placeholder@2.23.0':
resolution: {integrity: sha512-xzfjHvuukbch4i5O/5uyS2K2QgNEaMKi6e6GExTTgVwnFjKfJmgTqee33tt5JCqSItBvtSZlU3SX/vpiaIof+w==} resolution: {integrity: sha512-I5RQk0qn6nj7l7z4mWKIxjO2nluvKsm00W2CbC75b4YcScBfsMInHQdjN2s+W8xuF0zquhwVITxA+Bmn4zynqg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-text@2.14.0': '@tiptap/extension-text@2.23.0':
resolution: {integrity: sha512-rHny566nGZHq61zRLwQ9BPG55W/O+eDKwUJl+LhrLiVWwzpvAl9QQYixtoxJKOY48VK41PKwxe3bgDYgNs/Fhg==} resolution: {integrity: sha512-hF+CU1H4B4UgqjBXXPPaACVZdSGuMH0TDYTd7h403qUAIBKkYbjuan7laQpiT0qnF0Dg+sGgvmGcd4H1tTBM8g==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm@2.12.0': '@tiptap/pm@2.12.0':
resolution: {integrity: sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==} resolution: {integrity: sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==}
'@tiptap/suggestion@2.14.0': '@tiptap/suggestion@2.23.0':
resolution: {integrity: sha512-AXzEw0KYIyg5id8gz5geIffnBtkZqan5MWe29rGo3gXTfKH+Ik8tWbZdnlMVheycsUCllrymDRei4zw9DqVqkQ==} resolution: {integrity: sha512-WUUGADu8ZezXZ4hXZWdfGcfoitB5tiBrc2u1oXqqL8VmJJedhY4MdWUPYqgh3359tAI2yJWmv+gPabX361gBEA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
@@ -1596,44 +1609,41 @@ packages:
'@vite-pwa/assets-generator': '@vite-pwa/assets-generator':
optional: true optional: true
'@welshman/app@0.3.4': '@welshman/app@0.3.8':
resolution: {integrity: sha512-pb1I53hWog6plLVyOx6biN0uiB7zdheDqN2hWfb/uUKKzD484u+Yk3p5H2S6VaJnSbgNHnPuuJnjVstts1rhLg==} resolution: {integrity: sha512-WmvqB8Z/qPN0dJerf3QTp5MniZQGXToNXJWorEFA3LQwBJWAovPFfdq8xoNGe9tWwYl0m9Rt/ObZf3gFlX28Cw==}
'@welshman/content@0.3.4': '@welshman/content@0.3.8':
resolution: {integrity: sha512-kuyykt5SK4vHUciJBooqfLDI/HBxekZPK3qVgmKlpVf8cMRmaSOGTjsKljINgFsT/3J5z7wZI5AzQR/2FBo2ZQ==} resolution: {integrity: sha512-ic7imQR0cpolUlwnWVfUqiIo9zkOt6DS2M92BD4Y/mCLGrUMzlUw0/NE5TzBJ6dSywVh8/aBBOTWotzpmbttKg==}
'@welshman/dvm@0.3.4': '@welshman/editor@0.3.8':
resolution: {integrity: sha512-39uSVco5VZmYEE+BZ3Lx/hGVM/HzGfnlEMR0fysh4BqfMm3Po9KXfOLy4YDMoHt7Ai/rC4z/HdJ81F3jHhJZjw==} resolution: {integrity: sha512-XZ/cXEM3MIwhR7CZvboH2askm9dZJ9cH7/CS4Asd3Q/OaaPUrCTCoacEpR1z3raMMOz0TiDn+BgjGqrHYU/58Q==}
'@welshman/editor@0.3.4': '@welshman/feeds@0.3.8':
resolution: {integrity: sha512-fpUnacyZvbtytadtVwV4CEm1rGHgp8xWHQB7pZ0m/5bxlAHT1p/cL5a83CjdrVbu3+HMzBfgV4k6ldhH1XnJaQ==} resolution: {integrity: sha512-Rjf36Eng22PMY9p1yepMDfXH2dlCPg5yBVidu8WArFWLgxSv67DCxAsDlHkRd/mJD5DARFDob1WZP9bUsljJBw==}
'@welshman/feeds@0.3.4': '@welshman/lib@0.3.8':
resolution: {integrity: sha512-w4GLcWOxTUlx9MBbqHfee0ePWW8oyMGaf+Hey35FNsKcBvxiO58uW9j6BBvlu9yacthw0RGa/91giJn3zZzx1Q==} resolution: {integrity: sha512-fbq94UkyoC7kieAlWsDzH6zgZt+GVhkh1RiFvtE4iGug3bIPxh1QmmvO70EHlFkiEDcYAFEEHd9OMm6/JRc97Q==}
'@welshman/lib@0.3.4':
resolution: {integrity: sha512-37WDPZHsUFBLKdlkQXKAVuEDZB3lzQOkfIG0MMy7uZsg8+zR4VoA28g9B1vAKGky9lmWequHMPNigPRjC7RHQA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@welshman/net@0.3.4': '@welshman/net@0.3.8':
resolution: {integrity: sha512-btC7eXwEA8wfzaCsq8mdFIfbX61JeoyI3am53+L23ujbJQ9aqCciTA6EYEa2JzYB2HdydmCzdZrP/DTSjHN6bw==} resolution: {integrity: sha512-DWNL+BGmOGCfXdYOnpd3IJ7IuCskftUfXnjBo8F0rkHWUWAE6Q1zTJmdJG3kDVDoWl3X4XBfxkl4WGckF9pSkw==}
'@welshman/relay@0.3.4': '@welshman/relay@0.3.8':
resolution: {integrity: sha512-XjZY6XQXvNVFr3KC4O2Mlj+LHg/xJuQnhAHUYemvtJ9BcxvY01fq4ESadyiJklCjqNabsCPz76o+qFweZmym7Q==} resolution: {integrity: sha512-vWUOxvG4WV0+EsC/BYoCF9L2W/qml4TIAnGHmWpDt9jTS82kIQt3cu2+3uJs9IZli+etRJ3xYJLVvfzfQqBaig==}
'@welshman/router@0.3.4': '@welshman/router@0.3.8':
resolution: {integrity: sha512-ej4EjXWNj6srj6xnzcmFqhGXg6asTByPHPxQCkPG6HDOMVmG8GejlcatvjkzB4ppfdwYCz+jyJvjq30JBHmlbQ==} resolution: {integrity: sha512-3Gn3yjMbQ9sQ8qsX5bjUtTSKkwdOREqGVzcQiBq1FRATh42ih06Opu5/t8ujcMRMhV3w/02ckfZJ46DF34ozWQ==}
'@welshman/signer@0.3.4': '@welshman/signer@0.3.8':
resolution: {integrity: sha512-kg2d4dQutRYvuuiCFC2I0UZfEHu+71t6o2g+jMtJ6d55dSa6+xUMCQ0/8Ua0AMOc/D7tJ1HLvtbmOjsehKkkKQ==} resolution: {integrity: sha512-L3hAJlS0smS4uSC5b1xskHDbrkuCykXr5b6Z9aNV9MJjczbb5pCuYjHf9lCvcmqaz4NN4kcksXlLZgxMbE/5Jg==}
peerDependencies: peerDependencies:
nostr-signer-capacitor-plugin: ~0.0.4 nostr-signer-capacitor-plugin: ~0.0.4
'@welshman/store@0.3.4': '@welshman/store@0.3.8':
resolution: {integrity: sha512-AYDLTA7+ptbJYfAVRQ5lC/l65FtkAj7lKSadUguSf+R10JClNeYPWqQ6RTRpvyZ9qn9QetuYx8ecLfm8BxC7Ow==} resolution: {integrity: sha512-SHKF9RjcvoqcduHDleKSjhubrS10f5XmcU2kBPFy/U9G7hi4M5f4HQa46Kt7Mf4OHuofnjRw+4RLopkqjFL3ow==}
'@welshman/util@0.3.4': '@welshman/util@0.3.8':
resolution: {integrity: sha512-sBznEqAPlNu9CZQSNFPcL5kNOIoWYfUTY/lyXQXLiYhMorXQmVGXS4TT7knS41oKiLgkUK98T0DvjMr4iJd3/g==} resolution: {integrity: sha512-+doowqtIjUChPGdmGNopO6bAvC/0LkF2zKaEKPHnIfDAsw4gAJyuAvIlS/PUv8tY6scXORGeGMnoBC0Htd3Ovg==}
'@xml-tools/parser@1.0.11': '@xml-tools/parser@1.0.11':
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==} resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
@@ -3381,8 +3391,8 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
nostr-editor-coracle-workaround@0.0.4-pre.18: nostr-editor@1.0.0:
resolution: {integrity: sha512-QAoySZ9uOsR7C4nnVbcEpVgT0vLxwZxlYhE2NsJzmeoK7nTgkcHFaZkn/QMlUem3qjT8AolW8X5TrRdnZ5eIZQ==} resolution: {integrity: sha512-+TL3G0m7WsXeEAitxzQhun7hyARxqRANjGIS2z9CBbniCGvT/Wz6YLgUnUysnBg3tmSgMZg5FWhaDPwfvdvbSw==}
engines: {node: '>=18.16.1'} engines: {node: '>=18.16.1'}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.6.6 '@tiptap/core': ^2.6.6
@@ -3390,10 +3400,11 @@ packages:
'@tiptap/extension-link': ^2.6.6 '@tiptap/extension-link': ^2.6.6
'@tiptap/pm': ^2.6.6 '@tiptap/pm': ^2.6.6
linkifyjs: ^4.1.3 linkifyjs: ^4.1.3
nostr-tools: ^2.14.2 nostr-tools: ~2.14.2
prosemirror-markdown: ^1.13.0 prosemirror-markdown: ^1.13.0
prosemirror-model: ^1.22.3 prosemirror-model: ^1.22.3
prosemirror-state: ^1.4.3 prosemirror-state: ^1.4.3
prosemirror-view: ^1.39.3
tiptap-markdown: ^0.8.10 tiptap-markdown: ^0.8.10
nostr-signer-capacitor-plugin@0.0.4: nostr-signer-capacitor-plugin@0.0.4:
@@ -4687,8 +4698,8 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.2: ws@8.18.3:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
peerDependencies: peerDependencies:
bufferutil: ^4.0.1 bufferutil: ^4.0.1
@@ -5547,6 +5558,14 @@ snapshots:
dependencies: dependencies:
'@capacitor/core': 7.2.0 '@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': '@cspotcode/source-map-support@0.8.1':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
@@ -6150,58 +6169,58 @@ snapshots:
dependencies: dependencies:
'@tiptap/pm': 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-code-block@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.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-dropcursor@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.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-history@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.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-link@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.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 linkifyjs: 4.3.1
'@tiptap/extension-paragraph@2.14.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))': '@tiptap/extension-paragraph@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.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-placeholder@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
@@ -6226,7 +6245,7 @@ snapshots:
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.4
prosemirror-view: 1.39.3 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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0 '@tiptap/pm': 2.12.0
@@ -6435,18 +6454,17 @@ snapshots:
optionalDependencies: optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6 '@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.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
dependencies: dependencies:
'@types/throttle-debounce': 5.0.2 '@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.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)
'@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.8
'@welshman/lib': 0.3.4 '@welshman/net': 0.3.8(typescript@5.8.3)(ws@8.18.3)
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2) '@welshman/relay': 0.3.8(typescript@5.8.3)
'@welshman/relay': 0.3.4(typescript@5.8.3) '@welshman/router': 0.3.8(typescript@5.8.3)
'@welshman/router': 0.3.4(typescript@5.8.3) '@welshman/signer': 0.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.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.8(typescript@5.8.3)
'@welshman/store': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
'@welshman/util': 0.3.4(typescript@5.8.3)
fuse.js: 7.1.0 fuse.js: 7.1.0
idb: 8.0.2 idb: 8.0.2
svelte: 4.2.20 svelte: 4.2.20
@@ -6456,43 +6474,31 @@ snapshots:
- typescript - typescript
- ws - ws
'@welshman/content@0.3.4(typescript@5.8.3)': '@welshman/content@0.3.8(typescript@5.8.3)':
dependencies: dependencies:
'@braintree/sanitize-url': 7.1.1 '@braintree/sanitize-url': 7.1.1
nostr-tools: 2.14.2(typescript@5.8.3) nostr-tools: 2.14.2(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - 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)': '@welshman/editor@0.3.8(@tiptap/extension-image@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.23.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)(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:
'@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)':
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@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': 2.23.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-code-block': 2.23.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-document': 2.23.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-dropcursor': 2.23.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-gapcursor': 2.23.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-hard-break': 2.23.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-history': 2.23.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-paragraph': 2.23.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-placeholder': 2.23.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-text': 2.23.0(@tiptap/core@2.12.0(@tiptap/pm@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) '@tiptap/suggestion': 2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(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))) nostr-editor: 1.0.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-image@2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.23.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)(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) nostr-tools: 2.14.2(typescript@5.8.3)
tippy.js: 6.3.7 tippy.js: 6.3.7
transitivePeerDependencies: transitivePeerDependencies:
@@ -6502,82 +6508,82 @@ snapshots:
- prosemirror-markdown - prosemirror-markdown
- prosemirror-model - prosemirror-model
- prosemirror-state - prosemirror-state
- prosemirror-view
- tiptap-markdown - tiptap-markdown
- typescript - 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.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
dependencies: 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.8
'@welshman/lib': 0.3.4 '@welshman/net': 0.3.8(typescript@5.8.3)(ws@8.18.3)
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2) '@welshman/relay': 0.3.8(typescript@5.8.3)
'@welshman/relay': 0.3.4(typescript@5.8.3) '@welshman/router': 0.3.8(typescript@5.8.3)
'@welshman/router': 0.3.4(typescript@5.8.3) '@welshman/signer': 0.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.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.8(typescript@5.8.3)
'@welshman/util': 0.3.4(typescript@5.8.3)
trava: 1.2.1 trava: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- nostr-signer-capacitor-plugin - nostr-signer-capacitor-plugin
- typescript - typescript
- ws - ws
'@welshman/lib@0.3.4': '@welshman/lib@0.3.8':
dependencies: dependencies:
'@scure/base': 1.2.6 '@scure/base': 1.2.6
'@types/events': 3.0.3 '@types/events': 3.0.3
events: 3.3.0 events: 3.3.0
'@welshman/net@0.3.4(typescript@5.8.3)(ws@8.18.2)': '@welshman/net@0.3.8(typescript@5.8.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/relay': 0.3.4(typescript@5.8.3) '@welshman/relay': 0.3.8(typescript@5.8.3)
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
events: 3.3.0 events: 3.3.0
isomorphic-ws: 5.0.0(ws@8.18.2) isomorphic-ws: 5.0.0(ws@8.18.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/relay@0.3.4(typescript@5.8.3)': '@welshman/relay@0.3.8(typescript@5.8.3)':
dependencies: dependencies:
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@welshman/router@0.3.4(typescript@5.8.3)': '@welshman/router@0.3.8(typescript@5.8.3)':
dependencies: dependencies:
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/relay': 0.3.4(typescript@5.8.3) '@welshman/relay': 0.3.8(typescript@5.8.3)
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - 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.3.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0))(typescript@5.8.3)(ws@8.18.3)':
dependencies: dependencies:
'@noble/curves': 1.9.2 '@noble/curves': 1.9.2
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/net': 0.3.4(typescript@5.8.3)(ws@8.18.2) '@welshman/net': 0.3.8(typescript@5.8.3)(ws@8.18.3)
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.2.0) nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.2.0)
nostr-tools: 2.14.2(typescript@5.8.3) nostr-tools: 2.14.2(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/store@0.3.4(typescript@5.8.3)': '@welshman/store@0.3.8(typescript@5.8.3)':
dependencies: dependencies:
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
'@welshman/relay': 0.3.4(typescript@5.8.3) '@welshman/relay': 0.3.8(typescript@5.8.3)
'@welshman/util': 0.3.4(typescript@5.8.3) '@welshman/util': 0.3.8(typescript@5.8.3)
svelte: 4.2.20 svelte: 4.2.20
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@welshman/util@0.3.4(typescript@5.8.3)': '@welshman/util@0.3.8(typescript@5.8.3)':
dependencies: dependencies:
'@types/ws': 8.18.1 '@types/ws': 8.18.1
'@welshman/lib': 0.3.4 '@welshman/lib': 0.3.8
js-base64: 3.7.7 js-base64: 3.7.7
nostr-tools: 2.14.2(typescript@5.8.3) nostr-tools: 2.14.2(typescript@5.8.3)
nostr-wasm: 0.1.0 nostr-wasm: 0.1.0
@@ -8075,9 +8081,9 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
isomorphic-ws@5.0.0(ws@8.18.2): isomorphic-ws@5.0.0(ws@8.18.3):
dependencies: dependencies:
ws: 8.18.2 ws: 8.18.3
jackspeak@3.4.3: jackspeak@3.4.3:
dependencies: dependencies:
@@ -8409,11 +8415,11 @@ snapshots:
normalize-range@0.1.2: {} 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.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)))(@tiptap/extension-link@2.23.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)(prosemirror-view@1.39.3)(tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))):
dependencies: dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@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-image': 2.23.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-link': 2.23.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@tiptap/pm': 2.12.0 '@tiptap/pm': 2.12.0
js-base64: 3.7.7 js-base64: 3.7.7
light-bolt11-decoder: 3.2.0 light-bolt11-decoder: 3.2.0
@@ -8422,6 +8428,7 @@ snapshots:
prosemirror-markdown: 1.13.2 prosemirror-markdown: 1.13.2
prosemirror-model: 1.25.1 prosemirror-model: 1.25.1
prosemirror-state: 1.4.3 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)) 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): nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.2.0):
@@ -9924,7 +9931,7 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.18.2: {} ws@8.18.3: {}
xcode@3.0.1: xcode@3.0.1:
dependencies: dependencies:
+83 -75
View File
@@ -1,6 +1,6 @@
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store" 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 {Feed} from "@welshman/feeds"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {
@@ -12,14 +12,14 @@ import {
FOLLOWS, FOLLOWS,
REACTION, REACTION,
AUTH_JOIN, AUTH_JOIN,
GROUP_JOIN, ROOMS,
GROUP_LEAVE,
GROUP_CREATE,
GROUP_EDIT_META,
GROUPS,
COMMENT, COMMENT,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
isSignedEvent, isSignedEvent,
createEvent, makeEvent,
displayProfile, displayProfile,
normalizeRelayUrl, normalizeRelayUrl,
makeList, makeList,
@@ -55,11 +55,9 @@ import {
getThunkError, getThunkError,
} from "@welshman/app" } from "@welshman/app"
import { import {
tagRoom,
PROTECTED, PROTECTED,
userMembership, userMembership,
INDEXER_RELAYS, INDEXER_RELAYS,
ALERT,
NOTIFIER_PUBKEY, NOTIFIER_PUBKEY,
NOTIFIER_RELAY, NOTIFIER_RELAY,
userRoomsByUrl, userRoomsByUrl,
@@ -126,38 +124,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 // List updates
export const addSpaceMembership = async (url: string) => { 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 event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -165,7 +135,7 @@ export const addSpaceMembership = async (url: string) => {
} }
export const removeSpaceMembership = 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 pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -174,7 +144,7 @@ export const removeSpaceMembership = async (url: string) => {
} }
export const addRoomMembership = async (url: string, room: 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 = [ const newTags = [
["r", url], ["r", url],
["group", room, url], ["group", room, url],
@@ -186,7 +156,7 @@ export const addRoomMembership = async (url: string, room: string) => {
} }
export const removeRoomMembership = 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 pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -207,7 +177,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
} }
return publishThunk({ return publishThunk({
event: createEvent(list.kind, {tags}), event: makeEvent(list.kind, {tags}),
relays: [ relays: [
url, url,
...INDEXER_RELAYS, ...INDEXER_RELAYS,
@@ -229,7 +199,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
} }
return publishThunk({ return publishThunk({
event: createEvent(list.kind, {tags}), event: makeEvent(list.kind, {tags}),
relays: [ relays: [
...INDEXER_RELAYS, ...INDEXER_RELAYS,
...Router.get().FromUser().getUrls(), ...Router.get().FromUser().getUrls(),
@@ -241,13 +211,18 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
// Relay access // 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 = "") => { export const checkRelayAccess = async (url: string, claim = "") => {
const socket = Pool.get().get(url) const socket = Pool.get().get(url)
await socket.auth.attemptAuth(e => signer.get()?.sign(e)) await attemptAuth(url)
const thunk = publishThunk({ const thunk = publishThunk({
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}), event: makeEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
relays: [url], relays: [url],
}) })
@@ -266,6 +241,11 @@ export const checkRelayAccess = async (url: string, claim = "") => {
// Ignore messages about the relay ignoring ours // Ignore messages about the relay ignoring ours
if (error?.startsWith("mute: ")) return if (error?.startsWith("mute: ")) return
// Ignore rejected empty claims
if (!claim && error?.includes("invite code")) {
return `failed to request access to relay`
}
return message return message
} }
} }
@@ -297,7 +277,7 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
const socket = Pool.get().get(url) const socket = Pool.get().get(url)
const okStatuses = [AuthStatus.None, AuthStatus.Ok] 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. // Only raise an error if it's not a timeout.
// If it is, odds are the problem is with our signer, not the relay // If it is, odds are the problem is with our signer, not the relay
@@ -324,20 +304,26 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
// Actions // Actions
export const makeDelete = ({event}: {event: TrustedEvent}) => { export const makeDelete = ({event, tags = []}: {event: TrustedEvent; tags?: string[][]}) => {
const tags = [["k", String(event.kind)], ...tagEvent(event)] const thisTags = [["k", String(event.kind)], ...tagEvent(event), ...tags]
const groupTag = getTag("h", event.tags) const groupTag = getTag("h", event.tags)
if (groupTag) { if (groupTag) {
tags.push(PROTECTED) thisTags.push(PROTECTED, groupTag)
tags.push(groupTag)
} }
return createEvent(DELETE, {tags}) return makeEvent(DELETE, {tags: thisTags})
} }
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) => export const publishDelete = ({
publishThunk({event: makeDelete({event}), relays}) relays,
event,
tags = [],
}: {
relays: string[]
event: TrustedEvent
tags?: string[][]
}) => publishThunk({event: makeDelete({event, tags}), relays})
export type ReportParams = { export type ReportParams = {
event: TrustedEvent event: TrustedEvent
@@ -351,7 +337,7 @@ export const makeReport = ({event, reason, content}: ReportParams) => {
["e", event.id, reason], ["e", event.id, reason],
] ]
return createEvent(REPORT, {content, tags}) return makeEvent(REPORT, {content, tags})
} }
export const publishReport = ({ export const publishReport = ({
@@ -378,7 +364,7 @@ export const makeReaction = ({content, event, tags: paramTags = []}: ReactionPar
tags.push(groupTag) tags.push(groupTag)
} }
return createEvent(REACTION, {content, tags}) return makeEvent(REACTION, {content, tags})
} }
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) => export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
@@ -391,42 +377,64 @@ export type CommentParams = {
} }
export const makeComment = ({event, content, tags = []}: 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[]}) => export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
publishThunk({event: makeComment(params), relays}) publishThunk({event: makeComment(params), relays})
export type AlertParams = { export type AlertParams = {
feed: Feed feed: Feed
cron: string
email: string
bunker: string
secret: string
description: 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 = [ const tags = [
["feed", JSON.stringify(feed)], ["feed", JSON.stringify(params.feed)],
["cron", cron],
["email", email],
["locale", LOCALE], ["locale", LOCALE],
["timezone", TIMEZONE], ["timezone", TIMEZONE],
["description", description], ["description", params.description],
["channel", "email"],
[
"handler",
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
"wss://relay.nostr.band/",
"web",
],
] ]
if (bunker) { for (const [relay, claim] of Object.entries(params.claims)) {
tags.push(["nip46", secret, bunker]) 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)), content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
tags: [ tags: [
["d", randomId()], ["d", randomId()],
+172 -106
View File
@@ -1,24 +1,56 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib" import {decrypt} from "@welshman/signer"
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util" 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 {Filter} from "@welshman/util"
import type {Nip46ResponseWithResult} from "@welshman/signer"
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds" 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 Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import InfoBunker from "@app/components/InfoBunker.svelte" import {
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte" alerts,
import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state" getMembershipUrls,
import {loadAlertStatuses} from "@app/requests" userMembership,
import {publishAlert} from "@app/commands" 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 {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 timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
const minute = randomInt(0, 59) const minute = randomInt(0, 59)
@@ -26,49 +58,22 @@
const WEEKLY = `0 ${minute} ${hour} * * 1` const WEEKLY = `0 ${minute} ${hour} * * 1`
const DAILY = `0 ${minute} ${hour} * * *` const DAILY = `0 ${minute} ${hour} * * *`
let loading = false let loading = $state(false)
let cron = WEEKLY let cron = $state(WEEKLY)
let email = $alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "" let claim = $state("")
let relay = "" let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
let bunker = ""
let secret = ""
let notifyThreads = true
let notifyCalendar = true
let notifyChat = false
let showBunker = false
const back = () => history.back() 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 () => { const submit = async () => {
if (!email.includes("@")) { if (channel === "email" && !email.includes("@")) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please provide an email address", message: "Please provide an email address",
}) })
} }
if (!relay) { if (!url) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please select a space", message: "Please select a space",
@@ -99,22 +104,69 @@
if (notifyChat) { if (notifyChat) {
display.push("chat") display.push("chat")
filters.push({ filters.push({kinds: [MESSAGE]})
kinds: [MESSAGE],
"#h": getMembershipRoomsByUrl(relay, $userMembership),
})
} }
loading = true loading = true
try { try {
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily" const claims = claim ? {[url]: claim} : {}
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.` const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay)) const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
const thunk = await publishAlert({cron, email, feed, bunker, secret, description}) const params: AlertParams = {feed, claims, description}
await thunk.result if (channel === "email") {
await loadAlertStatuses($pubkey!) 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!"}) pushToast({message: "Your alert has been successfully created!"})
back() back()
@@ -122,6 +174,20 @@
loading = false loading = false
} }
} }
onMount(() => {
if (!canSendPushNotifications()) {
channel = "email"
}
if (url) {
requestRelayClaim(url).then(code => {
if (code) {
claim = code
}
})
}
})
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(submit)}> <form class="column gap-4" onsubmit={preventDefault(submit)}>
@@ -130,13 +196,20 @@
Add an Alert Add an Alert
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
{#if showBunker} {#if canSendPushNotifications()}
<div class="card2 flex flex-col items-center gap-4 bg-base-300"> <FieldInline>
<p>Scan using a nostr signer, or click to copy.</p> {#snippet label()}
<BunkerConnect {controller} /> <p>Alert Type*</p>
<Button class="btn btn-neutral btn-sm" onclick={hideBunker}>Cancel</Button> {/snippet}
</div> {#snippet input()}
{:else} <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> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Email Address*</p> <p>Email Address*</p>
@@ -158,12 +231,14 @@
</select> </select>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
{/if}
{#if !hideSpaceField}
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Space*</p> <p>Space*</p>
{/snippet} {/snippet}
{#snippet input()} {#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> <option value="" disabled selected>Choose a space URL</option>
{#each getMembershipUrls($userMembership) as url (url)} {#each getMembershipUrls($userMembership) as url (url)}
<option value={url}>{displayRelayUrl(url)}</option> <option value={url}>{displayRelayUrl(url)}</option>
@@ -171,59 +246,50 @@
</select> </select>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
<FieldInline> {/if}
{#snippet label()} <FieldInline>
<p>Notifications*</p> {#snippet label()}
{/snippet} <p>Notifications*</p>
{#snippet input()} {/snippet}
<div class="flex items-center justify-end gap-4"> {#snippet input()}
<span class="flex gap-3"> <div class="flex items-center justify-end gap-4">
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} /> <span class="flex gap-3">
Threads <input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
</span> Threads
<span class="flex gap-3"> </span>
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} /> <span class="flex gap-3">
Calendar <input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
</span> Calendar
<span class="flex gap-3"> </span>
<input type="checkbox" class="checkbox" bind:checked={notifyChat} /> <span class="flex gap-3">
Chat <input type="checkbox" class="checkbox" bind:checked={notifyChat} />
</span> Chat
</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}
</span> </span>
</div> </div>
<p class="text-sm"> {/snippet}
Required for receiving alerts about spaces with access controls. You can get one from your </FieldInline>
<Button class="text-primary" onclick={() => pushModal(InfoBunker)}>remote signer app</Button <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> </p>
{#if bunker} {/snippet}
<Button class="btn btn-neutral btn-sm flex-grow" onclick={clearBunker}>Disconnect</Button> </FieldInline>
{:else}
<Button class="btn btn-primary btn-sm w-full flex-grow" onclick={connectBunker}
>Connect</Button>
{/if}
</div>
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || showBunker}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Confirm</Spinner> <Spinner {loading}>Confirm</Spinner>
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</Button> </Button>
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Confirm from "@lib/components/Confirm.svelte" import Confirm from "@lib/components/Confirm.svelte"
import type {Alert} from "@app/state" 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 {publishDelete} from "@app/commands"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -12,7 +12,7 @@
const {alert}: Props = $props() const {alert}: Props = $props()
const confirm = () => { 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!"}) pushToast({message: "Your alert has been deleted!"})
history.back() history.back()
} }
+8 -9
View File
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import {parseJson, nthEq} from "@welshman/lib" import {parseJson} from "@welshman/lib"
import {displayFeeds} from "@welshman/feeds" import {displayFeeds} from "@welshman/feeds"
import {getAddress, getTagValue, getTagValues} from "@welshman/util" import {getAddress, getTagValue, getTagValues} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import AlertDelete from "@app/components/AlertDelete.svelte" import AlertDelete from "@app/components/AlertDelete.svelte"
import type {Alert} from "@app/state" import type {Alert} from "@app/state"
import {alertStatuses} from "@app/state" import {deriveAlertStatus} from "@app/state"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
type Props = { type Props = {
@@ -15,8 +15,7 @@
const {alert}: Props = $props() const {alert}: Props = $props()
const address = $derived(getAddress(alert.event)) const status = deriveAlertStatus(getAddress(alert.event))
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
const cron = $derived(getTagValue("cron", alert.tags)) const cron = $derived(getTagValue("cron", alert.tags))
const channel = $derived(getTagValue("channel", alert.tags)) const channel = $derived(getTagValue("channel", alert.tags))
const feeds = $derived(getTagValues("feed", alert.tags)) const feeds = $derived(getTagValues("feed", alert.tags))
@@ -39,24 +38,24 @@
</Button> </Button>
<div class="flex-inline gap-1">{description}</div> <div class="flex-inline gap-1">{description}</div>
</div> </div>
{#if status} {#if $status}
{@const statusText = getTagValue("status", status.tags) || "error"} {@const statusText = getTagValue("status", $status.tags) || "error"}
{#if statusText === "ok"} {#if statusText === "ok"}
<span <span
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm" 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 Active
</span> </span>
{:else if statusText === "pending"} {:else if statusText === "pending"}
<span <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" 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 Pending
</span> </span>
{:else} {:else}
<span <span
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error" 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())} {statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
</span> </span>
{/if} {/if}
+15 -10
View File
@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {getTagValue} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import AlertAdd from "@app/components/AlertAdd.svelte" import AlertAdd from "@app/components/AlertAdd.svelte"
import AlertItem from "@app/components/AlertItem.svelte" import AlertItem from "@app/components/AlertItem.svelte"
import {loadAlertStatuses, loadAlerts} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {alerts} from "@app/state" import {alerts} from "@app/state"
const startAlert = () => pushModal(AlertAdd) type Props = {
url?: string
channel?: string
hideSpaceField?: boolean
}
onMount(() => { const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
loadAlertStatuses($pubkey!)
loadAlerts($pubkey!) const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
})
const filteredAlerts = $derived(
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
@@ -29,10 +34,10 @@
</Button> </Button>
</div> </div>
<div class="col-4"> <div class="col-4">
{#each $alerts as alert (alert.event.id)} {#each filteredAlerts as alert (alert.event.id)}
<AlertItem {alert} /> <AlertItem {alert} />
{:else} {: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} {/each}
</div> </div>
</div> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib" 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 {publishThunk} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
@@ -63,7 +63,7 @@
} }
const ed = await editor const ed = await editor
const event = createEvent(EVENT_TIME, { const event = makeEvent(EVENT_TIME, {
content: ed.getText({blockSeparator: "\n"}).trim(), content: ed.getText({blockSeparator: "\n"}).trim(),
tags: [ tags: [
["d", initialValues?.d || randomId()], ["d", initialValues?.d || randomId()],
+2 -2
View File
@@ -16,7 +16,7 @@
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util" import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
import {parse, isLink} from "@welshman/content" import {parse, isLink} from "@welshman/content"
import { import {
createEvent, makeEvent,
tagsFromIMeta, tagsFromIMeta,
getTags, getTags,
DIRECT_MESSAGE, DIRECT_MESSAGE,
@@ -97,7 +97,7 @@
content = content.trim() content = content.trim()
if (content) { if (content) {
templates.push(createEvent(kind, {content, tags: [...tags, ...ptags]})) templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
} }
} }
+35
View File
@@ -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>
+6 -8
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {ellipsize, postJson} from "@welshman/lib" import {ellipsize, displayUrl, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/state" import {dufflepud, imgproxy} from "@app/state"
import {preventDefault, stopPropagation} from "@lib/html" import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
@@ -54,13 +54,11 @@
src={imgproxy(preview.image)} src={imgproxy(preview.image)}
class="bg-alt max-h-72 object-contain object-center" /> class="bg-alt max-h-72 object-contain object-center" />
{/if} {/if}
{#if preview.title} <div class="flex flex-col gap-2 p-4">
<div class="flex flex-col gap-2 p-4"> <strong class="overflow-hidden text-ellipsis whitespace-nowrap"
<strong class="overflow-hidden text-ellipsis whitespace-nowrap" >{preview.title || displayUrl(url)}</strong>
>{preview.title}</strong> <p>{ellipsize(preview.description, 140)}</p>
<p>{ellipsize(preview.description, 140)}</p> </div>
</div>
{/if}
</div> </div>
{:catch} {:catch}
<p class="bg-alt p-12 text-center leading-normal"> <p class="bg-alt p-12 text-center leading-normal">
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeNil} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {displayProfile} from "@welshman/util" import {deriveProfileDisplay} from "@welshman/app"
import {deriveProfile} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -14,11 +13,11 @@
const {value, url}: Props = $props() 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}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
<Button onclick={openProfile} class="link-content"> <Button onclick={openProfile} class="link-content">
@{displayProfile($profile)} @{$display}
</Button> </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>
+24 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl, getTagValue} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -13,6 +13,8 @@
import SpaceExit from "@app/components/SpaceExit.svelte" import SpaceExit from "@app/components/SpaceExit.svelte"
import SpaceJoin from "@app/components/SpaceJoin.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte"
import ProfileList from "@app/components/ProfileList.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 RoomCreate from "@app/components/RoomCreate.svelte"
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte" import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte" import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
@@ -23,6 +25,7 @@
deriveUserRooms, deriveUserRooms,
deriveOtherRooms, deriveOtherRooms,
hasNip29, hasNip29,
alerts,
} from "@app/state" } from "@app/state"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -36,6 +39,7 @@
const calendarPath = makeSpacePath(url, "calendar") const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
const openMenu = () => { const openMenu = () => {
showMenu = true showMenu = true
@@ -62,6 +66,13 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) 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 showMenu = $state(false)
let replaceState = $state(false) let replaceState = $state(false)
let element: Element | undefined = $state() let element: Element | undefined = $state()
@@ -75,18 +86,20 @@
}) })
</script> </script>
<div bind:this={element}> <div bind:this={element} class="flex h-full flex-col justify-between">
<SecondaryNavSection class="max-h-screen"> <SecondaryNavSection>
<div> <div>
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}> <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" /> <Icon icon="alt-arrow-down" />
</SecondaryNavItem> </SecondaryNavItem>
{#if showMenu} {#if showMenu}
<Popover hideOnClick onClose={toggleMenu}> <Popover hideOnClick onClose={toggleMenu}>
<ul <ul
transition:fly 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> <li>
<Button onclick={showMembers}> <Button onclick={showMembers}>
<Icon icon="user-rounded" /> <Icon icon="user-rounded" />
@@ -171,4 +184,10 @@
{/if} {/if}
</div> </div>
</SecondaryNavSection> </SecondaryNavSection>
<div class="p-4">
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
<Icon icon="bell" />
Manage Alerts
</button>
</div>
</div> </div>
+2 -2
View File
@@ -5,7 +5,7 @@
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store" import {deriveEvents} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" 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 {repository, loadRelaySelections} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
@@ -35,7 +35,7 @@
// Load groups and at least one note, regardless of time frame // Load groups and at least one note, regardless of time frame
load({ load({
filters: [ filters: [
{authors: [pubkey], kinds: [GROUPS]}, {authors: [pubkey], kinds: [ROOMS]},
{authors: [pubkey], limit: 1, kinds: [NOTE, MESSAGE, THREAD, COMMENT]}, {authors: [pubkey], limit: 1, kinds: [NOTE, MESSAGE, THREAD, COMMENT]},
], ],
relays: Router.get().FromPubkeys([pubkey]).getUrls(), relays: Router.get().FromPubkeys([pubkey]).getUrls(),
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {chunk, sleep, uniq} from "@welshman/lib" import {chunk, sleep, uniq} from "@welshman/lib"
import { import {
createEvent, makeEvent,
createProfile, createProfile,
PROFILE, PROFILE,
DELETE, DELETE,
@@ -36,8 +36,8 @@
} }
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}])) const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
const profileEvent = createEvent(PROFILE, createProfile({name: "[deleted]"})) const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
const vanishEvent = createEvent(62, {tags: [["relay", "ALL_RELAYS"]]}) const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2 const denominator = chunks.length + 2
const relays = uniq([ const relays = uniq([
...INDEXER_RELAYS, ...INDEXER_RELAYS,
@@ -75,7 +75,7 @@
} }
} }
await publishThunk({relays, event: createEvent(DELETE, {tags})}) await publishThunk({relays, event: makeEvent(DELETE, {tags})})
await incrementProgress() await incrementProgress()
} }
+8 -4
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import {nthNe} from "@welshman/lib"
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import { import {
getTag, getTag,
createEvent, makeEvent,
makeProfile, makeProfile,
editProfile, editProfile,
createProfile, createProfile,
@@ -24,16 +25,19 @@
const back = () => history.back() const back = () => history.back()
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => { const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
const router = Router.get()
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile) const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const relays = [...getMembershipUrls($userMembership)] const scenarios = [router.FromRelays(getMembershipUrls($userMembership))]
if (shouldBroadcast) { if (shouldBroadcast) {
relays.push(...Router.get().FromUser().getUrls()) scenarios.push(router.FromUser(), router.Index())
template.tags = template.tags.filter(nthNe(0, "-"))
} else { } else {
template.tags = uniqTags([...template.tags, PROTECTED]) 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}) publishThunk({event, relays})
pushToast({message: "Your profile has been updated!"}) pushToast({message: "Your profile has been updated!"})
+9 -8
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {randomId} from "@welshman/lib" import {uniqBy, nth} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import {deriveRelay, getThunkError} from "@welshman/app" import {deriveRelay, getThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -11,25 +11,26 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {hasNip29, loadChannel} from "@app/state" import {hasNip29, loadChannel} from "@app/state"
import {createRoom, editRoom, joinRoom} from "@app/commands"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
const {url} = $props() const {url} = $props()
const room = randomId() const room = makeRoomMeta()
const relay = deriveRelay(url) const relay = deriveRelay(url)
const back = () => history.back() const back = () => history.back()
const tryCreate = async () => { const tryCreate = async () => {
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
const createMessage = await getThunkError(createRoom(url, room)) const createMessage = await getThunkError(createRoom(url, room))
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) { if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
return pushToast({theme: "error", message: createMessage}) return pushToast({theme: "error", message: createMessage})
} }
const editMessage = await getThunkError(editRoom(url, room, {name})) const editMessage = await getThunkError(editRoom(url, room))
if (editMessage) { if (editMessage) {
return pushToast({theme: "error", message: editMessage}) return pushToast({theme: "error", message: editMessage})
@@ -41,9 +42,9 @@
return pushToast({theme: "error", message: joinMessage}) 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 () => { const create = async () => {
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util" 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 {loginWithNip01, publishThunk} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
@@ -18,7 +18,7 @@
} }
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => { 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 : [] const relays = shouldBroadcast ? INDEXER_RELAYS : []
loginWithNip01(secret) loginWithNip01(secret)
+2 -1
View File
@@ -7,7 +7,7 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {clearModals} from "@app/modal" import {clearModals} from "@app/modal"
import {addSpaceMembership} from "@app/commands" import {addSpaceMembership, broadcastUserData} from "@app/commands"
const {url} = $props() const {url} = $props()
@@ -16,6 +16,7 @@
const tryJoin = async () => { const tryJoin = async () => {
await addSpaceMembership(url) await addSpaceMembership(url)
broadcastUserData([url])
clearModals() clearModals()
} }
+15 -2
View File
@@ -1,13 +1,26 @@
<script module lang="ts"> <script module lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {ROOM_META} from "@welshman/util"
import {load} from "@welshman/net"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {addSpaceMembership} from "@app/commands" import {addSpaceMembership, broadcastUserData} from "@app/commands"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
export const confirmSpaceJoin = async (url: string) => { export const confirmSpaceJoin = async (url: string) => {
await addSpaceMembership(url) 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({ pushToast({
message: "Welcome to the space!", message: "Welcome to the space!",
+37 -24
View File
@@ -6,7 +6,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte" import RoomCreate from "@app/components/RoomCreate.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import ChannelName from "@app/components/ChannelName.svelte"
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes" import {makeThreadPath, makeCalendarPath, makeRoomPath, makeSpacePath} from "@app/routes"
import { import {
hasNip29, hasNip29,
deriveUserRooms, deriveUserRooms,
@@ -25,6 +25,7 @@
const relay = deriveRelay(url) const relay = deriveRelay(url)
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const chatPath = makeSpacePath(url, "chat")
const threadsPath = makeThreadPath(url) const threadsPath = makeThreadPath(url)
const calendarPath = makeCalendarPath(url) const calendarPath = makeCalendarPath(url)
@@ -76,34 +77,46 @@
{/if} {/if}
</div> </div>
</Link> </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 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"> <Button onclick={addRoom} class="btn btn-neutral btn-sm w-full justify-start">
<Icon icon="add-circle" /> <Icon icon="add-circle" />
Create Room Create Room
</Button> </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} {/if}
</div> </div>
</div> </div>
+18 -51
View File
@@ -1,16 +1,12 @@
<script lang="ts"> <script lang="ts">
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {groupBy, ago, MONTH, first, last, uniq, avg, overlappingPairs} from "@welshman/lib" import {groupBy, ago, MONTH, first, last, uniq, avg, overlappingPairs} from "@welshman/lib"
import {formatTimestamp} from "@welshman/lib"
import {MESSAGE, getTagValue} from "@welshman/util" import {MESSAGE, getTagValue} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte" import ConversationCard from "@app/components/ConversationCard.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {deriveEventsForUrl} from "@app/state" import {deriveEventsForUrl} from "@app/state"
import {goToEvent} from "@app/routes"
type Props = { type Props = {
url: string url: string
@@ -76,54 +72,25 @@
</h3> </h3>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{#if $conversations.length === 0} {#if $conversations.length === 0}
<div class="py-8 text-center opacity-70"> {#if $messages.length > 0}
<p>No recent conversations</p> {@const events = $messages.slice(0, 1)}
</div> {@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} {:else}
{#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)} {#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)}
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}> <ConversationCard {url} {room} {events} {latest} {earliest} {participants} />
<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>
{/each} {/each}
{#if $conversations.length > limit} {#if $conversations.length > limit}
<Button class="btn btn-primary" onclick={viewMore}> <Button class="btn btn-primary" onclick={viewMore}>
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {createEvent, THREAD} from "@welshman/util" import {makeEvent, THREAD} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -45,7 +45,7 @@
publishThunk({ publishThunk({
relays: [url], relays: [url],
event: createEvent(THREAD, {content, tags}), event: makeEvent(THREAD, {content, tags}),
}) })
history.back() history.back()
+8
View File
@@ -9,6 +9,7 @@ import {
makeChatPath, makeChatPath,
makeThreadPath, makeThreadPath,
makeCalendarPath, makeCalendarPath,
makeSpaceChatPath,
makeRoomPath, makeRoomPath,
} from "@app/routes" } from "@app/routes"
import {chats, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/state" import {chats, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/state"
@@ -75,8 +76,10 @@ export const notifications = derived(
const spacePath = makeSpacePath(url) const spacePath = makeSpacePath(url)
const threadPath = makeThreadPath(url) const threadPath = makeThreadPath(url)
const calendarPath = makeCalendarPath(url) const calendarPath = makeCalendarPath(url)
const messagesPath = makeSpaceChatPath(url)
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url)) const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
const calendarEvents = allCalendarEvents.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])) { if (hasNotification(threadPath, threadEvents[0])) {
paths.add(spacePath) paths.add(spacePath)
@@ -88,6 +91,11 @@ export const notifications = derived(
paths.add(calendarPath) paths.add(calendarPath)
} }
if (hasNotification(messagesPath, messagesEvents[0])) {
paths.add(spacePath)
paths.add(messagesPath)
}
const commentsByThreadId = groupBy( const commentsByThreadId = groupBy(
e => getTagValue("E", e.tags), e => getTagValue("E", e.tags),
threadEvents.filter(spec({kind: COMMENT})), threadEvents.filter(spec({kind: COMMENT})),
+131
View File
@@ -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 messaging 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
View File
@@ -13,13 +13,22 @@ import {
sortBy, sortBy,
assoc, assoc,
now, now,
isNotNil,
filterVals,
fromPairs,
} from "@welshman/lib" } from "@welshman/lib"
import { import {
MESSAGE, MESSAGE,
DELETE, DELETE,
THREAD, THREAD,
EVENT_TIME, EVENT_TIME,
AUTH_INVITE,
COMMENT, COMMENT,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
ALERT_STATUS,
matchFilters, matchFilters,
getTagValues, getTagValues,
getTagValue, getTagValue,
@@ -48,8 +57,6 @@ import {
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import { import {
ALERT,
ALERT_STATUS,
NOTIFIER_RELAY, NOTIFIER_RELAY,
INDEXER_RELAYS, INDEXER_RELAYS,
getDefaultPubkeys, getDefaultPubkeys,
@@ -343,7 +350,7 @@ export const makeCalendarFeed = ({
export const loadAlerts = (pubkey: string) => export const loadAlerts = (pubkey: string) =>
load({ load({
relays: [NOTIFIER_RELAY], 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) => export const loadAlertStatuses = (pubkey: string) =>
@@ -359,7 +366,7 @@ export const listenForNotifications = () => {
for (const [url, allRooms] of userRoomsByUrl.get()) { for (const [url, allRooms] of userRoomsByUrl.get()) {
// Limit how many rooms we load at a time, since we have to send a separate filter // 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) const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
load({ load({
@@ -367,6 +374,7 @@ export const listenForNotifications = () => {
relays: [url], relays: [url],
filters: [ filters: [
{kinds: [THREAD], limit: 1}, {kinds: [THREAD], limit: 1},
{kinds: [MESSAGE], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1}, {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})), ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
], ],
@@ -377,6 +385,7 @@ export const listenForNotifications = () => {
relays: [url], relays: [url],
filters: [ filters: [
{kinds: [THREAD], since: now()}, {kinds: [THREAD], since: now()},
{kinds: [MESSAGE], since: now()},
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()}, {kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})), ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
], ],
@@ -430,3 +439,18 @@ export const discoverRelays = (lists: List[]) =>
.filter(isShareableRelayUrl) .filter(isShareableRelayUrl)
.map(url => loadRelay(url)), .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)]))),
)
+24 -16
View File
@@ -35,6 +35,8 @@ export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}` export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
export const makeThreadPath = (url: string, eventId?: string) => export const makeThreadPath = (url: string, eventId?: string) =>
makeSpacePath(url, "threads", eventId) makeSpacePath(url, "threads", eventId)
@@ -61,34 +63,40 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
} }
} }
export const goToMessage = async (url: string, room: string | undefined, id: string) => { export const goToEvent = async (event: TrustedEvent, options: Record<string, any> = {}) => {
await goto(room ? makeRoomPath(url, room) : makeSpacePath(url, "chat"))
await sleep(300)
return scrollToEvent(id)
}
export const goToEvent = async (event: TrustedEvent) => {
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) { 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 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) const room = getTagValue(ROOM, event.tags)
if (urls.length > 0) { if (urls.length > 0) {
const url = urls[0] const url = urls[0]
if (event.kind === THREAD) { if (event.kind === THREAD) {
return goto(makeThreadPath(url, event.id)) return makeThreadPath(url, event.id)
} }
if (event.kind === EVENT_TIME) { if (event.kind === EVENT_TIME) {
return goto(makeCalendarPath(url, event.id)) return makeCalendarPath(url, event.id)
} }
if (event.kind === MESSAGE) { 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 kind = event.tags.find(nthEq(0, "K"))?.[1]
@@ -96,18 +104,18 @@ export const goToEvent = async (event: TrustedEvent) => {
if (id && kind) { if (id && kind) {
if (parseInt(kind) === THREAD) { if (parseInt(kind) === THREAD) {
return goto(makeThreadPath(url, id)) return makeThreadPath(url, id)
} }
if (parseInt(kind) === EVENT_TIME) { if (parseInt(kind) === EVENT_TIME) {
return goto(makeCalendarPath(url, id)) return makeCalendarPath(url, id)
} }
if (parseInt(kind) === MESSAGE) { 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}))
} }
+53 -38
View File
@@ -23,7 +23,14 @@ import {
} from "@welshman/lib" } from "@welshman/lib"
import type {Socket} from "@welshman/net" import type {Socket} from "@welshman/net"
import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net" import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net"
import {collection, custom} from "@welshman/store" import {
collection,
custom,
deriveEvents,
deriveEventsMapped,
withGetter,
synced,
} from "@welshman/store"
import { import {
getIdFilters, getIdFilters,
WRAP, WRAP,
@@ -33,14 +40,19 @@ import {
ZAP_RESPONSE, ZAP_RESPONSE,
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE, DIRECT_MESSAGE_FILE,
GROUP_META, ROOM_META,
MESSAGE, MESSAGE,
GROUPS, ROOMS,
THREAD, THREAD,
COMMENT, COMMENT,
GROUP_JOIN, ROOM_JOIN,
GROUP_ADD_USER, ROOM_ADD_USER,
GROUP_REMOVE_USER, ROOM_REMOVE_USER,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
ALERT_STATUS,
getGroupTags, getGroupTags,
getRelayTagValues, getRelayTagValues,
getPubkeyTagValues, getPubkeyTagValues,
@@ -51,6 +63,7 @@ import {
asDecryptedEvent, asDecryptedEvent,
normalizeRelayUrl, normalizeRelayUrl,
getTag, getTag,
getTagValue,
getTagValues, getTagValues,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
@@ -76,7 +89,6 @@ import {
appContext, appContext,
} from "@welshman/app" } from "@welshman/app"
import type {Thunk, Relay} 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) export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
@@ -84,14 +96,12 @@ export const ROOM = "h"
export const PROTECTED = ["-"] export const PROTECTED = ["-"]
export const ALERT = 32830
export const ALERT_STATUS = 32831
export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY 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 INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS) export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
@@ -124,7 +134,7 @@ export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
export const NIP46_PERMS = export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," + "nip44_encrypt,nip44_decrypt," +
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION] [CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION]
.map(k => `sign_event:${k}`) .map(k => `sign_event:${k}`)
.join(",") .join(",")
@@ -171,8 +181,6 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}`
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) => export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
entityLink(nip19.nprofileEncode({pubkey, relays})) entityLink(nip19.nprofileEncode({pubkey, relays}))
export const tagRoom = (room: string, url: string) => [ROOM, room]
export const getDefaultPubkeys = () => { export const getDefaultPubkeys = () => {
const appPubkeys = DEFAULT_PUBKEYS.split(",") const appPubkeys = DEFAULT_PUBKEYS.split(",")
const userPubkeys = shuffle(getPubkeyTagValues(getListTags(get(userFollows)))) const userPubkeys = shuffle(getPubkeyTagValues(getListTags(get(userFollows))))
@@ -345,15 +353,17 @@ export type Alert = {
tags: string[][] tags: string[][]
} }
export const alerts = deriveEventsMapped<Alert>(repository, { export const alerts = withGetter(
filters: [{kinds: [ALERT]}], deriveEventsMapped<Alert>(repository, {
itemToEvent: item => item.event, filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
eventToItem: async event => { itemToEvent: item => item.event,
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
return {event, tags} return {event, tags}
}, },
}) }),
)
// Alert Statuses // Alert Statuses
@@ -362,15 +372,20 @@ export type AlertStatus = {
tags: string[][] tags: string[][]
} }
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, { export const alertStatuses = withGetter(
filters: [{kinds: [ALERT_STATUS]}], deriveEventsMapped<AlertStatus>(repository, {
itemToEvent: item => item.event, filters: [{kinds: [ALERT_STATUS]}],
eventToItem: async event => { itemToEvent: item => item.event,
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) 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 // Membership
@@ -399,7 +414,7 @@ export const getMembershipRoomsByUrl = (url: string, list?: List) =>
sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1))) sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1)))
export const memberships = deriveEventsMapped<PublishedList>(repository, { export const memberships = deriveEventsMapped<PublishedList>(repository, {
filters: [{kinds: [GROUPS]}], filters: [{kinds: [ROOMS]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)), eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
}) })
@@ -412,7 +427,7 @@ export const {
name: "memberships", name: "memberships",
store: memberships, store: memberships,
getKey: list => list.event.pubkey, getKey: list => list.event.pubkey,
load: makeOutboxLoader(GROUPS), load: makeOutboxLoader(ROOMS),
}) })
// Chats // Chats
@@ -510,7 +525,7 @@ export const splitChannelId = (id: string) => id.split("'")
export const hasNip29 = (relay?: Relay) => export const hasNip29 = (relay?: Relay) =>
relay?.profile?.supported_nips?.map?.(String)?.includes?.("29") 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( export const channels = derived(
[channelEvents, getUrlsForEvent], [channelEvents, getUrlsForEvent],
@@ -559,7 +574,7 @@ export const {
await load({ await load({
relays: [url], relays: [url],
filters: [{kinds: [GROUP_META], "#d": [room]}], filters: [{kinds: [ROOM_META], "#d": [room]}],
}) })
}, },
}) })
@@ -645,22 +660,22 @@ export const deriveUserMembershipStatus = (url: string, room: string) =>
[ [
pubkey, pubkey,
deriveEventsForUrl(url, [ 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]) => { ([$pubkey, $events]) => {
let status = MembershipStatus.Initial let status = MembershipStatus.Initial
for (const event of $events) { for (const event of $events) {
if (event.kind === GROUP_JOIN && event.pubkey === $pubkey) { if (event.kind === ROOM_JOIN && event.pubkey === $pubkey) {
status = MembershipStatus.Pending 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 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 return MembershipStatus.Granted
} }
} }
+4
View File
@@ -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

+2
View File
@@ -9,6 +9,7 @@
import {switcher} from "@welshman/lib" import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl" import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.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 Bookmark from "@assets/icons/Bookmark.svg?dataurl"
import BillList from "@assets/icons/Bill List.svg?dataurl" import BillList from "@assets/icons/Bill List.svg?dataurl"
import Code2 from "@assets/icons/Code 2.svg?dataurl" import Code2 from "@assets/icons/Code 2.svg?dataurl"
@@ -108,6 +109,7 @@
const data = switcher(icon, { const data = switcher(icon, {
"add-square": AddSquare, "add-square": AddSquare,
"arrows-a-logout-2": ArrowsALogout2, "arrows-a-logout-2": ArrowsALogout2,
bell: Bell,
bookmark: Bookmark, bookmark: Bookmark,
"bill-list": BillList, "bill-list": BillList,
"code-2": Code2, "code-2": Code2,
+5 -2
View File
@@ -102,6 +102,7 @@ export const isIntersecting = async (element: Element) =>
export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => { export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => {
const element = document.querySelector(`[data-event="${id}"]`) as any const element = document.querySelector(`[data-event="${id}"]`) as any
const elements = Array.from(document.querySelectorAll("[data-event]"))
if (element) { if (element) {
element.scrollIntoView({behavior: "smooth", block: "center"}) element.scrollIntoView({behavior: "smooth", block: "center"})
@@ -116,8 +117,8 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
}, 800 + 400) }, 800 + 400)
return true return true
} else { } else if (elements.length > 0) {
const lastElement = last(Array.from(document.querySelectorAll("[data-event]"))) const lastElement = last(elements)
if (lastElement && !isIntersecting(lastElement)) { if (lastElement && !isIntersecting(lastElement)) {
lastElement.scrollIntoView({behavior: "smooth", block: "center"}) lastElement.scrollIntoView({behavior: "smooth", block: "center"})
@@ -131,4 +132,6 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
return false return false
} }
} }
return false
} }
+15
View File
@@ -44,6 +44,8 @@
} from "@welshman/app" } from "@welshman/app"
import * as lib from "@welshman/lib" import * as lib from "@welshman/lib"
import * as util from "@welshman/util" 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 welshmanSigner from "@welshman/signer"
import * as net from "@welshman/net" import * as net from "@welshman/net"
import * as app from "@welshman/app" import * as app from "@welshman/app"
@@ -61,6 +63,7 @@
} from "@app/state" } from "@app/state"
import {loadUserData, listenForNotifications} from "@app/requests" import {loadUserData, listenForNotifications} from "@app/requests"
import {theme} from "@app/theme" import {theme} from "@app/theme"
import {initializePushNotifications} from "@app/push"
import * as commands from "@app/commands" import * as commands from "@app/commands"
import * as requests from "@app/requests" import * as requests from "@app/requests"
import * as notifications from "@app/notifications" import * as notifications from "@app/notifications"
@@ -71,6 +74,9 @@
dropSession($session.pubkey) dropSession($session.pubkey)
} }
// Initialize push notification handler asap
initializePushNotifications()
const {children} = $props() const {children} = $props()
const ready = $state(defer<void>()) const ready = $state(defer<void>())
@@ -81,7 +87,9 @@
nip19, nip19,
...lib, ...lib,
...welshmanSigner, ...welshmanSigner,
...router,
...util, ...util,
...feeds,
...net, ...net,
...app, ...app,
...appState, ...appState,
@@ -90,6 +98,13 @@
...notifications, ...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 // Nstart login
if (window.location.hash?.startsWith("#nostr-login")) { if (window.location.hash?.startsWith("#nostr-login")) {
const params = new URLSearchParams(window.location.hash.slice(1)) const params = new URLSearchParams(window.location.hash.slice(1))
+5 -13
View File
@@ -2,13 +2,13 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import type {TrustedEvent} from "@welshman/util" 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 {load} from "@welshman/net"
import {page} from "$app/stores" import {page} from "$app/stores"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {scrollToEvent} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import {makeRoomPath, makeThreadPath} from "@app/routes" import {goToEvent} from "@app/routes"
const {bech32} = $page.params const {bech32} = $page.params
@@ -22,19 +22,11 @@
let found = false let found = false
load({ load({
relays: data.relays, relays: [LOCAL_RELAY_URL, ...data.relays],
filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]), filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]),
onEvent: (event: TrustedEvent) => { onEvent: (event: TrustedEvent) => {
found = true found = true
goToEvent(event, {replaceState: 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})
}
}, },
onClose: () => { onClose: () => {
if (!found) { if (!found) {
+8 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {WRAP} from "@welshman/util" import {WRAP} from "@welshman/util"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
@@ -10,18 +11,19 @@
import SecondaryNav from "@lib/components/SecondaryNav.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte" import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.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 ChatItem from "@app/components/ChatItem.svelte"
import {chatSearch} from "@app/state" import {chatSearch} from "@app/state"
import {pullConservatively} from "@app/requests" import {pullConservatively} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
interface Props {
children?: import("svelte").Snippet type Props = {
children?: Snippet
} }
const {children}: Props = $props() const {children}: Props = $props()
const startChat = () => pushModal(ChatStart) const openMenu = () => pushModal(ChatMenu)
const promise = pullConservatively({ const promise = pullConservatively({
filters: [{kinds: [WRAP], "#p": [$pubkey!]}], filters: [{kinds: [WRAP], "#p": [$pubkey!]}],
@@ -37,8 +39,8 @@
<SecondaryNavSection> <SecondaryNavSection>
<SecondaryNavHeader> <SecondaryNavHeader>
Chats Chats
<Button onclick={startChat}> <Button onclick={openMenu}>
<Icon icon="add-circle" /> <Icon icon="menu-dots" />
</Button> </Button>
</SecondaryNavHeader> </SecondaryNavHeader>
</SecondaryNavSection> </SecondaryNavSection>
+2 -2
View File
@@ -6,7 +6,7 @@
import ContentSearch from "@lib/components/ContentSearch.svelte" import ContentSearch from "@lib/components/ContentSearch.svelte"
import ChatItem from "@app/components/ChatItem.svelte" import ChatItem from "@app/components/ChatItem.svelte"
import ChatStart from "@app/components/ChatStart.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 {chatSearch} from "@app/state"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
@@ -15,7 +15,7 @@
const startChat = () => pushModal(ChatStart) const startChat = () => pushModal(ChatStart)
const openMenu = () => pushModal(ChatMenuMobile) const openMenu = () => pushModal(ChatMenu)
const chats = $derived($chatSearch.searchOptions(term)) const chats = $derived($chatSearch.searchOptions(term))
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {addToMapKey, dec, gt} from "@welshman/lib" import {addToMapKey, dec, gt} from "@welshman/lib"
import {GROUPS} from "@welshman/util" import {ROOMS} from "@welshman/util"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import type {Relay} from "@welshman/app" import type {Relay} from "@welshman/app"
@@ -29,7 +29,7 @@
const discoverRelays = () => const discoverRelays = () =>
Promise.all([ Promise.all([
load({ load({
filters: [{kinds: [GROUPS]}], filters: [{kinds: [ROOMS]}],
relays: Router.get().Index().getUrls(), relays: Router.get().Index().getUrls(),
}), }),
...getDefaultPubkeys().map(async pubkey => { ...getDefaultPubkeys().map(async pubkey => {
+4 -4
View File
@@ -2,7 +2,7 @@
import { import {
getListTags, getListTags,
tagger, tagger,
createEvent, makeEvent,
getPubkeyTagValues, getPubkeyTagValues,
getTagValues, getTagValues,
MUTES, MUTES,
@@ -38,17 +38,17 @@
const relays = Router.get().FromUser().getUrls() const relays = Router.get().FromUser().getUrls()
publishThunk({ publishThunk({
event: createEvent(SETTINGS, {content}), event: makeEvent(SETTINGS, {content}),
relays, relays,
}) })
publishThunk({ publishThunk({
event: createEvent(MUTES, {tags: mutedPubkeys.map(tagPubkey)}), event: makeEvent(MUTES, {tags: mutedPubkeys.map(tagPubkey)}),
relays, relays,
}) })
publishThunk({ publishThunk({
event: createEvent(BLOSSOM_SERVERS, {tags: blossomServers.map(tagger("server"))}), event: makeEvent(BLOSSOM_SERVERS, {tags: blossomServers.map(tagger("server"))}),
relays, relays,
}) })
+2 -2
View File
@@ -3,7 +3,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {ago, MONTH} from "@welshman/lib" 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 Page from "@lib/components/Page.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte" import MenuSpace from "@app/components/MenuSpace.svelte"
@@ -61,7 +61,7 @@
pullConservatively({ pullConservatively({
relays, relays,
filters: [ filters: [
{kinds: [GROUP_META]}, {kinds: [ROOM_META]},
{kinds: [THREAD, EVENT_TIME], since}, {kinds: [THREAD, EVENT_TIME], since},
{kinds: [COMMENT], "#K": [String(THREAD), String(EVENT_TIME)], since}, {kinds: [COMMENT], "#K": [String(THREAD), String(EVENT_TIME)], since},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since})), ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since})),
+11 -17
View File
@@ -8,14 +8,15 @@
import {request} from "@welshman/net" import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {
createEvent, makeEvent,
makeRoomMeta,
MESSAGE, MESSAGE,
DELETE, DELETE,
REACTION, REACTION,
GROUP_ADD_USER, ROOM_ADD_USER,
GROUP_REMOVE_USER, ROOM_REMOVE_USER,
} from "@welshman/util" } 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 {slide, fade, fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -32,20 +33,13 @@
userRoomsByUrl, userRoomsByUrl,
userSettingValues, userSettingValues,
decodeRelay, decodeRelay,
tagRoom,
getEventsForUrl, getEventsForUrl,
deriveUserMembershipStatus, deriveUserMembershipStatus,
deriveChannel, deriveChannel,
MembershipStatus, MembershipStatus,
} from "@app/state" } from "@app/state"
import {setChecked, checked} from "@app/notifications" import {setChecked, checked} from "@app/notifications"
import { import {addRoomMembership, removeRoomMembership, prependParent} from "@app/commands"
joinRoom,
leaveRoom,
addRoomMembership,
removeRoomMembership,
prependParent,
} from "@app/commands"
import {PROTECTED} from "@app/state" import {PROTECTED} from "@app/state"
import {makeFeed} from "@app/requests" import {makeFeed} from "@app/requests"
import {popKey} from "@app/implicit" import {popKey} from "@app/implicit"
@@ -68,7 +62,7 @@
joining = true joining = true
try { try {
const message = await getThunkError(joinRoom(url, room)) const message = await getThunkError(joinRoom(url, makeRoomMeta({id: room})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message}) return pushToast({theme: "error", message})
@@ -84,7 +78,7 @@
const leave = async () => { const leave = async () => {
leaving = true leaving = true
try { try {
const message = await getThunkError(leaveRoom(url, room)) const message = await getThunkError(leaveRoom(url, makeRoomMeta({id: room})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
@@ -108,7 +102,7 @@
} }
const onSubmit = ({content, tags}: EventContent) => { const onSubmit = ({content, tags}: EventContent) => {
tags.push(tagRoom(room, url)) tags.push(["h", room])
tags.push(PROTECTED) tags.push(PROTECTED)
let template = {content, tags} let template = {content, tags}
@@ -123,7 +117,7 @@
publishThunk({ publishThunk({
relays: [url], relays: [url],
event: createEvent(MESSAGE, template), event: makeEvent(MESSAGE, template),
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
@@ -251,7 +245,7 @@
relays: [url], relays: [url],
filters: [ filters: [
{ {
kinds: [GROUP_ADD_USER, GROUP_REMOVE_USER], kinds: [ROOM_ADD_USER, ROOM_REMOVE_USER],
"#p": [$pubkey!], "#p": [$pubkey!],
"#h": [room], "#h": [room],
limit: 10, limit: 10,
@@ -23,7 +23,7 @@
const url = decodeRelay($page.params.relay) 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) || "") const getStart = (event: TrustedEvent) => parseInt(getTagValue("start", event.tags) || "")
@@ -124,7 +124,7 @@
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div class="row-2"> <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" /> <Icon icon="calendar-add" />
Create an Event Create an Event
</Button> </Button>
+2 -2
View File
@@ -5,7 +5,7 @@
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib" import {now, formatTimestampAsDate} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util" import {makeEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
import {pubkey, publishThunk} from "@welshman/app" import {pubkey, publishThunk} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition" import {slide, fade, fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -58,7 +58,7 @@
publishThunk({ publishThunk({
relays: [url], relays: [url],
event: createEvent(MESSAGE, template), event: makeEvent(MESSAGE, template),
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
@@ -15,6 +15,7 @@
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ThreadActions from "@app/components/ThreadActions.svelte" import ThreadActions from "@app/components/ThreadActions.svelte"
import CommentActions from "@app/components/CommentActions.svelte"
import EventReply from "@app/components/EventReply.svelte" import EventReply from "@app/components/EventReply.svelte"
import {deriveEvent, decodeRelay} from "@app/state" import {deriveEvent, decodeRelay} from "@app/state"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
@@ -90,11 +91,11 @@
</Button> </Button>
</div> </div>
{/if} {/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"> <NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
<div class="col-3 ml-12"> <div class="col-3 ml-12">
<Content showEntire event={reply} {url} /> <Content showEntire event={reply} {url} />
<ThreadActions event={reply} {url} /> <CommentActions event={reply} {url} />
</div> </div>
</NoteCard> </NoteCard>
{/each} {/each}
+90
View File
@@ -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)
}
}),
)
})