diff --git a/.claude/skills/find-non-lambda-logs/SKILL.md b/.claude/skills/find-non-lambda-logs/SKILL.md
new file mode 100644
index 000000000..0b0009255
--- /dev/null
+++ b/.claude/skills/find-non-lambda-logs/SKILL.md
@@ -0,0 +1,86 @@
+---
+name: find-non-lambda-logs
+description: Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
+---
+
+# Find Non-Lambda Log Calls
+
+## Overview
+
+Locates `Log.d/i/w/e` calls that use string interpolation without the lambda overload, wasting string allocation when the log level is filtered out in release builds.
+
+## When to Use
+
+- After merging branches that add new logging
+- Periodic audit of logging hygiene
+- After migrating `android.util.Log` usages to the shared `Log` wrapper
+
+## What to Flag
+
+Calls with **string interpolation** (`$` in message) that do **not** pass a throwable:
+
+```kotlin
+// FLAG - interpolation without lambda, no throwable
+Log.d("Tag", "Processing ${event.id}")
+Log.w("Tag", "Failed for $url")
+
+// IGNORE - passes throwable (lambda overload doesn't accept throwable)
+Log.w("Tag", "Error: ${e.message}", e)
+Log.e("Tag", "Failed for $url", throwable)
+
+// IGNORE - no interpolation (no allocation benefit from lambda)
+Log.d("Tag", "Initialization complete")
+```
+
+## Search Commands
+
+**Important:** Tags can be string literals (`"Tag"`) or variables (`tag`, `LOG_TAG`). Run both patterns for each step.
+
+### Step 1: Find interpolated Log.d/Log.i (highest priority — filtered in release)
+
+```
+pattern: Log\.(d|i)\("[^"]+",\s*"[^"]*\$
+type: kotlin
+```
+```
+pattern: Log\.(d|i)\(\w+,\s*"[^"]*\$
+type: kotlin
+```
+
+### Step 2: Find interpolated Log.w/Log.e without throwable
+
+```
+pattern: Log\.(w|e)\("[^"]+",\s*"[^"]*\$
+type: kotlin
+```
+```
+pattern: Log\.(w|e)\(\w+,\s*"[^"]*\$
+type: kotlin
+```
+
+Then **manually exclude** lines where a throwable is passed as third argument (ending with `, e)`, `, throwable)`, etc.). Check the actual line — a catch block catching `e` doesn't mean `e` is passed to the Log call.
+
+### Step 3: Verify no android.util.Log leakage
+
+```
+pattern: android\.util\.Log\.(d|i|w|e|v)\(
+type: kotlin
+```
+
+These bypass the `Log.minLevel` filter entirely. Exclude `PlatformLog.android.kt` which is the wrapper implementation.
+
+## Fix Pattern
+
+```kotlin
+// Before
+Log.d("Tag", "Processing event ${event.id} from ${relay.url}")
+
+// After
+Log.d("Tag") { "Processing event ${event.id} from ${relay.url}" }
+```
+
+## Do NOT Convert
+
+- Calls passing a `Throwable` parameter - the lambda overload `(tag) { message }` has no throwable parameter
+- Static string calls with no `$` interpolation - no allocation benefit
+- Commented-out log calls
diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push
index 2c0dbdc27..1e9795cfc 100755
--- a/.git-hooks/pre-push
+++ b/.git-hooks/pre-push
@@ -12,8 +12,11 @@ echo "$JAVA_HOME"
echo "$(java -version)"
echo "Running test... "
-./gradlew test --quiet
-
+if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
+ ./gradlew test --quiet -x :desktopApp:test -x :desktopApp:upxDownload -x :desktopApp:vlcDownload
+else
+ ./gradlew test --quiet
+fi
status=$?
if [ "$status" = 0 ] ; then
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0377b3735..93176ac48 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,6 +6,9 @@ on:
push:
branches: [main]
+permissions:
+ contents: read
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -71,6 +74,10 @@ jobs:
- name: Test (gradle)
run: ./gradlew test --no-daemon
+ - name: Stop Gradle Daemon
+ if: always()
+ run: ./gradlew --stop
+
- name: Android Test Report
uses: asadmansr/android-test-report-action@v1.2.0
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
@@ -85,7 +92,7 @@ jobs:
build-android:
needs: test
runs-on: ubuntu-latest
- timeout-minutes: 30
+ timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -188,6 +195,10 @@ jobs:
- name: Build Desktop Distribution
run: ./gradlew :desktopApp:${{ matrix.task }}
+ - name: Stop Gradle Daemon
+ if: always()
+ run: ./gradlew --stop
+
- name: Upload Desktop Distribution
uses: actions/upload-artifact@v6
with:
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index beb08874b..fc619bc3f 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -5,6 +5,9 @@ on:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+permissions:
+ contents: write
+
jobs:
create-release:
runs-on: ubuntu-latest
diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml
index 923e7fa49..e23a28596 100644
--- a/.github/workflows/crowdin.yml
+++ b/.github/workflows/crowdin.yml
@@ -5,6 +5,10 @@ on:
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
branches: [ main ]
+permissions:
+ contents: write
+ pull-requests: write
+
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index de1c06cf0..bece7072e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
/.idea/deviceManager.xml
/.idea/inspectionProfiles/
/.idea/migrations.xml
+/desktopApp/vlc-temp/
/commons/.idea/gradle.xml
/commons/.idea/misc.xml
/commons/.idea/workspace.xml
diff --git a/README.md b/README.md
index a19f16550..dcf7f0753 100644
--- a/README.md
+++ b/README.md
@@ -39,107 +39,104 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
-- [x] Events / Relay Subscriptions (NIP-01)
+- [x] Basic protocol flow (NIP-01)
- [x] Follow List (NIP-02)
- [x] OpenTimestamps Attestations (NIP-03)
-- [x] Private Messages (NIP-04 -- to be removed)
-- [x] DNS Address (NIP-05)
-- [x] Mnemonic seed phrase (NIP-06)
-- [ ] WebBrowser Signer (NIP-07, Not applicable)
-- [x] Old-style mentions (NIP-08)
-- [x] Event Deletion (NIP-09)
+- [x] Encrypted Direct Message (NIP-04)
+- [x] DNS-based Identifiers (NIP-05)
+- [x] Key Derivation from Mnemonic (NIP-06)
+- [ ] window.nostr for Web Browsers (NIP-07, Not applicable)
+- [x] Handling Mentions (NIP-08)
+- [x] Event Deletion Request (NIP-09)
- [x] Text Notes and Threads (NIP-10)
- [x] Relay Information Document (NIP-11)
-- [x] Generic Tag Queries (NIP-12)
-- [x] Proof of Work Display (NIP-13)
-- [ ] Proof of Work Calculations (NIP-13)
-- [x] Events with a Subject (NIP-14)
-- [ ] Marketplace (NIP-15)
-- [x] Event Treatment (NIP-16)
+- [x] Proof of Work (NIP-13)
+- [x] Subject Tag in Text Events (NIP-14)
+- [x] Nostr Marketplace (NIP-15)
- [x] Private Direct Messages (NIP-17)
-- [x] Image/Video/Url/LnInvoice/Cashu Previews
-- [x] Reposts, Quotes, Generic Reposts (NIP-18)
-- [x] Bech32 Encoding support (NIP-19)
-- [x] Command Results (NIP-20)
-- [x] URI Support (NIP-21)
-- [x] Long-form Content (NIP-23) (view only)
-- [x] User Profile Fields / Relay list (NIP-24)
+- [x] Reposts (NIP-18)
+- [x] bech32-encoded Entities (NIP-19)
+- [x] nostr: URI Scheme (NIP-21)
+- [x] Comment (NIP-22)
+- [x] Long-form Content (NIP-23)
+- [x] Extra Metadata Fields and Tags (NIP-24)
- [x] Reactions (NIP-25)
-- [ ] Delegated Event Signing (NIP-26, Will not implement)
+- [x] Delegated Event Signing (NIP-26)
- [x] Text Note References (NIP-27)
-- [x] Public Chats (NIP-28)
-- [ ] Relay-based Groups (NIP-29)
+- [x] Public Chat (NIP-28)
+- [x] Relay-based Groups (NIP-29)
- [x] Custom Emoji (NIP-30)
-- [x] Event alt descriptors (NIP-31)
-- [ ] Labeling (NIP-32)
-- [x] Git Stuff (NIP-34)
+- [x] Dealing with Unknown Events (NIP-31)
+- [x] Labeling (NIP-32)
+- [x] git stuff (NIP-34)
- [x] Torrents (NIP-35)
- [x] Sensitive Content (NIP-36)
-- [x] Drafts (NIP-37)
-- [x] User Status Event (NIP-38)
-- [x] External Identities (NIP-39)
-- [x] Expiration Support (NIP-40)
-- [x] Relay Authentication (NIP-42)
-- [ ] Relay Access Metadata and Requests (NIP-43)
-- [x] Versioned Encrypted Payloads (NIP-44)
-- [x] Event Counts (NIP-45)
-- [o] Nostr Connect (NIP-46)
-- [o] Wallet Connect API (NIP-47)
-- [ ] Proxy Tags (NIP-48, Not applicable)
-- [x] Encryption for import/export (NIP-49)
-- [x] Relay Search (NIP-50)
+- [x] Draft Events (NIP-37)
+- [x] User Statuses (NIP-38)
+- [x] External Identities in Profiles (NIP-39)
+- [x] Expiration Timestamp (NIP-40)
+- [x] Authentication of Clients to Relays (NIP-42)
+- [x] Relay Access Metadata and Requests (NIP-43)
+- [x] Encrypted Payloads / Versioned (NIP-44)
+- [x] Counting Results (NIP-45)
+- [x] Nostr Remote Signing (NIP-46)
+- [x] Nostr Wallet Connect (NIP-47)
+- [x] Proxy Tags (NIP-48)
+- [x] Private Key Encryption (NIP-49)
+- [x] Search Capability (NIP-50)
- [x] Lists (NIP-51)
-- [o] Calendar Events (NIP-52)
-- [x] Live Activities & Live Chats (NIP-53)
+- [x] Calendar Events (NIP-52)
+- [x] Live Activities (NIP-53)
- [x] Wiki (NIP-54)
-- [x] Android Signer (NIP-55)
+- [x] Android Signer Application (NIP-55)
- [x] Reporting (NIP-56)
-- [x] Lightning Tips
-- [x] Zaps (NIP-57)
+- [x] Lightning Zaps (NIP-57)
- [x] Zap Splits (NIP-57)
-- [x] Private Zaps (NIP-57 / Draft)
-- [x] Zapraiser (NIP-57 / Draft)
+- [x] Private Zaps (NIP-57)
+- [x] Zapraiser (NIP-57)
- [x] Badges (NIP-58)
-- [x] Gift Wraps & Seals (NIP-59)
-- [ ] Cashu Wallets (NIP-60)
-- [ ] Nutzaps (NIP-61)
+- [x] Gift Wrap (NIP-59)
+- [x] Pubkey Static Websites (NIP-5A)
+- [x] Cashu Wallet (NIP-60)
+- [x] Nutzaps (NIP-61)
- [x] Request to Vanish (NIP-62)
-- [x] Chess (NIP-64)
+- [x] Chess / PGN (NIP-64)
- [x] Relay List Metadata (NIP-65)
-- [x] Relay Discovery and Monitoring (NIP-66)
-- [x] Picture-first feeds (NIP-68)
-- [ ] Peer-to-peer Orders (NIP-69)
-- [o] Protected Events (NIP-70)
+- [x] Relay Discovery and Liveness Monitoring (NIP-66)
+- [x] Picture-first Feeds (NIP-68)
+- [x] Peer-to-peer Order Events (NIP-69)
+- [x] Protected Events (NIP-70)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [x] External Content IDs (NIP-73)
-- [ ] Zap Goals (NIP-75)
-- [ ] Negentropy Syncing (NIP-77)
-- [x] Arbitrary Custom App Data (NIP-78)
-- [ ] NIP-29 Threads (NIP-7D)
+- [x] Zap Goals (NIP-75)
+- [x] Negentropy Syncing (NIP-77)
+- [x] Application-specific Data (NIP-78)
+- [x] Threads (NIP-7D)
- [x] Highlights (NIP-84)
- [x] Trusted Assertions (NIP-85)
-- [ ] Relay Management API (NIP-86)
-- [ ] ECash Mint Discoverability (NIP-87)
+- [x] Relay Management API (NIP-86)
+- [x] Ecash Mint Discoverability (NIP-87)
- [x] Polls (NIP-88)
-- [x] Relay Notify Request
- [x] Recommended Application Handlers (NIP-89)
-- [x] Data Vending Machine (NIP-90)
-- [x] Inline Metadata (NIP-92)
-- [x] Verifiable file URLs (NIP-94)
+- [x] Data Vending Machines (NIP-90)
+- [x] Media Attachments (NIP-92)
+- [x] File Metadata (NIP-94)
- [x] Binary Blobs (NIP-95/Draft)
- [x] HTTP File Storage Integration (NIP-96)
- [x] HTTP Auth (NIP-98)
-- [x] Classifieds (NIP-99)
+- [x] Classified Listings (NIP-99)
- [x] Voice Messages (NIP-A0)
- [x] Public Messages (NIP-A4)
-- [ ] Web Bookmarks
+- [x] Web Bookmarks (NIP-B0)
- [x] Blossom (NIP-B7)
-- [ ] Nostr BLE Communications Protocol (NIP-BE)
+- [x] Nostr BLE Communications Protocol (NIP-BE)
- [x] Code Snippets (NIP-C0)
-- [ ] NIP-29 Chats (NIP-C7)
+- [x] Chats (NIP-C7)
- [ ] MLS Protocol (NIP-EE)
- [x] Audio Tracks (zapstr.live) (kind:31337)
+- [x] Lightning Tips
+- [x] Image/Video/Url/LnInvoice/Cashu Previews
- [x] Push Notifications (Google and Unified Push)
- [x] In-Device Automatic Translations
- [x] Hashtag Following and Custom Hashtags
@@ -405,24 +402,24 @@ to `onPause` methods.
### Feature Parity Table
-| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
-| :--- | :--- | :---: | :---: | :--- |
-| **Cryptography** | Secp256k1 (Schnorr, Keys) | ✅ Full | ❌ No | Core Nostr signing/verification is missing on iOS. |
-| | LibSodium (ChaCha20, Poly1305) | ✅ Full | ❌ No | AEAD and stream ciphers are unimplemented. |
-| | AES Encryption (CBC & GCM) | ✅ Full | ❌ No | `AESCBC` and `AESGCM` are stubs on iOS. |
-| | Hashing (SHA-256, etc.) | ✅ Full | ❌ No | `DigestInstance` is unimplemented. |
-| | MAC (HmacSHA256, etc.) | ✅ Full | ❌ No | `MacInstance` is unimplemented. |
-| **Data & Serialization** | JSON Mapping (Optimized) | ✅ Full | ❌ No | `OptimizedJsonMapper` is a stub; cannot parse/serialize Events. |
-| | GZip Compression | ✅ Full | ❌ No | `GZip` implementation is missing. |
-| | BitSet | ✅ Full | ❌ No | `BitSet` utility is unimplemented. |
-| | LargeCache | ✅ Full | ❌ No | `LargeCache` methods (get, keys, size, etc.) are stubs. |
-| **NIP Support** | NIP-96 (File Storage Info) | ✅ Full | ❌ No | `ServerInfoParser` is unimplemented. |
-| | NIP-46 (Remote Signer) | ✅ Full | ⚠️ Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
-| | NIP-03 (OTS / Timestamps) | ✅ Full | ❌ No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
-| **Utilities** | URL Encoding / Decoding | ✅ Full | ❌ No | `UrlEncoder` and `URLs.ios.kt` are unimplemented. |
-| | Unicode Normalization | ✅ Full | ❌ No | `UnicodeNormalizer` is a stub. |
-| | Platform Logging | ✅ Full | ✅ Full | iOS uses `NSLog`, Android uses standard Log. |
-| | Current Time | ✅ Full | ✅ Full | Implemented using `NSDate` on iOS. |
+| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
+|:-------------------------|:-------------------------------|:---------------------:|:-----------:|:-----------------------------------------------------------------------|
+| **Cryptography** | Secp256k1 (Schnorr, Keys) | ✅ Full | ✅ Full | |
+| | LibSodium (ChaCha20, Poly1305) | ✅ Full | ✅ Full | |
+| | AES Encryption (CBC & GCM) | ✅ Full | ✅ Full | |
+| | Hashing (SHA-256, etc.) | ✅ Full | ✅ Full | |
+| | MAC (HmacSHA256, etc.) | ✅ Full | ✅ Full | |
+| **Data & Serialization** | JSON Mapping (Optimized) | ✅ Full | ✅ Full | A fully custom implementation exists in `commonMain`. |
+| | GZip Compression | ✅ Full | ✅ Full | |
+| | BitSet | ✅ Full | ✅ Full | |
+| | LargeCache | ✅ Full | ✅ Full | |
+| **NIP Support** | NIP-96 (File Storage Info) | ✅ Full | ✅ Full | |
+| | NIP-46 (Remote Signer) | ✅ Full | ⚠️ Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
+| | NIP-03 (OTS / Timestamps) | ✅ Full | ❌ No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
+| **Utilities** | URL Encoding / Decoding | ✅ Full | ✅ Full | |
+| | Unicode Normalization | ✅ Full | ✅ Full | |
+| | Platform Logging | ✅ Full | ✅ Full | iOS uses `NSLog`, Android uses standard Log. |
+| | Current Time | ✅ Full | ✅ Full | Implemented using `NSDate` on iOS. |
## Contributing
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
index 48ca191f7..a591dd575 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
@@ -70,9 +70,9 @@ class ThreadDualAxisChartAssemblerTest {
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
- geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading),
- nwcFilterAssembler = NWCPaymentFilterAssembler(client),
- otsResolverBuilder = EmptyOtsResolverBuilder,
+ geolocationFlow = { MutableStateFlow(LocationState.LocationResult.Loading) },
+ nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
+ otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
cache = LocalCache,
client = client,
scope = scope,
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
index ac14ef95b..5da1c62fb 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
@@ -46,7 +46,7 @@ object PushDistributorHandler : PushDistributorActions {
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
- Log.d("PushHandler", "New endpoint saved : $endpointInternal")
+ Log.d("PushHandler") { "New endpoint saved : $endpointInternal" }
}
fun removeEndpoint() {
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
index fe5cf2cd2..dffba6b57 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
@@ -53,7 +53,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val messageStr = message.content.decodeToString()
- Log.d(TAG, "New message $messageStr for Instance: $instance")
+ Log.d(TAG) { "New message $messageStr for Instance: $instance" }
scope.launch {
try {
parseMessage(messageStr)?.let {
@@ -61,7 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
}
} catch (e: Exception) {
if (e is CancellationException) throw e
- Log.d(TAG, "Message could not be parsed: ${e.message}")
+ Log.d(TAG) { "Message could not be parsed: ${e.message}" }
}
}
}
@@ -87,7 +87,7 @@ class PushMessageReceiver : MessagingReceiver() {
) {
val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
- Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
+ Log.d(TAG) { "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint" }
pushHandler.setEndpoint(sanitizedEndpoint)
scope.launch(Dispatchers.IO) {
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
@@ -97,7 +97,7 @@ class PushMessageReceiver : MessagingReceiver() {
NotificationUtils.getOrCreateDMChannel(appContext)
}
} else {
- Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
+ Log.d(TAG) { "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint" }
}
}
@@ -106,7 +106,7 @@ class PushMessageReceiver : MessagingReceiver() {
reason: FailedReason,
instance: String,
) {
- Log.d(TAG, "Registration failed for Instance: $instance")
+ Log.d(TAG) { "Registration failed for Instance: $instance" }
pushHandler.forceRemoveDistributor(context)
}
@@ -115,7 +115,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val removedEndpoint = pushHandler.getSavedEndpoint()
- Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
+ Log.d(TAG) { "Endpoint: $removedEndpoint removed for Instance: $instance" }
Log.d(TAG, "App is unregistered. ")
pushHandler.forceRemoveDistributor(context)
pushHandler.removeEndpoint()
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt
index 30c16940d..3ed4fe322 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt
@@ -35,7 +35,7 @@ object PushNotificationUtils {
accounts: List,
okHttpClient: (String) -> OkHttpClient,
) = with(Dispatchers.IO) {
- if (!pushHandler.savedDistributorExists()) return
+ if (!pushHandler.savedDistributorExists()) return@with
val currentDistributor = PushDistributorHandler.getSavedDistributor()
PushDistributorHandler.saveDistributor(currentDistributor)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
index 5c90cfd7f..8e488a83a 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
@@ -23,10 +23,12 @@ package com.vitorpamplona.amethyst
import android.app.Application
import com.vitorpamplona.amethyst.service.logging.Logging
import com.vitorpamplona.quartz.utils.Log
+import com.vitorpamplona.quartz.utils.LogLevel
class Amethyst : Application() {
init {
- Log.d("AmethystApp", "Creating App $this")
+ Log.minLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.ERROR
+ Log.d("AmethystApp") { "Creating App $this" }
}
companion object {
@@ -36,7 +38,7 @@ class Amethyst : Application() {
override fun onCreate() {
super.onCreate()
- Log.d("AmethystApp", "onCreate $this")
+ Log.d("AmethystApp") { "onCreate $this" }
instance = AppModules(this)
if (isDebug) {
@@ -48,7 +50,7 @@ class Amethyst : Application() {
override fun onTerminate() {
super.onTerminate()
- Log.d("AmethystApp", "onTerminate $this")
+ Log.d("AmethystApp") { "onTerminate $this" }
instance.terminate(this)
}
@@ -59,7 +61,7 @@ class Amethyst : Application() {
*/
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
- Log.d("AmethystApp", "onTrimMemory $level")
+ Log.d("AmethystApp") { "onTrimMemory $level" }
instance.trim()
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
index cf9256dc4..78a13e3f4 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
@@ -20,14 +20,15 @@
*/
package com.vitorpamplona.amethyst
-import android.content.ContentResolver
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.commons.model.NoteState
+import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
+import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
@@ -63,9 +64,11 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
+import com.vitorpamplona.amethyst.ui.resourceCacheInit
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.tor.TorManager
+import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
@@ -87,6 +90,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -94,6 +98,7 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import java.io.File
class AppModules(
@@ -109,31 +114,54 @@ class AppModules(
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
+ // Pre-load both preference DataStores in parallel on IO threads.
+ // Both constructors use runBlocking internally, so starting them concurrently
+ // reduces total blocking time from (torPrefs + uiPrefs) to ~max(torPrefs, uiPrefs).
+ private val uiPrefsDeferred =
+ applicationIOScope.async {
+ val prefs = UiSharedPreferences.uiPreferences(appContext) ?: UiSettings()
+ UiSharedPreferences(prefs, appContext, applicationIOScope)
+ }
+
+ private val torPrefsDeferred =
+ applicationIOScope.async {
+ val prefs = TorSharedPreferences.torPreferences(appContext) ?: TorSettings()
+ TorSharedPreferences(prefs, appContext, applicationIOScope)
+ }
+
// Blocking load of UI Preferences to avoid theme/language blinking
val uiPrefs by lazy {
- UiSharedPreferences(appContext, applicationIOScope)
+ Log.d("AppModules", "UiSharedPreferences Init")
+ runBlocking { uiPrefsDeferred.await() }
}
// Blocking load of Tor Settings to avoid connection leaks
val torPrefs by lazy {
- TorSharedPreferences(appContext, applicationIOScope)
+ Log.d("AppModules", "TorSharedPreferences Init")
+ runBlocking { torPrefsDeferred.await() }
}
// Namecoin ElectrumX server preferences (global, like Tor settings)
val namecoinPrefs by lazy {
+ Log.d("AppModules", "NamecoinSharedPreferences Init")
NamecoinSharedPreferences(appContext, applicationIOScope)
}
// OTS blockchain explorer preferences (global, like Tor settings)
val otsPrefs by lazy {
+ Log.d("AppModules", "OtsSharedPreferences Init")
OtsSharedPreferences(appContext, applicationIOScope)
}
// App services that should be run as soon as there are subscribers to their flows
- val locationManager = LocationState(appContext, applicationIOScope)
+ val locationManager by lazy {
+ Log.d("AppModules", "LocationManager Init")
+ LocationState(appContext, applicationIOScope)
+ }
val connManager = ConnectivityManager(appContext, applicationIOScope)
val uiState by lazy {
+ Log.d("AppModules", "UiSettingsState Init")
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
}
@@ -158,40 +186,67 @@ class AppModules(
// Offers easy methods to know when connections are happening through Tor or not
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
- // Custom fetcher that considers tor settings and avoids forwarding.
- val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
+ val electrumXClient by lazy {
+ Log.d("AppModules", "ElectrumXClient Init")
+ val client =
+ ElectrumXClient(
+ socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
+ )
+ applicationIOScope.launch {
+ try {
+ val pinnedCerts = namecoinPrefs.loadPinnedCerts()
+ if (pinnedCerts.isNotEmpty()) {
+ client.setDynamicCerts(pinnedCerts)
+ }
+ } catch (_: Exception) {
+ // Non-fatal — defaults will still work
+ }
+ }
+ client
+ }
- val namecoinResolver =
- NamecoinNameResolver(
- electrumxClient =
- ElectrumXClient(
- socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
- ),
- serverListProvider = {
- // User-configured custom servers take priority
- namecoinPrefs.customServersOrNull
- ?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
- TOR_ELECTRUMX_SERVERS
- } else {
- DEFAULT_ELECTRUMX_SERVERS
- }
- },
- )
- val nip05Client = Nip05Client(nip05Fetcher, namecoinResolver)
+ val namecoinResolver by
+ lazy {
+ Log.d("AppModules", "Namecoin Resolver Init")
+ NamecoinNameResolver(
+ electrumxClient = electrumXClient,
+ serverListProvider = {
+ // User-configured custom servers take priority
+ namecoinPrefs.customServersOrNull
+ ?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
+ TOR_ELECTRUMX_SERVERS
+ } else {
+ DEFAULT_ELECTRUMX_SERVERS
+ }
+ },
+ )
+ }
- // Application-wide block height request cache
- val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
+ val nip05Client by
+ lazy {
+ Log.d("AppModules", "NIP05Client Init")
+ Nip05Client(
+ fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05),
+ namecoinResolverBuilder = { namecoinResolver },
+ )
+ }
- val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
- TorAwareOkHttpOtsResolverBuilder(
- roleBasedHttpClientBuilder::okHttpClientForMoney,
- roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
- otsBlockHeightCache,
- customExplorerUrl = { otsPrefs.current.normalizedUrl() },
- )
+ val otsResolverBuilder by
+ lazy {
+ Log.d("AppModules", "OtsResolverBuilder Init")
+ TorAwareOkHttpOtsResolverBuilder(
+ roleBasedHttpClientBuilder::okHttpClientForMoney,
+ roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
+ OtsBlockHeightCache(),
+ customExplorerUrl = { otsPrefs.current.normalizedUrl() },
+ )
+ }
// Application-wide ots verification cache
- val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
+ val otsVerifCache by lazy {
+ Log.d("AppModules", "OtsCache Init")
+ VerificationStateCache(otsResolverBuilder)
+ }
val torEvaluatorFlow =
TorRelayState(
@@ -209,7 +264,7 @@ class AppModules(
scope = applicationIOScope,
)
- // Connects the NostrClient class with okHttp
+ // Connects the INostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder { url ->
val useTor = torEvaluatorFlow.flow.value.useTor(url)
@@ -243,7 +298,12 @@ class AppModules(
val authCoordinator = AuthCoordinator(client, applicationIOScope)
// Tries to verify new OTS events when they arrive.
- val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationIOScope)
+ val otsEventVerifier =
+ IncomingOtsEventVerifier(
+ otsVerifCache = { otsVerifCache },
+ cache = cache,
+ scope = applicationIOScope,
+ )
// Tracks if it is possible to connect to relays.
val failureTracker = RelayOfflineTracker(client)
@@ -269,10 +329,10 @@ class AppModules(
// keeps all accounts live
val accountsCache =
AccountCacheState(
- geolocationFlow = locationManager.geohashStateFlow,
- nwcFilterAssembler = sources.nwc,
- contentResolverFn = ::contentResolverFn,
- otsResolverBuilder = otsResolverBuilder,
+ geolocationFlow = { locationManager.geohashStateFlow },
+ nwcFilterAssembler = { sources.nwc },
+ contentResolverFn = { appContext.contentResolver },
+ otsResolverBuilder = { otsResolverBuilder.build() },
cache = cache,
client = client,
)
@@ -280,8 +340,8 @@ class AppModules(
val sessionManager =
AccountSessionManager(
accountsCache = accountsCache,
- nip05Client = nip05Client,
- client = client,
+ nip05ClientBuilder = { nip05Client },
+ clientBuilder = { client },
localPreferences = LocalPreferences,
scope = applicationIOScope,
)
@@ -307,7 +367,8 @@ class AppModules(
}
}
- val blossomResolver =
+ val blossomResolver by lazy {
+ Log.d("AppModules", "BlossomServerResolver Init")
BlossomServerResolver(
loggedInUsers = { listOfNotNull(sessionManager.loggedInAccount()?.pubKey) },
blossomServers = { addressesToSubscribe ->
@@ -323,45 +384,60 @@ class AppModules(
},
httpClientBuilder = roleBasedHttpClientBuilder,
)
+ }
// Organizes cache clearing
- val trimmingService = MemoryTrimmingService(cache)
+ val trimmingService by
+ lazy {
+ MemoryTrimmingService(cache)
+ }
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
// and reconnects relays if the configuration has been changed.
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationIOScope)
// saves the .content of NIP-95 blobs in disk to save memory
- val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
+ val nip95cache: File by lazy {
+ Log.d("AppModules", "NIP95 Cache Init")
+ Nip95CacheFactory.new(appContext)
+ }
// local video cache with disk + memory
- val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
+ val videoCache: VideoCache by lazy {
+ Log.d("AppModules", "VideoCache Init")
+ VideoCacheFactory.new(appContext)
+ }
// image cache in disk for coil
- val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
+ val diskCache: DiskCache by lazy {
+ Log.d("AppModules", "ImageCacheFactory Init")
+ ImageCacheFactory.newDisk(appContext)
+ }
// image cache in memory for coil
- val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
+ val memoryCache: MemoryCache by lazy {
+ Log.d("AppModules", "MemoryCache Init")
+ ImageCacheFactory.newMemory(appContext)
+ }
// crash report storage
- val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
+ val crashReportCache = CrashReportCache(appContext)
// cache for NIP-11 documents
val nip11Cache: Nip11CachedRetriever by lazy {
+ Log.d("AppModules", "Nip11CachedRetriever Init")
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
}
- fun contentResolverFn(): ContentResolver = appContext.contentResolver
-
fun setImageLoader() {
+ Log.d("AppModules", "ImageLoaderSetup Init")
ImageLoaderSetup.setup(
app = appContext,
diskCache = { diskCache },
memoryCache = { memoryCache },
- blossomServerResolver = blossomResolver,
- ) { url ->
- okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
- }
+ blossomServerResolver = { blossomResolver },
+ callFactory = { okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(it)) },
+ )
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
@@ -380,14 +456,20 @@ class AppModules(
// initializes diskcache on an IO thread.
applicationIOScope.launch {
- // preloads tor preferences
- torPrefs
+ // Sets Coil - Tor - OkHttp link
+ setImageLoader()
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Sets Coil - Tor - OkHttp link
- setImageLoader()
+ uiState
+ }
+
+ // LRUCache should not be instanciated in the Main thread due to blocking
+ applicationIOScope.launch {
+ CachedRobohash
+ resourceCacheInit()
}
// registers to receive events
@@ -395,15 +477,10 @@ class AppModules(
// initializes diskcache on an IO thread.
applicationIOScope.launch {
- // Sets Coil - Tor - OkHttp link
- delay(3000)
+ // Prepares video cache later
+ delay(10_000)
videoCache
}
-
- applicationIOScope.launch {
- // Eagerly initialize OtsSharedPreferences off the main thread
- otsPrefs
- }
}
fun terminate(appContext: Context) {
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
index 7e2b8dc3f..9a3efa00c 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
@@ -46,19 +46,19 @@ fun debugState(context: Context) {
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
- Log.d(STATE_DUMP_TAG, "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
+ Log.d(STATE_DUMP_TAG) { "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB" }
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
- Log.d(STATE_DUMP_TAG, "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
+ Log.d(STATE_DUMP_TAG) { "Total Native Heap Allocated: $nativeHeap/$maxNative MB" }
val activityManager: ActivityManager? = context.getSystemService()
if (activityManager != null) {
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
- Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
+ Log.d(STATE_DUMP_TAG) { "Memory Class $memClass MB (largeHeap $isLargeHeap)" }
}
Log.d(
@@ -68,14 +68,8 @@ fun debugState(context: Context) {
.size() + "/" + normalizedUrls.size(),
)
- Log.d(
- STATE_DUMP_TAG,
- "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
- )
- Log.d(
- STATE_DUMP_TAG,
- "Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
- )
+ Log.d(STATE_DUMP_TAG) { "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB" }
+ Log.d(STATE_DUMP_TAG) { "Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB" }
Log.d(
STATE_DUMP_TAG,
@@ -130,13 +124,12 @@ fun debugState(context: Context) {
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
)
LocalCache.chatroomList.forEach { key, room ->
- Log.d(
- STATE_DUMP_TAG,
+ Log.d(STATE_DUMP_TAG) {
"Private Chats $key: " +
room.rooms.size() +
" / " +
- room.rooms.sumOf { key, value -> value.messages.size },
- )
+ room.rooms.sumOf { key, value -> value.messages.size }
+ }
}
Log.d(
STATE_DUMP_TAG,
@@ -173,10 +166,10 @@ fun debugState(context: Context) {
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
- Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
+ Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB " }
}
qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
- Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
+ Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB " }
}
}
@@ -188,7 +181,7 @@ inline fun logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
- Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
+ Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage" }
}
result
} else {
@@ -203,7 +196,7 @@ inline fun logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
- Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
+ Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}" }
}
result
} else {
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
index 968a6e75c..9165e674e 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
-import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
@@ -59,9 +58,11 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
+import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -253,7 +254,7 @@ object LocalPreferences {
val prefsDir = File(prefsDirPath)
prefsDir.list()?.forEach {
if (it.contains(npub) && !File(prefsDir, it).delete()) {
- Log.w("LocalPreferences", "Failed to delete preference file: $it")
+ Log.w("LocalPreferences") { "Failed to delete preference file: $it" }
}
}
}
@@ -281,7 +282,7 @@ object LocalPreferences {
*/
@SuppressLint("ApplySharedPref")
suspend fun deleteAccount(accountInfo: AccountInfo) {
- Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
+ Log.d("LocalPreferences") { "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}" }
withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
removeAccount(accountInfo)
@@ -445,108 +446,139 @@ object LocalPreferences {
}
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
- Log.d("LocalPreferences", "Load account from file $npub")
+ Log.d("LocalPreferences") { "Load account from file $npub" }
val result =
withContext(Dispatchers.IO) {
- checkNotInMainThread()
-
return@withContext with(encryptedPreferences(npub)) {
+ Log.d("LocalPreferences") { "Load account from file $npub - opened file" }
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
- val externalSignerPackageName =
- getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
- ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
+ val externalSignerPackageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
- val defaultHomeFollowList = parseOrNull(PrefKeys.DEFAULT_HOME_FOLLOW_LIST) ?: TopFilter.AllFollows
- val defaultStoriesFollowList = parseOrNull(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST) ?: TopFilter.Global
- val defaultNotificationFollowList = parseOrNull(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST) ?: TopFilter.Global
- val defaultDiscoveryFollowList = parseOrNull(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST) ?: TopFilter.Global
+ val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
+
+ Log.d("LocalPreferences") { "Load account from file $npub - keys ready" }
- val zapPaymentRequestServer = parseOrNull(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
- val defaultFileServer = parseOrNull(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
val stripLocationOnUpload = getBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, true)
-
- val pendingAttestations = parseOrNull