mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-08 20:08:06 +02:00
- Adds a sync Signer to facilitate library
- Separates Account actions from Account state in two objects - Changes Startup procedures to start with Account state and not the full account object - Moves scope for flows in Account from an Application-wide scope to ViewModel scope - Removes all LiveData objects from Account in favor of flows from the state object - Migrates settings saving logic to flows - Migrates PushNotification services to work without Account and only Account Settings. - Migrates the spam filter from LiveData to Flows - Adds Default lists for NIP-65 inbox and outbox relays - Adds Default lists for Search relays - Adds local backup for UserMetadata objects - Adds local backup for Mute lists - Adds local backup for NIP-65 relays - Adds local backup for DM Relays - Adds local backup for private home relays - Rewrites state flows initializers to avoid inconsistent startups
This commit is contained in:
parent
f3161ada8d
commit
4e3b6d0299
@ -25,6 +25,7 @@ import android.graphics.Color
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.Nip96Retriever
|
||||
@ -33,6 +34,9 @@ import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Ignore
|
||||
@ -61,7 +65,11 @@ class ImageUploadTesting {
|
||||
val bytes = baos.toByteArray()
|
||||
val inputStream = bytes.inputStream()
|
||||
|
||||
val account = Account(KeyPair())
|
||||
val account =
|
||||
Account(
|
||||
AccountSettings(KeyPair()),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
|
||||
val result =
|
||||
Nip96Uploader(account)
|
||||
|
@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@ -30,7 +31,9 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.Test
|
||||
@ -133,7 +136,7 @@ class ThreadAssemblerTest {
|
||||
null,
|
||||
)
|
||||
|
||||
val account = Account(KeyPair())
|
||||
val account = Account(AccountSettings(KeyPair()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()))
|
||||
withContext(Dispatchers.Main) {
|
||||
val user = account.userProfile().live()
|
||||
}
|
||||
|
@ -33,10 +33,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
|
||||
@ -46,7 +43,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
}
|
||||
|
||||
private val appContext = Amethyst.instance.applicationContext
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val scope = Amethyst.instance.applicationIOScope
|
||||
private val eventCache = LruCache<String, String>(100)
|
||||
private val pushHandler = PushDistributorHandler
|
||||
|
||||
@ -111,7 +108,6 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
instance: String,
|
||||
) {
|
||||
Log.d(TAG, "Registration failed for Instance: $instance")
|
||||
scope.cancel()
|
||||
pushHandler.forceRemoveDistributor(context)
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ class Amethyst : Application() {
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// Service Manager is only active when the activity is active.
|
||||
val serviceManager = ServiceManager()
|
||||
val serviceManager = ServiceManager(applicationIOScope)
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
|
@ -26,7 +26,7 @@ import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
@ -47,9 +47,10 @@ import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -91,9 +92,13 @@ private object PrefKeys {
|
||||
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
|
||||
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
|
||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||
const val LATEST_USER_METADATA = "latestUserMetadata"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
|
||||
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
|
||||
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_MUTE_LIST = "latestMuteList"
|
||||
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
|
||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
||||
@ -117,58 +122,68 @@ object LocalPreferences {
|
||||
|
||||
private var currentAccount: String? = null
|
||||
private var savedAccounts: List<AccountInfo>? = null
|
||||
private var cachedAccounts: MutableMap<String, Account?> = mutableMapOf()
|
||||
private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
||||
|
||||
suspend fun currentAccount(): String? {
|
||||
if (currentAccount == null) {
|
||||
currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
currentAccount =
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
}
|
||||
}
|
||||
return currentAccount
|
||||
}
|
||||
|
||||
private fun updateCurrentAccount(npub: String?) {
|
||||
private suspend fun updateCurrentAccount(npub: String?) {
|
||||
if (npub == null) {
|
||||
currentAccount = null
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
}
|
||||
} else if (currentAccount != npub) {
|
||||
currentAccount = npub
|
||||
|
||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun savedAccounts(): List<AccountInfo> {
|
||||
private suspend fun savedAccounts(): List<AccountInfo> {
|
||||
if (savedAccounts == null) {
|
||||
with(encryptedPreferences()) {
|
||||
val newSystemOfAccounts =
|
||||
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
|
||||
Event.mapper.readValue<List<AccountInfo>>(it)
|
||||
}
|
||||
|
||||
if (!newSystemOfAccounts.isNullOrEmpty()) {
|
||||
savedAccounts = newSystemOfAccounts
|
||||
} else {
|
||||
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
|
||||
|
||||
val migrated =
|
||||
oldAccounts.map { npub ->
|
||||
AccountInfo(
|
||||
npub,
|
||||
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
|
||||
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "")
|
||||
.isNotBlank(),
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
with(encryptedPreferences()) {
|
||||
val newSystemOfAccounts =
|
||||
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
|
||||
Event.mapper.readValue<List<AccountInfo>>(it)
|
||||
}
|
||||
|
||||
savedAccounts = migrated
|
||||
if (!newSystemOfAccounts.isNullOrEmpty()) {
|
||||
savedAccounts = newSystemOfAccounts
|
||||
} else {
|
||||
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
|
||||
|
||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
|
||||
val migrated =
|
||||
oldAccounts.map { npub ->
|
||||
AccountInfo(
|
||||
npub,
|
||||
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
|
||||
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "")
|
||||
.isNotBlank(),
|
||||
)
|
||||
}
|
||||
|
||||
savedAccounts = migrated
|
||||
|
||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return savedAccounts!!
|
||||
}
|
||||
|
||||
fun cachedAccounts() = savedAccounts
|
||||
|
||||
private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (savedAccounts != accounts) {
|
||||
@ -189,35 +204,33 @@ object LocalPreferences {
|
||||
updateSavedAccounts(accounts)
|
||||
}
|
||||
|
||||
private suspend fun setCurrentAccount(account: Account) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val npub = account.userProfile().pubkeyNpub()
|
||||
val accInfo =
|
||||
AccountInfo(
|
||||
npub,
|
||||
account.isWriteable(),
|
||||
account.signer is NostrSignerExternal,
|
||||
)
|
||||
updateCurrentAccount(npub)
|
||||
addAccount(accInfo)
|
||||
}
|
||||
private suspend fun setCurrentAccount(accountSettings: AccountSettings) {
|
||||
val npub = accountSettings.keyPair.pubKey.toNpub()
|
||||
val accInfo =
|
||||
AccountInfo(
|
||||
npub,
|
||||
accountSettings.isWriteable(),
|
||||
accountSettings.externalSignerPackageName != null,
|
||||
)
|
||||
updateCurrentAccount(npub)
|
||||
addAccount(accInfo)
|
||||
}
|
||||
|
||||
suspend fun switchToAccount(accountInfo: AccountInfo) = withContext(Dispatchers.IO) { updateCurrentAccount(accountInfo.npub) }
|
||||
suspend fun switchToAccount(accountInfo: AccountInfo) = updateCurrentAccount(accountInfo.npub)
|
||||
|
||||
/** Removes the account from the app level shared preferences */
|
||||
private suspend fun removeAccount(accountInfo: AccountInfo) {
|
||||
val accounts = savedAccounts().filter { it.npub != accountInfo.npub }
|
||||
updateSavedAccounts(accounts)
|
||||
updateSavedAccounts(savedAccounts().filter { it.npub != accountInfo.npub })
|
||||
}
|
||||
|
||||
/** Deletes the npub-specific shared preference file */
|
||||
private fun deleteUserPreferenceFile(npub: String) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val prefsDir = File(prefsDirPath)
|
||||
prefsDir.list()?.forEach {
|
||||
if (it.contains(npub)) {
|
||||
File(prefsDir, it).delete()
|
||||
private suspend fun deleteUserPreferenceFile(npub: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val prefsDir = File(prefsDirPath)
|
||||
prefsDir.list()?.forEach {
|
||||
if (it.contains(npub)) {
|
||||
File(prefsDir, it).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -246,8 +259,7 @@ object LocalPreferences {
|
||||
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) {
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout")
|
||||
withContext(Dispatchers.IO) {
|
||||
val userPrefs = encryptedPreferences(accountInfo.npub)
|
||||
userPrefs.edit().clear().commit()
|
||||
encryptedPreferences(accountInfo.npub).edit().clear().commit()
|
||||
removeAccount(accountInfo)
|
||||
deleteUserPreferenceFile(accountInfo.npub)
|
||||
|
||||
@ -259,99 +271,134 @@ object LocalPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updatePrefsForLogin(account: Account) {
|
||||
setCurrentAccount(account)
|
||||
saveToEncryptedStorage(account)
|
||||
suspend fun updatePrefsForLogin(accountSettings: AccountSettings) {
|
||||
setCurrentAccount(accountSettings)
|
||||
saveToEncryptedStorage(accountSettings)
|
||||
}
|
||||
|
||||
fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
|
||||
suspend fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
|
||||
|
||||
suspend fun saveToEncryptedStorage(account: Account) {
|
||||
suspend fun saveToEncryptedStorage(settings: AccountSettings) {
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage")
|
||||
withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
|
||||
val prefs = encryptedPreferences(settings.keyPair.pubKey.toNpub())
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
|
||||
if (account.signer is NostrSignerExternal) {
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, settings.externalSignerPackageName != null)
|
||||
if (settings.externalSignerPackageName != null) {
|
||||
remove(PrefKeys.NOSTR_PRIVKEY)
|
||||
putString(PrefKeys.SIGNER_PACKAGE_NAME, account.signer.launcher.signerPackageName)
|
||||
putString(PrefKeys.SIGNER_PACKAGE_NAME, settings.externalSignerPackageName)
|
||||
} else {
|
||||
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||
remove(PrefKeys.SIGNER_PACKAGE_NAME)
|
||||
settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||
}
|
||||
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays))
|
||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, account.localRelayServers)
|
||||
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(settings.localRelays))
|
||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, settings.dontTranslateFrom)
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
||||
putString(
|
||||
PrefKeys.LANGUAGE_PREFS,
|
||||
Event.mapper.writeValueAsString(account.languagePreferences),
|
||||
Event.mapper.writeValueAsString(settings.languagePreferences),
|
||||
)
|
||||
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
|
||||
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(account.zapAmountChoices))
|
||||
putString(PrefKeys.TRANSLATE_TO, settings.translateTo)
|
||||
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(settings.zapAmountChoices.value))
|
||||
putString(
|
||||
PrefKeys.REACTION_CHOICES,
|
||||
Event.mapper.writeValueAsString(account.reactionChoices),
|
||||
Event.mapper.writeValueAsString(settings.reactionChoices.value),
|
||||
)
|
||||
putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.value.name)
|
||||
putString(PrefKeys.DEFAULT_ZAPTYPE, settings.defaultZapType.value.name)
|
||||
putString(
|
||||
PrefKeys.DEFAULT_FILE_SERVER,
|
||||
Event.mapper.writeValueAsString(account.defaultFileServer),
|
||||
Event.mapper.writeValueAsString(settings.defaultFileServer),
|
||||
)
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
|
||||
putString(
|
||||
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
|
||||
account.defaultNotificationFollowList.value,
|
||||
settings.defaultNotificationFollowList.value,
|
||||
)
|
||||
putString(
|
||||
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
|
||||
account.defaultDiscoveryFollowList.value,
|
||||
settings.defaultDiscoveryFollowList.value,
|
||||
)
|
||||
putString(
|
||||
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
||||
Event.mapper.writeValueAsString(account.zapPaymentRequest),
|
||||
Event.mapper.writeValueAsString(settings.zapPaymentRequest),
|
||||
)
|
||||
if (account.backupContactList != null) {
|
||||
if (settings.backupContactList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_CONTACT_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupContactList),
|
||||
Event.mapper.writeValueAsString(settings.backupContactList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_CONTACT_LIST)
|
||||
}
|
||||
|
||||
if (account.backupDMRelayList != null) {
|
||||
if (settings.backupUserMetadata != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_USER_METADATA,
|
||||
Event.mapper.writeValueAsString(settings.backupUserMetadata),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_USER_METADATA)
|
||||
}
|
||||
|
||||
if (settings.backupDMRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_DM_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupDMRelayList),
|
||||
Event.mapper.writeValueAsString(settings.backupDMRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (account.backupNIP65RelayList != null) {
|
||||
if (settings.backupNIP65RelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_NIP65_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupNIP65RelayList),
|
||||
Event.mapper.writeValueAsString(settings.backupNIP65RelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
}
|
||||
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog)
|
||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
|
||||
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
|
||||
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
|
||||
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
|
||||
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
|
||||
if (settings.backupSearchRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_SEARCH_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(settings.backupSearchRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupMuteList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_MUTE_LIST,
|
||||
Event.mapper.writeValueAsString(settings.backupMuteList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_MUTE_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupPrivateHomeRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
}
|
||||
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
|
||||
putBoolean(PrefKeys.USE_PROXY, settings.proxy != null)
|
||||
putInt(PrefKeys.PROXY_PORT, settings.proxyPort)
|
||||
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, settings.warnAboutPostsWithReports)
|
||||
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, settings.filterSpamFromStrangers)
|
||||
|
||||
val regularMap =
|
||||
account.lastReadPerRoute.value.mapValues {
|
||||
settings.lastReadPerRoute.value.mapValues {
|
||||
it.value.value
|
||||
}
|
||||
|
||||
@ -359,23 +406,23 @@ object LocalPreferences {
|
||||
PrefKeys.LAST_READ_PER_ROUTE,
|
||||
Event.mapper.writeValueAsString(regularMap),
|
||||
)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
|
||||
|
||||
if (account.showSensitiveContent.value == null) {
|
||||
if (settings.showSensitiveContent.value == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, settings.showSensitiveContent.value!!)
|
||||
}
|
||||
|
||||
putString(
|
||||
PrefKeys.PENDING_ATTESTATIONS,
|
||||
Event.mapper.writeValueAsString(account.pendingAttestations.value),
|
||||
Event.mapper.writeValueAsString(settings.pendingAttestations.value),
|
||||
)
|
||||
}.apply()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
|
||||
|
||||
suspend fun saveSharedSettings(
|
||||
sharedSettings: Settings,
|
||||
@ -408,39 +455,39 @@ object LocalPreferences {
|
||||
|
||||
val mutex = Mutex()
|
||||
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): Account? =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): AccountSettings? {
|
||||
// if already loaded, return right away
|
||||
if (cachedAccounts.containsKey(npub)) {
|
||||
return cachedAccounts[npub]
|
||||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
if (cachedAccounts.containsKey(npub)) {
|
||||
return@withContext cachedAccounts.get(npub)
|
||||
}
|
||||
|
||||
val account = innerLoadCurrentAccountFromEncryptedStorage(npub)
|
||||
account?.registerObservers()
|
||||
val accountSettings = innerLoadCurrentAccountFromEncryptedStorage(npub)
|
||||
|
||||
cachedAccounts.put(npub, account)
|
||||
cachedAccounts.put(npub, accountSettings)
|
||||
|
||||
return@withContext account
|
||||
return@withContext accountSettings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? {
|
||||
Log.d("LocalPreferences", "Load account from file")
|
||||
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
|
||||
Log.d("LocalPreferences", "Load account from file $npub")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
return@withContext with(encryptedPreferences(npub)) {
|
||||
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
|
||||
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
val privKey = if (loginWithExternalSigner) null else getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||
|
||||
val localRelays =
|
||||
getString(PrefKeys.RELAYS, "[]")?.let {
|
||||
println("LocalRelays: $it")
|
||||
Event.mapper.readValue<Set<RelaySetupInfo>?>(it)
|
||||
}
|
||||
?: setOf<RelaySetupInfo>()
|
||||
val externalSignerPackageName =
|
||||
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
|
||||
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||
|
||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||
@ -454,149 +501,27 @@ object LocalPreferences {
|
||||
val defaultDiscoveryFollowList =
|
||||
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
|
||||
val zapAmountChoices =
|
||||
getString(PrefKeys.ZAP_AMOUNTS, "[]")
|
||||
?.let { Event.mapper.readValue<List<Long>?>(it) }
|
||||
?.ifEmpty { DefaultZapAmounts }
|
||||
?: DefaultZapAmounts
|
||||
|
||||
val reactionChoices =
|
||||
getString(PrefKeys.REACTION_CHOICES, "[]")
|
||||
?.let { Event.mapper.readValue<List<String>?>(it) }
|
||||
?.ifEmpty { DefaultReactions }
|
||||
?: DefaultReactions
|
||||
|
||||
val defaultZapType =
|
||||
getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
||||
LnZapEvent.ZapType.values().firstOrNull { it.name == serverName }
|
||||
}
|
||||
?: LnZapEvent.ZapType.PUBLIC
|
||||
LnZapEvent.ZapType.entries.firstOrNull { it.name == serverName }
|
||||
} ?: LnZapEvent.ZapType.PUBLIC
|
||||
|
||||
val defaultFileServer =
|
||||
try {
|
||||
getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
||||
Event.mapper.readValue<Nip96MediaServers.ServerName>(serverName)
|
||||
}
|
||||
?: Nip96MediaServers.DEFAULT[0]
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
|
||||
e.printStackTrace()
|
||||
Nip96MediaServers.DEFAULT[0]
|
||||
}
|
||||
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()
|
||||
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||
|
||||
val zapPaymentRequestServer =
|
||||
try {
|
||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||
Event.mapper.readValue<Nip47WalletConnect.Nip47URI?>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}",
|
||||
e,
|
||||
)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
val defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
|
||||
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
|
||||
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
|
||||
|
||||
val latestContactList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
|
||||
if (it != "null") {
|
||||
println("Decoding Contact List: $it")
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val latestDmRelayList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_DM_RELAY_LIST, null)?.let {
|
||||
if (it != "null") {
|
||||
println("Decoding DM Relay List: $it")
|
||||
Event.fromJson(it) as ChatMessageRelayListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding DM Relay List ${getString(PrefKeys.LATEST_DM_RELAY_LIST, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val latestNip65RelayList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)?.let {
|
||||
if (it != "null") {
|
||||
println("Decoding NIP65 Relay List: $it")
|
||||
Event.fromJson(it) as AdvertisedRelayListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding NIP65 Relay List ${getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val pendingAttestations =
|
||||
try {
|
||||
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
|
||||
println("Decoding Attestation List: " + it)
|
||||
if (it != null) {
|
||||
Event.mapper.readValue<Map<HexKey, String>>(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val languagePreferences =
|
||||
try {
|
||||
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
||||
Event.mapper.readValue<Map<String, String>?>(it)
|
||||
}
|
||||
?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}",
|
||||
e,
|
||||
)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
|
||||
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
|
||||
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
|
||||
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
@ -615,82 +540,84 @@ object LocalPreferences {
|
||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||
|
||||
val lastReadPerRoute =
|
||||
try {
|
||||
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
|
||||
Event.mapper.readValue<Map<String, Long>?>(it)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
}
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}",
|
||||
e,
|
||||
)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
} ?: mapOf()
|
||||
|
||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||
val signer =
|
||||
if (loginWithExternalSigner) {
|
||||
val packageName =
|
||||
getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: "com.greenart7c3.nostrsigner"
|
||||
NostrSignerExternal(
|
||||
pubKey,
|
||||
ExternalSignerLauncher(pubKey.hexToByteArray().toNpub(), packageName),
|
||||
)
|
||||
} else {
|
||||
NostrSignerInternal(keyPair)
|
||||
}
|
||||
|
||||
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||
|
||||
val account =
|
||||
Account(
|
||||
keyPair = keyPair,
|
||||
signer = signer,
|
||||
localRelays = localRelays,
|
||||
localRelayServers = localRelayServers,
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
translateTo = translateTo,
|
||||
zapAmountChoices = zapAmountChoices,
|
||||
reactionChoices = reactionChoices,
|
||||
defaultZapType = MutableStateFlow(defaultZapType),
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
backupContactList = latestContactList,
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = MutableStateFlow(showSensitiveContent),
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
hasDonatedInVersion = hasDonatedInVersion,
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()),
|
||||
)
|
||||
|
||||
// Loads from DB
|
||||
account.userProfile()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// Loads Live Objects
|
||||
account.userProfile().live()
|
||||
}
|
||||
|
||||
return@with account
|
||||
return@with AccountSettings(
|
||||
keyPair = keyPair,
|
||||
externalSignerPackageName = externalSignerPackageName,
|
||||
localRelays = localRelays,
|
||||
localRelayServers = localRelayServers,
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
translateTo = translateTo,
|
||||
zapAmountChoices = MutableStateFlow(zapAmountChoices),
|
||||
reactionChoices = MutableStateFlow(reactionChoices),
|
||||
defaultZapType = MutableStateFlow(defaultZapType),
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
backupUserMetadata = latestUserMetadata,
|
||||
backupContactList = latestContactList,
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
backupSearchRelayList = latestSearchRelayList,
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||
backupMuteList = latestMuteList,
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = MutableStateFlow(showSensitiveContent),
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
|
||||
val value = getString(key, null)
|
||||
if (value.isNullOrEmpty() || value == "null") {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
if (T::class.java.isInstance(Event::class.java)) {
|
||||
Event.fromJson(value) as T?
|
||||
} else {
|
||||
Event.mapper.readValue<T?>(value)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> SharedPreferences.parseEventOrNull(key: String): T? {
|
||||
val value = getString(key, null)
|
||||
if (value.isNullOrEmpty() || value == "null") {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
Event.fromJson(value) as T?
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.size.Precision
|
||||
import coil.util.DebugLogger
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.Base64Fetcher
|
||||
@ -57,18 +58,19 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Stable
|
||||
class ServiceManager {
|
||||
class ServiceManager(
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var isStarted: Boolean =
|
||||
false // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var account: Account? = null
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var collectorJob: Job? = null
|
||||
|
||||
private fun start(account: Account) {
|
||||
@ -86,9 +88,9 @@ class ServiceManager {
|
||||
val myAccount = account
|
||||
|
||||
// Resets Proxy Use
|
||||
HttpClientManager.setDefaultProxy(account?.proxy)
|
||||
HttpClientManager.setDefaultProxy(account?.settings?.proxy)
|
||||
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
||||
LocalCache.antiSpam.active = account?.settings?.filterSpamFromStrangers ?: true
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance
|
||||
.imageLoaderBuilder()
|
||||
@ -100,8 +102,11 @@ class ServiceManager {
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
add(Base64Fetcher.Factory)
|
||||
} // .logger(DebugLogger())
|
||||
.okHttpClient { HttpClientManager.getHttpClient() }
|
||||
}.apply {
|
||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
||||
this.logger(DebugLogger())
|
||||
}
|
||||
}.okHttpClient { HttpClientManager.getHttpClient() }
|
||||
.precision(Precision.INEXACT)
|
||||
.respectCacheHeaders(false)
|
||||
.build()
|
||||
@ -126,20 +131,23 @@ class ServiceManager {
|
||||
|
||||
// start services
|
||||
NostrAccountDataSource.account = myAccount
|
||||
NostrAccountDataSource.otherAccounts =
|
||||
LocalPreferences.allSavedAccounts().mapNotNull {
|
||||
try {
|
||||
it.npub.bechToBytes().toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
NostrHomeDataSource.account = myAccount
|
||||
NostrChatroomListDataSource.account = myAccount
|
||||
NostrVideoDataSource.account = myAccount
|
||||
NostrDiscoveryDataSource.account = myAccount
|
||||
|
||||
NostrAccountDataSource.otherAccounts =
|
||||
runBlocking {
|
||||
LocalPreferences.allSavedAccounts().mapNotNull {
|
||||
try {
|
||||
it.npub.bechToBytes().toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Elements
|
||||
NostrHomeDataSource.start()
|
||||
NostrAccountDataSource.start()
|
||||
@ -190,7 +198,7 @@ class ServiceManager {
|
||||
LocalCache.cleanObservers()
|
||||
}
|
||||
|
||||
fun trimMemory() {
|
||||
suspend fun trimMemory() {
|
||||
LocalCache.cleanObservers()
|
||||
|
||||
val accounts =
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,522 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.ammolite.relays.Constants
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.net.Proxy
|
||||
import java.util.Locale
|
||||
|
||||
val DefaultChannels =
|
||||
setOf(
|
||||
// Anigma's Nostr
|
||||
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||
// Amethyst's Group
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||
)
|
||||
|
||||
val DefaultReactions =
|
||||
listOf(
|
||||
"\uD83D\uDE80",
|
||||
"\uD83E\uDEC2",
|
||||
"\uD83D\uDC40",
|
||||
"\uD83D\uDE02",
|
||||
"\uD83C\uDF89",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
)
|
||||
|
||||
val DefaultNIP65List =
|
||||
listOf(
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nos.lol/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
)
|
||||
|
||||
val DefaultDMRelayList =
|
||||
listOf(
|
||||
RelayUrlFormatter.normalize("wss://auth.nostr1.com/"),
|
||||
RelayUrlFormatter.normalize("wss://nostr.mom/"),
|
||||
RelayUrlFormatter.normalize("wss://nos.lol/"),
|
||||
)
|
||||
|
||||
val DefaultSearchRelayList =
|
||||
listOf(
|
||||
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
|
||||
RelayUrlFormatter.normalize("wss://nostr.wine"),
|
||||
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
|
||||
)
|
||||
|
||||
val DefaultZapAmounts = listOf(100L, 500L, 1000L)
|
||||
|
||||
fun getLanguagesSpokenByUser(): Set<String> {
|
||||
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
|
||||
val codedList = mutableSetOf<String>()
|
||||
for (i in 0 until languageList.size()) {
|
||||
languageList.get(i)?.let { codedList.add(it.language) }
|
||||
}
|
||||
return codedList
|
||||
}
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val GLOBAL_FOLLOWS = " Global "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val KIND3_FOLLOWS = " All Follows "
|
||||
|
||||
@Stable
|
||||
class AccountSettings(
|
||||
val keyPair: KeyPair,
|
||||
var externalSignerPackageName: String? = null,
|
||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||
var localRelayServers: Set<String> = setOf(),
|
||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
var zapAmountChoices: MutableStateFlow<List<Long>> = MutableStateFlow(DefaultZapAmounts),
|
||||
var reactionChoices: MutableStateFlow<List<String>> = MutableStateFlow(DefaultReactions),
|
||||
val defaultZapType: MutableStateFlow<LnZapEvent.ZapType> = MutableStateFlow(LnZapEvent.ZapType.PUBLIC),
|
||||
var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0],
|
||||
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
|
||||
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
var zapPaymentRequest: Nip47WalletConnect.Nip47URI? = null,
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
var hideNIP17WarningDialog: Boolean = false,
|
||||
var backupUserMetadata: MetadataEvent? = null,
|
||||
var backupContactList: ContactListEvent? = null,
|
||||
var backupDMRelayList: ChatMessageRelayListEvent? = null,
|
||||
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
|
||||
var backupSearchRelayList: SearchRelayListEvent? = null,
|
||||
var backupMuteList: MuteListEvent? = null,
|
||||
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
||||
var proxy: Proxy? = null,
|
||||
var proxyPort: Int = 9050,
|
||||
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
var filterSpamFromStrangers: Boolean = true,
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
) {
|
||||
val saveable = MutableStateFlow(AccountSettingsUpdater(this))
|
||||
|
||||
fun saveAccountSettings() {
|
||||
saveable.update { AccountSettingsUpdater(this) }
|
||||
}
|
||||
|
||||
fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null
|
||||
|
||||
fun createSigner() =
|
||||
if (keyPair.privKey != null) {
|
||||
NostrSignerInternal(keyPair)
|
||||
} else {
|
||||
when (val packageName = externalSignerPackageName) {
|
||||
null -> NostrSignerInternal(keyPair)
|
||||
else -> NostrSignerExternal(keyPair.pubKey.toHexKey(), ExternalSignerLauncher(keyPair.pubKey.toNpub(), packageName))
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// Zaps and Reactions
|
||||
// ---
|
||||
|
||||
fun changeDefaultZapType(zapType: LnZapEvent.ZapType) {
|
||||
if (defaultZapType.value != zapType) {
|
||||
defaultZapType.tryEmit(zapType)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||
if (zapAmountChoices.value != newAmounts) {
|
||||
zapAmountChoices.tryEmit(newAmounts)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?) {
|
||||
if (zapPaymentRequest != newServer) {
|
||||
zapPaymentRequest = newServer
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeReactionTypes(newTypes: List<String>) {
|
||||
if (reactionChoices.value != newTypes) {
|
||||
reactionChoices.tryEmit(newTypes)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// file servers
|
||||
// ---
|
||||
|
||||
fun changeDefaultFileServer(server: Nip96MediaServers.ServerName) {
|
||||
if (defaultFileServer != server) {
|
||||
defaultFileServer = server
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// list names
|
||||
// ---
|
||||
|
||||
fun changeDefaultHomeFollowList(name: String) {
|
||||
if (defaultHomeFollowList.value != name) {
|
||||
defaultHomeFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultStoriesFollowList(name: String) {
|
||||
if (defaultStoriesFollowList.value != name) {
|
||||
defaultStoriesFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNotificationFollowList(name: String) {
|
||||
if (defaultNotificationFollowList.value != name) {
|
||||
defaultNotificationFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultDiscoveryFollowList(name: String) {
|
||||
if (defaultDiscoveryFollowList.value != name) {
|
||||
defaultDiscoveryFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// proxy settings
|
||||
// ---
|
||||
|
||||
fun isProxyEnabled() = proxy != null
|
||||
|
||||
fun updateProxy(
|
||||
enabled: Boolean,
|
||||
portNumber: String,
|
||||
) {
|
||||
val port = portNumber.toIntOrNull() ?: return
|
||||
if (proxyPort != port && isProxyEnabled() != enabled) {
|
||||
proxyPort = portNumber.toInt()
|
||||
proxy = HttpClientManager.initProxy(enabled, "127.0.0.1", proxyPort)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// language services
|
||||
// ---
|
||||
fun addDontTranslateFrom(languageCode: String) {
|
||||
if (!dontTranslateFrom.contains(languageCode)) {
|
||||
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
|
||||
|
||||
fun updateTranslateTo(languageCode: Locale) {
|
||||
if (translateTo != languageCode.language) {
|
||||
translateTo = languageCode.language
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun prefer(
|
||||
source: String,
|
||||
target: String,
|
||||
preference: String,
|
||||
) {
|
||||
val key = "$source,$target"
|
||||
if (key !in languagePreferences) {
|
||||
languagePreferences = languagePreferences + Pair(key, preference)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun preferenceBetween(
|
||||
source: String,
|
||||
target: String,
|
||||
): String? = languagePreferences["$source,$target"]
|
||||
|
||||
// ----
|
||||
// Backup Lists
|
||||
// ----
|
||||
|
||||
fun updateLocalRelayServers(servers: Set<String>) {
|
||||
if (localRelayServers != servers) {
|
||||
localRelayServers = servers
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUserMetadata(newMetadata: MetadataEvent?) {
|
||||
if (newMetadata == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupUserMetadata?.id != newMetadata.id) {
|
||||
backupUserMetadata = newMetadata
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactListTo(newContactList: ContactListEvent?) {
|
||||
if (newContactList == null || newContactList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupContactList?.id != newContactList.id) {
|
||||
backupContactList = newContactList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) {
|
||||
if (newDMRelayList == null || newDMRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupDMRelayList?.id != newDMRelayList.id) {
|
||||
backupDMRelayList = newDMRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNIP65RelayList(newNIP65RelayList: AdvertisedRelayListEvent?) {
|
||||
if (newNIP65RelayList == null || newNIP65RelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupNIP65RelayList?.id != newNIP65RelayList.id) {
|
||||
backupNIP65RelayList = newNIP65RelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSearchRelayList(newSearchRelayList: SearchRelayListEvent?) {
|
||||
if (newSearchRelayList == null || newSearchRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupSearchRelayList?.id != newSearchRelayList.id) {
|
||||
backupSearchRelayList = newSearchRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePrivateHomeRelayList(newPrivateHomeRelayList: PrivateOutboxRelayListEvent?) {
|
||||
if (newPrivateHomeRelayList == null || newPrivateHomeRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupPrivateHomeRelayList?.id != newPrivateHomeRelayList.id) {
|
||||
backupPrivateHomeRelayList = newPrivateHomeRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMuteList(newMuteList: MuteListEvent?) {
|
||||
if (newMuteList == null || newMuteList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupMuteList?.id != newMuteList.id) {
|
||||
backupMuteList = newMuteList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// Warning dialogs
|
||||
// ----
|
||||
|
||||
fun setHideDeleteRequestDialog() {
|
||||
if (!hideDeleteRequestDialog) {
|
||||
hideDeleteRequestDialog = true
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun setHideNIP17WarningDialog() {
|
||||
if (!hideNIP17WarningDialog) {
|
||||
hideNIP17WarningDialog = true
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun setHideBlockAlertDialog() {
|
||||
if (!hideBlockAlertDialog) {
|
||||
hideBlockAlertDialog = true
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?): Boolean {
|
||||
if (showSensitiveContent.value != show) {
|
||||
showSensitiveContent.update { show }
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---
|
||||
// donations
|
||||
// ---
|
||||
|
||||
fun hasDonatedInVersion(versionName: String) = hasDonatedInVersion.value.contains(versionName)
|
||||
|
||||
fun observeDonatedInVersion(versionName: String) =
|
||||
hasDonatedInVersion
|
||||
.map {
|
||||
it.contains(versionName)
|
||||
}
|
||||
|
||||
fun markDonatedInThisVersion(versionName: String): Boolean {
|
||||
if (!hasDonatedInVersion.value.contains(versionName)) {
|
||||
hasDonatedInVersion.update {
|
||||
it + BuildConfig.VERSION_NAME
|
||||
}
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ----
|
||||
// last read flows
|
||||
// ----
|
||||
|
||||
fun getLastReadFlow(route: String): StateFlow<Long> = lastReadPerRoute.value[route] ?: addLastRead(route, 0)
|
||||
|
||||
private fun addLastRead(
|
||||
route: String,
|
||||
timestampInSecs: Long,
|
||||
): MutableStateFlow<Long> =
|
||||
MutableStateFlow<Long>(timestampInSecs).also { newFlow ->
|
||||
lastReadPerRoute.update { it + Pair(route, newFlow) }
|
||||
saveAccountSettings()
|
||||
}
|
||||
|
||||
fun markAsRead(
|
||||
route: String,
|
||||
timestampInSecs: Long,
|
||||
): Boolean {
|
||||
val lastTime = lastReadPerRoute.value[route]
|
||||
return if (lastTime == null) {
|
||||
addLastRead(route, timestampInSecs)
|
||||
true
|
||||
} else if (timestampInSecs > lastTime.value) {
|
||||
lastTime.tryEmit(timestampInSecs)
|
||||
saveAccountSettings()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// local relays
|
||||
// ----
|
||||
|
||||
fun updateLocalRelays(newLocalRelays: Set<RelaySetupInfo>) {
|
||||
if (!localRelays.equals(newLocalRelays)) {
|
||||
localRelays = newLocalRelays
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// attestations
|
||||
// ---
|
||||
|
||||
fun addPendingAttestation(
|
||||
id: HexKey,
|
||||
stamp: String,
|
||||
) {
|
||||
val current = pendingAttestations.value.get(id)
|
||||
if (current == null) {
|
||||
pendingAttestations.update {
|
||||
it + Pair(id, stamp)
|
||||
}
|
||||
saveAccountSettings()
|
||||
} else {
|
||||
if (current != stamp) {
|
||||
pendingAttestations.update {
|
||||
it + Pair(id, stamp)
|
||||
}
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// filters
|
||||
// ---
|
||||
fun updateOptOutOptions(
|
||||
warnReports: Boolean,
|
||||
filterSpam: Boolean,
|
||||
): Boolean =
|
||||
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) {
|
||||
warnAboutPostsWithReports = warnReports
|
||||
filterSpamFromStrangers = filterSpam
|
||||
|
||||
saveAccountSettings()
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class AccountSettingsUpdater(
|
||||
val accountSettings: AccountSettings,
|
||||
)
|
@ -22,17 +22,14 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayStats
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
data class Spammer(
|
||||
val pubkeyHex: HexKey,
|
||||
@ -86,7 +83,7 @@ class AntiSpamFilter {
|
||||
RelayStats.newSpam(relay.url, njumpLink(Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay.url)))
|
||||
}
|
||||
|
||||
liveSpam.invalidateData()
|
||||
flowSpam.tryEmit(AntiSpamState(this))
|
||||
|
||||
return true
|
||||
}
|
||||
@ -109,27 +106,7 @@ class AntiSpamFilter {
|
||||
}
|
||||
}
|
||||
|
||||
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AntiSpamLiveData(
|
||||
val cache: AntiSpamFilter,
|
||||
) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
postValue(AntiSpamState(cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
val flowSpam = MutableStateFlow<AntiSpamState>(AntiSpamState(this))
|
||||
}
|
||||
|
||||
class AntiSpamState(
|
||||
|
@ -331,6 +331,7 @@ class User(
|
||||
}
|
||||
}
|
||||
|
||||
flowSet?.metadata?.invalidateData()
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
}
|
||||
|
||||
@ -434,12 +435,17 @@ class UserFlowSet(
|
||||
u: User,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val metadata = UserBundledRefresherFlow(u)
|
||||
val follows = UserBundledRefresherFlow(u)
|
||||
val relays = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
|
||||
fun isInUse(): Boolean =
|
||||
metadata.stateFlow.subscriptionCount.value > 0 ||
|
||||
relays.stateFlow.subscriptionCount.value > 0 ||
|
||||
follows.stateFlow.subscriptionCount.value > 0
|
||||
|
||||
fun destroy() {
|
||||
metadata.destroy()
|
||||
relays.destroy()
|
||||
follows.destroy()
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultNotificationFollowList.value)
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -183,7 +183,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
var since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultNotificationFollowList.value)
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList
|
||||
?.toMutableMap()
|
||||
|
||||
@ -229,7 +229,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
val since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultNotificationFollowList.value)
|
||||
?.get(account.settings.defaultNotificationFollowList.value)
|
||||
?.relayList
|
||||
?: account.connectToRelays.value.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
|
||||
?: account.convertLocalRelays().associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
|
||||
@ -278,7 +278,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.defaultNotificationFollowList.value,
|
||||
account.settings.defaultNotificationFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
|
@ -108,18 +108,18 @@ object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
|
||||
if (followingEvents.isEmpty()) return null
|
||||
|
||||
return listOf(
|
||||
return followingEvents.map {
|
||||
TypedFilter(
|
||||
// Metadata comes from any relay
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ChannelMetadataEvent.KIND),
|
||||
tags = mapOf("e" to followingEvents.toList()),
|
||||
limit = followingEvents.size * 2,
|
||||
tags = mapOf("e" to listOf(it)),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createLastMessageOfEachChannelFilter(): List<TypedFilter>? {
|
||||
|
@ -90,7 +90,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
@ -111,7 +111,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -133,7 +133,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -153,7 +153,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
@ -179,7 +179,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
@ -194,7 +194,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -217,7 +217,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
),
|
||||
@ -232,7 +232,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -255,7 +255,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -285,7 +285,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -315,7 +315,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -346,7 +346,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -377,7 +377,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -407,7 +407,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -437,7 +437,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultDiscoveryFollowList.value)
|
||||
?.get(account.settings.defaultDiscoveryFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -447,7 +447,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.defaultDiscoveryFollowList.value,
|
||||
account.settings.defaultDiscoveryFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
|
@ -46,7 +46,6 @@ import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
lateinit var account: Account
|
||||
@ -61,8 +60,6 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
job?.cancel()
|
||||
job =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
// creates cache on main
|
||||
withContext(Dispatchers.Main) { account.userProfile().live() }
|
||||
account.liveHomeFollowLists.collect {
|
||||
if (this@NostrHomeDataSource::account.isInitialized) {
|
||||
invalidateFilters()
|
||||
@ -73,8 +70,6 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
job2?.cancel()
|
||||
job2 =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
// creates cache on main
|
||||
withContext(Dispatchers.Main) { account.userProfile().live() }
|
||||
account.liveHomeListAuthorsPerRelay.collect {
|
||||
if (this@NostrHomeDataSource::account.isInitialized) {
|
||||
invalidateFilters()
|
||||
@ -119,7 +114,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.get(account.settings.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -142,7 +137,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.get(account.settings.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -184,7 +179,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.get(account.settings.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -223,7 +218,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.get(account.settings.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -258,7 +253,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.get(account.settings.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -268,7 +263,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.defaultHomeFollowList.value,
|
||||
account.settings.defaultHomeFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
|
@ -78,7 +78,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultStoriesFollowList.value)
|
||||
?.get(account.settings.defaultStoriesFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -109,7 +109,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultStoriesFollowList.value)
|
||||
?.get(account.settings.defaultStoriesFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -140,7 +140,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultStoriesFollowList.value)
|
||||
?.get(account.settings.defaultStoriesFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
@ -150,7 +150,7 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(
|
||||
account.userProfile(),
|
||||
account.defaultStoriesFollowList.value,
|
||||
account.settings.defaultStoriesFollowList.value,
|
||||
relayUrl,
|
||||
time,
|
||||
)
|
||||
|
@ -26,23 +26,23 @@ import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
class EventNotificationConsumer(
|
||||
@ -73,28 +73,31 @@ class EventNotificationConsumer(
|
||||
|
||||
private suspend fun consumeIfMatchesAccount(
|
||||
pushWrappedEvent: GiftWrapEvent,
|
||||
account: Account,
|
||||
account: AccountSettings,
|
||||
) {
|
||||
// no need to cache
|
||||
pushWrappedEvent.unwrap(account.signer) { notificationEvent ->
|
||||
// TODO: Modify the external launcher to launch as different users.
|
||||
// Right now it only registers if Amber has already approved this signature
|
||||
val signer = account.createSigner()
|
||||
|
||||
pushWrappedEvent.unwrap(signer) { notificationEvent ->
|
||||
val consumed = LocalCache.hasConsumed(notificationEvent)
|
||||
val verified = LocalCache.justVerify(notificationEvent)
|
||||
Log.d("EventNotificationConsumer", "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
|
||||
Log.d("EventNotificationConsumer", "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed && verified= $verified")
|
||||
if (!consumed && verified) {
|
||||
Log.d("EventNotificationConsumer", "New Notification was verified")
|
||||
unwrapAndConsume(notificationEvent, account) { innerEvent ->
|
||||
unwrapAndConsume(notificationEvent, signer) { innerEvent ->
|
||||
|
||||
Log.d("EventNotificationConsumer", "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}")
|
||||
if (!consumed) {
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
Log.d("EventNotificationConsumer", "New Nip-04 DM to Notify")
|
||||
notify(innerEvent, account)
|
||||
notify(innerEvent, signer, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
Log.d("EventNotificationConsumer", "New Zap to Notify")
|
||||
notify(innerEvent, account)
|
||||
notify(innerEvent, signer, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
Log.d("EventNotificationConsumer", "New ChatMessage to Notify")
|
||||
notify(innerEvent, account)
|
||||
notify(innerEvent, signer, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,7 +107,7 @@ class EventNotificationConsumer(
|
||||
|
||||
private fun unwrapAndConsume(
|
||||
event: Event,
|
||||
account: Account,
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
if (!LocalCache.justVerify(event)) return
|
||||
@ -112,13 +115,13 @@ class EventNotificationConsumer(
|
||||
|
||||
when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
event.unwrap(account.signer) {
|
||||
unwrapAndConsume(it, account, onReady)
|
||||
event.unwrap(signer) {
|
||||
unwrapAndConsume(it, signer, onReady)
|
||||
LocalCache.justConsume(event, null)
|
||||
}
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
event.unseal(account.signer) {
|
||||
event.unseal(signer) {
|
||||
if (!LocalCache.hasConsumed(it)) {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
@ -136,25 +139,25 @@ class EventNotificationConsumer(
|
||||
|
||||
private fun notify(
|
||||
event: ChatMessageEvent,
|
||||
acc: Account,
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
if (
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
|
||||
// old event being re-broadcasted
|
||||
event.pubKey != acc.userProfile().pubkeyHex
|
||||
event.pubKey != signer.pubKey
|
||||
) { // from the user
|
||||
|
||||
val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return
|
||||
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
|
||||
val chatRoom = event.chatroomKey(signer.pubKey)
|
||||
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return
|
||||
|
||||
val isKnownRoom =
|
||||
(
|
||||
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(chatRoom)
|
||||
) &&
|
||||
!acc.isAllHidden(chatRoom.users)
|
||||
myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
myUser.hasSentMessagesTo(chatRoom)
|
||||
)
|
||||
|
||||
if (isKnownRoom) {
|
||||
val content = chatNote.event?.content() ?: ""
|
||||
@ -177,32 +180,27 @@ class EventNotificationConsumer(
|
||||
|
||||
private fun notify(
|
||||
event: PrivateDmEvent,
|
||||
acc: Account,
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
val note = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return
|
||||
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
|
||||
|
||||
if (acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
if (signer.pubKey == event.verifiedRecipientPubKey()) {
|
||||
val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return
|
||||
|
||||
val knownChatrooms =
|
||||
acc
|
||||
.userProfile()
|
||||
.privateChatrooms
|
||||
.keys
|
||||
.filter {
|
||||
(
|
||||
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(it)
|
||||
) &&
|
||||
!acc.isAllHidden(it.users)
|
||||
}.toSet()
|
||||
val chatRoom = event.chatroomKey(signer.pubKey)
|
||||
|
||||
note.author?.let {
|
||||
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
|
||||
acc.decryptContent(note) { content ->
|
||||
val isKnownRoom =
|
||||
myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
myUser.hasSentMessagesTo(chatRoom)
|
||||
|
||||
if (isKnownRoom) {
|
||||
note.author?.let {
|
||||
decryptContent(note, signer) { content ->
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
@ -214,9 +212,44 @@ class EventNotificationConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZapContentAuthor(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
val event = note.event
|
||||
if (event is LnZapRequestEvent) {
|
||||
if (event.isPrivateZap()) {
|
||||
event.decryptPrivateZap(signer) { onReady(it) }
|
||||
} else {
|
||||
onReady(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptContent(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val event = note.event
|
||||
if (event is PrivateDmEvent) {
|
||||
event.plainContent(signer, onReady)
|
||||
} else if (event is LnZapRequestEvent) {
|
||||
decryptZapContentAuthor(note, signer) { onReady(it.content) }
|
||||
} else if (event is DraftEvent) {
|
||||
event.cachedDraft(signer) {
|
||||
onReady(it.content)
|
||||
}
|
||||
} else {
|
||||
event?.content()?.let { onReady(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(
|
||||
event: LnZapEvent,
|
||||
acc: Account,
|
||||
signer: NostrSigner,
|
||||
acc: AccountSettings,
|
||||
) {
|
||||
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
|
||||
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
@ -238,20 +271,20 @@ class EventNotificationConsumer(
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
|
||||
|
||||
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
|
||||
if (event.isTaggedUser(signer.pubKey)) {
|
||||
val amount = showAmount(event.amount)
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount $amount")
|
||||
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
acc.decryptZapContentAuthor(noteZapRequest) {
|
||||
decryptZapContentAuthor(noteZapRequest, signer) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
|
||||
|
||||
val author = LocalCache.getOrCreateUser(it.pubKey)
|
||||
val senderInfo = Pair(author, it.content.ifBlank { null })
|
||||
|
||||
if (noteZapped.event?.content() != null) {
|
||||
acc.decryptContent(noteZapped) {
|
||||
decryptContent(noteZapped, signer) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
|
||||
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
@ -275,7 +308,7 @@ class EventNotificationConsumer(
|
||||
zappedContent,
|
||||
)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val userPicture = senderInfo.first.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
|
||||
|
@ -24,11 +24,8 @@ import android.util.Log
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -55,7 +52,7 @@ class RegisterAccounts(
|
||||
|
||||
private suspend fun signAllAuths(
|
||||
notificationToken: String,
|
||||
remainingTos: List<Pair<Account, List<String>>>,
|
||||
remainingTos: List<Pair<AccountSettings, List<String>>>,
|
||||
output: MutableList<RelayAuthEvent>,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||
) {
|
||||
@ -71,7 +68,10 @@ class RegisterAccounts(
|
||||
val result =
|
||||
withTimeoutOrNull(10000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
|
||||
val signer = accountRelayPair.first.createSigner()
|
||||
// TODO: Modify the external launcher to launch as different users.
|
||||
// Right now it only registers if Amber has already approved this signature
|
||||
RelayAuthEvent.create(accountRelayPair.second, notificationToken, signer) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
@ -104,32 +104,18 @@ class RegisterAccounts(
|
||||
Log.d(tag, "Register Account ${it.npub}")
|
||||
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val nip65Read =
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
|
||||
)?.event as? AdvertisedRelayListEvent
|
||||
)?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList<String>()
|
||||
val nip65Read = acc.backupNIP65RelayList?.readRelays() ?: emptyList()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP65 Reads ${nip65Read.joinToString(", ")}")
|
||||
Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ")}")
|
||||
|
||||
val nip17Read =
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
ChatMessageRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
|
||||
)?.event as? ChatMessageRelayListEvent
|
||||
)?.relays() ?: acc.backupDMRelayList?.relays() ?: emptyList<String>()
|
||||
val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList<String>()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP17 Reads ${nip17Read.joinToString(", ")}")
|
||||
Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ")}")
|
||||
|
||||
val kind3Relays = (acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays())
|
||||
val readKind3Relays = kind3Relays?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
|
||||
val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||
Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||
|
||||
val relays = (nip65Read + nip17Read + readKind3Relays)
|
||||
|
||||
|
@ -329,11 +329,11 @@ fun EditPostView(
|
||||
) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
accountViewModel.account.defaultFileServer,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, false, server, accountViewModel::toast, context)
|
||||
if (!server.isNip95) {
|
||||
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server.server)
|
||||
}
|
||||
},
|
||||
onCancel = { postViewModel.contentToAddUrl = null },
|
||||
|
@ -333,7 +333,7 @@ open class NewMediaModel : ViewModel() {
|
||||
|
||||
fun isVideo() = mediaType?.startsWith("video")
|
||||
|
||||
fun defaultServer() = account?.defaultFileServer ?: Nip96MediaServers.DEFAULT[0]
|
||||
fun defaultServer() = account?.settings?.defaultFileServer ?: Nip96MediaServers.DEFAULT[0]
|
||||
|
||||
fun onceUploaded(onceUploaded: () -> Unit) {
|
||||
this.onceUploaded = onceUploaded
|
||||
|
@ -164,7 +164,7 @@ fun NewMediaView(
|
||||
}
|
||||
postViewModel.selectedServer?.let {
|
||||
if (!it.isNip95) {
|
||||
account.changeDefaultFileServer(it.server)
|
||||
account.settings.changeDefaultFileServer(it.server)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -290,7 +290,7 @@ fun ImageVideoPost(
|
||||
label = stringRes(id = R.string.file_server),
|
||||
placeholder =
|
||||
fileServers
|
||||
.firstOrNull { it.server == accountViewModel.account.defaultFileServer }
|
||||
.firstOrNull { it.server == accountViewModel.account.settings.defaultFileServer }
|
||||
?.server
|
||||
?.name
|
||||
?: fileServers[0].server.name,
|
||||
|
@ -491,11 +491,11 @@ fun NewPostView(
|
||||
) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
accountViewModel.account.defaultFileServer,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, false, server, accountViewModel::toast, context)
|
||||
if (!server.isNip95) {
|
||||
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server.server)
|
||||
}
|
||||
},
|
||||
onCancel = { postViewModel.contentToAddUrl = null },
|
||||
@ -1778,7 +1778,7 @@ fun ImageVideoDescription(
|
||||
label = stringRes(id = R.string.file_server),
|
||||
placeholder =
|
||||
fileServers
|
||||
.firstOrNull { it.server == accountViewModel.account.defaultFileServer }
|
||||
.firstOrNull { it.server == accountViewModel.account.settings.defaultFileServer }
|
||||
?.server
|
||||
?.name
|
||||
?: fileServers[0].server.name,
|
||||
|
@ -184,7 +184,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
size = size,
|
||||
alt = null,
|
||||
sensitiveContent = null,
|
||||
server = account.defaultFileServer,
|
||||
server = account.settings.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = {},
|
||||
context = context,
|
||||
|
@ -98,7 +98,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
||||
relayFile
|
||||
.map {
|
||||
val localInfoFeedTypes =
|
||||
account.localRelays
|
||||
account.settings.localRelays
|
||||
.filter { localRelay -> localRelay.url == it.key }
|
||||
.firstOrNull()
|
||||
?.feedTypes
|
||||
@ -119,7 +119,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
||||
.sortedBy { it.relayStat.receivedBytes }
|
||||
.reversed()
|
||||
} else {
|
||||
account.localRelays
|
||||
account.settings.localRelays
|
||||
.map {
|
||||
Kind3BasicRelaySetupInfo(
|
||||
url = RelayUrlFormatter.normalize(it.url),
|
||||
|
@ -21,9 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions.relays
|
||||
|
||||
class LocalRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||
override fun getRelayList(): List<String>? = account.localRelayServers.toList()
|
||||
override fun getRelayList(): List<String> = account.settings.localRelayServers.toList()
|
||||
|
||||
override fun saveRelayList(urlList: List<String>) {
|
||||
account.updateLocalRelayServers(urlList.toSet())
|
||||
account.settings.updateLocalRelayServers(urlList.toSet())
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,9 @@ fun SensitivityWarning(
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val accountState = accountViewModel.account.showSensitiveContent.collectAsStateWithLifecycle()
|
||||
val accountState =
|
||||
accountViewModel.account.settings.showSensitiveContent
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
|
||||
|
||||
|
@ -32,12 +32,12 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
open class DiscoverChatFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultDiscoveryFollowList.value ==
|
||||
account.settings.defaultDiscoveryFollowList.value ==
|
||||
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultDiscoveryFollowList.value ==
|
||||
account.settings.defaultDiscoveryFollowList.value ==
|
||||
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -67,7 +67,7 @@ open class DiscoverChatFeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultDiscoveryFollowList.value,
|
||||
selectedListName = account.settings.defaultDiscoveryFollowList.value,
|
||||
followLists = account.liveDiscoveryFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -32,19 +32,19 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
open class DiscoverCommunityFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultDiscoveryFollowList.value ==
|
||||
account.settings.defaultDiscoveryFollowList.value ==
|
||||
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultDiscoveryFollowList.value ==
|
||||
account.settings.defaultDiscoveryFollowList.value ==
|
||||
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val filterParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultDiscoveryFollowList.value,
|
||||
selectedListName = account.settings.defaultDiscoveryFollowList.value,
|
||||
followLists = account.liveDiscoveryFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
@ -73,7 +73,7 @@ open class DiscoverCommunityFeedFilter(
|
||||
val filterParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultDiscoveryFollowList.value,
|
||||
selectedListName = account.settings.defaultDiscoveryFollowList.value,
|
||||
followLists = account.liveDiscoveryFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ open class DiscoverLiveFeedFilter(
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList()
|
||||
|
||||
open fun followList(): String = account.defaultDiscoveryFollowList.value
|
||||
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
@ -57,7 +57,7 @@ open class DiscoverLiveFeedFilter(
|
||||
val filterParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultDiscoveryFollowList.value,
|
||||
selectedListName = account.settings.defaultDiscoveryFollowList.value,
|
||||
followLists = account.liveDiscoveryFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -32,7 +32,7 @@ open class DiscoverMarketplaceFeedFilter(
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList()
|
||||
|
||||
open fun followList(): String = account.defaultDiscoveryFollowList.value
|
||||
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
@ -55,7 +55,7 @@ open class DiscoverMarketplaceFeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.userProfile().pubkeyHex,
|
||||
account.defaultDiscoveryFollowList.value,
|
||||
account.settings.defaultDiscoveryFollowList.value,
|
||||
account.liveDiscoveryFollowLists.value,
|
||||
account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ open class DiscoverNIP89FeedFilter(
|
||||
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList()
|
||||
|
||||
open fun followList(): String = account.defaultDiscoveryFollowList.value
|
||||
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
@ -58,7 +58,7 @@ open class DiscoverNIP89FeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.userProfile().pubkeyHex,
|
||||
account.defaultDiscoveryFollowList.value,
|
||||
account.settings.defaultDiscoveryFollowList.value,
|
||||
account.liveDiscoveryFollowLists.value,
|
||||
account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -33,11 +33,11 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
class HomeConversationsFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
account.settings.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.settings.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val filterParams = buildFilterParams(account)
|
||||
@ -54,7 +54,7 @@ class HomeConversationsFeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultHomeFollowList.value,
|
||||
selectedListName = account.settings.defaultHomeFollowList.value,
|
||||
followLists = account.liveHomeFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -39,16 +39,16 @@ import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
class HomeNewThreadFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
account.settings.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.settings.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultHomeFollowList.value,
|
||||
selectedListName = account.settings.defaultHomeFollowList.value,
|
||||
followLists = account.liveHomeFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ open class NIP90ContentDiscoveryResponseFilter(
|
||||
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + request
|
||||
|
||||
open fun followList(): String = account.defaultDiscoveryFollowList.value
|
||||
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
@ -71,7 +71,7 @@ open class NIP90ContentDiscoveryResponseFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.userProfile().pubkeyHex,
|
||||
account.defaultDiscoveryFollowList.value,
|
||||
account.settings.defaultDiscoveryFollowList.value,
|
||||
account.liveDiscoveryFollowLists.value,
|
||||
account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -47,18 +47,18 @@ import com.vitorpamplona.quartz.events.RepostEvent
|
||||
class NotificationFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultNotificationFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultNotificationFollowList.value ==
|
||||
account.settings.defaultNotificationFollowList.value ==
|
||||
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultNotificationFollowList.value ==
|
||||
account.settings.defaultNotificationFollowList.value ==
|
||||
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultNotificationFollowList.value,
|
||||
selectedListName = account.settings.defaultNotificationFollowList.value,
|
||||
followLists = account.liveNotificationFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -35,8 +35,8 @@ class UserProfileGalleryFeedFilter(
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + "ProfileGallery"
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
account.settings.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.settings.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
@ -74,7 +74,7 @@ class UserProfileGalleryFeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultStoriesFollowList.value,
|
||||
selectedListName = account.settings.defaultStoriesFollowList.value,
|
||||
followLists = account.liveStoriesFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -34,11 +34,11 @@ import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
class VideoFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultStoriesFollowList.value
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
account.settings.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.settings.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
@ -78,7 +78,7 @@ class VideoFeedFilter(
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultStoriesFollowList.value,
|
||||
selectedListName = account.settings.defaultStoriesFollowList.value,
|
||||
followLists = account.liveStoriesFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
@ -51,6 +51,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -87,7 +88,12 @@ fun AccountSwitchBottomSheet(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
) {
|
||||
val accounts = LocalPreferences.allSavedAccounts()
|
||||
val accounts by
|
||||
produceState(initialValue = LocalPreferences.cachedAccounts()) {
|
||||
if (value == null) {
|
||||
value = LocalPreferences.allSavedAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
@ -103,7 +109,7 @@ fun AccountSwitchBottomSheet(
|
||||
) {
|
||||
Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
accounts.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) }
|
||||
accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) }
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
|
@ -448,13 +448,14 @@ fun StoriesTopBar(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
GenericMainTopBar(openDrawer, accountViewModel, nav) {
|
||||
val list by accountViewModel.account.defaultStoriesFollowList.collectAsStateWithLifecycle()
|
||||
val list by accountViewModel.account.settings.defaultStoriesFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
|
||||
accountViewModel.account.settings.changeDefaultStoriesFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -467,7 +468,8 @@ fun HomeTopBar(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
GenericMainTopBar(openDrawer, accountViewModel, nav) {
|
||||
val list by accountViewModel.account.defaultHomeFollowList.collectAsStateWithLifecycle()
|
||||
val list by accountViewModel.account.settings.defaultHomeFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
@ -476,7 +478,7 @@ fun HomeTopBar(
|
||||
if (listName.type == CodeNameType.ROUTE) {
|
||||
nav(listName.code)
|
||||
} else {
|
||||
accountViewModel.account.changeDefaultHomeFollowList(listName.code)
|
||||
accountViewModel.account.settings.changeDefaultHomeFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -490,13 +492,14 @@ fun NotificationTopBar(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
GenericMainTopBar(openDrawer, accountViewModel, nav) {
|
||||
val list by accountViewModel.account.defaultNotificationFollowList.collectAsStateWithLifecycle()
|
||||
val list by accountViewModel.account.settings.defaultNotificationFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
|
||||
accountViewModel.account.settings.changeDefaultNotificationFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -509,13 +512,14 @@ fun DiscoveryTopBar(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
GenericMainTopBar(openDrawer, accountViewModel, nav) {
|
||||
val list by accountViewModel.account.defaultDiscoveryFollowList.collectAsStateWithLifecycle()
|
||||
val list by accountViewModel.account.settings.defaultDiscoveryFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
|
||||
accountViewModel.account.settings.changeDefaultDiscoveryFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -446,10 +446,17 @@ fun ListContent(
|
||||
var editMediaServers by remember { mutableStateOf(false) }
|
||||
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) }
|
||||
var checked by remember { mutableStateOf(accountViewModel.account.settings.proxy != null) }
|
||||
var disconnectTorDialog by remember { mutableStateOf(false) }
|
||||
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
|
||||
val proxyPort =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
accountViewModel.account.settings.proxyPort
|
||||
.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(modifier) {
|
||||
@ -507,7 +514,7 @@ fun ListContent(
|
||||
route = Route.BlockedUsers.route,
|
||||
)
|
||||
|
||||
accountViewModel.account.keyPair.privKey?.let {
|
||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
||||
IconRow(
|
||||
title = stringRes(R.string.backup_keys),
|
||||
icon = R.drawable.ic_key,
|
||||
|
@ -181,7 +181,8 @@ fun LoadOts(
|
||||
(earliestDate as? GenericLoadable.Loaded)?.let {
|
||||
whenConfirmed(it.loaded)
|
||||
} ?: run {
|
||||
val pendingAttestations by accountViewModel.account.pendingAttestations.collectAsStateWithLifecycle()
|
||||
val pendingAttestations by accountViewModel.account.settings.pendingAttestations
|
||||
.collectAsStateWithLifecycle()
|
||||
val id = note.event?.id() ?: note.idHex
|
||||
|
||||
if (pendingAttestations[id] != null) {
|
||||
|
@ -367,7 +367,7 @@ private fun RenderMainPopup(
|
||||
Icons.Default.Block,
|
||||
stringRes(R.string.quick_action_block),
|
||||
) {
|
||||
if (accountViewModel.hideBlockAlertDialog) {
|
||||
if (accountViewModel.account.settings.hideBlockAlertDialog) {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
} else {
|
||||
@ -385,7 +385,7 @@ private fun RenderMainPopup(
|
||||
Icons.Default.Delete,
|
||||
stringRes(R.string.quick_action_delete),
|
||||
) {
|
||||
if (accountViewModel.hideDeleteRequestDialog) {
|
||||
if (accountViewModel.account.settings.hideDeleteRequestDialog) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
} else {
|
||||
@ -560,7 +560,7 @@ private fun RenderDeleteFromGalleryPopup(
|
||||
Icons.Default.Delete,
|
||||
stringRes(R.string.quick_action_delete),
|
||||
) {
|
||||
if (accountViewModel.hideDeleteRequestDialog) {
|
||||
if (accountViewModel.account.settings.hideDeleteRequestDialog) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
} else {
|
||||
@ -699,7 +699,7 @@ fun DeleteAlertDialog(
|
||||
},
|
||||
onClickDontShowAgain = {
|
||||
accountViewModel.delete(note)
|
||||
accountViewModel.dontShowDeleteRequestDialog()
|
||||
accountViewModel.account.settings.setHideDeleteRequestDialog()
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
@ -727,7 +727,7 @@ private fun BlockAlertDialog(
|
||||
},
|
||||
onClickDontShowAgain = {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
accountViewModel.dontShowBlockAlertDialog()
|
||||
accountViewModel.account.settings.setHideBlockAlertDialog()
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
|
@ -67,6 +67,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@ -380,14 +381,16 @@ fun ZapVote(
|
||||
)
|
||||
return@combinedClickable
|
||||
} else if (
|
||||
accountViewModel.account.zapAmountChoices.size == 1 &&
|
||||
accountViewModel.account.settings.zapAmountChoices.value.size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(
|
||||
accountViewModel.account.zapAmountChoices.first(),
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first(),
|
||||
)
|
||||
) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.account.zapAmountChoices.first() * 1000,
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first() * 1000,
|
||||
poolOption.option,
|
||||
"",
|
||||
context,
|
||||
@ -519,12 +522,14 @@ fun FilteredZapAmountChoicePopup(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
// TODO: Move this to the viewModel
|
||||
val zapPaymentChoices by accountViewModel.account.settings.zapAmountChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
val zapMessage = ""
|
||||
|
||||
val sortedOptions =
|
||||
remember(accountState) { pollViewModel.createZapOptionsThatMatchThePollingParameters() }
|
||||
remember(zapPaymentChoices) { pollViewModel.createZapOptionsThatMatchThePollingParameters(zapPaymentChoices) }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
|
@ -262,10 +262,11 @@ class PollNoteViewModel : ViewModel() {
|
||||
}
|
||||
?: BigDecimal.ZERO
|
||||
|
||||
fun createZapOptionsThatMatchThePollingParameters(): List<Long> {
|
||||
fun createZapOptionsThatMatchThePollingParameters(zapPaymentChoices: List<Long>): List<Long> {
|
||||
val options =
|
||||
account?.zapAmountChoices?.filter { isValidInputVoteAmount(it) }?.toMutableList()
|
||||
?: mutableListOf()
|
||||
zapPaymentChoices
|
||||
.filter { isValidInputVoteAmount(it) }
|
||||
.toMutableList()
|
||||
if (options.isEmpty()) {
|
||||
valueMinimum?.let { minimum ->
|
||||
valueMaximum?.let { maximum ->
|
||||
|
@ -93,6 +93,7 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@ -954,7 +955,9 @@ private fun likeClick(
|
||||
)
|
||||
return
|
||||
}
|
||||
if (accountViewModel.account.reactionChoices.isEmpty()) {
|
||||
if (accountViewModel.account.settings.reactionChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
accountViewModel.toast(
|
||||
R.string.no_reactions_setup,
|
||||
R.string.no_reaction_type_setup_long_press_to_change,
|
||||
@ -964,9 +967,9 @@ private fun likeClick(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_like_posts,
|
||||
)
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
} else if (accountViewModel.account.settings.reactionChoices.value.size == 1) {
|
||||
onWantsToSignReaction()
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
} else if (accountViewModel.account.settings.reactionChoices.value.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
@ -1028,7 +1031,6 @@ fun ZapReaction(
|
||||
if (wantsToZap) {
|
||||
ZapAmountChoicePopup(
|
||||
baseNote = baseNote,
|
||||
zapAmountChoices = accountViewModel.account.zapAmountChoices,
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onDismiss = {
|
||||
@ -1161,7 +1163,9 @@ fun zapClick(
|
||||
return
|
||||
}
|
||||
|
||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
accountViewModel.toast(
|
||||
R.string.error_dialog_zap_error,
|
||||
R.string.no_zap_amount_setup_long_press_to_change,
|
||||
@ -1171,10 +1175,11 @@ fun zapClick(
|
||||
R.string.error_dialog_zap_error,
|
||||
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
|
||||
)
|
||||
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size == 1) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.account.zapAmountChoices.first() * 1000,
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first() * 1000,
|
||||
null,
|
||||
"",
|
||||
context,
|
||||
@ -1182,7 +1187,7 @@ fun zapClick(
|
||||
onProgress = { onZappingProgress(it) },
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
)
|
||||
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
@ -1312,21 +1317,19 @@ fun ReactionChoicePopup(
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val toRemove = remember { baseNote.reactedBy(account.userProfile()).toImmutableSet() }
|
||||
val reactions = remember { account.reactionChoices.toImmutableList() }
|
||||
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
val reactions by accountViewModel.account.settings.reactionChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
reactions.toImmutableList(),
|
||||
toRemove = toRemove,
|
||||
onClick = { reactionType ->
|
||||
accountViewModel.reactToOrDelete(
|
||||
@ -1462,6 +1465,24 @@ fun RenderReaction(reactionType: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val zapAmountChoices by
|
||||
accountViewModel.account.settings.zapAmountChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
|
@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -75,7 +74,7 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
@ -97,13 +96,13 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdateReactionTypeViewModel(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModel() {
|
||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||
var reactionSet by mutableStateOf(listOf<String>())
|
||||
|
||||
fun load() {
|
||||
this.reactionSet = account.reactionChoices
|
||||
this.reactionSet = accountSettings.reactionChoices.value
|
||||
}
|
||||
|
||||
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
@ -124,7 +123,7 @@ class UpdateReactionTypeViewModel(
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
account.changeReactionTypes(reactionSet)
|
||||
accountSettings.changeReactionTypes(reactionSet)
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
@ -132,12 +131,12 @@ class UpdateReactionTypeViewModel(
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun hasChanged(): Boolean = reactionSet != account.reactionChoices
|
||||
fun hasChanged(): Boolean = reactionSet != accountSettings.reactionChoices.value
|
||||
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel = UpdateReactionTypeViewModel(account) as UpdateReactionTypeViewModel
|
||||
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel = UpdateReactionTypeViewModel(accountSettings) as UpdateReactionTypeViewModel
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +150,7 @@ fun UpdateReactionTypeDialog(
|
||||
val postViewModel: UpdateReactionTypeViewModel =
|
||||
viewModel(
|
||||
key = "UpdateReactionTypeViewModel",
|
||||
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account),
|
||||
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
LaunchedEffect(accountViewModel) { postViewModel.load() }
|
||||
|
@ -47,7 +47,6 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
@ -87,7 +86,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
@ -110,7 +109,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class UpdateZapAmountViewModel(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModel() {
|
||||
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||
var amountSet by mutableStateOf(listOf<Long>())
|
||||
@ -127,14 +126,14 @@ class UpdateZapAmountViewModel(
|
||||
}
|
||||
|
||||
fun load() {
|
||||
this.amountSet = account.zapAmountChoices
|
||||
this.amountSet = accountSettings.zapAmountChoices.value
|
||||
this.walletConnectPubkey =
|
||||
account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
accountSettings.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.walletConnectRelay =
|
||||
account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
accountSettings.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.walletConnectSecret =
|
||||
account.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.selectedZapType = account.defaultZapType.value
|
||||
accountSettings.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.selectedZapType = accountSettings.defaultZapType.value
|
||||
}
|
||||
|
||||
fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
@ -153,8 +152,8 @@ class UpdateZapAmountViewModel(
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
account?.changeZapAmounts(amountSet)
|
||||
account?.changeDefaultZapType(selectedZapType)
|
||||
accountSettings.changeZapAmounts(amountSet)
|
||||
accountSettings.changeDefaultZapType(selectedZapType)
|
||||
|
||||
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
|
||||
val pubkeyHex =
|
||||
@ -173,7 +172,7 @@ class UpdateZapAmountViewModel(
|
||||
val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) }
|
||||
|
||||
if (pubkeyHex != null) {
|
||||
account.changeZapPaymentRequest(
|
||||
accountSettings.changeZapPaymentRequest(
|
||||
Nip47WalletConnect.Nip47URI(
|
||||
pubkeyHex,
|
||||
relayUrl,
|
||||
@ -181,10 +180,10 @@ class UpdateZapAmountViewModel(
|
||||
),
|
||||
)
|
||||
} else {
|
||||
account?.changeZapPaymentRequest(null)
|
||||
accountSettings.changeZapPaymentRequest(null)
|
||||
}
|
||||
} else {
|
||||
account?.changeZapPaymentRequest(null)
|
||||
accountSettings.changeZapPaymentRequest(null)
|
||||
}
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
@ -196,11 +195,11 @@ class UpdateZapAmountViewModel(
|
||||
|
||||
fun hasChanged(): Boolean =
|
||||
(
|
||||
selectedZapType != account?.defaultZapType?.value ||
|
||||
amountSet != account?.zapAmountChoices ||
|
||||
walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") ||
|
||||
walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") ||
|
||||
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
|
||||
selectedZapType != accountSettings.defaultZapType.value ||
|
||||
amountSet != accountSettings.zapAmountChoices.value ||
|
||||
walletConnectPubkey.text != (accountSettings.zapPaymentRequest?.pubKeyHex ?: "") ||
|
||||
walletConnectRelay.text != (accountSettings.zapPaymentRequest?.relayUri ?: "") ||
|
||||
walletConnectSecret.text != (accountSettings.zapPaymentRequest?.secret ?: "")
|
||||
)
|
||||
|
||||
fun updateNIP47(uri: String) {
|
||||
@ -213,9 +212,9 @@ class UpdateZapAmountViewModel(
|
||||
}
|
||||
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel = UpdateZapAmountViewModel(account) as UpdateZapAmountViewModel
|
||||
override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel = UpdateZapAmountViewModel(accountSettings) as UpdateZapAmountViewModel
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +231,7 @@ fun UpdateZapAmountDialog(
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account),
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
|
@ -153,7 +153,7 @@ fun ZapCustomDialog(
|
||||
}
|
||||
|
||||
var selectedZapType by
|
||||
remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType.value) }
|
||||
remember(accountViewModel) { mutableStateOf(accountViewModel.account.settings.defaultZapType.value) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@ -224,7 +224,7 @@ fun ZapCustomDialog(
|
||||
label = stringRes(id = R.string.zap_type),
|
||||
placeholder =
|
||||
zapTypes
|
||||
.filter { it.first == accountViewModel.account.defaultZapType.value }
|
||||
.filter { it.first == accountViewModel.account.settings.defaultZapType.value }
|
||||
.first()
|
||||
.second,
|
||||
options = zapOptions,
|
||||
|
@ -38,59 +38,26 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.AddDMRelayListDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AddInboxRelayForDMCardPreview() {
|
||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||
val myCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
sharedPreferencesViewModel.init()
|
||||
sharedPreferencesViewModel.updateTheme(ThemeType.DARK)
|
||||
|
||||
val pubkey = "989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"
|
||||
|
||||
val myAccount =
|
||||
Account(
|
||||
keyPair =
|
||||
KeyPair(
|
||||
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
|
||||
pubKey = Hex.decode(pubkey),
|
||||
forcePubKeyCheck = false,
|
||||
),
|
||||
scope = myCoroutineScope,
|
||||
)
|
||||
|
||||
val accountViewModel =
|
||||
AccountViewModel(
|
||||
myAccount,
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
AddInboxRelayForDMCard(
|
||||
accountViewModel = accountViewModel,
|
||||
accountViewModel = mockAccountViewModel(),
|
||||
nav = {},
|
||||
)
|
||||
}
|
||||
|
@ -38,59 +38,26 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.AddSearchRelayListDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AddInboxRelayForSearchCardPreview() {
|
||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||
val myCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
sharedPreferencesViewModel.init()
|
||||
sharedPreferencesViewModel.updateTheme(ThemeType.DARK)
|
||||
|
||||
val pubkey = "989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"
|
||||
|
||||
val myAccount =
|
||||
Account(
|
||||
keyPair =
|
||||
KeyPair(
|
||||
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
|
||||
pubKey = Hex.decode(pubkey),
|
||||
forcePubKeyCheck = false,
|
||||
),
|
||||
scope = myCoroutineScope,
|
||||
)
|
||||
|
||||
val accountViewModel =
|
||||
AccountViewModel(
|
||||
myAccount,
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
AddInboxRelayForSearchCard(
|
||||
accountViewModel = accountViewModel,
|
||||
accountViewModel = mockAccountViewModel(),
|
||||
nav = {},
|
||||
)
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
@ -396,10 +397,8 @@ fun WatchBookmarksFollowsAndAccount(
|
||||
.live()
|
||||
.bookmarks
|
||||
.observeAsState()
|
||||
val showSensitiveContent by
|
||||
accountViewModel.showSensitiveContentChanges.observeAsState(
|
||||
accountViewModel.account.showSensitiveContent,
|
||||
)
|
||||
val showSensitiveContent by accountViewModel.account.settings.showSensitiveContent
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
|
||||
launch(Dispatchers.IO) {
|
||||
@ -411,7 +410,7 @@ fun WatchBookmarksFollowsAndAccount(
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = showSensitiveContent.value,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
|
@ -326,7 +326,6 @@ fun ZapDonationButton(
|
||||
if (wantsToZap != null) {
|
||||
ZapAmountChoicePopup(
|
||||
baseNote = baseNote,
|
||||
zapAmountChoices = wantsToZap ?: accountViewModel.account.zapAmountChoices,
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onDismiss = {
|
||||
@ -452,7 +451,9 @@ fun customZapClick(
|
||||
return
|
||||
}
|
||||
|
||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
|
||||
@ -462,8 +463,10 @@ fun customZapClick(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
)
|
||||
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
|
||||
val amount = accountViewModel.account.zapAmountChoices.first()
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size == 1) {
|
||||
val amount =
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first()
|
||||
|
||||
if (amount > 1100) {
|
||||
accountViewModel.zap(
|
||||
@ -481,9 +484,11 @@ fun customZapClick(
|
||||
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
|
||||
// recommends amounts for a monthly release.
|
||||
}
|
||||
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
||||
if (accountViewModel.account.zapAmountChoices.any { it > 1100 }) {
|
||||
onMultipleChoices(accountViewModel.account.zapAmountChoices)
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) {
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.any { it > 1100 }
|
||||
) {
|
||||
onMultipleChoices(accountViewModel.account.settings.zapAmountChoices.value)
|
||||
} else {
|
||||
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -50,7 +51,7 @@ import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@ -59,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AccountScreen(
|
||||
@ -95,7 +97,7 @@ fun AccountScreen(
|
||||
LocalViewModelStoreOwner provides state.currentViewModelStore,
|
||||
) {
|
||||
LoggedInPage(
|
||||
state.account,
|
||||
state.accountSettings,
|
||||
accountStateViewModel,
|
||||
sharedPreferencesViewModel,
|
||||
)
|
||||
@ -106,7 +108,7 @@ fun AccountScreen(
|
||||
LocalViewModelStoreOwner provides state.currentViewModelStore,
|
||||
) {
|
||||
LoggedInPage(
|
||||
state.account,
|
||||
state.accountSettings,
|
||||
accountStateViewModel,
|
||||
sharedPreferencesViewModel,
|
||||
)
|
||||
@ -118,7 +120,7 @@ fun AccountScreen(
|
||||
|
||||
@Composable
|
||||
fun LoggedInPage(
|
||||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||
) {
|
||||
@ -127,11 +129,15 @@ fun LoggedInPage(
|
||||
key = "AccountViewModel",
|
||||
factory =
|
||||
AccountViewModel.Factory(
|
||||
account,
|
||||
accountSettings,
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = accountViewModel) {
|
||||
accountViewModel.restartServices()
|
||||
}
|
||||
|
||||
val activity = getActivity() as MainActivity
|
||||
|
||||
if (accountViewModel.account.signer is NostrSignerExternal) {
|
||||
@ -208,7 +214,7 @@ fun LoggedInPage(
|
||||
}
|
||||
|
||||
class AccountCentricViewModelStore(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModelStoreOwner {
|
||||
override val viewModelStore = ViewModelStore()
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
|
||||
sealed class AccountState {
|
||||
object Loading : AccountState()
|
||||
@ -28,14 +28,14 @@ sealed class AccountState {
|
||||
object LoggedOff : AccountState()
|
||||
|
||||
class LoggedInViewOnly(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : AccountState() {
|
||||
val currentViewModelStore = AccountCentricViewModelStore(account)
|
||||
val currentViewModelStore = AccountCentricViewModelStore(accountSettings)
|
||||
}
|
||||
|
||||
class LoggedIn(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
) : AccountState() {
|
||||
val currentViewModelStore = AccountCentricViewModelStore(account)
|
||||
val currentViewModelStore = AccountCentricViewModelStore(accountSettings)
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,13 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.DefaultChannels
|
||||
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultNIP65List
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.Constants
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@ -39,16 +43,20 @@ import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.Contact
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -63,15 +71,17 @@ class AccountStateViewModel : ViewModel() {
|
||||
private val _accountContent = MutableStateFlow<AccountState>(AccountState.Loading)
|
||||
val accountContent = _accountContent.asStateFlow()
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
|
||||
fun tryLoginExistingAccountAsync() {
|
||||
// pulls account from storage.
|
||||
viewModelScope.launch(Dispatchers.IO) { tryLoginExistingAccount() }
|
||||
viewModelScope.launch { tryLoginExistingAccount() }
|
||||
}
|
||||
|
||||
private suspend fun tryLoginExistingAccount() =
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) } ?: run { requestLoginUI() }
|
||||
}
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()
|
||||
}?.let { startUI(it) } ?: run { requestLoginUI() }
|
||||
|
||||
private suspend fun requestLoginUI() {
|
||||
_accountContent.update { AccountState.LoggedOff }
|
||||
@ -108,49 +118,35 @@ class AccountStateViewModel : ViewModel() {
|
||||
|
||||
val account =
|
||||
if (loginWithExternalSigner) {
|
||||
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
||||
val localPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" }
|
||||
Account(
|
||||
keyPair,
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
signer =
|
||||
NostrSignerExternal(
|
||||
keyPair.pubKey.toHexKey(),
|
||||
ExternalSignerLauncher(keyPair.pubKey.toNpub(), localPackageName),
|
||||
),
|
||||
)
|
||||
} else if (key.startsWith("nsec")) {
|
||||
val keyPair = KeyPair(privKey = key.bechToBytes())
|
||||
Account(
|
||||
keyPair,
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(privKey = key.bechToBytes()),
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
)
|
||||
} else if (key.contains(" ") && CryptoUtils.isValidMnemonic(key)) {
|
||||
val keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key))
|
||||
Account(
|
||||
keyPair,
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key)),
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
)
|
||||
} else if (pubKeyParsed != null) {
|
||||
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
||||
Account(
|
||||
keyPair,
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
)
|
||||
} else {
|
||||
val keyPair = KeyPair(Hex.decode(key))
|
||||
Account(
|
||||
keyPair,
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(Hex.decode(key)),
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
)
|
||||
}
|
||||
|
||||
@ -159,51 +155,37 @@ class AccountStateViewModel : ViewModel() {
|
||||
startUI(account)
|
||||
}
|
||||
|
||||
suspend fun startUI(
|
||||
account: Account,
|
||||
onServicesReady: (() -> Unit)? = null,
|
||||
) = withContext(Dispatchers.Main) {
|
||||
if (account.isWriteable()) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// Prepares livedata objects on the main user.
|
||||
account.userProfile().live()
|
||||
}
|
||||
serviceManager?.restartIfDifferentAccount(account)
|
||||
|
||||
if (onServicesReady != null) {
|
||||
// waits for the connection to go through
|
||||
delay(1000)
|
||||
onServicesReady()
|
||||
}
|
||||
}
|
||||
|
||||
account.saveable.observeForever(saveListener)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalPreferences.saveToEncryptedStorage(it.account) }
|
||||
}
|
||||
|
||||
private suspend fun prepareLogoutOrSwitch() =
|
||||
@OptIn(FlowPreview::class)
|
||||
suspend fun startUI(accountSettings: AccountSettings) =
|
||||
withContext(Dispatchers.Main) {
|
||||
when (val state = _accountContent.value) {
|
||||
is AccountState.LoggedIn -> {
|
||||
state.account.saveable.removeObserver(saveListener)
|
||||
withContext(Dispatchers.IO) { state.currentViewModelStore.viewModelStore.clear() }
|
||||
}
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
state.account.saveable.removeObserver(saveListener)
|
||||
withContext(Dispatchers.IO) { state.currentViewModelStore.viewModelStore.clear() }
|
||||
}
|
||||
else -> {}
|
||||
if (accountSettings.isWriteable()) {
|
||||
_accountContent.update { AccountState.LoggedIn(accountSettings) }
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(accountSettings) }
|
||||
}
|
||||
|
||||
collectorJob?.cancel()
|
||||
collectorJob =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountSettings.saveable.debounce(1000).collect {
|
||||
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareLogoutOrSwitch() =
|
||||
when (val state = _accountContent.value) {
|
||||
is AccountState.LoggedIn -> {
|
||||
collectorJob?.cancel()
|
||||
state.currentViewModelStore.viewModelStore.clear()
|
||||
}
|
||||
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
collectorJob?.cancel()
|
||||
state.currentViewModelStore.viewModelStore.clear()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
fun login(
|
||||
@ -290,24 +272,34 @@ class AccountStateViewModel : ViewModel() {
|
||||
name: String? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val keyPair = KeyPair()
|
||||
val account =
|
||||
Account(
|
||||
keyPair,
|
||||
proxy = proxy,
|
||||
val tempSigner = NostrSignerSync(keyPair)
|
||||
|
||||
val accountSettings =
|
||||
AccountSettings(
|
||||
keyPair = keyPair,
|
||||
backupUserMetadata = MetadataEvent.newUser(name, tempSigner),
|
||||
backupContactList =
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = listOf(Contact(keyPair.pubKey.toHexKey(), null)),
|
||||
followEvents = DefaultChannels.toList(),
|
||||
relayUse =
|
||||
Constants.defaultRelays.associate {
|
||||
it.url to ContactListEvent.ReadWrite(it.read, it.write)
|
||||
},
|
||||
signer = tempSigner,
|
||||
),
|
||||
backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner),
|
||||
backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner),
|
||||
backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList, tempSigner),
|
||||
proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort),
|
||||
proxyPort = proxyPort,
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
)
|
||||
|
||||
account.follow(account.userProfile())
|
||||
|
||||
// saves to local preferences
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account) {
|
||||
account.userProfile().latestContactList?.let { Client.send(it) }
|
||||
account.sendNewUserMetadata(name = name)
|
||||
}
|
||||
LocalPreferences.updatePrefsForLogin(accountSettings)
|
||||
|
||||
startUI(accountSettings)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,7 +368,7 @@ private fun copyNSec(
|
||||
account: Account,
|
||||
clipboardManager: ClipboardManager,
|
||||
) {
|
||||
account.keyPair.privKey?.let {
|
||||
account.settings.keyPair.privKey?.let {
|
||||
clipboardManager.setText(AnnotatedString(it.toNsec()))
|
||||
scope.launch {
|
||||
Toast
|
||||
@ -398,7 +398,7 @@ private fun encryptCopyNSec(
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
accountViewModel.account.keyPair.privKey?.let {
|
||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
||||
val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text)
|
||||
if (key != null) {
|
||||
clipboardManager.setText(AnnotatedString(key))
|
||||
|
@ -28,10 +28,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@ -42,7 +40,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@ -73,14 +71,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
@ -107,10 +103,8 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@ -127,7 +121,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@ -152,11 +145,11 @@ import kotlin.time.measureTimedValue
|
||||
|
||||
@Stable
|
||||
class AccountViewModel(
|
||||
val account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
val settings: SettingsState,
|
||||
) : ViewModel(),
|
||||
Dao {
|
||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||
val account = Account(accountSettings, accountSettings.createSigner(), viewModelScope)
|
||||
|
||||
// TODO: contact lists are not notes yet
|
||||
// val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
|
||||
@ -185,9 +178,6 @@ class AccountViewModel(
|
||||
|
||||
val feedStates = AccountFeedContentStates(this)
|
||||
|
||||
val showSensitiveContentChanges =
|
||||
account.live.map { it.account.showSensitiveContent }.distinctUntilChanged()
|
||||
|
||||
fun clearToasts() {
|
||||
viewModelScope.launch { toasts.emit(null) }
|
||||
}
|
||||
@ -259,7 +249,9 @@ class AccountViewModel(
|
||||
|
||||
fun reactToOrDelete(note: Note) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val reaction = account.reactionChoices.first()
|
||||
val reaction =
|
||||
account.settings.reactionChoices.value
|
||||
.first()
|
||||
if (hasReactedTo(note, reaction)) {
|
||||
deleteReactionTo(note, reaction)
|
||||
} else {
|
||||
@ -641,7 +633,7 @@ class AccountViewModel(
|
||||
onProgress(it)
|
||||
},
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
zapType = zapType ?: account.defaultZapType.value,
|
||||
zapType = zapType ?: account.settings.defaultZapType.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -768,22 +760,6 @@ class AccountViewModel(
|
||||
account.decryptZapContentAuthor(note, onReady)
|
||||
}
|
||||
|
||||
fun translateTo(lang: Locale) {
|
||||
account.updateTranslateTo(lang.language)
|
||||
}
|
||||
|
||||
fun dontTranslateFrom(lang: String) {
|
||||
account.addDontTranslateFrom(lang)
|
||||
}
|
||||
|
||||
fun prefer(
|
||||
source: String,
|
||||
target: String,
|
||||
preference: String,
|
||||
) {
|
||||
account.prefer(source, target, preference)
|
||||
}
|
||||
|
||||
fun follow(user: User) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.follow(user) }
|
||||
}
|
||||
@ -827,27 +803,6 @@ class AccountViewModel(
|
||||
|
||||
fun isFollowing(user: HexKey): Boolean = account.isFollowing(user)
|
||||
|
||||
val hideDeleteRequestDialog: Boolean
|
||||
get() = account.hideDeleteRequestDialog
|
||||
|
||||
fun dontShowDeleteRequestDialog() {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.setHideDeleteRequestDialog() }
|
||||
}
|
||||
|
||||
val hideNIP17WarningDialog: Boolean
|
||||
get() = account.hideNIP17WarningDialog
|
||||
|
||||
fun dontShowNIP17WarningDialog() {
|
||||
account.setHideNIP17WarningDialog()
|
||||
}
|
||||
|
||||
val hideBlockAlertDialog: Boolean
|
||||
get() = account.hideBlockAlertDialog
|
||||
|
||||
fun dontShowBlockAlertDialog() {
|
||||
account.setHideBlockAlertDialog()
|
||||
}
|
||||
|
||||
fun hideSensitiveContent() {
|
||||
account.updateShowSensitiveContent(false)
|
||||
}
|
||||
@ -866,7 +821,7 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultZapType(): LnZapEvent.ZapType = account.defaultZapType.value
|
||||
fun defaultZapType(): LnZapEvent.ZapType = account.settings.defaultZapType.value
|
||||
|
||||
fun unwrap(
|
||||
event: GiftWrapEvent,
|
||||
@ -1206,18 +1161,22 @@ class AccountViewModel(
|
||||
portNumber: MutableState<String>,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.proxyPort = portNumber.value.toInt()
|
||||
account.proxy = HttpClientManager.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
account.saveable.invalidateData()
|
||||
account.settings.updateProxy(checked, portNumber.value)
|
||||
Amethyst.instance.serviceManager.forceRestart()
|
||||
}
|
||||
}
|
||||
|
||||
fun restartServices() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Amethyst.instance.serviceManager.restartIfDifferentAccount(account)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
val settings: SettingsState,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel = AccountViewModel(account, settings) as AccountViewModel
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel = AccountViewModel(accountSettings, settings) as AccountViewModel
|
||||
}
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
@ -1478,7 +1437,7 @@ class AccountViewModel(
|
||||
val noteEvent = note.event
|
||||
noteEvent is NIP90ContentDiscoveryResponseEvent &&
|
||||
noteEvent.pubKey == pubkeyHex &&
|
||||
noteEvent.isTaggedUser(account.keyPair.pubKey.toHexKey()) &&
|
||||
noteEvent.isTaggedUser(account.signer.pubKey) &&
|
||||
noteEvent.createdAt > fifteenMinsAgo
|
||||
},
|
||||
comparator = CreatedAtComparator,
|
||||
@ -1519,7 +1478,7 @@ class AccountViewModel(
|
||||
context: Context,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (account.defaultZapType.value == LnZapEvent.ZapType.NONZAP) {
|
||||
if (account.settings.defaultZapType.value == LnZapEvent.ZapType.NONZAP) {
|
||||
LightningAddressResolver()
|
||||
.lnAddressInvoice(
|
||||
lnaddress,
|
||||
@ -1532,7 +1491,7 @@ class AccountViewModel(
|
||||
context = context,
|
||||
)
|
||||
} else {
|
||||
account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType.value) { zapRequest ->
|
||||
account.createZapRequestFor(toUserPubKeyHex, message, account.settings.defaultZapType.value) { zapRequest ->
|
||||
LocalCache.justConsume(zapRequest, null)
|
||||
LightningAddressResolver()
|
||||
.lnAddressInvoice(
|
||||
@ -1692,7 +1651,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
sharedPreferencesViewModel.init()
|
||||
|
||||
return AccountViewModel(
|
||||
Account(
|
||||
AccountSettings(
|
||||
// blank keys
|
||||
keyPair =
|
||||
KeyPair(
|
||||
@ -1700,7 +1659,6 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"),
|
||||
forcePubKeyCheck = false,
|
||||
),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
),
|
||||
sharedPreferencesViewModel.sharedPrefs,
|
||||
)
|
||||
|
@ -148,10 +148,8 @@ fun HiddenUsersScreen(
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
val pagerState = rememberPagerState { 3 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var warnAboutReports by remember {
|
||||
mutableStateOf(accountViewModel.account.warnAboutPostsWithReports)
|
||||
}
|
||||
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
|
||||
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.warnAboutPostsWithReports) }
|
||||
var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.filterSpamFromStrangers) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
@ -237,7 +235,10 @@ private fun AddMuteWordTextField(accountViewModel: AccountViewModel) {
|
||||
value = currentWordToAdd.value,
|
||||
onValueChange = { currentWordToAdd.value = it },
|
||||
label = { Text(text = stringRes(R.string.hide_new_word_label)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.hide_new_word_label),
|
||||
@ -272,10 +273,10 @@ fun WatchAccountAndBlockList(
|
||||
accountViewModel: AccountViewModel,
|
||||
invalidate: () -> Unit,
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val transientSpammers by accountViewModel.account.transientHiddenUsers.collectAsStateWithLifecycle()
|
||||
val blockListState by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState, blockListState) {
|
||||
LaunchedEffect(accountViewModel, transientSpammers, blockListState) {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
@ -467,7 +467,7 @@ fun EditFieldRow(
|
||||
galleryUri = it,
|
||||
alt = null,
|
||||
sensitiveContent = false,
|
||||
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||
server = ServerOption(accountViewModel.account.settings.defaultFileServer, false),
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
)
|
||||
|
@ -447,7 +447,7 @@ fun PrivateMessageEditFieldRow(
|
||||
alt = null,
|
||||
sensitiveContent = false,
|
||||
isPrivate = isPrivate,
|
||||
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||
server = ServerOption(accountViewModel.account.settings.defaultFileServer, false),
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
)
|
||||
@ -467,7 +467,7 @@ fun PrivateMessageEditFieldRow(
|
||||
modifier = Size30Modifier,
|
||||
onClick = {
|
||||
if (
|
||||
!accountViewModel.hideNIP17WarningDialog &&
|
||||
!accountViewModel.account.settings.hideNIP17WarningDialog &&
|
||||
!channelScreenModel.nip17 &&
|
||||
!channelScreenModel.requiresNIP17
|
||||
) {
|
||||
@ -549,13 +549,13 @@ fun NewFeatureNIP17AlertDialog(
|
||||
buttonIconResource = R.drawable.incognito,
|
||||
buttonText = stringRes(R.string.new_feature_nip17_activate),
|
||||
onClickDoOnce = {
|
||||
scope.launch(Dispatchers.IO) { onConfirm() }
|
||||
scope.launch { onConfirm() }
|
||||
onDismiss()
|
||||
},
|
||||
onClickDontShowAgain = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
scope.launch {
|
||||
onConfirm()
|
||||
accountViewModel.dontShowNIP17WarningDialog()
|
||||
accountViewModel.account.settings.setHideNIP17WarningDialog()
|
||||
}
|
||||
onDismiss()
|
||||
},
|
||||
|
@ -33,7 +33,6 @@ import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -153,7 +152,12 @@ private fun FeedLoaded(
|
||||
key = { _, item -> item.id() },
|
||||
contentType = { _, item -> item.javaClass.simpleName },
|
||||
) { _, item ->
|
||||
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
|
||||
val defaultModifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
RenderCardItem(
|
||||
@ -176,18 +180,20 @@ private fun ShowDonationCard(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val account by accountViewModel.account.live.observeAsState()
|
||||
if (account?.account?.hasDonatedInThisVersion() == false) {
|
||||
LoadNote(
|
||||
BuildConfig.RELEASE_NOTES_ID,
|
||||
accountViewModel,
|
||||
) { loadedNoteId ->
|
||||
if (loadedNoteId != null) {
|
||||
ZapTheDevsCard(
|
||||
loadedNoteId,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
if (!accountViewModel.account.hasDonatedInThisVersion()) {
|
||||
val donated by accountViewModel.account.observeDonatedInThisVersion().collectAsStateWithLifecycle()
|
||||
if (!donated) {
|
||||
LoadNote(
|
||||
BuildConfig.RELEASE_NOTES_ID,
|
||||
accountViewModel,
|
||||
) { loadedNoteId ->
|
||||
if (loadedNoteId != null) {
|
||||
ZapTheDevsCard(
|
||||
loadedNoteId,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +230,7 @@ private fun TranslationMessage(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||
if (source in accountViewModel.account.settings.dontTranslateFrom) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -251,17 +251,15 @@ private fun TranslationMessage(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
accountViewModel.account.settings.addDontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == source) {
|
||||
if (accountViewModel.account.settings.preferenceBetween(source, target) == source) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -283,7 +281,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, source)
|
||||
accountViewModel.account.settings.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -291,7 +289,7 @@ private fun TranslationMessage(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == target) {
|
||||
if (accountViewModel.account.settings.preferenceBetween(source, target) == target) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -313,7 +311,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
accountViewModel.account.settings.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -326,7 +324,7 @@ private fun TranslationMessage(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
if (lang.language in accountViewModel.account.translateTo) {
|
||||
if (accountViewModel.account.settings.translateToContains(lang)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -348,7 +346,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.translateTo(lang)
|
||||
accountViewModel.account.settings.updateTranslateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -375,13 +373,13 @@ fun TranslateAndWatchLanguageChanges(
|
||||
LanguageTranslatorService
|
||||
.autoTranslate(
|
||||
content,
|
||||
accountViewModel.account.dontTranslateFrom,
|
||||
accountViewModel.account.translateTo,
|
||||
accountViewModel.account.settings.dontTranslateFrom,
|
||||
accountViewModel.account.settings.translateTo,
|
||||
).addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
val preference =
|
||||
accountViewModel.account.preferenceBetween(
|
||||
accountViewModel.account.settings.preferenceBetween(
|
||||
task.result.sourceLang!!,
|
||||
task.result.targetLang!!,
|
||||
)
|
||||
|
@ -41,17 +41,7 @@ class SinceAuthorPerRelayFilter(
|
||||
// don't send it.
|
||||
override fun isValidFor(forRelay: String) = authors == null || !authors[forRelay].isNullOrEmpty()
|
||||
|
||||
override fun toJson(forRelay: String): String {
|
||||
// if authors is empty, but not null
|
||||
val authorsForThisRelay =
|
||||
if (authors != null) {
|
||||
authors[forRelay]?.ifEmpty { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||
}
|
||||
override fun toJson(forRelay: String): String = FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||
|
||||
override fun match(
|
||||
event: Event,
|
||||
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@ -139,6 +140,17 @@ class AdvertisedRelayListEvent(
|
||||
|
||||
signer.sign(createdAt, KIND, tags, msg, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
list: List<AdvertisedRelayInfo>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): AdvertisedRelayListEvent? {
|
||||
val tags = createTagArray(list)
|
||||
val msg = ""
|
||||
|
||||
return signer.sign(createdAt, KIND, tags, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable data class AdvertisedRelayInfo(
|
||||
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@ -97,5 +98,11 @@ class ChatMessageRelayListEvent(
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ChatMessageRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable data class Contact(
|
||||
@ -118,6 +119,46 @@ class ContactListEvent(
|
||||
const val KIND = 3
|
||||
const val ALT = "Follow List"
|
||||
|
||||
fun createFromScratch(
|
||||
followUsers: List<Contact> = emptyList(),
|
||||
followTags: List<String> = emptyList(),
|
||||
followGeohashes: List<String> = emptyList(),
|
||||
followCommunities: List<ATag> = emptyList(),
|
||||
followEvents: List<String> = emptyList(),
|
||||
relayUse: Map<String, ReadWrite>? = emptyMap(),
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ContactListEvent? {
|
||||
val content =
|
||||
if (relayUse != null) {
|
||||
mapper.writeValueAsString(relayUse)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val tags =
|
||||
listOf(arrayOf("alt", ALT)) +
|
||||
followUsers.map {
|
||||
if (it.relayUri != null) {
|
||||
arrayOf("p", it.pubKeyHex, it.relayUri)
|
||||
} else {
|
||||
arrayOf("p", it.pubKeyHex)
|
||||
}
|
||||
} +
|
||||
followTags.map { arrayOf("t", it) } +
|
||||
followEvents.map { arrayOf("e", it) } +
|
||||
followCommunities.map {
|
||||
if (it.relay != null) {
|
||||
arrayOf("a", it.toTag(), it.relay)
|
||||
} else {
|
||||
arrayOf("a", it.toTag())
|
||||
}
|
||||
} +
|
||||
followGeohashes.map { arrayOf("g", it) }
|
||||
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
followUsers: List<Contact>,
|
||||
followTags: List<String>,
|
||||
|
@ -33,6 +33,7 @@ import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import com.fasterxml.jackson.module.kotlin.addDeserializer
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
|
@ -78,7 +78,14 @@ class GiftWrapEvent(
|
||||
) {
|
||||
try {
|
||||
plainContent(signer) { giftStr ->
|
||||
val gift = fromJson(giftStr)
|
||||
val gift =
|
||||
try {
|
||||
fromJson(giftStr)
|
||||
} catch (e: Exception) {
|
||||
Log.w("GiftWrapEvent", "Couldn't Parse the content " + this.toNostrUri() + " " + giftStr)
|
||||
return@plainContent
|
||||
}
|
||||
|
||||
if (gift is WrappedEvent) {
|
||||
gift.host = HostStub(this.id, this.pubKey, this.kind)
|
||||
}
|
||||
@ -87,7 +94,7 @@ class GiftWrapEvent(
|
||||
onReady(gift)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("GiftWrapEvent", "Couldn't Decrypt the content", e)
|
||||
Log.w("GiftWrapEvent", "Couldn't Decrypt the content " + this.toNostrUri())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@ -47,5 +48,12 @@ class LnZapPrivateEvent(
|
||||
) {
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
signer: NostrSignerSync,
|
||||
tags: Array<Array<String>> = emptyArray(),
|
||||
content: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): LnZapPrivateEvent? = signer.sign(createdAt, KIND, tags, content)
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.StringWriter
|
||||
@ -176,6 +177,27 @@ class MetadataEvent(
|
||||
companion object {
|
||||
const val KIND = 0
|
||||
|
||||
fun newUser(
|
||||
name: String?,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MetadataEvent? {
|
||||
// Tries to not delete any existing attribute that we do not work with.
|
||||
val currentJson = ObjectMapper().createObjectNode()
|
||||
|
||||
name?.let { addIfNotBlank(currentJson, "name", it.trim()) }
|
||||
val writer = StringWriter()
|
||||
ObjectMapper().writeValue(writer, currentJson)
|
||||
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
tags.add(
|
||||
arrayOf("alt", "User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
|
||||
)
|
||||
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString())
|
||||
}
|
||||
|
||||
fun updateFromPast(
|
||||
latest: MetadataEvent?,
|
||||
name: String?,
|
||||
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@ -97,5 +98,11 @@ class SearchRelayListEvent(
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): SearchRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
|
@ -20,20 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.signers
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventFactory
|
||||
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
|
||||
class NostrSignerInternal(
|
||||
val keyPair: KeyPair,
|
||||
) : NostrSigner(keyPair.pubKey.toHexKey()) {
|
||||
val signerSync = NostrSignerSync(keyPair)
|
||||
|
||||
override fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
@ -41,46 +39,7 @@ class NostrSignerInternal(
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
if (isUnsignedPrivateEvent(kind, tags)) {
|
||||
// this is a private zap
|
||||
signPrivateZap(createdAt, kind, tags, content, onReady)
|
||||
} else {
|
||||
signNormal(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun isUnsignedPrivateEvent(
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
): Boolean =
|
||||
kind == LnZapRequestEvent.KIND &&
|
||||
tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() }
|
||||
|
||||
fun <T : Event> signNormal(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
val id = Event.generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey()
|
||||
|
||||
onReady(
|
||||
EventFactory.create(
|
||||
id.toHexKey(),
|
||||
pubKey,
|
||||
createdAt,
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
sig,
|
||||
) as T,
|
||||
)
|
||||
signerSync.sign<T>(createdAt, kind, tags, content)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip04Encrypt(
|
||||
@ -88,15 +47,7 @@ class NostrSignerInternal(
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
onReady(
|
||||
CryptoUtils.encryptNIP04(
|
||||
decryptedContent,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
),
|
||||
)
|
||||
signerSync.nip04Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip04Decrypt(
|
||||
@ -104,16 +55,7 @@ class NostrSignerInternal(
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
try {
|
||||
val sharedSecret =
|
||||
CryptoUtils.getSharedSecretNIP04(keyPair.privKey, fromPublicKey.hexToByteArray())
|
||||
|
||||
onReady(CryptoUtils.decryptNIP04(encryptedContent, sharedSecret))
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent")
|
||||
}
|
||||
signerSync.nip04Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip44Encrypt(
|
||||
@ -121,16 +63,7 @@ class NostrSignerInternal(
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
onReady(
|
||||
CryptoUtils
|
||||
.encryptNIP44(
|
||||
decryptedContent,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
).encodePayload(),
|
||||
)
|
||||
signerSync.nip44Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip44Decrypt(
|
||||
@ -138,119 +71,13 @@ class NostrSignerInternal(
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
CryptoUtils
|
||||
.decryptNIP44(
|
||||
payload = encryptedContent,
|
||||
privateKey = keyPair.privKey,
|
||||
pubKey = fromPublicKey.hexToByteArray(),
|
||||
)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
private fun <T> signPrivateZap(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
val zappedEvent = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
|
||||
val userHex = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } ?: return
|
||||
|
||||
// if it is a Zap for an Event, use event.id if not, use the user's pubkey
|
||||
val idToGeneratePrivateKey = zappedEvent ?: userHex
|
||||
|
||||
val encryptionPrivateKey =
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
idToGeneratePrivateKey,
|
||||
createdAt,
|
||||
)
|
||||
|
||||
val fullTagsNoAnon = tags.filter { t -> t.getOrNull(0) != "anon" }.toTypedArray()
|
||||
|
||||
LnZapPrivateEvent.create(this, fullTagsNoAnon, content) {
|
||||
val noteJson = it.toJson()
|
||||
val encryptedContent =
|
||||
LnZapRequestEvent.encryptPrivateZapMessage(
|
||||
noteJson,
|
||||
encryptionPrivateKey,
|
||||
userHex.hexToByteArray(),
|
||||
)
|
||||
|
||||
val newTags =
|
||||
tags.filter { t -> t.getOrNull(0) != "anon" } + listOf(arrayOf("anon", encryptedContent))
|
||||
val newContent = ""
|
||||
|
||||
NostrSignerInternal(KeyPair(encryptionPrivateKey))
|
||||
.signNormal(createdAt, kind, newTags.toTypedArray(), newContent, onReady)
|
||||
}
|
||||
signerSync.nip44Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
val recipientPK = event.zappedAuthor().firstOrNull()
|
||||
val recipientPost = event.zappedPost().firstOrNull()
|
||||
val privateEvent =
|
||||
if (recipientPK == pubKey) {
|
||||
// if the receiver is logged in, these are the params.
|
||||
val privateKeyToUse = keyPair.privKey
|
||||
val pubkeyToUse = event.pubKey
|
||||
|
||||
event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse)
|
||||
} else {
|
||||
// if the sender is logged in, these are the params
|
||||
val altPubkeyToUse = recipientPK
|
||||
val altPrivateKeyToUse =
|
||||
if (recipientPost != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
recipientPost,
|
||||
event.createdAt,
|
||||
)
|
||||
} else if (recipientPK != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
recipientPK,
|
||||
event.createdAt,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
try {
|
||||
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
||||
val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
|
||||
|
||||
if (altPubKeyFromPrivate == event.pubKey) {
|
||||
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
|
||||
|
||||
if (result == null) {
|
||||
Log.w(
|
||||
"Private ZAP Decrypt",
|
||||
"Fail to decrypt Zap from ${event.id}",
|
||||
)
|
||||
}
|
||||
result
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
privateEvent?.let { onReady(it) }
|
||||
signerSync.decryptZapEvent(event)?.let { onReady(it) }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.signers
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventFactory
|
||||
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
|
||||
class NostrSignerSync(
|
||||
val keyPair: KeyPair,
|
||||
val pubKey: HexKey = keyPair.pubKey.toHexKey(),
|
||||
) {
|
||||
fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
return if (isUnsignedPrivateZapEvent(kind, tags)) {
|
||||
// this is a private zap
|
||||
signPrivateZap(createdAt, kind, tags, content)
|
||||
} else {
|
||||
signNormal(createdAt, kind, tags, content)
|
||||
}
|
||||
}
|
||||
|
||||
fun isUnsignedPrivateZapEvent(
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
): Boolean =
|
||||
kind == LnZapRequestEvent.KIND &&
|
||||
tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() }
|
||||
|
||||
fun <T : Event> signNormal(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
val id = Event.generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey()
|
||||
|
||||
return EventFactory.create(
|
||||
id.toHexKey(),
|
||||
pubKey,
|
||||
createdAt,
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
sig,
|
||||
) as T
|
||||
}
|
||||
|
||||
fun nip04Encrypt(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
return CryptoUtils.encryptNIP04(
|
||||
decryptedContent,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
)
|
||||
}
|
||||
|
||||
fun nip04Decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
return try {
|
||||
val sharedSecret =
|
||||
CryptoUtils.getSharedSecretNIP04(keyPair.privKey, fromPublicKey.hexToByteArray())
|
||||
|
||||
CryptoUtils.decryptNIP04(encryptedContent, sharedSecret)
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun nip44Encrypt(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
return CryptoUtils
|
||||
.encryptNIP44(
|
||||
decryptedContent,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
).encodePayload()
|
||||
}
|
||||
|
||||
fun nip44Decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
return CryptoUtils
|
||||
.decryptNIP44(
|
||||
payload = encryptedContent,
|
||||
privateKey = keyPair.privKey,
|
||||
pubKey = fromPublicKey.hexToByteArray(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> signPrivateZap(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
val zappedEvent = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
|
||||
val userHex = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } ?: return null
|
||||
|
||||
// if it is a Zap for an Event, use event.id if not, use the user's pubkey
|
||||
val idToGeneratePrivateKey = zappedEvent ?: userHex
|
||||
|
||||
val encryptionPrivateKey =
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
idToGeneratePrivateKey,
|
||||
createdAt,
|
||||
)
|
||||
|
||||
val fullTagsNoAnon = tags.filter { t -> t.getOrNull(0) != "anon" }.toTypedArray()
|
||||
|
||||
val privateEvent = LnZapPrivateEvent.create(this, fullTagsNoAnon, content) ?: return null
|
||||
|
||||
val noteJson = privateEvent.toJson()
|
||||
val encryptedContent =
|
||||
LnZapRequestEvent.encryptPrivateZapMessage(
|
||||
noteJson,
|
||||
encryptionPrivateKey,
|
||||
userHex.hexToByteArray(),
|
||||
)
|
||||
|
||||
val newTags =
|
||||
tags.filter { t -> t.getOrNull(0) != "anon" } + listOf(arrayOf("anon", encryptedContent))
|
||||
val newContent = ""
|
||||
|
||||
return NostrSignerSync(KeyPair(encryptionPrivateKey)).signNormal(createdAt, kind, newTags.toTypedArray(), newContent)
|
||||
}
|
||||
|
||||
fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? {
|
||||
if (keyPair.privKey == null) return null
|
||||
|
||||
val recipientPK = event.zappedAuthor().firstOrNull()
|
||||
val recipientPost = event.zappedPost().firstOrNull()
|
||||
val privateEvent =
|
||||
if (recipientPK == pubKey) {
|
||||
// if the receiver is logged in, these are the params.
|
||||
val privateKeyToUse = keyPair.privKey
|
||||
val pubkeyToUse = event.pubKey
|
||||
|
||||
event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse)
|
||||
} else {
|
||||
// if the sender is logged in, these are the params
|
||||
val altPubkeyToUse = recipientPK
|
||||
val altPrivateKeyToUse =
|
||||
if (recipientPost != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
recipientPost,
|
||||
event.createdAt,
|
||||
)
|
||||
} else if (recipientPK != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
keyPair.privKey.toHexKey(),
|
||||
recipientPK,
|
||||
event.createdAt,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
try {
|
||||
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
||||
val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
|
||||
|
||||
if (altPubKeyFromPrivate == event.pubKey) {
|
||||
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
|
||||
|
||||
if (result == null) {
|
||||
Log.w(
|
||||
"Private ZAP Decrypt",
|
||||
"Fail to decrypt Zap from ${event.id}",
|
||||
)
|
||||
}
|
||||
result
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return privateEvent
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user