Compare commits

...

41 Commits

Author SHA1 Message Date
Jon Staab 7334cd26f8 Bump version 2025-10-13 15:17:46 -07:00
Jon Staab 44555215cf Track shards separately, upgrade deps 2025-10-13 13:41:27 -07:00
Jon Staab 0cc25913c0 Optimize event storage 2025-10-13 12:46:56 -07:00
Jon Staab 004b30b737 Update caniuse 2025-10-13 11:48:22 -07:00
Jon Staab 632f330b4c Re-work storage to optimize file access 2025-10-06 17:01:25 -07:00
Jon Staab 666433912f Only show send toast in chat if send_delay is set 2025-10-06 11:27:07 -07:00
Jon Staab db98ce8db7 Bump welshman 2025-10-06 11:26:27 -07:00
Jon Staab 71dcfae5ff Add heading, update changelog, bump version 2025-10-02 12:43:19 -07:00
Jon Staab 04155f5b23 Bump welshman 2025-10-01 17:03:25 -07:00
Jon Staab b4058389ec Avoid decrypt errors 2025-10-01 10:06:21 -07:00
Jon Staab 483fa81b74 Fix some storage bugs 2025-09-30 17:04:52 -07:00
Jon Staab a8d1c4bbbc Refactor storage 2025-09-30 16:28:12 -07:00
Matthew Remmel 0a8c2faa74 Add filestorage adapters 2025-09-30 10:52:24 -07:00
Jon Staab dd3231e70f Bring back blossom server auth, fix duplicate direct messages 2025-09-29 14:25:07 -07:00
Jon Staab 7ff9c00032 Force url extension for encrypted uploads 2025-09-29 14:25:07 -07:00
Matthew Remmel 9ed483abf7 Ignore exception from if failing to set badge 2025-09-29 10:32:13 -07:00
Matthew Remmel b9aeaf29a4 Disable notification sound when tab is focused 2025-09-29 10:32:13 -07:00
Matthew Remmel 65e3f81f36 Remove comments and test lines 2025-09-29 10:32:13 -07:00
Matthew Remmel c6641dba31 Move notification sound and badge settings to settings store 2025-09-29 10:32:13 -07:00
Matthew Remmel e48d1e0e59 Fix async bug and add sound component for notification sound 2025-09-29 10:32:13 -07:00
Matthew Remmel d1e5aee84e Add naive badge count implementation 2025-09-29 10:32:13 -07:00
Matthew Remmel 5cb22d0bed Add checkboxes for badge/sound settings 2025-09-29 10:32:13 -07:00
Matthew Remmel d1c6f53d7c Add royalty-free sound effect for new notification 2025-09-29 10:32:13 -07:00
Matthew Remmel 6e238f98c0 Auto-add receiving address on wallet setup 2025-09-29 10:25:02 -07:00
Jon Staab 290274d6c8 Tweak light theme, remove conditional button classes 2025-09-25 10:52:32 -07:00
Jon Staab e1de0239c9 Remove css that was breaking tooltips 2025-09-25 10:43:55 -07:00
Jon Staab bec77d59e8 Add theme toggle on mobile, change button color for quick links 2025-09-25 10:37:53 -07:00
Jon Staab 84f8794d7c Make link previews less aggressive 2025-09-25 10:12:58 -07:00
Jon Staab 4cddf41bf3 Set initial delay to 0 2025-09-24 09:50:07 -07:00
Jon Staab 125a7e238e Add qr scanner to discover page 2025-09-22 15:56:48 -07:00
Jon Staab 468200b717 Link directly to discover page 2025-09-22 15:48:13 -07:00
Jon Staab bdfcb99781 Show more information about signer type 2025-09-22 15:09:41 -07:00
Jon Staab 38da650861 Add qr code to invite screen 2025-09-22 14:57:43 -07:00
Jon Staab dd006badfc Bring back blossom feature detection 2025-09-22 14:05:57 -07:00
Jon Staab 87e4e3fe5b Catch all upload errors 2025-09-22 11:43:44 -07:00
Jon Staab af3e38254f Fix focus on input list 2025-09-22 11:06:16 -07:00
Jon Staab 70843f54d3 Increase contrast on mention badges in editor 2025-09-22 10:40:54 -07:00
Jon Staab bda75b29b4 Handle bunker login errors better 2025-09-22 10:35:31 -07:00
Jon Staab 750830d593 Bump version again 2025-09-18 14:39:15 -07:00
Jon Staab 3c0f1a1d2f Restore icons 2025-09-18 14:13:08 -07:00
Jon Staab 4253b0ed29 Remove all icons 2025-09-18 14:12:08 -07:00
98 changed files with 3302 additions and 2278 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
VITE_BURROW_URL=
VITE_PLATFORM_URL=https://flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms
@@ -10,7 +11,7 @@ VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28"
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://offchain.pub/
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
+23
View File
@@ -1,5 +1,28 @@
# Changelog
# 1.3.1
* Fix memory leak in storage adapter
* Show fewer annoying toast messages
# 1.3.0
* Add optional badge and sound for notifications
* Improve link rendering
* Remove imgproxy
* Bring back blossom feature detection for spaces
* Improve light theme
* Add more info to signer status
* Simplify navigation for adding a space
* Add ability to scan QR code for invite links
* Streamline wallet setup and move receive address setting
* Remove indexeddb on mobile, use capacitor file storage API
* Fix duplicate DMs showing up
# 1.2.5
* Fix icons in build
# 1.2.4
* Add direct message alerts
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 25
versionName "1.2.4"
versionCode 28
versionName "1.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1
View File
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
+12 -9
View File
@@ -1,27 +1,30 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/android/capacitor')
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/android/capacitor')
include ':capacitor-community-safe-area'
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area/android')
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences/android')
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences/android')
include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications/android')
include ':capawesome-capacitor-android-dark-mode-support'
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge/android')
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin/android')
+8 -4
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
@@ -21,6 +22,7 @@
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -58,6 +60,7 @@
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
@@ -162,6 +165,7 @@
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
@@ -354,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4;
MARKETING_VERSION = 1.3.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -380,14 +384,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4;
MARKETING_VERSION = 1.3.1;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
+11 -10
View File
@@ -1,4 +1,4 @@
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios/scripts/pods_helpers'
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
@@ -9,15 +9,16 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin'
end
target 'Flotilla Chat' do
+17
View File
@@ -0,0 +1,17 @@
<?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>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
</dict>
</plist>
+53 -52
View File
@@ -1,6 +1,6 @@
{
"name": "flotilla",
"version": "1.2.4",
"version": "1.3.1",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -15,71 +15,72 @@
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.26.0",
"@sentry/cli": "^2.40.0",
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.19",
"@eslint/js": "^9.37.0",
"@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.21",
"classnames": "^2.5.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"globals": "^15.0.0",
"postcss": "^8.4.40",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.4"
"eslint": "^9.37.0",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.39.12",
"svelte-check": "^4.3.3",
"tailwindcss": "^3.4.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^5.4.20"
},
"type": "module",
"dependencies": {
"@capacitor-community/safe-area": "7.0.0-alpha.1",
"@capacitor/android": "^7.0.0",
"@capacitor/app": "^7.0.0",
"@capacitor/cli": "^7.0.0",
"@capacitor/core": "^7.0.1",
"@capacitor/ios": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
"@capacitor/android": "^7.4.3",
"@capacitor/app": "^7.1.0",
"@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.4.3",
"@capacitor/filesystem": "^7.1.4",
"@capacitor/ios": "^7.4.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.2",
"@capacitor/push-notifications": "^7.0.1",
"@capacitor/push-notifications": "^7.0.3",
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1",
"@getalby/sdk": "^5.1.0",
"@getalby/sdk": "^5.1.2",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0",
"@sveltejs/adapter-static": "^3.0.4",
"@tiptap/core": "^2.12.0",
"@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5",
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "^0.4.7",
"@welshman/content": "^0.4.7",
"@welshman/editor": "^0.4.7",
"@welshman/feeds": "^0.4.7",
"@welshman/lib": "^0.4.7",
"@welshman/net": "^0.4.7",
"@welshman/relay": "^0.4.7",
"@welshman/router": "^0.4.7",
"@welshman/signer": "^0.4.7",
"@welshman/store": "^0.4.7",
"@welshman/util": "^0.4.7",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.5.3",
"@welshman/content": "^0.5.3",
"@welshman/editor": "^0.5.3",
"@welshman/feeds": "^0.5.3",
"@welshman/lib": "^0.5.3",
"@welshman/net": "^0.5.3",
"@welshman/relay": "^0.5.3",
"@welshman/router": "^0.5.3",
"@welshman/signer": "^0.5.3",
"@welshman/store": "^0.5.3",
"@welshman/util": "^0.5.3",
"compressorjs": "^1.2.1",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5",
"emoji-picker-element": "^1.22.8",
"fuse.js": "^7.0.0",
"husky": "^9.1.6",
"idb": "^8.0.0",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0",
"dotenv": "^16.6.1",
"emoji-picker-element": "^1.27.0",
"fuse.js": "^7.1.0",
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"nostr-tools": "^2.17.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
+2008 -1837
View File
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -62,6 +62,8 @@
--primary-content: oklch(var(--pc));
--secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc));
--neutral: oklch(var(--n));
--neutral-content: oklch(var(--nc));
}
/* safe area insets */
@@ -215,12 +217,6 @@
@apply ellipsize;
}
@media (max-width: 639px) {
[data-tip]::before {
display: none;
}
}
.content-padding-x {
@apply px-4 sm:px-8 md:px-12;
}
@@ -278,8 +274,8 @@
}
.tiptap {
--tiptap-object-bg: var(--base-100);
--tiptap-object-fg: var(--base-content);
--tiptap-object-bg: var(--neutral);
--tiptap-object-fg: var(--neutral-content);
--tiptap-active-bg: var(--primary);
--tiptap-active-fg: var(--primary-content);
}
+49 -6
View File
@@ -3,6 +3,7 @@
import {getTagValue, getAddress} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds"
import Inbox from "@assets/icons/inbox.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -10,8 +11,16 @@
import AlertItem from "@app/components/AlertItem.svelte"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {alerts, dmAlert, deriveAlertStatus, userInboxRelays, getAlertFeed} from "@app/core/state"
import {
alerts,
dmAlert,
deriveAlertStatus,
userInboxRelays,
getAlertFeed,
userSettingsValues,
} from "@app/core/state"
import {deleteAlert, createDmAlert} from "@app/core/commands"
import {clearBadges} from "../util/notifications"
type Props = {
url?: string
@@ -42,11 +51,11 @@
const uncheckDmAlert = async (message: string) => {
await sleep(100)
toggle.checked = false
directMessagesNotificationToggle.checked = false
pushToast({theme: "error", message})
}
const onToggle = async () => {
const onDirectMessagesNotificationToggle = async () => {
if ($dmAlert) {
deleteAlert($dmAlert)
} else {
@@ -64,7 +73,19 @@
}
}
let toggle: HTMLInputElement
const onShowBadgeOnUnreadToggle = async () => {
$userSettingsValues.show_notifications_badge = !$userSettingsValues.show_notifications_badge
if (!$userSettingsValues.show_notifications_badge) {
await clearBadges()
}
}
const onDirectMessagesNotificationSoundToggle = async () => {
$userSettingsValues.play_notification_sound = !$userSettingsValues.play_notification_sound
}
let directMessagesNotificationToggle: HTMLInputElement
</script>
<div class="col-4">
@@ -88,14 +109,36 @@
</div>
</div>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
<div class="flex items-center justify-between">
<strong class="flex items-center gap-3">
<Icon icon={Bell} />
Notifications
</strong>
</div>
<div class="flex justify-between">
<p>Notify me about new direct messages</p>
<input
type="checkbox"
class="toggle toggle-primary"
bind:this={toggle}
bind:this={directMessagesNotificationToggle}
checked={Boolean($dmAlert)}
oninput={onToggle} />
oninput={onDirectMessagesNotificationToggle} />
</div>
<div class="flex justify-between">
<p>Show badge for unread direct messages</p>
<input
type="checkbox"
class="toggle toggle-primary"
checked={Boolean($userSettingsValues.show_notifications_badge)}
oninput={onShowBadgeOnUnreadToggle} />
</div>
<div class="flex justify-between">
<p>Play sound for new direct messages</p>
<input
type="checkbox"
class="toggle toggle-primary"
checked={Boolean($userSettingsValues.play_notification_sound)}
oninput={onDirectMessagesNotificationSoundToggle} />
</div>
{#if $dmStatus}
{@const status = getTagValue("status", $dmStatus.tags) || "error"}
+2 -3
View File
@@ -10,11 +10,10 @@
import {makeEditor} from "@app/editor"
type Props = {
url?: string
onSubmit: (event: EventContent) => void
}
const {onSubmit, url}: Props = $props()
const {onSubmit}: Props = $props()
const autofocus = !isMobile
@@ -39,11 +38,11 @@
}
const editor = makeEditor({
url,
autofocus,
submit,
uploading,
aggressive: true,
encryptFiles: true,
})
</script>
+2 -4
View File
@@ -69,11 +69,11 @@
if (!parsed || hideMediaAtDepth <= depth) return false
if (isLink(parsed) && $userSettingsValues.show_media && isStartOrEnd(i)) {
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
return true
}
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
if ((isEvent(parsed) || isAddress(parsed)) && isStartAndEnd(i)) {
return true
}
@@ -95,8 +95,6 @@
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
const isStartOrEnd = (i: number) => isStart(i) || isEnd(i)
const ignoreWarning = () => {
warning = null
}
+1 -5
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import type {ParsedEmojiValue} from "@welshman/content"
import {imgproxy} from "@app/core/state"
export let value: ParsedEmojiValue
@@ -8,10 +7,7 @@
</script>
{#if value.url}
<img
{alt}
src={imgproxy(value.url, {w: 24, h: 24})}
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
<img {alt} src={value.url} class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{:else}
{alt}
{/if}
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/core/state"
import {dufflepud} from "@app/core/state"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
@@ -51,7 +51,7 @@
<img
alt="Link preview"
onerror={onError}
src={imgproxy(preview.image)}
src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
{/if}
<div class="flex flex-col gap-2 p-4">
@@ -1,10 +1,17 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib"
import {getTags, decryptFile, getTagValue, tagsFromIMeta} from "@welshman/util"
import {
getTags,
getBlob,
decryptFile,
getTagValue,
tagsFromIMeta,
makeBlossomAuthEvent,
} from "@welshman/util"
import {signer} from "@welshman/app"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import {imgproxy} from "@app/core/state"
const {value, event, ...props} = $props()
@@ -14,18 +21,34 @@
.map(tagsFromIMeta)
.find(meta => getTagValue("url", meta) === url) || event.tags
const hash = getTagValue("x", meta)
const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta)
const onError = () => {
hasError = true
const onError = async () => {
// If the image failed to load, try authenticating
if (hash && $signer) {
const server = new URL(url).origin
const template = makeBlossomAuthEvent({action: "get", server, hashes: [hash]})
const authEvent = await $signer.sign(template)
const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) {
src = URL.createObjectURL(await res.blob())
} else {
hasError = true
}
} else {
hasError = true
}
}
let hasError = $state(false)
let src = $state(imgproxy(url))
let src = $state("")
onMount(async () => {
// If we have an encryption algorithm, fetch and decrypt
if (algorithm === "aes-gcm" && key && nonce) {
const response = await fetch(url)
@@ -33,8 +56,10 @@
const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([decryptedData]))
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
}
} else {
src = url
}
})
@@ -48,6 +73,6 @@
<Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)}
</a>
{:else}
{:else if src}
<img alt="" {src} onerror={onError} {...props} />
{/if}
+1 -1
View File
@@ -35,7 +35,7 @@
{/snippet}
</CardButton>
</Button>
<Button onclick={signUp} class="dark:btn-neutral">
<Button onclick={signUp} class="btn-neutral">
<CardButton>
{#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div>
+11 -2
View File
@@ -32,9 +32,10 @@
onNostrConnect: async (response: Nip46ResponseWithResult) => {
const pubkey = await controller.broker.getPublicKey()
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
await loadUserData(pubkey)
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
setChecked("*")
clearModals()
},
@@ -48,13 +49,20 @@
try {
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
if (!signerPubkey || relays.length === 0) {
if (!signerPubkey) {
return pushToast({
theme: "error",
message: "Sorry, it looks like that's an invalid bunker link.",
})
}
if (relays.length === 0) {
return pushToast({
theme: "error",
message: "That bunker link does not include any relays.",
})
}
controller.loading.set(true)
const {clientSecret} = controller
@@ -91,6 +99,7 @@
}
const selectConnect = () => {
controller.loading.set(false)
mode = "connect"
}
+23 -6
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Server from "@assets/icons/server.svg?dataurl"
import Moon from "@assets/icons/moon.svg?dataurl"
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl"
@@ -13,13 +14,16 @@
import LogOut from "@app/components/LogOut.svelte"
import {PLATFORM_NAME} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme"
const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script>
<div class="column menu gap-2">
<Link replaceState href="/settings/profile">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={UserRounded} size={7} /></div>
{/snippet}
@@ -32,7 +36,7 @@
</CardButton>
</Link>
<Link replaceState href="/settings/alerts">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Bell} size={7} /></div>
{/snippet}
@@ -45,7 +49,7 @@
</CardButton>
</Link>
<Link replaceState href="/settings/wallet">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Wallet} size={7} /></div>
{/snippet}
@@ -58,7 +62,7 @@
</CardButton>
</Link>
<Link replaceState href="/settings/relays">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Server} size={7} /></div>
{/snippet}
@@ -71,7 +75,7 @@
</CardButton>
</Link>
<Link replaceState href="/settings/content">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Settings} size={7} /></div>
{/snippet}
@@ -83,8 +87,21 @@
{/snippet}
</CardButton>
</Link>
<Button onclick={toggleTheme}>
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Moon} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Theme</div>
{/snippet}
{#snippet info()}
<div>Switch between light and dark mode</div>
{/snippet}
</CardButton>
</Button>
<Link replaceState href="/settings/about">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Code2} size={7} /></div>
{/snippet}
+8 -12
View File
@@ -1,15 +1,11 @@
<script lang="ts">
import Login from "@assets/icons/login-3.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Divider from "@lib/components/Divider.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userRoomsByUrl, PLATFORM_RELAYS} from "@app/core/state"
import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd)
</script>
<div class="column menu gap-2">
@@ -22,18 +18,18 @@
{/each}
<Divider />
{/if}
<Button onclick={addSpace}>
<CardButton class="dark:btn-neutral">
<Link href="/discover">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Login} size={7} /></div>
<div><Icon icon={Compass} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Add a space</div>
<div>Explore Spaces</div>
{/snippet}
{#snippet info()}
<div>Join or create a new space</div>
<div>Join create, or browse spaces</div>
{/snippet}
</CardButton>
</Button>
</Link>
{/each}
</div>
+1 -1
View File
@@ -13,7 +13,7 @@
</script>
<Link replaceState href={path}>
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><SpaceAvatar {url} /></div>
{/snippet}
@@ -0,0 +1,38 @@
<script lang="ts">
import {onMount} from "svelte"
import {userSettingsValues} from "@app/core/state"
import {notifications} from "../util/notifications"
let audioElement: HTMLAudioElement
let enabled = $state(false)
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
enabled = true
} else {
enabled = false
}
})
let notificationCount = $state($notifications.size)
const playSound = () => {
if (enabled && $userSettingsValues.play_notification_sound) {
audioElement.play()
}
}
onMount(() => {
audioElement.load()
notifications.subscribe(notifications => {
if (notifications.size > notificationCount) {
playSound()
}
notificationCount = notifications.size
})
})
</script>
<audio bind:this={audioElement} src="/new-notification-3-398649.mp3"></audio>
+3 -5
View File
@@ -18,7 +18,7 @@
import {makeSpacePath} from "@app/util/routes"
import {notifications} from "@app/util/notifications"
import Widget from "@assets/icons/widget.svg?dataurl"
import AddSquare from "@assets/icons/add-square.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@@ -31,8 +31,6 @@
const {children}: Props = $props()
const addSpace = () => pushModal(SpaceAdd)
const showSpacesMenu = () => (spaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
@@ -83,8 +81,8 @@
<Avatar icon={Widget} class="!h-10 !w-10" />
</PrimaryNavItem>
{/if}
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right">
<Avatar icon={AddSquare} class="!h-10 !w-10" />
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={Compass} class="!h-10 !w-10" />
</PrimaryNavItem>
{/each}
</div>
+5 -28
View File
@@ -1,22 +1,13 @@
<script lang="ts">
import {nthNe} from "@welshman/lib"
import type {Profile} from "@welshman/util"
import {
getTag,
makeEvent,
makeProfile,
editProfile,
createProfile,
isPublishedProfile,
uniqTags,
} from "@welshman/util"
import {Router} from "@welshman/router"
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
import {getTag, makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {PROTECTED, getMembershipUrls, userMembership} from "@app/core/state"
import {PROTECTED} from "@app/core/state"
import {updateProfile} from "../core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
@@ -25,21 +16,7 @@
const back = () => history.back()
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
const router = Router.get()
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const scenarios = [router.FromRelays(getMembershipUrls($userMembership))]
if (shouldBroadcast) {
scenarios.push(router.FromUser(), router.Index())
template.tags = template.tags.filter(nthNe(0, "-"))
} else {
template.tags = uniqTags([...template.tags, PROTECTED])
}
const event = makeEvent(template.kind, template)
const relays = router.merge(scenarios).getUrls()
publishThunk({event, relays})
updateProfile({profile, shouldBroadcast})
pushToast({message: "Your profile has been updated!"})
clearModals()
}
+2 -2
View File
@@ -26,8 +26,8 @@
})
</script>
<Button class="max-w-full {props.class}" onclick={copy}>
<div bind:this={wrapper} style={`height: ${height}px`}>
<Button class="flex w-full justify-center {props.class}" onclick={copy}>
<div bind:this={wrapper} class="w-md" style={`height: ${height}px`}>
<canvas
class="rounded-box"
bind:this={canvas}
+41 -23
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {spec, prop, avg} from "@welshman/lib"
import {signerLog, SignerLogEntryStatus} from "@welshman/app"
import {session, SessionMethod, signerLog, SignerLogEntryStatus} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
@@ -26,27 +26,45 @@
const logout = () => pushModal(LogOut)
</script>
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xl font-bold">Signer Status</span>
<span class="flex items-center gap-2">
{#if isDisconnected}
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
{:else if recentFailure > 3}
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
{:else if recentAvg > 1000 || recentPending > 3}
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
{:else if recentSuccess === 0 && recentFailure > 0}{:else}
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok
{/if}
</span>
{#if $session && $session.method !== SessionMethod.Anonymous}
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xl font-bold">Signer Status</span>
<span class="flex items-center gap-2">
{#if isDisconnected}
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
{:else if recentFailure > 3}
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
{:else if recentAvg > 1000 || recentPending > 3}
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
{:else if recentSuccess === 0 && recentFailure > 0}{:else}
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok
{/if}
</span>
</div>
<div class="flex justify-between text-sm opacity-75">
<p>
Logged in with
{#if $session.method === SessionMethod.Nip01}
private key
{:else if $session.method === SessionMethod.Nip07}
browser extension
{:else if $session.method === SessionMethod.Nip46}
remote signer
{:else if $session.method === SessionMethod.Nip55}
{$session.signer}
{:else if $session.method === SessionMethod.Pubkey}
public key (readonly)
{/if}
</p>
<p>
{success} requests succeeded, {failure} failed, {pending} pending
</p>
</div>
</div>
<p class="text-sm opacity-75">
{success} requests succeeded, {failure} failed, {pending} pending
</p>
{#if isDisconnected}
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
{/if}
</div>
{#if isDisconnected}
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
{/if}
</div>
{/if}
+4 -6
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {displayUrl} from "@welshman/lib"
import {AuthStatus} from "@welshman/net"
import {waitForThunkError} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
@@ -15,7 +14,7 @@
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
import {publishJoinRequest} from "@app/core/commands"
import {checkRelayAccess} from "@app/core/commands"
import {deriveSocket} from "@app/core/state"
type Props = {
@@ -32,11 +31,10 @@
loading = true
try {
const thunk = publishJoinRequest({url, claim})
const error = await waitForThunkError(thunk)
const message = await checkRelayAccess(url, claim)
if (error) {
return pushToast({theme: "error", message: error, timeout: 30_000})
if (message) {
return pushToast({theme: "error", message, timeout: 30_000})
}
if ($socket.auth.status === AuthStatus.None) {
+3 -18
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import Compass from "@assets/icons/compass-big.svg?dataurl"
import Login from "@assets/icons/login-3.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@@ -25,21 +23,8 @@
<div>Spaces are places where communities come together to work, play, and hang out.</div>
{/snippet}
</ModalHeader>
<Link href="/discover">
<CardButton class="btn-primary">
{#snippet icon()}
<div><Icon icon={Compass} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Discover spaces</div>
{/snippet}
{#snippet info()}
<div>Browse spaces on the discover page.</div>
{/snippet}
</CardButton>
</Link>
<Button onclick={startJoin}>
<CardButton class="dark:btn-neutral">
<CardButton class="btn-primary">
{#snippet icon()}
<div><Icon icon={Login} size={7} /></div>
{/snippet}
@@ -47,12 +32,12 @@
<div>Join a space</div>
{/snippet}
{#snippet info()}
<div>Enter an invite code or url to join an existing space.</div>
<div>Enter an invite link to join an existing space.</div>
{/snippet}
</CardButton>
</Button>
<Button onclick={startCreate}>
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div>
{/snippet}
+6 -5
View File
@@ -3,15 +3,15 @@
import {sleep, nthEq} from "@welshman/lib"
import {request} from "@welshman/net"
import {displayRelayUrl, AUTH_INVITE} from "@welshman/util"
import {slide} from "@lib/transition"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte"
import {clip} from "@app/util/toast"
import {PLATFORM_URL} from "@app/core/state"
@@ -62,11 +62,12 @@
</ModalHeader>
<div>
{#if loading}
<p class="center" out:slide>
<p class="center">
<Spinner {loading}>Requesting an invite link...</Spinner>
</p>
{:else}
<div in:slide>
<div class="flex flex-col items-center gap-6">
<QRCode code={invite} />
<Field>
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
+24 -10
View File
@@ -3,6 +3,7 @@
import {fade} from "@lib/transition"
import CompassBig from "@assets/icons/compass-big.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@@ -14,7 +15,7 @@
import Button from "@lib/components/Button.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
import ChannelName from "@app/components/ChannelName.svelte"
import {makeThreadPath, makeCalendarPath, makeRoomPath, makeSpacePath} from "@app/util/routes"
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
import {
hasNip29,
deriveUserRooms,
@@ -34,8 +35,9 @@
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const chatPath = makeSpacePath(url, "chat")
const threadsPath = makeThreadPath(url)
const calendarPath = makeCalendarPath(url)
const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads")
const calendarPath = makeSpacePath(url, "calendar")
const addRoom = () => pushModal(RoomCreate, {url})
@@ -61,25 +63,37 @@
Quick Links
</h3>
<div class="flex flex-col gap-2">
<Link href={threadsPath} class="btn btn-primary w-full justify-start">
<Link href={goalsPath} class="btn btn-neutral w-full justify-start">
<div class="relative flex items-center gap-2">
<Icon icon={NotesMinimalistic} />
Threads
{#if $notifications.has(threadsPath)}
<Icon icon={StarFallMinimalistic} />
Goals
{#if $notifications.has(goalsPath)}
<div
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-primary-content"
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
transition:fade>
</div>
{/if}
</div>
</Link>
<Link href={calendarPath} class="btn btn-secondary w-full justify-start">
<Link href={threadsPath} class="btn btn-neutral w-full justify-start">
<div class="relative flex items-center gap-2">
<Icon icon={NotesMinimalistic} />
Threads
{#if $notifications.has(threadsPath)}
<div
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
transition:fade>
</div>
{/if}
</div>
</Link>
<Link href={calendarPath} class="btn btn-neutral w-full justify-start">
<div class="relative flex items-center gap-2">
<Icon icon={CalendarMinimalistic} />
Calendar
{#if $notifications.has(calendarPath)}
<div
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-primary-content"
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
transition:fade>
</div>
{/if}
+1 -2
View File
@@ -46,8 +46,7 @@
{#if showFailure}
{@const url = failedUrls[0]}
{@const status = $thunk.status[url]}
{@const message = $thunk.details[url]}
{@const {status, detail: message} = $thunk.results[url]}
<button
class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}>
@@ -0,0 +1,61 @@
<script lang="ts">
import {getWalletAddress} from "@welshman/util"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {updateProfile} from "@app/core/commands"
import {clearModals} from "@app/util/modal"
import {userProfile, session} from "@welshman/app"
import {makeProfile} from "@welshman/util"
const lud16 = getWalletAddress($session!.wallet!)
const confirm = async () => {
const profile = $userProfile || makeProfile()
loading = true
try {
await updateProfile({profile: {...profile, lud16}})
clearModals()
} finally {
loading = false
}
}
const cancel = () => {
clearModals()
}
let loading = $state(false)
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
Set as Receiving Address?
{/snippet}
</ModalHeader>
{#if $userProfile?.lud16}
<p>
Your current receiving address is different from the one provided by your connected wallet.
</p>
<p>
Would you like to update your receiving address to <span class="text-primary">{lud16}</span>?
</p>
{:else}
<p>
You don't currently have a receiving address set, which means other people can't send you
lightning payments.
</p>
<p>Would you like to use the one associated with your connected wallet?</p>
{/if}
<ModalFooter>
<Button class="btn btn-neutral" onclick={cancel} disabled={loading}>No, skip this</Button>
<Button class="btn btn-primary" onclick={confirm} disabled={loading}>
<Spinner {loading}>Yes, set as receiving address</Spinner>
</Button>
</ModalFooter>
</div>
+9 -3
View File
@@ -3,7 +3,7 @@
import {nwc} from "@getalby/sdk"
import {sleep, assoc} from "@welshman/lib"
import type {NWCInfo} from "@welshman/util"
import {pubkey, updateSession} from "@welshman/app"
import {pubkey, userProfile, updateSession} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@@ -15,11 +15,13 @@
import Scanner from "@lib/components/Scanner.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte"
import Divider from "@lib/components/Divider.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {getWebLn} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
import Divider from "@src/lib/components/Divider.svelte"
const back = () => history.back()
@@ -76,6 +78,10 @@
await sleep(400)
back()
if (info.lud16 && info.lud16 !== $userProfile?.lud16) {
pushModal(WalletAsReceivingAddress)
}
}
} catch (e) {
console.error(e)
@@ -109,7 +115,7 @@
<div>Connect a Wallet</div>
{/snippet}
{#snippet info()}
Use Nostr Wallet Connect to send Bitcoin payments over Bolt.
Use Nostr Wallet Connect to send Bitcoin payments over lightning.
{/snippet}
</ModalHeader>
{#if getWebLn()}
@@ -0,0 +1,99 @@
<script lang="ts">
import {getWalletAddress} from "@welshman/util"
import {session, userProfile} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Wallet from "@assets/icons/wallet.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import {updateProfile} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
const back = () => history.back()
let address = $state($userProfile?.lud16 || "")
let isLoading = $state(false)
const walletLud16 = $derived($session?.wallet ? getWalletAddress($session.wallet) : undefined)
const useWalletAddress = () => {
if (walletLud16) {
address = walletLud16
}
}
const save = async () => {
isLoading = true
try {
await updateProfile({
profile: {
...$userProfile,
lud06: undefined,
lud16: address.trim() || undefined,
},
})
back()
} catch (error) {
pushToast({theme: "error", message: "Failed to update profile"})
} finally {
isLoading = false
}
}
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
Update Lightning Address
{/snippet}
{#snippet info()}
Update your lightning address for receiving payments.
{/snippet}
</ModalHeader>
<div class="column gap-4">
<div class="column gap-2">
<span> Lightning Address </span>
<input
type="text"
placeholder="user@domain.com"
bind:value={address}
class="input input-bordered flex w-full"
disabled={isLoading} />
<p class="text-xs opacity-75">
You can enter one manually or use your connected wallet's address (if available). Leave
empty to remove your lightning address
</p>
</div>
{#if walletLud16 && walletLud16 !== address}
<div class="card bg-base-200 p-4">
<div class="flex items-center justify-between gap-3">
<div class="column gap-1">
<div class="flex items-center gap-2">
<Icon icon={Wallet} size={4} />
<span class="text-sm font-medium">Wallet Address</span>
</div>
<p class="text-xs opacity-75">{walletLud16}</p>
</div>
<Button class="btn btn-outline btn-sm" onclick={useWalletAddress} disabled={isLoading}>
Use This
</Button>
</div>
</div>
{/if}
</div>
<ModalFooter>
<Button class="btn btn-neutral" onclick={back} disabled={isLoading}>Cancel</Button>
<Button class="btn btn-primary" onclick={save} disabled={isLoading}>
{#if isLoading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={CheckCircle} />
{/if}
Save Changes
</Button>
</ModalFooter>
</div>
+116 -41
View File
@@ -3,6 +3,7 @@ import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store"
import type {Override, MakeOptional} from "@welshman/lib"
import {
first,
sha256,
randomId,
append,
@@ -16,12 +17,15 @@ import {
parseJson,
fromPairs,
last,
simpleCache,
normalizeUrl,
nthNe,
} from "@welshman/lib"
import {decrypt, Nip01Signer} from "@welshman/signer"
import type {UploadTask} from "@welshman/editor"
import type {Feed} from "@welshman/feeds"
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} from "@welshman/feeds"
import type {TrustedEvent, EventContent} from "@welshman/util"
import type {TrustedEvent, EventContent, Profile} from "@welshman/util"
import {
WRAP,
DELETE,
@@ -57,8 +61,13 @@ import {
getTagValue,
getTagValues,
uploadBlob,
canUploadBlob,
encryptFile,
makeBlossomAuthEvent,
isPublishedProfile,
editProfile,
createProfile,
uniqTags,
} from "@welshman/util"
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
import {Router} from "@welshman/router"
@@ -75,7 +84,6 @@ import {
userInboxRelaySelections,
nip44EncryptToSelf,
loadRelay,
clearStorage,
dropSession,
tagEventForComment,
tagEventForQuote,
@@ -92,15 +100,17 @@ import {
INDEXER_RELAYS,
NOTIFIER_PUBKEY,
NOTIFIER_RELAY,
DEFAULT_BLOSSOM_SERVERS,
userRoomsByUrl,
userSettingsValues,
canDecrypt,
ensureUnwrapped,
userInboxRelays,
getMembershipUrls,
} from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push"
import {preferencesStorageProvider} from "@src/lib/storage"
import {preferencesStorageProvider, Collection} from "@src/lib/storage"
// Utils
@@ -143,10 +153,10 @@ export const logout = async () => {
dropSession($pubkey)
}
await clearStorage()
localStorage.clear()
await preferencesStorageProvider.clear()
await Collection.clearAll()
}
// Synchronization
@@ -158,7 +168,7 @@ export const broadcastUserData = async (relays: string[]) => {
for (const event of events) {
if (isSignedEvent(event)) {
await publishThunk({event, relays}).result
await publishThunk({event, relays}).complete
}
}
}
@@ -660,17 +670,53 @@ export const enableGiftWraps = () => {
// File upload
export const getBlossomServer = () => {
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
const server = normalizeBlossomUrl(url)
const $signer = signer.get() || Nip01Signer.ephemeral()
const headers: Record<string, string> = {
"X-Content-Type": "text/plain",
"X-Content-Length": "1",
"X-SHA-256": "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac",
}
try {
const authEvent = await $signer.sign(makeBlossomAuthEvent({action: "upload", server}))
const res = await canUploadBlob(server, {authEvent, headers})
return res.status === 200
} catch (e) {
if (!String(e).match(/Failed to fetch|NetworkError/)) {
console.error(e)
}
}
return false
})
export type GetBlossomServerOptions = {
url?: string
}
export const getBlossomServer = async (options: GetBlossomServerOptions = {}) => {
if (options.url) {
if (await hasBlossomSupport(options.url)) {
return normalizeBlossomUrl(options.url)
}
}
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
for (const url of userUrls) {
return url.replace(/^ws/, "http")
return normalizeBlossomUrl(url)
}
return "https://cdn.satellite.earth"
return first(DEFAULT_BLOSSOM_SERVERS)!
}
export type UploadFileOptions = {
url?: string
encrypt?: boolean
}
@@ -680,35 +726,35 @@ export type UploadFileResult = {
}
export const uploadFile = async (file: File, options: UploadFileOptions = {}) => {
const {name, type} = file
if (!type.match("image/(webp|gif)")) {
file = await compressFile(file)
}
const tags: string[][] = []
if (options.encrypt) {
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
tags.push(
["decryption-key", key],
["decryption-nonce", nonce],
["encryption-algorithm", algorithm],
)
file = new File([new Blob([ciphertext])], name, {
type: "application/octet-stream",
})
}
const server = getBlossomServer()
const hashes = [await sha256(await file.arrayBuffer())]
const $signer = signer.get() || Nip01Signer.ephemeral()
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
const authEvent = await $signer.sign(authTemplate)
try {
const {name, type} = file
if (!type.match("image/(webp|gif)")) {
file = await compressFile(file)
}
const tags: string[][] = []
if (options.encrypt) {
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
tags.push(
["decryption-key", key],
["decryption-nonce", nonce],
["encryption-algorithm", algorithm],
)
file = new File([new Uint8Array(ciphertext)], name, {
type: "application/octet-stream",
})
}
const ext = "." + type.split("/")[1]
const server = await getBlossomServer(options)
const hashes = [await sha256(await file.arrayBuffer())]
const $signer = signer.get() || Nip01Signer.ephemeral()
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
const authEvent = await $signer.sign(authTemplate)
const res = await uploadBlob(server, file, {authEvent})
const text = await res.text()
@@ -718,16 +764,45 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
return {error: text}
}
// Always append file extension if missing
if (new URL(url).pathname.split(".").length === 1) {
url += "." + type.split("/")[1]
// Always append correct file extension if we encrypted the file, or if it's missing
if (options.encrypt) {
url = url.replace(/\.\w+$/, "") + ext
} else if (new URL(url).pathname.split(".").length === 1) {
url += ext
}
const result = {...task, tags, url}
return {result}
} catch (e: any) {
console.error(e)
console.error("Error caught when uploading file:", e)
return {error: e.toString()}
}
}
// Update Profile
export const updateProfile = async ({
profile,
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
}: {
profile: Profile
shouldBroadcast?: boolean
}) => {
const router = Router.get()
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const scenarios = [router.FromRelays(getMembershipUrls(userMembership.get()))]
if (shouldBroadcast) {
scenarios.push(router.FromUser(), router.Index())
template.tags = template.tags.filter(nthNe(0, "-"))
} else {
template.tags = uniqTags([...template.tags, PROTECTED])
}
const event = makeEvent(template.kind, template)
const relays = router.merge(scenarios).getUrls()
await publishThunk({event, relays}).complete
}
+24 -21
View File
@@ -140,14 +140,14 @@ export const PLATFORM_ACCENT = import.meta.env.VITE_PLATFORM_ACCENT
export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION
export const DEFAULT_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)
export const BURROW_URL = import.meta.env.VITE_BURROW_URL
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const IMGPROXY_URL = "https://imgproxy.coracle.social"
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST]
@@ -178,20 +178,6 @@ export const colors = [
export const dufflepud = (path: string) => DUFFLEPUD_URL + "/" + path
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
if (!url || url.match("gif$")) {
return url
}
url = url.split("?")[0]
try {
return url ? `${IMGPROXY_URL}/x/s:${w}:${h}/${btoa(url)}` : url
} catch (e) {
return url
}
}
export const entityLink = (entity: string) => `https://coracle.social/${entity}`
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
@@ -336,6 +322,8 @@ export type SettingsValues = {
report_errors: boolean
send_delay: number
font_size: number
play_notification_sound: boolean
show_notifications_badge: boolean
}
export type Settings = {
@@ -349,8 +337,10 @@ export const defaultSettings = {
trusted_relays: [],
report_usage: true,
report_errors: true,
send_delay: 3000,
send_delay: 0,
font_size: 1,
play_notification_sound: true,
show_notifications_badge: true,
}
export const settings = deriveEventsMapped<Settings>(repository, {
@@ -405,9 +395,13 @@ export const alerts = withGetter(
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
const $signer = signer.get()
return {event, tags}
if ($signer) {
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
return {event, tags}
}
},
}),
)
@@ -435,9 +429,13 @@ export const alertStatuses = withGetter(
filters: [{kinds: [ALERT_STATUS]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
const $signer = signer.get()
return {event, tags}
if ($signer) {
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
return {event, tags}
}
},
}),
)
@@ -527,6 +525,11 @@ export const chats = derived(
const messagesByChatId = new Map<string, TrustedEvent[]>()
for (const message of $messages) {
// Filter out messages we sent but aren't addressed to the user
if (!getPubkeyTagValues(message.wrap?.tags || []).includes($pubkey!)) {
continue
}
const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
pushToMapKey(messagesByChatId, chatId, message)
+4 -1
View File
@@ -11,6 +11,7 @@ import {uploadFile} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
export const makeEditor = async ({
encryptFiles = false,
aggressive = false,
autofocus = false,
charCount,
@@ -21,6 +22,7 @@ export const makeEditor = async ({
uploading,
wordCount,
}: {
encryptFiles?: boolean
aggressive?: boolean
autofocus?: boolean
charCount?: Writable<number>
@@ -51,7 +53,8 @@ export const makeEditor = async ({
},
fileUpload: {
config: {
upload: (attrs: FileAttributes) => uploadFile(attrs.file, {encrypt: true}),
upload: (attrs: FileAttributes) =>
uploadFile(attrs.file, {url, encrypt: encryptFiles}),
onDrop: () => uploading?.set(true),
onComplete: () => uploading?.set(false),
onUploadError(currentEditor, task) {
+30 -2
View File
@@ -1,4 +1,4 @@
import {derived} from "svelte/store"
import {derived, get} from "svelte/store"
import {synced, throttled} from "@welshman/store"
import {pubkey, relaysByUrl} from "@welshman/app"
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
@@ -12,8 +12,16 @@ import {
makeSpaceChatPath,
makeRoomPath,
} from "@app/util/routes"
import {chats, hasNip29, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/core/state"
import {
chats,
hasNip29,
getUrlsForEvent,
userRoomsByUrl,
repositoryStore,
userSettingsValues,
} from "@app/core/state"
import {preferencesStorageProvider} from "@src/lib/storage"
import {Badge} from "@capawesome/capacitor-badge"
// Checked state
@@ -150,3 +158,23 @@ export const notifications = derived(
return paths
},
)
export const badgeCount = derived(notifications, notifications => {
return notifications.size
})
export const handleBadgeCountChanges = async (count: number) => {
if (get(userSettingsValues).show_notifications_badge) {
try {
await Badge.set({count})
} catch (err) {
// failed to set badge
}
} else {
await clearBadges()
}
}
export const clearBadges = async () => {
await Badge.clear()
}
+1 -1
View File
@@ -64,7 +64,7 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
case "discover":
return urls.length + 2
case "spaces": {
const routeUrl = decodeRelay($page.params.relay)
const routeUrl = decodeRelay($page.params.relay || "")
return urls.findIndex(url => url === routeUrl) + 1
}
+276
View File
@@ -0,0 +1,276 @@
import {
always,
on,
hash,
last,
groupBy,
throttle,
fromPairs,
batch,
sortBy,
concat,
} from "@welshman/lib"
import {throttled, freshness} from "@welshman/store"
import {
PROFILE,
FOLLOWS,
MUTES,
RELAYS,
BLOSSOM_SERVERS,
INBOX_RELAYS,
ROOMS,
APP_DATA,
ALERT_STATUS,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
EVENT_TIME,
THREAD,
MESSAGE,
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
verifiedSymbol,
} from "@welshman/util"
import type {Zapper, TrustedEvent} from "@welshman/util"
import type {RepositoryUpdate} from "@welshman/relay"
import type {Handle, Relay} from "@welshman/app"
import {
plaintext,
tracker,
relays,
repository,
handles,
zappers,
onZapper,
onHandle,
} from "@welshman/app"
import {Collection} from "@lib/storage"
const syncEvents = async () => {
const collection = new Collection<TrustedEvent>({
table: "events",
shards: Array.from("0123456789abcdef"),
getShard: (event: TrustedEvent) => last(event.id),
})
const initialEvents = await collection.get()
// Mark events verified to avoid re-verification of signatures
for (const event of initialEvents) {
event[verifiedSymbol] = true
}
repository.load(initialEvents)
const rankEvent = (event: TrustedEvent) => {
switch (event.kind) {
case PROFILE:
return 1
case FOLLOWS:
return 1
case MUTES:
return 1
case RELAYS:
return 1
case BLOSSOM_SERVERS:
return 1
case INBOX_RELAYS:
return 1
case ROOMS:
return 1
case APP_DATA:
return 1
case ALERT_STATUS:
return 1
case ALERT_EMAIL:
return 1
case ALERT_WEB:
return 1
case ALERT_IOS:
return 1
case ALERT_ANDROID:
return 1
case EVENT_TIME:
return 0.9
case THREAD:
return 0.9
case MESSAGE:
return 0.9
case DIRECT_MESSAGE:
return 0.9
case DIRECT_MESSAGE_FILE:
return 0.9
default:
return 0
}
}
return on(
repository,
"update",
batch(3000, async (updates: RepositoryUpdate[]) => {
let added: TrustedEvent[] = []
const removed = new Set<string>()
for (const update of updates) {
for (const event of update.added) {
if (rankEvent(event) > 0) {
added.push(event)
removed.delete(event.id)
}
}
for (const id of update.removed) {
removed.add(id)
}
}
if (removed.size > 0) {
added = added.filter(e => !removed.has(e.id))
const removedByShard = groupBy(id => last(id), removed)
const addedByShard = groupBy(e => last(e.id), added)
const shards = new Set([...removedByShard.keys(), ...addedByShard.keys()])
for (const shard of shards) {
const removedInShard = removedByShard.get(shard)
const addedInShard = addedByShard.get(shard) || []
const current = await collection.getShard(shard)
const filtered = current.filter(e => !removedInShard?.includes(e.id))
const sorted = sortBy(e => -rankEvent(e), concat(filtered, addedInShard))
const pruned = sorted.slice(0, 10_000)
await collection.setShard(shard, pruned)
}
} else if (added.length > 0) {
await collection.add(added)
}
}),
)
}
type TrackerItem = [string, string[]]
const syncTracker = async () => {
const collection = new Collection<TrackerItem>({
table: "tracker",
shards: Array.from("0123456789abcdef"),
getShard: (item: TrackerItem) => last(item[0]),
})
const relaysById = new Map<string, Set<string>>()
for (const [id, relays] of await collection.get()) {
relaysById.set(id, new Set(relays))
}
tracker.load(relaysById)
let p = Promise.resolve()
const updateOne = batch(3000, (ids: string[]) => {
p = p.then(() => {
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
})
})
const updateAll = throttle(3000, () => {
p = p.then(() => {
collection.set(
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
)
})
})
tracker.on("add", updateOne)
tracker.on("remove", updateOne)
tracker.on("load", updateAll)
tracker.on("clear", updateAll)
return () => {
tracker.off("add", updateOne)
tracker.off("remove", updateOne)
tracker.off("load", updateAll)
tracker.off("clear", updateAll)
}
}
const syncRelays = async () => {
const collection = new Collection<Relay>({
table: "relays",
shards: Array.from("0123456789"),
getShard: (item: Relay) => last(hash(item.url)),
})
relays.set(await collection.get())
return throttled(3000, relays).subscribe(collection.set)
}
const syncHandles = async () => {
const collection = new Collection<Handle>({
table: "handles",
shards: Array.from("0123456789"),
getShard: (item: Handle) => last(hash(item.nip05)),
})
handles.set(await collection.get())
return onHandle(batch(3000, collection.add))
}
const syncZappers = async () => {
const collection = new Collection<Zapper>({
table: "zappers",
shards: Array.from("0123456789"),
getShard: (item: Zapper) => last(hash(item.lnurl)),
})
zappers.set(await collection.get())
return onZapper(batch(3000, collection.add))
}
type FreshnessItem = [string, number]
const syncFreshness = async () => {
const collection = new Collection<FreshnessItem>({
table: "freshness",
shards: ["0"],
getShard: always("0"),
})
freshness.set(fromPairs(await collection.get()))
return throttled(3000, freshness).subscribe($freshness => {
collection.set(Object.entries($freshness))
})
}
type PlaintextItem = [string, string]
const syncPlaintext = async () => {
const collection = new Collection<PlaintextItem>({
table: "plaintext",
shards: ["0"],
getShard: always("0"),
})
plaintext.set(fromPairs(await collection.get()))
return throttled(3000, plaintext).subscribe($plaintext => {
collection.set(Object.entries($plaintext))
})
}
export const syncDataStores = () =>
Promise.all([
syncEvents(),
syncTracker(),
syncRelays(),
syncHandles(),
syncZappers(),
syncFreshness(),
syncPlaintext(),
])

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 723 B

After

Width:  |  Height:  |  Size: 723 B

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 919 B

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 826 B

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

Before

Width:  |  Height:  |  Size: 805 B

After

Width:  |  Height:  |  Size: 805 B

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 597 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 826 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 780 B

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 787 B

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Before

Width:  |  Height:  |  Size: 773 B

After

Width:  |  Height:  |  Size: 773 B

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 527 B

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 959 B

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+1 -1
View File
@@ -49,7 +49,7 @@
</script>
<div class="flex flex-col gap-2" role="list">
{#each value as item, index (item)}
{#each value as item, index}
<div
class="flex items-center gap-2"
draggable="true"
+1 -1
View File
@@ -13,7 +13,7 @@
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2">
<div
class="flex min-h-12 items-center justify-between gap-4 rounded-xl rounded-xl bg-base-100 px-4 shadow-xl">
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
{@render props.icon?.()}
{@render props.title?.()}
+102 -6
View File
@@ -1,7 +1,11 @@
import {flatten, identity, groupBy} from "@welshman/lib"
import {type StorageProvider} from "@welshman/store"
import {Preferences} from "@capacitor/preferences"
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
export class PreferencesStorageProvider implements StorageProvider {
p = Promise.resolve()
get = async <T>(key: string): Promise<T | undefined> => {
const result = await Preferences.get({key})
if (!result.value) return undefined
@@ -12,17 +16,109 @@ export class PreferencesStorageProvider implements StorageProvider {
}
}
p = Promise.resolve()
set = async <T>(key: string, value: T): Promise<void> => {
this.p = this.p.then(async () => await Preferences.set({key, value: JSON.stringify(value)}))
this.p = this.p.then(() => Preferences.set({key, value: JSON.stringify(value)}))
await this.p
}
clear = async (): Promise<void> => {
await Preferences.clear()
this.p = Promise.resolve()
clear = async () => {
this.p = this.p.then(() => Preferences.clear())
await this.p
}
}
// singleton instance of PreferencesStorageProvider
export const preferencesStorageProvider = new PreferencesStorageProvider()
export type CollectionOptions<T> = {
table: string
shards: string[]
getShard: (item: T) => string
}
export class Collection<T> {
#promises = new Map<string, Promise<any>>()
constructor(readonly options: CollectionOptions<T>) {}
static clearAll = async (): Promise<void> => {
const res = await Filesystem.readdir({
path: "",
directory: Directory.Data,
})
await Promise.all(
res.files.map(file =>
Filesystem.deleteFile({
path: file.name,
directory: Directory.Data,
}),
),
)
}
#then = <R>(shard: string, f: () => Promise<R>) => {
const oldPromise = this.#promises.get(shard) || Promise.resolve()
const newPromise = oldPromise.then(f)
this.#promises.set(shard, newPromise)
return newPromise
}
#path = (shard: string) => `collection_${this.options.table}_${shard}.json`
getShard = (shard: string): Promise<T[]> =>
this.#then(shard, async () => {
try {
const file = await Filesystem.readFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
})
// Speed things up by parsing only once
return JSON.parse("[" + file.data.toString().split("\n").filter(identity).join(",") + "]")
} catch (err) {
// file doesn't exist, or isn't valid json
return []
}
})
get = async (): Promise<T[]> => flatten(await Promise.all(this.options.shards.map(this.getShard)))
setShard = (shard: string, items: T[]) =>
this.#then(shard, async () => {
await Filesystem.writeFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
data: items.map(v => JSON.stringify(v)).join("\n"),
})
})
set = (items: T[]) =>
Promise.all(
Array.from(groupBy(this.options.getShard, items)).map(([shard, chunk]) =>
this.setShard(shard, chunk),
),
)
addToShard = (shard: string, items: T[]) =>
this.#then(shard, async () => {
await Filesystem.appendFile({
path: this.#path(shard),
directory: Directory.Data,
encoding: Encoding.UTF8,
data: "\n" + items.map(v => JSON.stringify(v)).join("\n"),
})
})
add = (items: T[]) =>
Promise.all(
Array.from(groupBy(this.options.getShard, items)).map(([shard, chunk]) =>
this.addToShard(shard, chunk),
),
)
}
+18 -64
View File
@@ -24,27 +24,7 @@
WEEK,
} from "@welshman/lib"
import type {TrustedEvent, StampedEvent} from "@welshman/util"
import {
WRAP,
ALERT_STATUS,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
EVENT_TIME,
APP_DATA,
THREAD,
MESSAGE,
INBOX_RELAYS,
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
MUTES,
FOLLOWS,
PROFILE,
RELAYS,
BLOSSOM_SERVERS,
ROOMS,
} from "@welshman/util"
import {WRAP} from "@welshman/util"
import {Nip46Broker, makeSecret} from "@welshman/signer"
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import {
@@ -61,8 +41,6 @@
} from "@welshman/net"
import {
loadRelay,
db,
initStorage,
repository,
pubkey,
session,
@@ -70,10 +48,8 @@
signer,
signerLog,
dropSession,
defaultStorageAdapters,
loginWithNip01,
loginWithNip46,
EventsStorageAdapter,
loadRelaySelections,
SignerLogEntryStatus,
} from "@welshman/app"
@@ -107,8 +83,13 @@
import {initializePushNotifications} from "@app/util/push"
import * as commands from "@app/core/commands"
import * as requests from "@app/core/requests"
import * as notifications from "@app/util/notifications"
import * as appState from "@app/core/state"
import * as notifications from "@app/util/notifications"
import * as storage from "@app/util/storage"
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
// Migration: delete old indexeddb database
indexedDB?.deleteDatabase("flotilla")
// Migration: old nostrtalk instance used different sessions
if ($session && !$signer) {
@@ -122,6 +103,8 @@
const ready = $state(defer<void>())
let initialized = false
onMount(async () => {
Object.assign(window, {
get,
@@ -239,7 +222,8 @@
document.documentElement.style["font-size"] = `${$userSettingsValues.font_size}rem`
})
if (!db) {
if (!initialized) {
initialized = true
setupTracking()
setupAnalytics()
@@ -278,44 +262,10 @@
storage: preferencesStorageProvider,
})
await initStorage("flotilla", 8, {
...defaultStorageAdapters,
events: new EventsStorageAdapter({
name: "events",
limit: 10_000,
repository,
rankEvent: (e: TrustedEvent) => {
if (
[
PROFILE,
FOLLOWS,
MUTES,
RELAYS,
BLOSSOM_SERVERS,
INBOX_RELAYS,
ROOMS,
APP_DATA,
ALERT_STATUS,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
].includes(e.kind)
) {
return 1
}
if (
[EVENT_TIME, THREAD, MESSAGE, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE].includes(e.kind)
) {
return 0.9
}
return 0
},
}),
})
// Sync application data (relay, events, etc)
await storage.syncDataStores()
// Wait 300 ms for any throttled stores to finish
sleep(300).then(() => ready.resolve())
defaultSocketPolicies.push(
@@ -465,6 +415,9 @@
},
)
// subscribe to badge count for changes
notifications.badgeCount.subscribe(notifications.handleBadgeCountChanges)
// Listen for signer errors, report to user via toast
signerLog.subscribe(
throttle(10_000, $log => {
@@ -504,5 +457,6 @@
</AppContainer>
<ModalContainer />
<div class="tippy-target"></div>
<NewNotificationSound />
</div>
{/await}
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import type {MakeNonOptional} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {Address, getIdFilters} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay"
@@ -10,7 +11,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import {goToEvent} from "@app/util/routes"
const {bech32} = $page.params
const {bech32} = $page.params as MakeNonOptional<typeof $page.params>
const attemptToNavigate = async () => {
const {type, data} = nip19.decode(bech32) as any
+4 -1
View File
@@ -1,8 +1,11 @@
<script lang="ts">
import {page} from "$app/stores"
import type {MakeNonOptional} from "@welshman/lib"
import Chat from "@app/components/Chat.svelte"
import {notifications, setChecked} from "@app/util/notifications"
const {chat} = $page.params as MakeNonOptional<typeof $page.params>
// We have to watch this one, since on mobile the badge will be visible when active
$effect(() => {
if ($notifications.has($page.url.pathname)) {
@@ -11,4 +14,4 @@
})
</script>
<Chat id={$page.params.chat} />
<Chat id={chat} />
+77 -28
View File
@@ -1,24 +1,44 @@
<script lang="ts">
import {onMount} from "svelte"
import {dec} from "@welshman/lib"
import {ROOMS} from "@welshman/util"
import {debounce} from "throttle-debounce"
import {dec, tryCatch} from "@welshman/lib"
import {ROOMS, normalizeRelayUrl, isRelayUrl} from "@welshman/util"
import {Router} from "@welshman/router"
import {load} from "@welshman/net"
import type {Relay} from "@welshman/app"
import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app"
import {createScroller} from "@lib/html"
import {fly} from "@lib/transition"
import QrCode from "@assets/icons/qr-code.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
import Scanner from "@lib/components/Scanner.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import PageHeader from "@lib/components/PageHeader.svelte"
import ContentSearch from "@lib/components/ContentSearch.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceCheck from "@app/components/SpaceCheck.svelte"
import {getMembershipUrls, loadMembership, defaultPubkeys, membersByUrl} from "@app/core/state"
import {pushModal} from "@app/util/modal"
const openMenu = () => pushModal(SpaceAdd)
const termUrl = $derived(tryCatch(() => normalizeRelayUrl(term)) || "")
const toggleScanner = () => {
showScanner = !showScanner
}
const onScan = debounce(1000, async (data: string) => {
showScanner = false
pushModal(SpaceInviteAccept, {invite: data})
})
const discoverRelays = () =>
Promise.all([
load({
@@ -37,7 +57,7 @@
const relaySearch = $derived(
createSearch(
$relays.filter(r => $membersByUrl.has(r.url)),
$relays.filter(r => $membersByUrl.has(r.url) && r.url !== termUrl),
{
getValue: (relay: Relay) => relay.url,
sortFn: ({score, item}) => {
@@ -59,6 +79,7 @@
let term = $state("")
let limit = $state(20)
let showScanner = $state(false)
let element: Element
onMount(() => {
@@ -76,30 +97,58 @@
</script>
<Page class="cw-full">
<div class="content column gap-4" bind:this={element}>
<PageHeader>
{#snippet title()}
Discover Spaces
{/snippet}
{#snippet info()}
Find communities all across the nostr network
{/snippet}
</PageHeader>
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Magnifier} />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
</label>
{#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)}
<Button
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
onclick={() => openSpace(relay.url)}>
<RelaySummary url={relay.url} />
</Button>
{/each}
{#await discoverRelays()}
<div class="flex justify-center py-20" out:fly>
<Spinner loading>Looking for spaces...</Spinner>
<ContentSearch>
{#snippet input()}
<div class="flex flex-col gap-2">
<PageHeader>
{#snippet title()}
Discover Spaces
{/snippet}
{#snippet info()}
Find communities all across the nostr network
{/snippet}
</PageHeader>
<div class="row-2 min-w-0 flex-grow items-center">
<label class="input input-bordered flex flex-grow items-center gap-2">
<Icon icon={Magnifier} />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
<Button onclick={toggleScanner} class="center">
<Icon icon={QrCode} />
</Button>
</label>
<Button class="btn btn-primary" onclick={openMenu}>
<Icon icon={AddCircle} />
</Button>
</div>
{#if showScanner}
<Scanner onscan={onScan} />
{/if}
</div>
{/await}
</div>
{/snippet}
{#snippet content()}
<div class="col-2 scroll-container" bind:this={element}>
{#key termUrl}
{#if isRelayUrl(termUrl)}
<Button
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
onclick={() => openSpace(termUrl)}>
<RelaySummary url={termUrl} />
</Button>
{/if}
{/key}
{#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)}
<Button
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
onclick={() => openSpace(relay.url)}>
<RelaySummary url={relay.url} />
</Button>
{/each}
{#await discoverRelays()}
<div class="flex justify-center py-20" out:fly>
<Spinner loading>Looking for spaces...</Spinner>
</div>
{/await}
</div>
{/snippet}
</ContentSearch>
</Page>
+3 -3
View File
@@ -32,7 +32,7 @@
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="col-3">
<Button onclick={addSpace}>
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div>
{/snippet}
@@ -45,7 +45,7 @@
</CardButton>
</Button>
<Link href="/discover">
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Compass} size={7} /></div>
{/snippet}
@@ -58,7 +58,7 @@
</CardButton>
</Link>
<Button onclick={openChat}>
<CardButton class="dark:btn-neutral">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={ChatRound} size={7} /></div>
{/snippet}
+1 -3
View File
@@ -61,9 +61,7 @@
</div>
</div>
</div>
<Button
class="center btn btn-circle -mr-4 -mt-4 h-12 w-12 dark:btn-neutral"
onclick={startEdit}>
<Button class="center btn btn-circle btn-neutral -mr-4 -mt-4 h-12 w-12" onclick={startEdit}>
<Icon icon={PenNewSquare} />
</Button>
</div>
+3 -3
View File
@@ -50,7 +50,7 @@
</script>
<div class="content column gap-4">
<Collapse class="card2 bg-alt column gap-4">
<Collapse class="card2 bg-alt column gap-4 shadow-xl">
{#snippet title()}
<h2 class="flex items-center gap-3 text-xl">
<Icon icon={Globus} />
@@ -83,7 +83,7 @@
</Button>
</div>
</Collapse>
<Collapse class="card2 bg-alt column gap-4">
<Collapse class="card2 bg-alt column gap-4 shadow-xl">
{#snippet title()}
<h2 class="flex items-center gap-3 text-xl">
<Icon icon={Inbox} />
@@ -115,7 +115,7 @@
</Button>
</div>
</Collapse>
<Collapse class="card2 bg-alt column gap-4">
<Collapse class="card2 bg-alt column gap-4 shadow-xl">
{#snippet title()}
<h2 class="flex items-center gap-3 text-xl">
<Icon icon={Mailbox} />
+31 -2
View File
@@ -1,22 +1,32 @@
<script lang="ts">
import {nwc} from "@getalby/sdk"
import {LOCALE} from "@welshman/lib"
import {displayRelayUrl, fromMsats} from "@welshman/util"
import {session} from "@welshman/app"
import {displayRelayUrl, isNWCWallet, fromMsats} from "@welshman/util"
import {session, pubkey, profilesByPubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import WalletConnect from "@app/components/WalletConnect.svelte"
import WalletDisconnect from "@app/components/WalletDisconnect.svelte"
import WalletUpdateReceivingAddress from "@app/components/WalletUpdateReceivingAddress.svelte"
import {pushModal} from "@app/util/modal"
import {getWebLn} from "@app/core/commands"
import Wallet2 from "@assets/icons/wallet.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
const connect = () => pushModal(WalletConnect)
const disconnect = () => pushModal(WalletDisconnect)
const updateReceivingAddress = () => pushModal(WalletUpdateReceivingAddress)
const profile = $derived($profilesByPubkey.get($pubkey || ""))
const profileLightningAddress = $derived(profile?.lud16)
const walletLud16 = $derived(
$session?.wallet && isNWCWallet($session.wallet) ? $session.wallet.info.lud16 : undefined,
)
</script>
<div class="content column gap-4">
@@ -89,4 +99,23 @@
{/if}
</div>
</div>
<div
class="card2 bg-alt flex flex-col shadow-xl"
class:gap-6={profileLightningAddress && walletLud16 && profile?.lud16 !== walletLud16}>
<div class="flex items-center justify-between">
<strong>Lightning Address</strong>
<div class="flex items-center gap-2">
<span class={profileLightningAddress ? "" : "text-warning"}>
{profileLightningAddress ? profileLightningAddress : "Not set"}
</span>
<Button class="btn btn-neutral btn-xs ml-3" onclick={updateReceivingAddress}>Update</Button>
</div>
</div>
{#if profileLightningAddress && walletLud16 && profile?.lud16 !== walletLud16}
<div class="card2 bg-alt flex items-center gap-2 text-xs">
<Icon icon={InfoCircle} size={4} />
Your profile has a different lightning address than your connected wallet.
</div>
{/if}
</div>
</div>
+5 -1
View File
@@ -22,6 +22,7 @@
userRoomsByUrl,
} from "@app/core/state"
import {pullConservatively} from "@app/core/requests"
import {hasBlossomSupport} from "@app/core/commands"
import {notifications} from "@app/util/notifications"
type Props = {
@@ -30,7 +31,7 @@
const {children}: Props = $props()
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const rooms = Array.from($userRoomsByUrl.get(url) || [])
@@ -66,6 +67,9 @@
}
})
// Prime our cache so we can upload images quicker
hasBlossomSupport(url)
// Load group meta, threads, calendar events, comments, and recent messages
// for user rooms to help with a quick page transition
pullConservatively({
+1 -1
View File
@@ -26,7 +26,7 @@
import {makeChatPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const relay = deriveRelay(url)
const joinSpace = () => pushModal(SpaceJoin, {url})
+12 -9
View File
@@ -5,6 +5,7 @@
import {page} from "$app/stores"
import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {
@@ -57,10 +58,10 @@
import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast"
const {room} = $page.params
const {room, relay} = $page.params as MakeNonOptional<typeof $page.params>
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay)
const url = decodeRelay(relay)
const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [room]}
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
@@ -137,13 +138,15 @@
delay: $userSettingsValues.send_delay,
})
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
if ($userSettingsValues.send_delay) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
}
clearParent()
clearShare()
@@ -23,7 +23,7 @@
import {makeCalendarFeed} from "@app/core/requests"
import {setChecked} from "@app/util/notifications"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const makeEvent = () => pushModal(CalendarEventCreate, {url})
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {request} from "@welshman/net"
import {repository} from "@welshman/app"
@@ -25,7 +26,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]
+10 -8
View File
@@ -36,7 +36,7 @@
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const filter = {kinds: [MESSAGE]}
const shouldProtect = canEnforceNip70(url)
@@ -74,13 +74,15 @@
delay: $userSettingsValues.send_delay,
})
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
if ($userSettingsValues.send_delay) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
}
clearParent()
clearShare()
+1 -1
View File
@@ -20,7 +20,7 @@
import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const goals: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
import {request} from "@welshman/net"
@@ -24,7 +25,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]
@@ -21,7 +21,7 @@
import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay)
const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const threads: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app"
import {request} from "@welshman/net"
@@ -23,7 +24,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications"
const {relay, id} = $page.params
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}]
Binary file not shown.
+2
View File
@@ -36,6 +36,8 @@ export default {
},
light: {
...themes["winter"],
neutral: '#F2F7FF',
warning: '#FD8D0B',
primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY,