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_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_BURROW_URL=
VITE_PLATFORM_URL=https://flotilla.social VITE_PLATFORM_URL=https://flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms VITE_PLATFORM_TERMS=https://flotilla.social/terms
@@ -10,7 +11,7 @@ VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28" VITE_PLATFORM_SECONDARY="#EB5E28"
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities." 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_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_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/ VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
+23
View File
@@ -1,5 +1,28 @@
# Changelog # 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 # 1.2.4
* Add direct message alerts * Add direct message alerts
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 25 versionCode 28
versionName "1.2.4" versionName "1.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1
View File
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':capacitor-community-safe-area') implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-keyboard') implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences') implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications') 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 // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android' 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' 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' 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' 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' 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' 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' 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' 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' 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 */ /* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 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 */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; 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>"; }; 051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; }; 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
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>"; }; 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; }; 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>"; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -58,6 +60,7 @@
504EC2FB1FED79650016851F = { 504EC2FB1FED79650016851F = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */, 051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
@@ -162,6 +165,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */, 50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */, 504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
@@ -354,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4; MARKETING_VERSION = 1.3.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -380,14 +384,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4; MARKETING_VERSION = 1.3.1;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; 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' platform :ios, '14.0'
use_frameworks! use_frameworks!
@@ -9,15 +9,16 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios' 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.2.0_@capacitor+core@7.2.0/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.2.0/node_modules/@capacitor-community/safe-area' pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/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 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences' pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications' pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge' pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
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 '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 end
target 'Flotilla Chat' do 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", "name": "flotilla",
"version": "1.2.4", "version": "1.3.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -15,71 +15,72 @@
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.37.0",
"@sentry/cli": "^2.40.0", "@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.5.27", "@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.21",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"eslint": "^9.0.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.45.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.0.0", "globals": "^15.15.0",
"postcss": "^8.4.40", "postcss": "^8.5.6",
"prettier": "^3.1.1", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.0.0", "svelte": "^5.39.12",
"svelte-check": "^4.0.0", "svelte-check": "^4.3.3",
"tailwindcss": "^3.4.7", "tailwindcss": "^3.4.18",
"typescript": "^5.5.0", "typescript": "^5.9.3",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.46.1",
"vite": "^5.4.4" "vite": "^5.4.20"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@capacitor-community/safe-area": "7.0.0-alpha.1", "@capacitor-community/safe-area": "7.0.0-alpha.1",
"@capacitor/android": "^7.0.0", "@capacitor/android": "^7.4.3",
"@capacitor/app": "^7.0.0", "@capacitor/app": "^7.1.0",
"@capacitor/cli": "^7.0.0", "@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.0.1", "@capacitor/core": "^7.4.3",
"@capacitor/ios": "^7.0.0", "@capacitor/filesystem": "^7.1.4",
"@capacitor/keyboard": "^7.0.0", "@capacitor/ios": "^7.4.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.2", "@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-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1", "@capawesome/capacitor-badge": "^7.0.1",
"@getalby/sdk": "^5.1.0", "@getalby/sdk": "^5.1.2",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0", "@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.4", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.12.0", "@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.4.7", "@welshman/app": "^0.5.3",
"@welshman/content": "^0.4.7", "@welshman/content": "^0.5.3",
"@welshman/editor": "^0.4.7", "@welshman/editor": "^0.5.3",
"@welshman/feeds": "^0.4.7", "@welshman/feeds": "^0.5.3",
"@welshman/lib": "^0.4.7", "@welshman/lib": "^0.5.3",
"@welshman/net": "^0.4.7", "@welshman/net": "^0.5.3",
"@welshman/relay": "^0.4.7", "@welshman/relay": "^0.5.3",
"@welshman/router": "^0.4.7", "@welshman/router": "^0.5.3",
"@welshman/signer": "^0.4.7", "@welshman/signer": "^0.5.3",
"@welshman/store": "^0.4.7", "@welshman/store": "^0.5.3",
"@welshman/util": "^0.4.7", "@welshman/util": "^0.5.3",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"daisyui": "^4.12.10", "daisyui": "^4.12.24",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.16.0",
"dotenv": "^16.4.5", "dotenv": "^16.6.1",
"emoji-picker-element": "^1.22.8", "emoji-picker-element": "^1.27.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.1.0",
"husky": "^9.1.6", "husky": "^9.1.7",
"idb": "^8.0.0", "idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4", "nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2", "nostr-tools": "^2.17.0",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2", "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)); --primary-content: oklch(var(--pc));
--secondary: oklch(var(--s)); --secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc)); --secondary-content: oklch(var(--sc));
--neutral: oklch(var(--n));
--neutral-content: oklch(var(--nc));
} }
/* safe area insets */ /* safe area insets */
@@ -215,12 +217,6 @@
@apply ellipsize; @apply ellipsize;
} }
@media (max-width: 639px) {
[data-tip]::before {
display: none;
}
}
.content-padding-x { .content-padding-x {
@apply px-4 sm:px-8 md:px-12; @apply px-4 sm:px-8 md:px-12;
} }
@@ -278,8 +274,8 @@
} }
.tiptap { .tiptap {
--tiptap-object-bg: var(--base-100); --tiptap-object-bg: var(--neutral);
--tiptap-object-fg: var(--base-content); --tiptap-object-fg: var(--neutral-content);
--tiptap-active-bg: var(--primary); --tiptap-active-bg: var(--primary);
--tiptap-active-fg: var(--primary-content); --tiptap-active-fg: var(--primary-content);
} }
+49 -6
View File
@@ -3,6 +3,7 @@
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds" import {isRelayFeed, findFeed} from "@welshman/feeds"
import Inbox from "@assets/icons/inbox.svg?dataurl" 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 AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -10,8 +11,16 @@
import AlertItem from "@app/components/AlertItem.svelte" import AlertItem from "@app/components/AlertItem.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" 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 {deleteAlert, createDmAlert} from "@app/core/commands"
import {clearBadges} from "../util/notifications"
type Props = { type Props = {
url?: string url?: string
@@ -42,11 +51,11 @@
const uncheckDmAlert = async (message: string) => { const uncheckDmAlert = async (message: string) => {
await sleep(100) await sleep(100)
toggle.checked = false directMessagesNotificationToggle.checked = false
pushToast({theme: "error", message}) pushToast({theme: "error", message})
} }
const onToggle = async () => { const onDirectMessagesNotificationToggle = async () => {
if ($dmAlert) { if ($dmAlert) {
deleteAlert($dmAlert) deleteAlert($dmAlert)
} else { } 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> </script>
<div class="col-4"> <div class="col-4">
@@ -88,14 +109,36 @@
</div> </div>
</div> </div>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl"> <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"> <div class="flex justify-between">
<p>Notify me about new direct messages</p> <p>Notify me about new direct messages</p>
<input <input
type="checkbox" type="checkbox"
class="toggle toggle-primary" class="toggle toggle-primary"
bind:this={toggle} bind:this={directMessagesNotificationToggle}
checked={Boolean($dmAlert)} 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> </div>
{#if $dmStatus} {#if $dmStatus}
{@const status = getTagValue("status", $dmStatus.tags) || "error"} {@const status = getTagValue("status", $dmStatus.tags) || "error"}
+2 -3
View File
@@ -10,11 +10,10 @@
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
type Props = { type Props = {
url?: string
onSubmit: (event: EventContent) => void onSubmit: (event: EventContent) => void
} }
const {onSubmit, url}: Props = $props() const {onSubmit}: Props = $props()
const autofocus = !isMobile const autofocus = !isMobile
@@ -39,11 +38,11 @@
} }
const editor = makeEditor({ const editor = makeEditor({
url,
autofocus, autofocus,
submit, submit,
uploading, uploading,
aggressive: true, aggressive: true,
encryptFiles: true,
}) })
</script> </script>
+2 -4
View File
@@ -69,11 +69,11 @@
if (!parsed || hideMediaAtDepth <= depth) return false if (!parsed || hideMediaAtDepth <= depth) return false
if (isLink(parsed) && $userSettingsValues.show_media && isStartOrEnd(i)) { if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
return true return true
} }
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) { if ((isEvent(parsed) || isAddress(parsed)) && isStartAndEnd(i)) {
return true return true
} }
@@ -95,8 +95,6 @@
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i) const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
const isStartOrEnd = (i: number) => isStart(i) || isEnd(i)
const ignoreWarning = () => { const ignoreWarning = () => {
warning = null warning = null
} }
+1 -5
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type {ParsedEmojiValue} from "@welshman/content" import type {ParsedEmojiValue} from "@welshman/content"
import {imgproxy} from "@app/core/state"
export let value: ParsedEmojiValue export let value: ParsedEmojiValue
@@ -8,10 +7,7 @@
</script> </script>
{#if value.url} {#if value.url}
<img <img {alt} src={value.url} class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{alt}
src={imgproxy(value.url, {w: 24, h: 24})}
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{:else} {:else}
{alt} {alt}
{/if} {/if}
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib" 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 {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
@@ -51,7 +51,7 @@
<img <img
alt="Link preview" alt="Link preview"
onerror={onError} onerror={onError}
src={imgproxy(preview.image)} src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" /> class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
{/if} {/if}
<div class="flex flex-col gap-2 p-4"> <div class="flex flex-col gap-2 p-4">
@@ -1,10 +1,17 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib" 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 LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import {imgproxy} from "@app/core/state"
const {value, event, ...props} = $props() const {value, event, ...props} = $props()
@@ -14,18 +21,34 @@
.map(tagsFromIMeta) .map(tagsFromIMeta)
.find(meta => getTagValue("url", meta) === url) || event.tags .find(meta => getTagValue("url", meta) === url) || event.tags
const hash = getTagValue("x", meta)
const key = getTagValue("decryption-key", meta) const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta) const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta) const algorithm = getTagValue("encryption-algorithm", meta)
const onError = () => { const onError = async () => {
hasError = true // 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 hasError = $state(false)
let src = $state(imgproxy(url)) let src = $state("")
onMount(async () => { onMount(async () => {
// If we have an encryption algorithm, fetch and decrypt
if (algorithm === "aes-gcm" && key && nonce) { if (algorithm === "aes-gcm" && key && nonce) {
const response = await fetch(url) const response = await fetch(url)
@@ -33,8 +56,10 @@
const ciphertext = new Uint8Array(await response.arrayBuffer()) const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm}) 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" /> <Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)} {displayUrl(url)}
</a> </a>
{:else} {:else if src}
<img alt="" {src} onerror={onError} {...props} /> <img alt="" {src} onerror={onError} {...props} />
{/if} {/if}
+1 -1
View File
@@ -35,7 +35,7 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button onclick={signUp} class="dark:btn-neutral"> <Button onclick={signUp} class="btn-neutral">
<CardButton> <CardButton>
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
+11 -2
View File
@@ -32,9 +32,10 @@
onNostrConnect: async (response: Nip46ResponseWithResult) => { onNostrConnect: async (response: Nip46ResponseWithResult) => {
const pubkey = await controller.broker.getPublicKey() const pubkey = await controller.broker.getPublicKey()
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
await loadUserData(pubkey) await loadUserData(pubkey)
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
setChecked("*") setChecked("*")
clearModals() clearModals()
}, },
@@ -48,13 +49,20 @@
try { try {
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker) const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
if (!signerPubkey || relays.length === 0) { if (!signerPubkey) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Sorry, it looks like that's an invalid bunker link.", 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) controller.loading.set(true)
const {clientSecret} = controller const {clientSecret} = controller
@@ -91,6 +99,7 @@
} }
const selectConnect = () => { const selectConnect = () => {
controller.loading.set(false)
mode = "connect" mode = "connect"
} }
+23 -6
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Server from "@assets/icons/server.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 Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl" import Exit from "@assets/icons/logout-3.svg?dataurl"
@@ -13,13 +14,16 @@
import LogOut from "@app/components/LogOut.svelte" import LogOut from "@app/components/LogOut.svelte"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme"
const logout = () => pushModal(LogOut) const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script> </script>
<div class="column menu gap-2"> <div class="column menu gap-2">
<Link replaceState href="/settings/profile"> <Link replaceState href="/settings/profile">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={UserRounded} size={7} /></div> <div><Icon icon={UserRounded} size={7} /></div>
{/snippet} {/snippet}
@@ -32,7 +36,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/alerts"> <Link replaceState href="/settings/alerts">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Bell} size={7} /></div> <div><Icon icon={Bell} size={7} /></div>
{/snippet} {/snippet}
@@ -45,7 +49,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/wallet"> <Link replaceState href="/settings/wallet">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Wallet} size={7} /></div> <div><Icon icon={Wallet} size={7} /></div>
{/snippet} {/snippet}
@@ -58,7 +62,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/relays"> <Link replaceState href="/settings/relays">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Server} size={7} /></div> <div><Icon icon={Server} size={7} /></div>
{/snippet} {/snippet}
@@ -71,7 +75,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/content"> <Link replaceState href="/settings/content">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Settings} size={7} /></div> <div><Icon icon={Settings} size={7} /></div>
{/snippet} {/snippet}
@@ -83,8 +87,21 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Link> </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"> <Link replaceState href="/settings/about">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Code2} size={7} /></div> <div><Icon icon={Code2} size={7} /></div>
{/snippet} {/snippet}
+8 -12
View File
@@ -1,15 +1,11 @@
<script lang="ts"> <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 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 Divider from "@lib/components/Divider.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte" import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userRoomsByUrl, PLATFORM_RELAYS} from "@app/core/state" import {userRoomsByUrl, PLATFORM_RELAYS} from "@app/core/state"
import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd)
</script> </script>
<div class="column menu gap-2"> <div class="column menu gap-2">
@@ -22,18 +18,18 @@
{/each} {/each}
<Divider /> <Divider />
{/if} {/if}
<Button onclick={addSpace}> <Link href="/discover">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Login} size={7} /></div> <div><Icon icon={Compass} size={7} /></div>
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div>Add a space</div> <div>Explore Spaces</div>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<div>Join or create a new space</div> <div>Join create, or browse spaces</div>
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Link>
{/each} {/each}
</div> </div>
+1 -1
View File
@@ -13,7 +13,7 @@
</script> </script>
<Link replaceState href={path}> <Link replaceState href={path}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><SpaceAvatar {url} /></div> <div><SpaceAvatar {url} /></div>
{/snippet} {/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 {makeSpacePath} from "@app/util/routes"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import Widget from "@assets/icons/widget.svg?dataurl" 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 Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl" import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@@ -31,8 +31,6 @@
const {children}: Props = $props() const {children}: Props = $props()
const addSpace = () => pushModal(SpaceAdd)
const showSpacesMenu = () => (spaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)) const showSpacesMenu = () => (spaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls}) const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
@@ -83,8 +81,8 @@
<Avatar icon={Widget} class="!h-10 !w-10" /> <Avatar icon={Widget} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right"> <PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={AddSquare} class="!h-10 !w-10" /> <Avatar icon={Compass} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/each} {/each}
</div> </div>
+5 -28
View File
@@ -1,22 +1,13 @@
<script lang="ts"> <script lang="ts">
import {nthNe} from "@welshman/lib"
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import { import {getTag, makeProfile} from "@welshman/util"
getTag, import {pubkey, profilesByPubkey} from "@welshman/app"
makeEvent,
makeProfile,
editProfile,
createProfile,
isPublishedProfile,
uniqTags,
} from "@welshman/util"
import {Router} from "@welshman/router"
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" 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 profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []) const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
@@ -25,21 +16,7 @@
const back = () => history.back() const back = () => history.back()
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => { const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
const router = Router.get() updateProfile({profile, shouldBroadcast})
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})
pushToast({message: "Your profile has been updated!"}) pushToast({message: "Your profile has been updated!"})
clearModals() clearModals()
} }
+2 -2
View File
@@ -26,8 +26,8 @@
}) })
</script> </script>
<Button class="max-w-full {props.class}" onclick={copy}> <Button class="flex w-full justify-center {props.class}" onclick={copy}>
<div bind:this={wrapper} style={`height: ${height}px`}> <div bind:this={wrapper} class="w-md" style={`height: ${height}px`}>
<canvas <canvas
class="rounded-box" class="rounded-box"
bind:this={canvas} bind:this={canvas}
+41 -23
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {spec, prop, avg} from "@welshman/lib" 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 CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
@@ -26,27 +26,45 @@
const logout = () => pushModal(LogOut) const logout = () => pushModal(LogOut)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4"> {#if $session && $session.method !== SessionMethod.Anonymous}
<div class="flex flex-col gap-2"> <div class="card2 bg-alt flex flex-col gap-4">
<div class="flex items-center justify-between"> <div class="flex flex-col gap-2">
<span class="text-xl font-bold">Signer Status</span> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="text-xl font-bold">Signer Status</span>
{#if isDisconnected} <span class="flex items-center gap-2">
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected {#if isDisconnected}
{:else if recentFailure > 3} <Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure {:else if recentFailure > 3}
{:else if recentAvg > 1000 || recentPending > 3} <Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection {:else if recentAvg > 1000 || recentPending > 3}
{:else if recentSuccess === 0 && recentFailure > 0}{:else} <Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok {:else if recentSuccess === 0 && recentFailure > 0}{:else}
{/if} <Icon icon={CheckCircle} class="text-success" size={4} /> Ok
</span> {/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> </div>
<p class="text-sm opacity-75"> {#if isDisconnected}
{success} requests succeeded, {failure} failed, {pending} pending <Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
</p> {/if}
</div> </div>
{#if isDisconnected} {/if}
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
{/if}
</div>
+4 -6
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {displayUrl} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import {AuthStatus} from "@welshman/net" import {AuthStatus} from "@welshman/net"
import {waitForThunkError} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -15,7 +14,7 @@
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {publishJoinRequest} from "@app/core/commands" import {checkRelayAccess} from "@app/core/commands"
import {deriveSocket} from "@app/core/state" import {deriveSocket} from "@app/core/state"
type Props = { type Props = {
@@ -32,11 +31,10 @@
loading = true loading = true
try { try {
const thunk = publishJoinRequest({url, claim}) const message = await checkRelayAccess(url, claim)
const error = await waitForThunkError(thunk)
if (error) { if (message) {
return pushToast({theme: "error", message: error, timeout: 30_000}) return pushToast({theme: "error", message, timeout: 30_000})
} }
if ($socket.auth.status === AuthStatus.None) { if ($socket.auth.status === AuthStatus.None) {
+3 -18
View File
@@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import Compass from "@assets/icons/compass-big.svg?dataurl"
import Login from "@assets/icons/login-3.svg?dataurl" import Login from "@assets/icons/login-3.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import ModalHeader from "@lib/components/ModalHeader.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> <div>Spaces are places where communities come together to work, play, and hang out.</div>
{/snippet} {/snippet}
</ModalHeader> </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}> <Button onclick={startJoin}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-primary">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Login} size={7} /></div> <div><Icon icon={Login} size={7} /></div>
{/snippet} {/snippet}
@@ -47,12 +32,12 @@
<div>Join a space</div> <div>Join a space</div>
{/snippet} {/snippet}
{#snippet info()} {#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} {/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button onclick={startCreate}> <Button onclick={startCreate}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
{/snippet} {/snippet}
+6 -5
View File
@@ -3,15 +3,15 @@
import {sleep, nthEq} from "@welshman/lib" import {sleep, nthEq} from "@welshman/lib"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {displayRelayUrl, AUTH_INVITE} from "@welshman/util" 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 Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.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 Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte"
import {clip} from "@app/util/toast" import {clip} from "@app/util/toast"
import {PLATFORM_URL} from "@app/core/state" import {PLATFORM_URL} from "@app/core/state"
@@ -62,11 +62,12 @@
</ModalHeader> </ModalHeader>
<div> <div>
{#if loading} {#if loading}
<p class="center" out:slide> <p class="center">
<Spinner {loading}>Requesting an invite link...</Spinner> <Spinner {loading}>Requesting an invite link...</Spinner>
</p> </p>
{:else} {:else}
<div in:slide> <div class="flex flex-col items-center gap-6">
<QRCode code={invite} />
<Field> <Field>
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <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 {fade} from "@lib/transition"
import CompassBig from "@assets/icons/compass-big.svg?dataurl" import CompassBig from "@assets/icons/compass-big.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.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 CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl" import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@@ -14,7 +15,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte" import RoomCreate from "@app/components/RoomCreate.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import ChannelName from "@app/components/ChannelName.svelte"
import {makeThreadPath, makeCalendarPath, makeRoomPath, makeSpacePath} from "@app/util/routes" import {makeRoomPath, makeSpacePath} from "@app/util/routes"
import { import {
hasNip29, hasNip29,
deriveUserRooms, deriveUserRooms,
@@ -34,8 +35,9 @@
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const chatPath = makeSpacePath(url, "chat") const chatPath = makeSpacePath(url, "chat")
const threadsPath = makeThreadPath(url) const goalsPath = makeSpacePath(url, "goals")
const calendarPath = makeCalendarPath(url) const threadsPath = makeSpacePath(url, "threads")
const calendarPath = makeSpacePath(url, "calendar")
const addRoom = () => pushModal(RoomCreate, {url}) const addRoom = () => pushModal(RoomCreate, {url})
@@ -61,25 +63,37 @@
Quick Links Quick Links
</h3> </h3>
<div class="flex flex-col gap-2"> <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"> <div class="relative flex items-center gap-2">
<Icon icon={NotesMinimalistic} /> <Icon icon={StarFallMinimalistic} />
Threads Goals
{#if $notifications.has(threadsPath)} {#if $notifications.has(goalsPath)}
<div <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> transition:fade>
</div> </div>
{/if} {/if}
</div> </div>
</Link> </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"> <div class="relative flex items-center gap-2">
<Icon icon={CalendarMinimalistic} /> <Icon icon={CalendarMinimalistic} />
Calendar Calendar
{#if $notifications.has(calendarPath)} {#if $notifications.has(calendarPath)}
<div <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> transition:fade>
</div> </div>
{/if} {/if}
+1 -2
View File
@@ -46,8 +46,7 @@
{#if showFailure} {#if showFailure}
{@const url = failedUrls[0]} {@const url = failedUrls[0]}
{@const status = $thunk.status[url]} {@const {status, detail: message} = $thunk.results[url]}
{@const message = $thunk.details[url]}
<button <button
class="flex w-full justify-end px-1 text-xs {restProps.class}" class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}> 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 {nwc} from "@getalby/sdk"
import {sleep, assoc} from "@welshman/lib" import {sleep, assoc} from "@welshman/lib"
import type {NWCInfo} from "@welshman/util" 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 Link from "@lib/components/Link.svelte"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl" import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl" import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@@ -15,11 +15,13 @@
import Scanner from "@lib/components/Scanner.svelte" import Scanner from "@lib/components/Scanner.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Divider from "@lib/components/Divider.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {getWebLn} from "@app/core/commands" import {getWebLn} from "@app/core/commands"
import {pushToast} from "@app/util/toast" 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() const back = () => history.back()
@@ -76,6 +78,10 @@
await sleep(400) await sleep(400)
back() back()
if (info.lud16 && info.lud16 !== $userProfile?.lud16) {
pushModal(WalletAsReceivingAddress)
}
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -109,7 +115,7 @@
<div>Connect a Wallet</div> <div>Connect a Wallet</div>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
Use Nostr Wallet Connect to send Bitcoin payments over Bolt. Use Nostr Wallet Connect to send Bitcoin payments over lightning.
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
{#if getWebLn()} {#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 {get} from "svelte/store"
import type {Override, MakeOptional} from "@welshman/lib" import type {Override, MakeOptional} from "@welshman/lib"
import { import {
first,
sha256, sha256,
randomId, randomId,
append, append,
@@ -16,12 +17,15 @@ import {
parseJson, parseJson,
fromPairs, fromPairs,
last, last,
simpleCache,
normalizeUrl,
nthNe,
} from "@welshman/lib" } from "@welshman/lib"
import {decrypt, Nip01Signer} from "@welshman/signer" import {decrypt, Nip01Signer} from "@welshman/signer"
import type {UploadTask} from "@welshman/editor" import type {UploadTask} from "@welshman/editor"
import type {Feed} from "@welshman/feeds" import type {Feed} from "@welshman/feeds"
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} 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 { import {
WRAP, WRAP,
DELETE, DELETE,
@@ -57,8 +61,13 @@ import {
getTagValue, getTagValue,
getTagValues, getTagValues,
uploadBlob, uploadBlob,
canUploadBlob,
encryptFile, encryptFile,
makeBlossomAuthEvent, makeBlossomAuthEvent,
isPublishedProfile,
editProfile,
createProfile,
uniqTags,
} from "@welshman/util" } from "@welshman/util"
import {Pool, AuthStatus, SocketStatus} from "@welshman/net" import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
@@ -75,7 +84,6 @@ import {
userInboxRelaySelections, userInboxRelaySelections,
nip44EncryptToSelf, nip44EncryptToSelf,
loadRelay, loadRelay,
clearStorage,
dropSession, dropSession,
tagEventForComment, tagEventForComment,
tagEventForQuote, tagEventForQuote,
@@ -92,15 +100,17 @@ import {
INDEXER_RELAYS, INDEXER_RELAYS,
NOTIFIER_PUBKEY, NOTIFIER_PUBKEY,
NOTIFIER_RELAY, NOTIFIER_RELAY,
DEFAULT_BLOSSOM_SERVERS,
userRoomsByUrl, userRoomsByUrl,
userSettingsValues, userSettingsValues,
canDecrypt, canDecrypt,
ensureUnwrapped, ensureUnwrapped,
userInboxRelays, userInboxRelays,
getMembershipUrls,
} from "@app/core/state" } from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests" import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push" import {platform, platformName, getPushInfo} from "@app/util/push"
import {preferencesStorageProvider} from "@src/lib/storage" import {preferencesStorageProvider, Collection} from "@src/lib/storage"
// Utils // Utils
@@ -143,10 +153,10 @@ export const logout = async () => {
dropSession($pubkey) dropSession($pubkey)
} }
await clearStorage()
localStorage.clear() localStorage.clear()
await preferencesStorageProvider.clear() await preferencesStorageProvider.clear()
await Collection.clearAll()
} }
// Synchronization // Synchronization
@@ -158,7 +168,7 @@ export const broadcastUserData = async (relays: string[]) => {
for (const event of events) { for (const event of events) {
if (isSignedEvent(event)) { if (isSignedEvent(event)) {
await publishThunk({event, relays}).result await publishThunk({event, relays}).complete
} }
} }
} }
@@ -660,17 +670,53 @@ export const enableGiftWraps = () => {
// File upload // 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())) const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
for (const url of userUrls) { 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 = { export type UploadFileOptions = {
url?: string
encrypt?: boolean encrypt?: boolean
} }
@@ -680,35 +726,35 @@ export type UploadFileResult = {
} }
export const uploadFile = async (file: File, options: UploadFileOptions = {}) => { 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 { 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 res = await uploadBlob(server, file, {authEvent})
const text = await res.text() const text = await res.text()
@@ -718,16 +764,45 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
return {error: text} return {error: text}
} }
// Always append file extension if missing // Always append correct file extension if we encrypted the file, or if it's missing
if (new URL(url).pathname.split(".").length === 1) { if (options.encrypt) {
url += "." + type.split("/")[1] url = url.replace(/\.\w+$/, "") + ext
} else if (new URL(url).pathname.split(".").length === 1) {
url += ext
} }
const result = {...task, tags, url} const result = {...task, tags, url}
return {result} return {result}
} catch (e: any) { } catch (e: any) {
console.error(e) console.error("Error caught when uploading file:", e)
return {error: e.toString()} 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 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 BURROW_URL = import.meta.env.VITE_BURROW_URL
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com" export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const IMGPROXY_URL = "https://imgproxy.coracle.social"
export const NIP46_PERMS = export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," + "nip44_encrypt,nip44_decrypt," +
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST] [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 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 entityLink = (entity: string) => `https://coracle.social/${entity}`
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) => export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
@@ -336,6 +322,8 @@ export type SettingsValues = {
report_errors: boolean report_errors: boolean
send_delay: number send_delay: number
font_size: number font_size: number
play_notification_sound: boolean
show_notifications_badge: boolean
} }
export type Settings = { export type Settings = {
@@ -349,8 +337,10 @@ export const defaultSettings = {
trusted_relays: [], trusted_relays: [],
report_usage: true, report_usage: true,
report_errors: true, report_errors: true,
send_delay: 3000, send_delay: 0,
font_size: 1, font_size: 1,
play_notification_sound: true,
show_notifications_badge: true,
} }
export const settings = deriveEventsMapped<Settings>(repository, { export const settings = deriveEventsMapped<Settings>(repository, {
@@ -405,9 +395,13 @@ export const alerts = withGetter(
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}], filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: async 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]}], filters: [{kinds: [ALERT_STATUS]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: async 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[]>() const messagesByChatId = new Map<string, TrustedEvent[]>()
for (const message of $messages) { 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)) const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
pushToMapKey(messagesByChatId, chatId, message) pushToMapKey(messagesByChatId, chatId, message)
+4 -1
View File
@@ -11,6 +11,7 @@ import {uploadFile} from "@app/core/commands"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
export const makeEditor = async ({ export const makeEditor = async ({
encryptFiles = false,
aggressive = false, aggressive = false,
autofocus = false, autofocus = false,
charCount, charCount,
@@ -21,6 +22,7 @@ export const makeEditor = async ({
uploading, uploading,
wordCount, wordCount,
}: { }: {
encryptFiles?: boolean
aggressive?: boolean aggressive?: boolean
autofocus?: boolean autofocus?: boolean
charCount?: Writable<number> charCount?: Writable<number>
@@ -51,7 +53,8 @@ export const makeEditor = async ({
}, },
fileUpload: { fileUpload: {
config: { config: {
upload: (attrs: FileAttributes) => uploadFile(attrs.file, {encrypt: true}), upload: (attrs: FileAttributes) =>
uploadFile(attrs.file, {url, encrypt: encryptFiles}),
onDrop: () => uploading?.set(true), onDrop: () => uploading?.set(true),
onComplete: () => uploading?.set(false), onComplete: () => uploading?.set(false),
onUploadError(currentEditor, task) { 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 {synced, throttled} from "@welshman/store"
import {pubkey, relaysByUrl} from "@welshman/app" import {pubkey, relaysByUrl} from "@welshman/app"
import {prop, spec, identity, now, groupBy} from "@welshman/lib" import {prop, spec, identity, now, groupBy} from "@welshman/lib"
@@ -12,8 +12,16 @@ import {
makeSpaceChatPath, makeSpaceChatPath,
makeRoomPath, makeRoomPath,
} from "@app/util/routes" } 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 {preferencesStorageProvider} from "@src/lib/storage"
import {Badge} from "@capawesome/capacitor-badge"
// Checked state // Checked state
@@ -150,3 +158,23 @@ export const notifications = derived(
return paths 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": case "discover":
return urls.length + 2 return urls.length + 2
case "spaces": { case "spaces": {
const routeUrl = decodeRelay($page.params.relay) const routeUrl = decodeRelay($page.params.relay || "")
return urls.findIndex(url => url === routeUrl) + 1 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> </script>
<div class="flex flex-col gap-2" role="list"> <div class="flex flex-col gap-2" role="list">
{#each value as item, index (item)} {#each value as item, index}
<div <div
class="flex items-center gap-2" class="flex items-center gap-2"
draggable="true" draggable="true"
+1 -1
View File
@@ -13,7 +13,7 @@
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2"> <div data-component="PageBar" class="cw top-sai fixed z-feature p-2">
<div <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"> <div class="ellipsize flex items-center gap-4 whitespace-nowrap">
{@render props.icon?.()} {@render props.icon?.()}
{@render props.title?.()} {@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 {type StorageProvider} from "@welshman/store"
import {Preferences} from "@capacitor/preferences" import {Preferences} from "@capacitor/preferences"
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
export class PreferencesStorageProvider implements StorageProvider { export class PreferencesStorageProvider implements StorageProvider {
p = Promise.resolve()
get = async <T>(key: string): Promise<T | undefined> => { get = async <T>(key: string): Promise<T | undefined> => {
const result = await Preferences.get({key}) const result = await Preferences.get({key})
if (!result.value) return undefined 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> => { 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 await this.p
} }
clear = async (): Promise<void> => { clear = async () => {
await Preferences.clear() this.p = this.p.then(() => Preferences.clear())
this.p = Promise.resolve()
await this.p
} }
} }
// singleton instance of PreferencesStorageProvider
export const preferencesStorageProvider = new 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, WEEK,
} from "@welshman/lib" } from "@welshman/lib"
import type {TrustedEvent, StampedEvent} from "@welshman/util" import type {TrustedEvent, StampedEvent} from "@welshman/util"
import { import {WRAP} from "@welshman/util"
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 {Nip46Broker, makeSecret} from "@welshman/signer" import {Nip46Broker, makeSecret} from "@welshman/signer"
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net" import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import { import {
@@ -61,8 +41,6 @@
} from "@welshman/net" } from "@welshman/net"
import { import {
loadRelay, loadRelay,
db,
initStorage,
repository, repository,
pubkey, pubkey,
session, session,
@@ -70,10 +48,8 @@
signer, signer,
signerLog, signerLog,
dropSession, dropSession,
defaultStorageAdapters,
loginWithNip01, loginWithNip01,
loginWithNip46, loginWithNip46,
EventsStorageAdapter,
loadRelaySelections, loadRelaySelections,
SignerLogEntryStatus, SignerLogEntryStatus,
} from "@welshman/app" } from "@welshman/app"
@@ -107,8 +83,13 @@
import {initializePushNotifications} from "@app/util/push" import {initializePushNotifications} from "@app/util/push"
import * as commands from "@app/core/commands" import * as commands from "@app/core/commands"
import * as requests from "@app/core/requests" import * as requests from "@app/core/requests"
import * as notifications from "@app/util/notifications"
import * as appState from "@app/core/state" 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 // Migration: old nostrtalk instance used different sessions
if ($session && !$signer) { if ($session && !$signer) {
@@ -122,6 +103,8 @@
const ready = $state(defer<void>()) const ready = $state(defer<void>())
let initialized = false
onMount(async () => { onMount(async () => {
Object.assign(window, { Object.assign(window, {
get, get,
@@ -239,7 +222,8 @@
document.documentElement.style["font-size"] = `${$userSettingsValues.font_size}rem` document.documentElement.style["font-size"] = `${$userSettingsValues.font_size}rem`
}) })
if (!db) { if (!initialized) {
initialized = true
setupTracking() setupTracking()
setupAnalytics() setupAnalytics()
@@ -278,44 +262,10 @@
storage: preferencesStorageProvider, storage: preferencesStorageProvider,
}) })
await initStorage("flotilla", 8, { // Sync application data (relay, events, etc)
...defaultStorageAdapters, await storage.syncDataStores()
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
},
}),
})
// Wait 300 ms for any throttled stores to finish
sleep(300).then(() => ready.resolve()) sleep(300).then(() => ready.resolve())
defaultSocketPolicies.push( 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 // Listen for signer errors, report to user via toast
signerLog.subscribe( signerLog.subscribe(
throttle(10_000, $log => { throttle(10_000, $log => {
@@ -504,5 +457,6 @@
</AppContainer> </AppContainer>
<ModalContainer /> <ModalContainer />
<div class="tippy-target"></div> <div class="tippy-target"></div>
<NewNotificationSound />
</div> </div>
{/await} {/await}
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import type {MakeNonOptional} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Address, getIdFilters} from "@welshman/util" import {Address, getIdFilters} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay" import {LOCAL_RELAY_URL} from "@welshman/relay"
@@ -10,7 +11,7 @@
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
const {bech32} = $page.params const {bech32} = $page.params as MakeNonOptional<typeof $page.params>
const attemptToNavigate = async () => { const attemptToNavigate = async () => {
const {type, data} = nip19.decode(bech32) as any const {type, data} = nip19.decode(bech32) as any
+4 -1
View File
@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import {page} from "$app/stores" import {page} from "$app/stores"
import type {MakeNonOptional} from "@welshman/lib"
import Chat from "@app/components/Chat.svelte" import Chat from "@app/components/Chat.svelte"
import {notifications, setChecked} from "@app/util/notifications" 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 // We have to watch this one, since on mobile the badge will be visible when active
$effect(() => { $effect(() => {
if ($notifications.has($page.url.pathname)) { if ($notifications.has($page.url.pathname)) {
@@ -11,4 +14,4 @@
}) })
</script> </script>
<Chat id={$page.params.chat} /> <Chat id={chat} />
+77 -28
View File
@@ -1,24 +1,44 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {dec} from "@welshman/lib" import {debounce} from "throttle-debounce"
import {ROOMS} from "@welshman/util" import {dec, tryCatch} from "@welshman/lib"
import {ROOMS, normalizeRelayUrl, isRelayUrl} from "@welshman/util"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import type {Relay} from "@welshman/app" import type {Relay} from "@welshman/app"
import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app" import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {fly} from "@lib/transition" 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 Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte" import Page from "@lib/components/Page.svelte"
import Scanner from "@lib/components/Scanner.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import PageHeader from "@lib/components/PageHeader.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 RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceCheck from "@app/components/SpaceCheck.svelte" import SpaceCheck from "@app/components/SpaceCheck.svelte"
import {getMembershipUrls, loadMembership, defaultPubkeys, membersByUrl} from "@app/core/state" import {getMembershipUrls, loadMembership, defaultPubkeys, membersByUrl} from "@app/core/state"
import {pushModal} from "@app/util/modal" 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 = () => const discoverRelays = () =>
Promise.all([ Promise.all([
load({ load({
@@ -37,7 +57,7 @@
const relaySearch = $derived( const relaySearch = $derived(
createSearch( createSearch(
$relays.filter(r => $membersByUrl.has(r.url)), $relays.filter(r => $membersByUrl.has(r.url) && r.url !== termUrl),
{ {
getValue: (relay: Relay) => relay.url, getValue: (relay: Relay) => relay.url,
sortFn: ({score, item}) => { sortFn: ({score, item}) => {
@@ -59,6 +79,7 @@
let term = $state("") let term = $state("")
let limit = $state(20) let limit = $state(20)
let showScanner = $state(false)
let element: Element let element: Element
onMount(() => { onMount(() => {
@@ -76,30 +97,58 @@
</script> </script>
<Page class="cw-full"> <Page class="cw-full">
<div class="content column gap-4" bind:this={element}> <ContentSearch>
<PageHeader> {#snippet input()}
{#snippet title()} <div class="flex flex-col gap-2">
Discover Spaces <PageHeader>
{/snippet} {#snippet title()}
{#snippet info()} Discover Spaces
Find communities all across the nostr network {/snippet}
{/snippet} {#snippet info()}
</PageHeader> Find communities all across the nostr network
<label class="input input-bordered flex w-full items-center gap-2"> {/snippet}
<Icon icon={Magnifier} /> </PageHeader>
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." /> <div class="row-2 min-w-0 flex-grow items-center">
</label> <label class="input input-bordered flex flex-grow items-center gap-2">
{#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)} <Icon icon={Magnifier} />
<Button <input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]" <Button onclick={toggleScanner} class="center">
onclick={() => openSpace(relay.url)}> <Icon icon={QrCode} />
<RelaySummary url={relay.url} /> </Button>
</Button> </label>
{/each} <Button class="btn btn-primary" onclick={openMenu}>
{#await discoverRelays()} <Icon icon={AddCircle} />
<div class="flex justify-center py-20" out:fly> </Button>
<Spinner loading>Looking for spaces...</Spinner> </div>
{#if showScanner}
<Scanner onscan={onScan} />
{/if}
</div> </div>
{/await} {/snippet}
</div> {#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> </Page>
+3 -3
View File
@@ -32,7 +32,7 @@
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1> <h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="col-3"> <div class="col-3">
<Button onclick={addSpace}> <Button onclick={addSpace}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
{/snippet} {/snippet}
@@ -45,7 +45,7 @@
</CardButton> </CardButton>
</Button> </Button>
<Link href="/discover"> <Link href="/discover">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Compass} size={7} /></div> <div><Icon icon={Compass} size={7} /></div>
{/snippet} {/snippet}
@@ -58,7 +58,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Button onclick={openChat}> <Button onclick={openChat}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={ChatRound} size={7} /></div> <div><Icon icon={ChatRound} size={7} /></div>
{/snippet} {/snippet}
+1 -3
View File
@@ -61,9 +61,7 @@
</div> </div>
</div> </div>
</div> </div>
<Button <Button class="center btn btn-circle btn-neutral -mr-4 -mt-4 h-12 w-12" onclick={startEdit}>
class="center btn btn-circle -mr-4 -mt-4 h-12 w-12 dark:btn-neutral"
onclick={startEdit}>
<Icon icon={PenNewSquare} /> <Icon icon={PenNewSquare} />
</Button> </Button>
</div> </div>
+3 -3
View File
@@ -50,7 +50,7 @@
</script> </script>
<div class="content column gap-4"> <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()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Globus} /> <Icon icon={Globus} />
@@ -83,7 +83,7 @@
</Button> </Button>
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4"> <Collapse class="card2 bg-alt column gap-4 shadow-xl">
{#snippet title()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Inbox} /> <Icon icon={Inbox} />
@@ -115,7 +115,7 @@
</Button> </Button>
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4"> <Collapse class="card2 bg-alt column gap-4 shadow-xl">
{#snippet title()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Mailbox} /> <Icon icon={Mailbox} />
+31 -2
View File
@@ -1,22 +1,32 @@
<script lang="ts"> <script lang="ts">
import {nwc} from "@getalby/sdk" import {nwc} from "@getalby/sdk"
import {LOCALE} from "@welshman/lib" import {LOCALE} from "@welshman/lib"
import {displayRelayUrl, fromMsats} from "@welshman/util" import {displayRelayUrl, isNWCWallet, fromMsats} from "@welshman/util"
import {session} from "@welshman/app" import {session, pubkey, profilesByPubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import WalletConnect from "@app/components/WalletConnect.svelte" import WalletConnect from "@app/components/WalletConnect.svelte"
import WalletDisconnect from "@app/components/WalletDisconnect.svelte" import WalletDisconnect from "@app/components/WalletDisconnect.svelte"
import WalletUpdateReceivingAddress from "@app/components/WalletUpdateReceivingAddress.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {getWebLn} from "@app/core/commands" import {getWebLn} from "@app/core/commands"
import Wallet2 from "@assets/icons/wallet.svg?dataurl" import Wallet2 from "@assets/icons/wallet.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import CloseCircle from "@assets/icons/close-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 connect = () => pushModal(WalletConnect)
const disconnect = () => pushModal(WalletDisconnect) 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> </script>
<div class="content column gap-4"> <div class="content column gap-4">
@@ -89,4 +99,23 @@
{/if} {/if}
</div> </div>
</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> </div>
+5 -1
View File
@@ -22,6 +22,7 @@
userRoomsByUrl, userRoomsByUrl,
} from "@app/core/state" } from "@app/core/state"
import {pullConservatively} from "@app/core/requests" import {pullConservatively} from "@app/core/requests"
import {hasBlossomSupport} from "@app/core/commands"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
type Props = { type Props = {
@@ -30,7 +31,7 @@
const {children}: Props = $props() const {children}: Props = $props()
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const rooms = Array.from($userRoomsByUrl.get(url) || []) 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 // Load group meta, threads, calendar events, comments, and recent messages
// for user rooms to help with a quick page transition // for user rooms to help with a quick page transition
pullConservatively({ pullConservatively({
+1 -1
View File
@@ -26,7 +26,7 @@
import {makeChatPath} from "@app/util/routes" import {makeChatPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const relay = deriveRelay(url) const relay = deriveRelay(url)
const joinSpace = () => pushModal(SpaceJoin, {url}) const joinSpace = () => pushModal(SpaceJoin, {url})
+12 -9
View File
@@ -5,6 +5,7 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib" import {now, formatTimestampAsDate} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {
@@ -57,10 +58,10 @@
import {popKey} from "@lib/implicit" import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
const {room} = $page.params const {room, relay} = $page.params as MakeNonOptional<typeof $page.params>
const mounted = now() const mounted = now()
const lastChecked = $checked[$page.url.pathname] const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay) const url = decodeRelay(relay)
const channel = deriveChannel(url, room) const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [room]} const filter = {kinds: [MESSAGE], "#h": [room]}
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room)) const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
@@ -137,13 +138,15 @@
delay: $userSettingsValues.send_delay, delay: $userSettingsValues.send_delay,
}) })
pushToast({ if ($userSettingsValues.send_delay) {
timeout: 30_000, pushToast({
children: { timeout: 30_000,
component: ThunkToast, children: {
props: {thunk}, component: ThunkToast,
}, props: {thunk},
}) },
})
}
clearParent() clearParent()
clearShare() clearShare()
@@ -23,7 +23,7 @@
import {makeCalendarFeed} from "@app/core/requests" import {makeCalendarFeed} from "@app/core/requests"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const makeEvent = () => pushModal(CalendarEventCreate, {url}) const makeEvent = () => pushModal(CalendarEventCreate, {url})
@@ -2,6 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib" import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util" import {COMMENT, getTagValue} from "@welshman/util"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository} from "@welshman/app" import {repository} from "@welshman/app"
@@ -25,7 +26,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state" import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications" 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 url = decodeRelay(relay)
const event = deriveEvent(id) const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}] const filters = [{kinds: [COMMENT], "#E": [id]}]
+10 -8
View File
@@ -36,7 +36,7 @@
const mounted = now() const mounted = now()
const lastChecked = $checked[$page.url.pathname] const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const filter = {kinds: [MESSAGE]} const filter = {kinds: [MESSAGE]}
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -74,13 +74,15 @@
delay: $userSettingsValues.send_delay, delay: $userSettingsValues.send_delay,
}) })
pushToast({ if ($userSettingsValues.send_delay) {
timeout: 30_000, pushToast({
children: { timeout: 30_000,
component: ThunkToast, children: {
props: {thunk}, component: ThunkToast,
}, props: {thunk},
}) },
})
}
clearParent() clearParent()
clearShare() clearShare()
+1 -1
View File
@@ -20,7 +20,7 @@
import {makeFeed} from "@app/core/requests" import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes)) const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const goals: TrustedEvent[] = $state([]) const goals: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([]) const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib" import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util" import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app" import {repository} from "@welshman/app"
import {request} from "@welshman/net" import {request} from "@welshman/net"
@@ -24,7 +25,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state" import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications" 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 url = decodeRelay(relay)
const event = deriveEvent(id) const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}] const filters = [{kinds: [COMMENT], "#E": [id]}]
@@ -21,7 +21,7 @@
import {makeFeed} from "@app/core/requests" import {makeFeed} from "@app/core/requests"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay!)
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes)) const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const threads: TrustedEvent[] = $state([]) const threads: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([]) const comments: TrustedEvent[] = $state([])
@@ -2,6 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib" import {sortBy, sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util" import {COMMENT, getTagValue} from "@welshman/util"
import {repository} from "@welshman/app" import {repository} from "@welshman/app"
import {request} from "@welshman/net" import {request} from "@welshman/net"
@@ -23,7 +24,7 @@
import {deriveEvent, decodeRelay} from "@app/core/state" import {deriveEvent, decodeRelay} from "@app/core/state"
import {setChecked} from "@app/util/notifications" 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 url = decodeRelay(relay)
const event = deriveEvent(id) const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}] const filters = [{kinds: [COMMENT], "#E": [id]}]
Binary file not shown.
+2
View File
@@ -36,6 +36,8 @@ export default {
}, },
light: { light: {
...themes["winter"], ...themes["winter"],
neutral: '#F2F7FF',
warning: '#FD8D0B',
primary: process.env.VITE_PLATFORM_ACCENT, primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF", "primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY, secondary: process.env.VITE_PLATFORM_SECONDARY,