mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main
# Conflicts: # gradle/libs.versions.toml
This commit is contained in:
Generated
+1
-1
@@ -11,6 +11,6 @@
|
||||
<option name="languageVersion" value="2.3" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.3.0" />
|
||||
<option name="version" value="2.3.10" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -0,0 +1,230 @@
|
||||
# Amethyst Builder Skill
|
||||
|
||||
Build customized Amethyst Nostr clients for Android. Fork, rebrand, customize, and distribute your own version.
|
||||
|
||||
## Overview
|
||||
|
||||
[Amethyst](https://github.com/vitorpamplona/amethyst) is the premier Nostr client for Android. This skill enables you to:
|
||||
- Create rebranded versions (custom name, package, icons)
|
||||
- Build F-Droid-compatible releases (no Google Play dependencies)
|
||||
- Add or modify features
|
||||
- Sign and distribute APKs
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Tools
|
||||
|
||||
1. **Java 21** (via SDKMAN)
|
||||
```bash
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
sdk install java 21.0.5-tem
|
||||
sdk use java 21.0.5-tem
|
||||
```
|
||||
|
||||
2. **Android SDK**
|
||||
- Command-line tools from https://developer.android.com/studio#command-line-tools-only
|
||||
- Required components: build-tools, platform-tools, platforms;android-35
|
||||
|
||||
3. **Git** for cloning the repository
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=$HOME/Android/Sdk
|
||||
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
|
||||
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||
```
|
||||
|
||||
## Build Workflow
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
mkdir -p ~/projects/your-app-name
|
||||
cd ~/projects/your-app-name
|
||||
git clone https://github.com/vitorpamplona/amethyst.git .
|
||||
```
|
||||
|
||||
### 2. Create Signing Key
|
||||
|
||||
```bash
|
||||
keytool -genkeypair -v \
|
||||
-keystore ./release-key.jks \
|
||||
-alias your-app \
|
||||
-keyalg RSA -keysize 2048 \
|
||||
-validity 10000
|
||||
```
|
||||
|
||||
Create `keystore.properties` in project root:
|
||||
```properties
|
||||
storeFile=release-key.jks
|
||||
storePassword=your-password
|
||||
keyAlias=your-app
|
||||
keyPassword=your-password
|
||||
```
|
||||
|
||||
### 3. Configure Signing
|
||||
|
||||
Add to `amethyst/build.gradle` inside the `android {}` block:
|
||||
|
||||
```gradle
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
storeFile rootProject.file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update the release buildType to use the signing config:
|
||||
```gradle
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
// ... existing config
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Disable Google Services (Required for F-Droid)
|
||||
|
||||
**⚠️ CRITICAL:** The Google Services plugin fails when you change the package name. For F-Droid builds, disable it.
|
||||
|
||||
Edit `amethyst/build.gradle`, comment out the plugin:
|
||||
```gradle
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
// alias(libs.plugins.googleServices) // DISABLED for F-Droid
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
sdk use java 21.0.5-tem
|
||||
./gradlew assembleFdroidRelease
|
||||
```
|
||||
|
||||
**Build time:** ~9 minutes first build, faster on subsequent builds.
|
||||
|
||||
### 6. Locate APKs
|
||||
|
||||
Output directory: `amethyst/build/outputs/apk/fdroid/release/`
|
||||
|
||||
Files generated:
|
||||
- `amethyst-fdroid-arm64-v8a-release.apk` - ARM64 (most phones)
|
||||
- `amethyst-fdroid-universal-release.apk` - All architectures (larger)
|
||||
|
||||
## Customizations
|
||||
|
||||
### Change App Name
|
||||
|
||||
Edit `amethyst/src/main/res/values/strings.xml`:
|
||||
```xml
|
||||
<string name="app_name" translatable="false">YourAppName</string>
|
||||
<string name="app_name_debug" translatable="false">YourAppName Debug</string>
|
||||
```
|
||||
|
||||
### Change Package ID
|
||||
|
||||
Edit `amethyst/build.gradle`:
|
||||
```gradle
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "com.yourcompany.yourapp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change Project Name
|
||||
|
||||
Edit `settings.gradle`:
|
||||
```gradle
|
||||
rootProject.name = "YourAppName"
|
||||
```
|
||||
|
||||
### Change App Icon
|
||||
|
||||
Replace icon files in:
|
||||
- `amethyst/src/main/res/mipmap-*/ic_launcher.webp`
|
||||
- `amethyst/src/main/res/mipmap-*/ic_launcher_round.webp`
|
||||
|
||||
### Add Client Tag to Posts
|
||||
|
||||
Make your app identify itself on posts with `["client", "YourAppName"]`.
|
||||
|
||||
**1. Create tag builder extension:**
|
||||
|
||||
Create `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/clientTag/TagArrayBuilderExt.kt`:
|
||||
```kotlin
|
||||
package com.vitorpamplona.quartz.nip01Core.tags.clientTag
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.client(clientName: String) =
|
||||
addUnique(arrayOf(ClientTag.TAG_NAME, clientName))
|
||||
```
|
||||
|
||||
**2. Add to TextNoteEvent:**
|
||||
|
||||
Edit `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt`:
|
||||
|
||||
Add import:
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.clientTag.client
|
||||
```
|
||||
|
||||
In both `build()` functions, add after `alt(...)`:
|
||||
```kotlin
|
||||
client("YourAppName")
|
||||
```
|
||||
|
||||
### Modify Default Relays
|
||||
|
||||
Edit relay configuration in `quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/` or the UI settings files.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### google-services.json error
|
||||
Disable the Google Services plugin (see step 4).
|
||||
|
||||
### Java version error
|
||||
```bash
|
||||
sdk use java 21.0.5-tem
|
||||
java -version # Must show 21.x
|
||||
```
|
||||
|
||||
### Out of memory
|
||||
Edit `gradle.properties`:
|
||||
```properties
|
||||
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m
|
||||
```
|
||||
|
||||
### Clean build
|
||||
```bash
|
||||
./gradlew --stop
|
||||
./gradlew clean
|
||||
./gradlew assembleFdroidRelease
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
Deploy APKs via:
|
||||
- **Surge.sh:** `surge ./releases your-app.surge.sh`
|
||||
- **Zapstore:** Submit to zapstore.dev
|
||||
- **Direct download:** Host on any web server
|
||||
@@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.googleServices)
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
alias(libs.plugins.serialization)
|
||||
@@ -192,6 +191,7 @@ android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
resValues = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+42
-7
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -28,6 +28,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -77,8 +78,19 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val original = "@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
|
||||
|
||||
@@ -185,8 +197,19 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val transformedText =
|
||||
buildAnnotatedStringWithUrlHighlighting(
|
||||
@@ -224,8 +247,20 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val transformedText =
|
||||
buildAnnotatedStringWithUrlHighlighting(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -26,10 +26,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -65,6 +65,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -137,6 +139,10 @@ 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 nip05Client = Nip05Client(nip05Fetcher)
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -98,9 +98,7 @@ fun debugState(context: Context) {
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Users: " +
|
||||
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
LocalCache.users.filter { _, it -> it.latestMetadata != null }.size +
|
||||
LocalCache.users.filter { _, it -> it.metadataOrNull() != null }.size +
|
||||
" / " +
|
||||
LocalCache.users.size(),
|
||||
)
|
||||
@@ -147,10 +145,8 @@ fun debugState(context: Context) {
|
||||
)
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Observable Events: " +
|
||||
LocalCache.observablesByKindAndETag.size +
|
||||
" / " +
|
||||
LocalCache.observablesByKindAndAuthor.size,
|
||||
"Observables: " +
|
||||
LocalCache.observables.size,
|
||||
)
|
||||
|
||||
Log.d(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -250,7 +250,7 @@ class Account(
|
||||
|
||||
val userMetadata = UserMetadataState(signer, cache, scope, settings)
|
||||
|
||||
override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings)
|
||||
override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings.zapPaymentRequest)
|
||||
|
||||
val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings)
|
||||
val localRelayList = LocalRelayListState(signer, cache, scope, settings)
|
||||
@@ -740,7 +740,7 @@ class Account(
|
||||
outboxRelays.flow.value
|
||||
} else {
|
||||
replyToAuthor.inboxRelays()?.ifEmpty { null }?.toSet()
|
||||
?: replyToAuthor.relaysBeingUsed.keys.ifEmpty { null }
|
||||
?: replyToAuthor.allUsedRelaysOrNull()
|
||||
?: cache.relayHints
|
||||
.hintsForKey(replyToAuthor.pubkeyHex)
|
||||
.ifEmpty { null }
|
||||
@@ -767,7 +767,7 @@ class Account(
|
||||
notificationRelays.flow.value
|
||||
} else {
|
||||
user.inboxRelays()?.ifEmpty { null }?.toSet()
|
||||
?: (cache.relayHints.hintsForKey(user.pubkeyHex).toSet() + user.relaysBeingUsed.keys)
|
||||
?: (cache.relayHints.hintsForKey(user.pubkeyHex).toSet() + user.allUsedRelays())
|
||||
}
|
||||
|
||||
private fun computeRelayListForLinkedUser(pubkey: HexKey): Set<NormalizedRelayUrl> =
|
||||
@@ -822,7 +822,7 @@ class Account(
|
||||
} else {
|
||||
val relays =
|
||||
author.outboxRelays()?.ifEmpty { null }
|
||||
?: author.relaysBeingUsed.keys.ifEmpty { null }
|
||||
?: author.allUsedRelaysOrNull()
|
||||
?: cache.relayHints.hintsForKey(author.pubkeyHex)
|
||||
|
||||
relayList.addAll(relays)
|
||||
@@ -1741,7 +1741,7 @@ class Account(
|
||||
|
||||
val relayList =
|
||||
dvmPublicKey.inboxRelays()?.toSet()?.ifEmpty { null }
|
||||
?: (dvmPublicKey.relaysBeingUsed.keys + cache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex))
|
||||
?: (dvmPublicKey.allUsedRelays() + cache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex))
|
||||
|
||||
cache.justConsumeMyOwnEvent(request)
|
||||
onReady(request)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -81,9 +81,9 @@ val DefaultNIP65List =
|
||||
|
||||
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
|
||||
|
||||
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
|
||||
val DefaultSearchRelayList = setOf(Constants.wine, Constants.where, Constants.nostoday, Constants.antiprimal)
|
||||
|
||||
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds)
|
||||
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds, Constants.yabu, Constants.nostr1)
|
||||
|
||||
val DefaultSignerPermissions =
|
||||
listOf(
|
||||
@@ -122,7 +122,7 @@ class AccountSettings(
|
||||
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
var zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
|
||||
val zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
var hideNIP17WarningDialog: Boolean = false,
|
||||
@@ -144,7 +144,7 @@ class AccountSettings(
|
||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||
var backupTrustProviderList: TrustProviderListEvent? = null,
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
var backupNipA3PaymentTargets: PaymentTargetsEvent? = null,
|
||||
) : EphemeralChatRepository,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -28,7 +28,6 @@ object Constants {
|
||||
val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net")
|
||||
val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io")
|
||||
val wine = RelayUrlNormalizer.normalize("wss://nostr.wine")
|
||||
val band = RelayUrlNormalizer.normalize("wss://relay.nostr.band")
|
||||
|
||||
val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com")
|
||||
val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org")
|
||||
@@ -37,6 +36,7 @@ object Constants {
|
||||
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
|
||||
|
||||
val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today")
|
||||
val antiprimal = RelayUrlNormalizer.normalize("wss://antiprimal.net")
|
||||
|
||||
val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com")
|
||||
val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com")
|
||||
@@ -44,9 +44,9 @@ object Constants {
|
||||
val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es")
|
||||
val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social")
|
||||
val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es")
|
||||
val yabu = RelayUrlNormalizer.normalize("wss://directory.yabu.me")
|
||||
val nostr1 = RelayUrlNormalizer.normalize("wss://profiles.nostr1.com")
|
||||
|
||||
val bootstrapInbox = setOf(band, damus, primal, mom, nos, bitcoiner, oxtr)
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr)
|
||||
|
||||
val defaultSearchRelaySet = setOf(band, wine, where)
|
||||
val bootstrapInbox = setOf(damus, primal, mom, nos, bitcoiner, oxtr, yabu)
|
||||
val eventFinderRelays = setOf(wine, damus, primal, mom, nos, bitcoiner, oxtr)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.Skull
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Weed
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Zap
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.ui.components.HashTag
|
||||
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -28,12 +28,14 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtIdHexComparator
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.EventListMatchingFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.NoteListMatchingFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.Observable
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList
|
||||
import com.vitorpamplona.amethyst.commons.services.nwc.NwcPaymentTracker
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
||||
import com.vitorpamplona.amethyst.service.BundledInsert
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
||||
@@ -62,6 +64,9 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isAddressable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isRegular
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isReplaceable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.tagValueContains
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
@@ -72,13 +77,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.forEachTaggedEventId
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
|
||||
@@ -129,7 +133,6 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
@@ -194,19 +197,22 @@ import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.SortedSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
interface ILocalCache {
|
||||
@@ -234,77 +240,104 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
val deletionIndex = DeletionIndex()
|
||||
|
||||
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
|
||||
val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
|
||||
val observables = ConcurrentHashMap<Observable, Observable>(10)
|
||||
|
||||
fun <T : Event> observeETag(
|
||||
kind: Int,
|
||||
eventId: HexKey,
|
||||
scope: CoroutineScope,
|
||||
): LatestByKindWithETag<T> {
|
||||
var eTagList = observablesByKindAndETag.get(kind)
|
||||
|
||||
if (eTagList == null) {
|
||||
eTagList = ConcurrentHashMap<HexKey, LatestByKindWithETag<T>>(1) as ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>
|
||||
observablesByKindAndETag.put(kind, eTagList)
|
||||
}
|
||||
|
||||
val value = eTagList.get(eventId)
|
||||
|
||||
return if (value != null) {
|
||||
value
|
||||
fun Filter.match(note: Note): Boolean {
|
||||
val event = note.event
|
||||
return if (event != null) {
|
||||
match(event)
|
||||
} else {
|
||||
val newObject = LatestByKindWithETag<T>(kind, eventId) as LatestByKindWithETag<Event>
|
||||
val obj = eTagList.putIfAbsent(eventId, newObject) ?: newObject
|
||||
if (obj == newObject) {
|
||||
// initialize
|
||||
scope.launch(Dispatchers.IO) {
|
||||
obj.init()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(filter: Filter): SortedSet<Note> {
|
||||
val byKinds = filter.kinds?.filter { it.isAddressable() || it.isReplaceable() }
|
||||
|
||||
val addressableMatches =
|
||||
if (!byKinds.isNullOrEmpty()) {
|
||||
val byAuthors = filter.authors
|
||||
if (!byAuthors.isNullOrEmpty()) {
|
||||
// optimized
|
||||
byKinds.flatMap { kind ->
|
||||
byAuthors.flatMap { pubkey ->
|
||||
addressables.filter(kind, pubkey) { _, note ->
|
||||
filter.match(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// optimized
|
||||
byKinds.flatMap { kind ->
|
||||
addressables.filter(kind) { _, note ->
|
||||
filter.match(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addressables.filter { _, note ->
|
||||
filter.match(note)
|
||||
}
|
||||
}
|
||||
obj
|
||||
} as LatestByKindWithETag<T>
|
||||
}
|
||||
|
||||
fun <T : Event> observeAuthor(
|
||||
kind: Int,
|
||||
pubkey: HexKey,
|
||||
scope: CoroutineScope,
|
||||
): LatestByKindAndAuthor<T> {
|
||||
var authorObsList = observablesByKindAndAuthor.get(kind)
|
||||
|
||||
if (authorObsList == null) {
|
||||
authorObsList = ConcurrentHashMap<HexKey, LatestByKindAndAuthor<T>>(1) as ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>
|
||||
observablesByKindAndAuthor.put(kind, authorObsList)
|
||||
}
|
||||
|
||||
val value = authorObsList.get(pubkey)
|
||||
|
||||
return if (value != null) {
|
||||
value
|
||||
} else {
|
||||
val newObject = LatestByKindAndAuthor<T>(kind, pubkey) as LatestByKindAndAuthor<Event>
|
||||
val obj = authorObsList.putIfAbsent(pubkey, newObject) ?: newObject
|
||||
if (obj == newObject) {
|
||||
// initialize
|
||||
scope.launch(Dispatchers.IO) {
|
||||
obj.init()
|
||||
val noteMatches =
|
||||
notes.filter { _, note ->
|
||||
val event = note.event
|
||||
if (event != null && event.kind.isRegular()) {
|
||||
filter.match(event)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
obj
|
||||
} as LatestByKindAndAuthor<T>
|
||||
}
|
||||
|
||||
private fun updateObservables(event: Event) {
|
||||
observablesByKindAndETag[event.kind]?.let { observablesOfKind ->
|
||||
event.forEachTaggedEventId {
|
||||
observablesOfKind[it]?.updateIfMatches(event)
|
||||
val limit = filter.limit
|
||||
|
||||
val limitedSet =
|
||||
if (limit != null) {
|
||||
(addressableMatches + noteMatches).take(limit)
|
||||
} else {
|
||||
(addressableMatches + noteMatches)
|
||||
}
|
||||
}
|
||||
|
||||
observablesByKindAndAuthor[event.kind]?.get(event.pubKey)?.updateIfMatches(event)
|
||||
return limitedSet.toSortedSet(CreatedAtIdHexComparator)
|
||||
}
|
||||
|
||||
fun observeNotes(filter: Filter): Flow<List<Note>> =
|
||||
callbackFlow {
|
||||
val newFilter =
|
||||
NoteListMatchingFilter(filter, this@LocalCache::filter) {
|
||||
trySend(it)
|
||||
}
|
||||
|
||||
newFilter.init()
|
||||
|
||||
observables.put(newFilter, newFilter)
|
||||
|
||||
awaitClose {
|
||||
observables.remove(newFilter)
|
||||
}
|
||||
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
|
||||
|
||||
fun observeEvents(filter: Filter): Flow<List<Event>> =
|
||||
callbackFlow {
|
||||
val cachedFilter =
|
||||
EventListMatchingFilter(filter, this@LocalCache::filter) {
|
||||
trySend(it)
|
||||
}
|
||||
|
||||
cachedFilter.init()
|
||||
|
||||
observables.put(cachedFilter, cachedFilter)
|
||||
|
||||
awaitClose {
|
||||
observables.remove(cachedFilter)
|
||||
}
|
||||
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
|
||||
|
||||
fun <T : Event> observeLatestEvent(filter: Filter) = observeEvents(filter).map { it.firstNotNullOfOrNull { it as? T } }
|
||||
|
||||
fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() }
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull()
|
||||
|
||||
fun load(keys: List<String>): List<User> = keys.mapNotNull(::checkGetOrCreateUser)
|
||||
@@ -334,6 +367,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
return count
|
||||
}
|
||||
|
||||
fun countContactLists(predicate: (ContactListEvent) -> Boolean): Int {
|
||||
var count = 0
|
||||
addressables.filter(ContactListEvent.KIND).forEach { note ->
|
||||
val event = note.event as? ContactListEvent
|
||||
if (event != null && predicate(event)) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) }
|
||||
|
||||
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
|
||||
@@ -490,20 +532,16 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
// new event
|
||||
val oldUser = getOrCreateUser(event.pubKey)
|
||||
val currentMetadata = oldUser.latestMetadata
|
||||
consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
if (currentMetadata == null || event.createdAt > currentMetadata.createdAt) {
|
||||
oldUser.latestMetadata = event
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (user.metadata().shouldUpdateWith(event)) {
|
||||
val newUserMetadata = event.contactMetaData()
|
||||
if (newUserMetadata != null && (wasVerified || justVerify(event))) {
|
||||
oldUser.updateUserInfo(newUserMetadata, event)
|
||||
user.updateUserInfo(newUserMetadata, event)
|
||||
if (relay != null) {
|
||||
oldUser.addRelayBeingUsed(relay, event.createdAt)
|
||||
if (!relay.isLocalHost()) {
|
||||
oldUser.latestMetadataRelay = relay
|
||||
}
|
||||
user.addRelayBeingUsed(relay, event.createdAt)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -517,25 +555,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
event: ContactListEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
|
||||
// avoids processing empty contact lists.
|
||||
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty() && (wasVerified || justVerify(event))) {
|
||||
val needsToUpdateFollowers = user.updateContactList(event)
|
||||
// Log.d("CL", "Consumed contact list ${user.toNostrUri()} ${event.relays()?.size}")
|
||||
|
||||
needsToUpdateFollowers.forEach {
|
||||
getUserIfExists(it)?.flowSet?.followers?.invalidateData()
|
||||
}
|
||||
|
||||
updateObservables(event)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: BookmarkListEvent,
|
||||
@@ -878,59 +898,101 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
@Suppress("DEPRECATION")
|
||||
fun computeReplyTo(event: Event): List<Note> =
|
||||
when (event) {
|
||||
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is CommentEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is PollNoteEvent -> {
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is VoiceReplyEvent -> event.markedReplyTos().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LongTextNoteEvent -> {
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is ChatMessageEncryptedFileHeaderEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is GitReplyEvent -> {
|
||||
event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is LnZapEvent ->
|
||||
is TextNoteEvent -> {
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is CommentEvent -> {
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is VoiceReplyEvent -> {
|
||||
event.markedReplyTos().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is ChatMessageEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is ChatMessageEncryptedFileHeaderEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is LnZapEvent -> {
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||
(event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList())
|
||||
is LnZapRequestEvent ->
|
||||
}
|
||||
|
||||
is LnZapRequestEvent -> {
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is BadgeProfilesEvent ->
|
||||
}
|
||||
|
||||
is BadgeProfilesEvent -> {
|
||||
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
|
||||
is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) }
|
||||
is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is RepostEvent ->
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
event.awardDefinition().map { getOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is RepostEvent -> {
|
||||
listOfNotNull(
|
||||
event.boostedEventId()?.let { checkGetOrCreateNote(it) },
|
||||
event.boostedAddress()?.let { getOrCreateAddressableNote(it) },
|
||||
)
|
||||
is GenericRepostEvent ->
|
||||
}
|
||||
|
||||
is GenericRepostEvent -> {
|
||||
listOfNotNull(
|
||||
event.boostedEventId()?.let { checkGetOrCreateNote(it) },
|
||||
event.boostedAddress()?.let { getOrCreateAddressableNote(it) },
|
||||
)
|
||||
is CommunityPostApprovalEvent ->
|
||||
}
|
||||
|
||||
is CommunityPostApprovalEvent -> {
|
||||
event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.approvedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is ReactionEvent ->
|
||||
}
|
||||
|
||||
is ReactionEvent -> {
|
||||
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is ChannelMessageEvent ->
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.channelId() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LiveActivitiesChatMessageEvent ->
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.activity()?.toTag() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is TorrentCommentEvent ->
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
else -> emptyList()
|
||||
is ChannelMessageEvent -> {
|
||||
event.tagsWithoutCitations().filter { it != event.channelId() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is LiveActivitiesChatMessageEvent -> {
|
||||
event.tagsWithoutCitations().filter { it != event.activity()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is TorrentCommentEvent -> {
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(
|
||||
@@ -1183,33 +1245,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val note = event.toAddressableNote()
|
||||
val new = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
val isVerified =
|
||||
if (version.event == null && (wasVerified || justVerify(event))) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
true
|
||||
} else {
|
||||
wasVerified
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id == event.id) return false
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
author.flowSet?.statuses?.invalidateData()
|
||||
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
if (new) {
|
||||
author.statusState().addStatus(note)
|
||||
}
|
||||
|
||||
return false
|
||||
return new
|
||||
}
|
||||
|
||||
fun Event.toNote() = getOrCreateNote(id)
|
||||
@@ -1472,6 +1516,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
getUserIfExists(deletedEvent.aboutUser())?.cardsOrNull()?.removeCard(deleteNote)
|
||||
}
|
||||
|
||||
if (deleteNote is AddressableNote && deletedEvent is StatusEvent) {
|
||||
deleteNote.author?.statusStateOrNull()?.removeStatus(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is TorrentCommentEvent) {
|
||||
deletedEvent.torrentIds()?.let {
|
||||
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||
@@ -1838,13 +1886,11 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
}
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
repliesTo.forEach { it.addZap(zapRequest, note) }
|
||||
mentions.forEach { it.addZap(zapRequest, note) }
|
||||
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
@@ -1872,7 +1918,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
repliesTo.forEach { it.addZap(note, null) }
|
||||
mentions.forEach { it.addZap(note, null) }
|
||||
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
@@ -2125,7 +2170,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
val finds =
|
||||
users.filter { _, user: User ->
|
||||
(
|
||||
(user.anyNameStartsWith(username)) ||
|
||||
(user.metadataOrNull()?.anyNameStartsWith(username) == true) ||
|
||||
user.pubkeyHex.startsWith(username, true) ||
|
||||
user.pubkeyNpub().startsWith(username, true)
|
||||
) &&
|
||||
@@ -2154,7 +2199,9 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
note.event is ReactionEvent ||
|
||||
note.event is LnZapEvent ||
|
||||
note.event is LnZapRequestEvent ||
|
||||
note.event is FileHeaderEvent
|
||||
note.event is FileHeaderEvent ||
|
||||
note.event is MetadataEvent ||
|
||||
note.event is ContactListEvent
|
||||
)
|
||||
|
||||
fun findNotesStartingWith(
|
||||
@@ -2265,23 +2312,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
|
||||
|
||||
fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
checkNotInMainThread()
|
||||
|
||||
return addressables
|
||||
.filter { _, it ->
|
||||
val noteEvent = it.event
|
||||
(
|
||||
noteEvent is StatusEvent &&
|
||||
noteEvent.pubKey == user.pubkeyHex &&
|
||||
!noteEvent.isExpired() &&
|
||||
noteEvent.content.isNotBlank()
|
||||
)
|
||||
}.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt }, { it.idHex }))
|
||||
.reversed()
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
suspend fun findEarliestOtsForNote(
|
||||
note: Note,
|
||||
otsVerifCache: VerificationStateCache,
|
||||
@@ -2366,23 +2396,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
fun cleanObservers() {
|
||||
notes.forEach { _, it -> it.clearFlow() }
|
||||
addressables.forEach { _, it -> it.clearFlow() }
|
||||
users.forEach { _, it -> it.clearFlow() }
|
||||
|
||||
observablesByKindAndETag.forEach { _, list ->
|
||||
list.forEach { key, value ->
|
||||
if (value.canDelete()) {
|
||||
list.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observablesByKindAndAuthor.forEach { _, list ->
|
||||
list.forEach { key, value ->
|
||||
if (value.canDelete()) {
|
||||
list.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneHiddenMessagesChannel(
|
||||
@@ -2576,18 +2589,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is LnZapEvent) {
|
||||
noteEvent.zappedAuthor().forEach {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
}
|
||||
}
|
||||
if (noteEvent is LnZapRequestEvent) {
|
||||
noteEvent.zappedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
}
|
||||
}
|
||||
if (noteEvent is ReportEvent) {
|
||||
noteEvent.reportedAuthor().forEach {
|
||||
getUserIfExists(it.pubkey)?.reportsOrNull()?.removeReport(note)
|
||||
@@ -2601,10 +2602,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
||||
}
|
||||
}
|
||||
|
||||
if (note is AddressableNote && noteEvent is ContactCardEvent) {
|
||||
getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note)
|
||||
}
|
||||
|
||||
if (note is AddressableNote && noteEvent is StatusEvent) {
|
||||
note.author?.statusStateOrNull()?.removeStatus(note)
|
||||
}
|
||||
|
||||
note.clearFlow()
|
||||
|
||||
notes.remove(note.idHex)
|
||||
@@ -2663,24 +2669,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden")
|
||||
}
|
||||
|
||||
fun pruneContactLists(loggedIn: Set<HexKey>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
var removingContactList = 0
|
||||
users.forEach { _, user ->
|
||||
if (
|
||||
user.pubkeyHex !in loggedIn &&
|
||||
(user.flowSet == null || user.flowSet?.isInUse() == false) &&
|
||||
user.latestContactList != null
|
||||
) {
|
||||
user.latestContactList = null
|
||||
removingContactList++
|
||||
}
|
||||
}
|
||||
|
||||
println("PRUNE: $removingContactList contact lists")
|
||||
}
|
||||
|
||||
override fun markAsSeen(
|
||||
eventId: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
@@ -2701,11 +2689,23 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
private fun refreshNewNoteObservers(newNote: Note) {
|
||||
val event = newNote.event as Event
|
||||
updateObservables(event)
|
||||
|
||||
val observableBiConsumer =
|
||||
java.util.function.BiConsumer<Observable, Observable> { t, u ->
|
||||
u.new(event, newNote)
|
||||
}
|
||||
|
||||
observables.forEach(observableBiConsumer)
|
||||
live.newNote(newNote)
|
||||
}
|
||||
|
||||
private fun refreshDeletedNoteObservers(newNote: Note) {
|
||||
val observableBiConsumer =
|
||||
java.util.function.BiConsumer<Observable, Observable> { t, u ->
|
||||
u.remove(newNote)
|
||||
}
|
||||
|
||||
observables.forEach(observableBiConsumer)
|
||||
live.removedNote(newNote)
|
||||
}
|
||||
|
||||
@@ -2733,27 +2733,38 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
fun consume(nip19: Entity) {
|
||||
when (nip19) {
|
||||
is NSec -> getOrCreateUser(nip19.toPubKeyHex())
|
||||
is NPub -> getOrCreateUser(nip19.hex)
|
||||
is NSec -> {
|
||||
getOrCreateUser(nip19.toPubKeyHex())
|
||||
}
|
||||
|
||||
is NPub -> {
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
|
||||
is NProfile -> {
|
||||
nip19.relay.forEach { relayHint ->
|
||||
relayHints.addKey(nip19.hex, relayHint)
|
||||
}
|
||||
getOrCreateUser(nip19.hex)
|
||||
}
|
||||
|
||||
is NNote -> {
|
||||
getOrCreateNote(nip19.hex)
|
||||
}
|
||||
|
||||
is NEvent -> {
|
||||
nip19.relay.forEach { relayHint ->
|
||||
relayHints.addEvent(nip19.hex, relayHint)
|
||||
}
|
||||
getOrCreateNote(nip19.hex)
|
||||
}
|
||||
|
||||
is NEmbed -> {
|
||||
justConsume(nip19.event, null, false)
|
||||
}
|
||||
|
||||
is NRelay -> {}
|
||||
|
||||
is NAddress -> {
|
||||
val aTag = nip19.aTag()
|
||||
nip19.relay.forEach { relayHint ->
|
||||
@@ -2761,6 +2772,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
}
|
||||
getOrCreateAddressableNote(nip19.address())
|
||||
}
|
||||
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
@@ -2810,7 +2822,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
return justConsumeAndUpdateIndexes(event, relay?.url, wasVerified)
|
||||
}
|
||||
|
||||
private fun checkDeletionAndConsume(
|
||||
fun checkDeletionAndConsume(
|
||||
event: Event,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
@@ -3008,10 +3020,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
is VoiceReplyEvent -> consume(event, relay, wasVerified)
|
||||
is WikiNoteEvent -> consume(event, relay, wasVerified)
|
||||
is PaymentTargetsEvent -> consume(event, relay, wasVerified)
|
||||
else -> {
|
||||
Log.w("Event Not Supported", "From ${relay?.url}: ${event.toJson()}")
|
||||
false
|
||||
}
|
||||
else -> Log.w("Event Not Supported", "From ${relay?.url}: ${event.toJson()}").let { false }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.collection.LruCache
|
||||
|
||||
interface MutableMediaAspectRatioCache {
|
||||
fun get(url: String): Float?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -168,6 +168,7 @@ fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
|
||||
is Bundle -> {
|
||||
resource.entry.associateBy { it.id }.toImmutableMap()
|
||||
}
|
||||
|
||||
else -> {
|
||||
persistentMapOf(resource.id to resource)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -22,6 +22,5 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias Note = com.vitorpamplona.amethyst.commons.model.Note
|
||||
typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer
|
||||
typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -54,9 +54,7 @@ fun parseThemeType(code: Int?): ThemeType =
|
||||
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
|
||||
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
|
||||
ThemeType.DARK.screenCode -> ThemeType.DARK
|
||||
else -> {
|
||||
ThemeType.SYSTEM
|
||||
}
|
||||
else -> ThemeType.SYSTEM
|
||||
}
|
||||
|
||||
enum class ConnectivityType(
|
||||
@@ -91,9 +89,7 @@ fun parseConnectivityType(code: Boolean?): ConnectivityType =
|
||||
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
|
||||
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
|
||||
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
|
||||
else -> {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
else -> ConnectivityType.ALWAYS
|
||||
}
|
||||
|
||||
fun parseConnectivityType(screenCode: Int): ConnectivityType =
|
||||
@@ -101,9 +97,7 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType =
|
||||
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
|
||||
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
|
||||
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
|
||||
else -> {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
else -> ConnectivityType.ALWAYS
|
||||
}
|
||||
|
||||
fun parseFeatureSetType(screenCode: Int): FeatureSetType =
|
||||
@@ -111,18 +105,14 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType =
|
||||
FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE
|
||||
FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED
|
||||
FeatureSetType.PERFORMANCE.screenCode -> FeatureSetType.PERFORMANCE
|
||||
else -> {
|
||||
FeatureSetType.COMPLETE
|
||||
}
|
||||
else -> FeatureSetType.COMPLETE
|
||||
}
|
||||
|
||||
fun parseGalleryType(screenCode: Int): ProfileGalleryType =
|
||||
when (screenCode) {
|
||||
ProfileGalleryType.CLASSIC.screenCode -> ProfileGalleryType.CLASSIC
|
||||
ProfileGalleryType.MODERN.screenCode -> ProfileGalleryType.MODERN
|
||||
else -> {
|
||||
ProfileGalleryType.CLASSIC
|
||||
}
|
||||
else -> ProfileGalleryType.CLASSIC
|
||||
}
|
||||
|
||||
enum class BooleanType(
|
||||
@@ -138,18 +128,14 @@ fun parseBooleanType(code: Boolean?): BooleanType =
|
||||
when (code) {
|
||||
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.prefCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
else -> BooleanType.ALWAYS
|
||||
}
|
||||
|
||||
fun parseBooleanType(screenCode: Int): BooleanType =
|
||||
when (screenCode) {
|
||||
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.screenCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
else -> BooleanType.ALWAYS
|
||||
}
|
||||
|
||||
enum class WarningType(
|
||||
@@ -167,9 +153,7 @@ fun parseWarningType(screenCode: Int): WarningType =
|
||||
WarningType.WARN.screenCode -> WarningType.WARN
|
||||
WarningType.SHOW.screenCode -> WarningType.SHOW
|
||||
WarningType.HIDE.screenCode -> WarningType.HIDE
|
||||
else -> {
|
||||
WarningType.WARN
|
||||
}
|
||||
else -> WarningType.WARN
|
||||
}
|
||||
|
||||
fun parseWarningType(code: Boolean?): WarningType =
|
||||
@@ -177,7 +161,5 @@ fun parseWarningType(code: Boolean?): WarningType =
|
||||
WarningType.WARN.prefCode -> WarningType.WARN
|
||||
WarningType.HIDE.prefCode -> WarningType.HIDE
|
||||
WarningType.SHOW.prefCode -> WarningType.SHOW
|
||||
else -> {
|
||||
WarningType.WARN
|
||||
}
|
||||
else -> WarningType.WARN
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -22,5 +22,3 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias User = com.vitorpamplona.amethyst.commons.model.User
|
||||
typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo
|
||||
typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState
|
||||
|
||||
+7
-3
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -68,13 +68,17 @@ class AccountCacheState(
|
||||
NostrSignerInternal(accountSettings.keyPair)
|
||||
} else {
|
||||
when (val packageName = accountSettings.externalSignerPackageName) {
|
||||
null -> NostrSignerInternal(accountSettings.keyPair)
|
||||
else ->
|
||||
null -> {
|
||||
NostrSignerInternal(accountSettings.keyPair)
|
||||
}
|
||||
|
||||
else -> {
|
||||
NostrSignerExternal(
|
||||
pubKey = accountSettings.keyPair.pubKey.toHexKey(),
|
||||
packageName = packageName,
|
||||
contentResolver = contentResolverFn(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
accountSettings = accountSettings,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+12
-7
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -30,7 +29,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserMetadataState(
|
||||
@@ -42,11 +40,16 @@ class UserMetadataState(
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val note = cache.getOrCreateAddressableNote(getMetadataEventAddress())
|
||||
|
||||
fun getMetadataEventAddress() = MetadataEvent.createAddress(signer.pubKey)
|
||||
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getUserMetadataFlow(): StateFlow<UserState> = user.flow().metadata.stateFlow
|
||||
fun getUserMetadataFlow() = note.flow().metadata.stateFlow
|
||||
|
||||
fun getUserMetadataEvent(): MetadataEvent? = user.latestMetadata
|
||||
fun getUserMetadataEvent(): MetadataEvent? = note.event as? MetadataEvent
|
||||
|
||||
suspend fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
@@ -116,8 +119,10 @@ class UserMetadataState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
|
||||
getUserMetadataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
|
||||
settings.updateUserMetadata(it.user.latestMetadata)
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${user.toBestDisplayName()}")
|
||||
(it.note.event as? MetadataEvent)?.let {
|
||||
settings.updateUserMetadata(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -21,10 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -52,14 +52,19 @@ class Kind3FollowListState(
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getFollowListFlow(): StateFlow<UserState> = user.flow().follows.stateFlow
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val note = cache.getOrCreateAddressableNote(getFollowListAddress())
|
||||
|
||||
fun getFollowListEvent(): ContactListEvent? = user.latestContactList
|
||||
fun getFollowListAddress() = ContactListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getFollowListFlow(): StateFlow<NoteState> = note.flow().metadata.stateFlow
|
||||
|
||||
fun getFollowListEvent(): ContactListEvent? = note.event as? ContactListEvent
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val innerFlow: Flow<Kind3Follows> =
|
||||
getFollowListFlow().transformLatest {
|
||||
emit(buildKind3Follows(it.user.latestContactList ?: settings.backupContactList))
|
||||
emit(buildKind3Follows(it.note.event as? ContactListEvent ?: settings.backupContactList))
|
||||
}
|
||||
|
||||
val flow =
|
||||
@@ -149,7 +154,9 @@ class Kind3FollowListState(
|
||||
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
|
||||
getFollowListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
|
||||
settings.updateContactListTo(it.user.latestContactList)
|
||||
(it.note.event as? ContactListEvent)?.let {
|
||||
settings.updateContactListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+10
-3
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -74,7 +74,10 @@ class Nip11CachedRetriever(
|
||||
val doc = relayInformationDocumentCache.get(relay)
|
||||
if (doc != null) {
|
||||
when (doc) {
|
||||
is RetrieveResult.Success -> onInfo(doc.data)
|
||||
is RetrieveResult.Success -> {
|
||||
onInfo(doc.data)
|
||||
}
|
||||
|
||||
is RetrieveResult.Loading -> {
|
||||
if (doc.isValid()) {
|
||||
// just wait.
|
||||
@@ -82,6 +85,7 @@ class Nip11CachedRetriever(
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
|
||||
is RetrieveResult.Error -> {
|
||||
if (doc.isValid()) {
|
||||
onError(relay, doc.error, null)
|
||||
@@ -89,7 +93,10 @@ class Nip11CachedRetriever(
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
is RetrieveResult.Empty -> retrieve(relay, onInfo, onError)
|
||||
|
||||
is RetrieveResult.Empty -> {
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retrieve(relay, onInfo, onError)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+16
-16
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -21,11 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.model.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.INwcSignerState
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -59,14 +60,14 @@ import kotlinx.coroutines.launch
|
||||
* @property signer the main Nostr signer used for general Nostr operations
|
||||
* @property cache the local cache for handling notes and events
|
||||
* @property scope the coroutine scope used for async operations
|
||||
* @property settings the account settings containing NIP-47 configuration
|
||||
* @property nip47Setup the NIP-47 configuration
|
||||
*/
|
||||
class NwcSignerState(
|
||||
val signer: NostrSigner,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
val nip47Setup: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?>,
|
||||
) : INwcSignerState {
|
||||
/**
|
||||
* Derives a NIP-47 signer from the zap payment request in settings.
|
||||
@@ -74,14 +75,14 @@ class NwcSignerState(
|
||||
* Flows updates whenever settings change.
|
||||
*/
|
||||
val nip47Signer =
|
||||
settings.zapPaymentRequest
|
||||
nip47Setup
|
||||
.map {
|
||||
buildSigner(it) ?: signer
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
buildSigner(settings.zapPaymentRequest.value) ?: signer,
|
||||
buildSigner(nip47Setup.value) ?: signer,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -111,30 +112,30 @@ class NwcSignerState(
|
||||
NostrSignerInternal(KeyPair(it))
|
||||
}
|
||||
|
||||
fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null
|
||||
fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null
|
||||
|
||||
override fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex
|
||||
override fun isNIP47Author(pubkey: HexKey?): Boolean = nip47Signer.value.pubKey == pubkey
|
||||
|
||||
/**
|
||||
* Decrypts a NIP-47 payment request using the current signer.
|
||||
*
|
||||
* @param nwcRequest the NIP-47 payment request event to decrypt
|
||||
* @param event the NIP-47 payment request event to decrypt
|
||||
* @return the decrypted request or null if not set up or decryption fails
|
||||
*/
|
||||
override suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? {
|
||||
override suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? {
|
||||
if (!hasWalletConnectSetup()) return null
|
||||
return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest)
|
||||
return zapPaymentRequestDecryptionCache.value.decryptRequest(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a NIP-47 payment response using the current signer.
|
||||
*
|
||||
* @param nwsResponse the NIP-47 payment response event to decrypt
|
||||
* @param event the NIP-47 payment response event to decrypt
|
||||
* @return the decrypted response or null if not set up or decryption fails
|
||||
*/
|
||||
override suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? {
|
||||
override suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? {
|
||||
if (!hasWalletConnectSetup()) return null
|
||||
return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse)
|
||||
return zapPaymentResponseDecryptionCache.value.decryptResponse(event)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +153,7 @@ class NwcSignerState(
|
||||
zappedNote: Note?,
|
||||
onResponse: (Response?) -> Unit,
|
||||
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
|
||||
val walletService = settings.zapPaymentRequest.value
|
||||
if (walletService == null) throw IllegalArgumentException("No NIP47 setup")
|
||||
val walletService = nip47Setup.value ?: throw IllegalArgumentException("No NIP47 setup")
|
||||
|
||||
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value)
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -81,7 +81,7 @@ class BlockPeopleListState(
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.create(
|
||||
name = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
title = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
person = UserTag(pubkeyHex),
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -87,7 +87,7 @@ class LabeledBookmarkListsState(
|
||||
suspend fun LabeledBookmarkListEvent.toLabeledBookmarkList() =
|
||||
LabeledBookmarkList(
|
||||
identifier = dTag(),
|
||||
title = nameOrTitle() ?: dTag(),
|
||||
title = titleOrName() ?: dTag(),
|
||||
description = description(),
|
||||
image = image(),
|
||||
privateBookmarks = privateBookmarks(signer)?.toSet() ?: emptySet(),
|
||||
@@ -160,7 +160,7 @@ class LabeledBookmarkListsState(
|
||||
) {
|
||||
val newList =
|
||||
LabeledBookmarkListEvent.create(
|
||||
name = listName,
|
||||
title = listName,
|
||||
description = listDescription,
|
||||
image = listImage,
|
||||
publicBookmarks = if (!isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(),
|
||||
@@ -200,7 +200,7 @@ class LabeledBookmarkListsState(
|
||||
val renamedList =
|
||||
LabeledBookmarkListEvent.modifyName(
|
||||
earlierVersion = listEvent,
|
||||
newName = newName,
|
||||
newTitle = newName,
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(renamedList)
|
||||
@@ -229,7 +229,7 @@ class LabeledBookmarkListsState(
|
||||
) {
|
||||
val clonedList =
|
||||
LabeledBookmarkListEvent.create(
|
||||
name = customCloneName ?: currentBookmarkList.title,
|
||||
title = customCloneName ?: currentBookmarkList.title,
|
||||
description = customCloneDescription ?: currentBookmarkList.description,
|
||||
publicBookmarks = currentBookmarkList.publicBookmarks.toList(),
|
||||
privateBookmarks = currentBookmarkList.privateBookmarks.toList(),
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -118,7 +118,7 @@ class PeopleListsState(
|
||||
suspend fun PeopleListEvent.toUI() =
|
||||
PeopleList(
|
||||
identifierTag = this.dTag(),
|
||||
title = this.nameOrTitle() ?: this.dTag(),
|
||||
title = this.titleOrName() ?: this.dTag(),
|
||||
description = this.description(),
|
||||
image = this.image(),
|
||||
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
||||
@@ -197,7 +197,7 @@ class PeopleListsState(
|
||||
val newListTemplate =
|
||||
PeopleListEvent.build(
|
||||
dTag = dTag,
|
||||
name = listName,
|
||||
title = listName,
|
||||
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
signer = account.signer,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user