Uses NIP-78 to start syncing some data between instances of Amethyst

This commit is contained in:
Vitor Pamplona 2024-10-22 14:52:11 -04:00
parent bb430605bc
commit a3b0436d91
23 changed files with 886 additions and 367 deletions

View File

@ -105,7 +105,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [ ] Zap Goals (NIP-75)
- [ ] Arbitrary Custom App Data (NIP-78)
- [x] Arbitrary Custom App Data (NIP-78)
- [x] Highlights (NIP-84)
- [x] Notify Request (NIP-88/Draft)
- [x] Recommended Application Handlers (NIP-89)

View File

@ -26,7 +26,12 @@ 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.AccountLanguagePreferencesInternal
import com.vitorpamplona.amethyst.model.AccountReactionPreferencesInternal
import com.vitorpamplona.amethyst.model.AccountSecurityPreferencesInternal
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
import com.vitorpamplona.amethyst.model.AccountZapPreferencesInternal
import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
@ -45,6 +50,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event
@ -53,11 +59,9 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@ -104,6 +108,7 @@ private object PrefKeys {
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
const val LATEST_MUTE_LIST = "latestMuteList"
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
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
@ -310,19 +315,7 @@ object LocalPreferences {
}
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(settings.languagePreferences),
)
putString(PrefKeys.TRANSLATE_TO, settings.translateTo)
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(settings.zapAmountChoices.value))
putString(
PrefKeys.REACTION_CHOICES,
Event.mapper.writeValueAsString(settings.reactionChoices.value),
)
putString(PrefKeys.DEFAULT_ZAPTYPE, settings.defaultZapType.value.name)
putString(
PrefKeys.DEFAULT_FILE_SERVER,
Event.mapper.writeValueAsString(settings.defaultFileServer),
@ -404,6 +397,15 @@ object LocalPreferences {
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
}
if (settings.backupAppSpecificData != null) {
putString(
PrefKeys.LATEST_APP_SPECIFIC_DATA,
Event.mapper.writeValueAsString(settings.backupAppSpecificData),
)
} else {
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
}
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)
@ -414,9 +416,6 @@ object LocalPreferences {
putString(PrefKeys.TOR_SETTINGS, Event.mapper.writeValueAsString(settings.torSettings.toSettings()))
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, settings.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, settings.filterSpamFromStrangers)
val regularMap =
settings.lastReadPerRoute.value.mapValues {
it.value.value
@ -428,12 +427,6 @@ object LocalPreferences {
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
if (settings.showSensitiveContent.value == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, settings.showSensitiveContent.value!!)
}
putString(
PrefKeys.PENDING_ATTESTATIONS,
Event.mapper.writeValueAsString(settings.pendingAttestations.value),
@ -510,9 +503,6 @@ object LocalPreferences {
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()
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
val defaultHomeFollowList =
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
val defaultStoriesFollowList =
@ -528,13 +518,12 @@ object LocalPreferences {
} ?: LnZapEvent.ZapType.PUBLIC
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 defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
@ -543,6 +532,54 @@ object LocalPreferences {
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 latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
val syncedSettings =
if (latestAppSpecificData != null) {
null
} else {
// previous version. Delete this when ready.
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
val showSensitiveContent =
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
} else {
null
}
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
AccountSyncedSettingsInternal(
reactions =
AccountReactionPreferencesInternal(
reactionChoices = reactionChoices,
),
zaps =
AccountZapPreferencesInternal(
zapAmountChoices = zapAmountChoices,
defaultZapType = defaultZapType,
),
languages =
AccountLanguagePreferencesInternal(
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
),
security =
AccountSecurityPreferencesInternal(
showSensitiveContent = showSensitiveContent,
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
),
)
}
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
@ -571,15 +608,6 @@ object LocalPreferences {
parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
}
val showSensitiveContent =
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
} else {
null
}
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val lastReadPerRoute =
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
MutableStateFlow(it.value)
@ -594,12 +622,6 @@ object LocalPreferences {
externalSignerPackageName = externalSignerPackageName,
localRelays = localRelays,
localRelayServers = localRelayServers,
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
zapAmountChoices = MutableStateFlow(zapAmountChoices.toImmutableList()),
reactionChoices = MutableStateFlow(reactionChoices.toImmutableList()),
defaultZapType = MutableStateFlow(defaultZapType),
defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
@ -616,10 +638,9 @@ object LocalPreferences {
backupSearchRelayList = latestSearchRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupSyncedSettings = syncedSettings,
torSettings = TorSettingsFlow.build(torSettings),
showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations),

View File

@ -115,7 +115,12 @@ class ServiceManager(
HttpClientManager.setDefaultProxy(null)
}
LocalCache.antiSpam.active = account?.settings?.filterSpamFromStrangers ?: true
// Convert this into a flow
LocalCache.antiSpam.active = account
?.settings
?.syncedSettings
?.security
?.filterSpamFromStrangers ?: true
Coil.setImageLoader {
Amethyst.instance
.imageLoaderBuilder()

View File

@ -27,6 +27,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
@ -44,9 +45,11 @@ 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.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
@ -128,7 +131,9 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal
import java.util.Locale
import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
@OptIn(DelicateCoroutinesApi::class)
@ -138,6 +143,10 @@ class Account(
val signer: NostrSigner = settings.createSigner(),
val scope: CoroutineScope,
) {
companion object {
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
}
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
data class PaymentRequest(
@ -959,7 +968,7 @@ class Account(
getBlockListNote().flow().metadata.stateFlow,
getMuteListNote().flow().metadata.stateFlow,
transientHiddenUsers,
settings.showSensitiveContent,
settings.syncedSettings.security.showSensitiveContent,
) { blockList, muteList, transientHiddenUsers, showSensitiveContent ->
checkNotInMainThread()
emit(assembleLiveHiddenUsers(blockList.note, muteList.note, transientHiddenUsers, showSensitiveContent))
@ -972,7 +981,7 @@ class Account(
getBlockListNote(),
getMuteListNote(),
transientHiddenUsers.value,
settings.showSensitiveContent.value,
settings.syncedSettings.security.showSensitiveContent.value,
)
},
)
@ -1025,14 +1034,83 @@ class Account(
fun updateOptOutOptions(
warnReports: Boolean,
filterSpam: Boolean,
) {
): Boolean {
if (settings.updateOptOutOptions(warnReports, filterSpam)) {
LocalCache.antiSpam.active = settings.filterSpamFromStrangers
if (!settings.filterSpamFromStrangers) {
if (!settings.syncedSettings.security.filterSpamFromStrangers) {
transientHiddenUsers.update {
emptySet()
}
}
sendNewAppSpecificData()
return true
}
return false
}
fun updateShowSensitiveContent(show: Boolean?) {
if (settings.updateShowSensitiveContent(show)) {
sendNewAppSpecificData()
}
}
fun changeReactionTypes(reactionSet: List<String>) {
if (settings.changeReactionTypes(reactionSet)) {
sendNewAppSpecificData()
}
}
fun updateZapAmounts(
amountSet: List<Long>,
selectedZapType: LnZapEvent.ZapType,
nip47Update: Nip47WalletConnect.Nip47URI?,
) {
var changed = false
if (settings.changeZapAmounts(amountSet)) changed = true
if (settings.changeDefaultZapType(selectedZapType)) changed = true
if (settings.changeZapPaymentRequest(nip47Update)) changed = true
if (changed) {
sendNewAppSpecificData()
}
}
fun toggleDontTranslateFrom(languageCode: String) {
settings.toggleDontTranslateFrom(languageCode)
sendNewAppSpecificData()
}
fun updateTranslateTo(languageCode: Locale) {
if (settings.updateTranslateTo(languageCode)) {
sendNewAppSpecificData()
}
}
fun prefer(
source: String,
target: String,
preference: String,
) {
settings.prefer(source, target, preference)
sendNewAppSpecificData()
}
private fun sendNewAppSpecificData() {
sendNewAppSpecificData(settings.syncedSettings.toInternal())
}
private fun sendNewAppSpecificData(toInternal: AccountSyncedSettingsInternal) {
signer.nip44Encrypt(Event.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted ->
AppSpecificDataEvent.create(
dTag = APP_SPECIFIC_DATA_D_TAG,
description = encrypted,
otherTags = emptyArray(),
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}
@ -2788,6 +2866,13 @@ class Account(
}
}
fun getAppSpecificDataNote(): AddressableNote {
val aTag = AppSpecificDataEvent.createTag(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG)
return LocalCache.getOrCreateAddressableNote(aTag)
}
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
fun getBlockListNote(): AddressableNote {
val aTag =
ATag(
@ -3118,7 +3203,7 @@ class Account(
return true
}
if (!settings.warnAboutPostsWithReports) {
if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
return !isHidden(user) &&
// if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
@ -3131,7 +3216,7 @@ class Account(
}
private fun isAcceptableDirect(note: Note): Boolean {
if (!settings.warnAboutPostsWithReports) {
if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
return !note.hasReportsBy(userProfile())
}
return !note.hasReportsBy(userProfile()) &&
@ -3355,8 +3440,6 @@ class Account(
(event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true)
}
fun updateShowSensitiveContent(show: Boolean?) = settings.updateShowSensitiveContent(show)
fun markAsRead(
route: String,
timestampInSecs: Long,
@ -3516,7 +3599,7 @@ class Account(
}
settings.backupPrivateHomeRelayList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved search relay list ${event.toJson()}")
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
GlobalScope.launch(Dispatchers.IO) {
event.privateTags(signer) {
LocalCache.verifyAndConsume(event, null)
@ -3524,6 +3607,24 @@ class Account(
}
}
settings.backupAppSpecificData?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}")
GlobalScope.launch(Dispatchers.IO) {
LocalCache.verifyAndConsume(event, null)
signer.decrypt(event.content, event.pubKey) { decrypted ->
try {
val syncedSettings = Event.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
settings.syncedSettings.updateFrom(syncedSettings)
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
e.printStackTrace()
AccountSyncedSettingsInternal()
}
}
}
}
settings.backupMuteList?.let {
Log.d("AccountRegisterObservers", "Loading saved mute list ${it.toJson()}")
GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) }
@ -3597,6 +3698,28 @@ class Account(
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
getAppSpecificDataFlow().collect {
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${userProfile().toBestDisplayName()}")
(it.note.event as? AppSpecificDataEvent)?.let {
signer.decrypt(it.content, it.pubKey) { decrypted ->
val syncedSettings =
try {
Event.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
e.printStackTrace()
AccountSyncedSettingsInternal()
}
settings.updateAppSpecificData(it, syncedSettings)
}
}
}
}
scope.launch(Dispatchers.Default) {
LocalCache.antiSpam.flowSpam.collect {
it.cache.spamMessages.snapshot().values.forEach { spammer ->

View File

@ -20,9 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import android.content.res.Resources
import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
@ -34,6 +32,7 @@ import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.LnZapEvent
@ -44,8 +43,6 @@ 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.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -61,17 +58,6 @@ val DefaultChannels =
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
)
val DefaultReactions =
persistentListOf(
"\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),
@ -93,17 +79,6 @@ val DefaultSearchRelayList =
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
)
val DefaultZapAmounts = persistentListOf(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 "
@ -117,12 +92,6 @@ class AccountSettings(
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<ImmutableList<Long>> = MutableStateFlow(DefaultZapAmounts),
var reactionChoices: MutableStateFlow<ImmutableList<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),
@ -139,16 +108,19 @@ class AccountSettings(
var backupSearchRelayList: SearchRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null,
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
var backupAppSpecificData: AppSpecificDataEvent? = null,
backupSyncedSettings: AccountSyncedSettingsInternal? = null, // only exist for migration purposes
val torSettings: TorSettingsFlow = TorSettingsFlow(),
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))
val syncedSettings: AccountSyncedSettings =
backupSyncedSettings?.let { AccountSyncedSettings(it) }
?: AccountSyncedSettings(AccountSyncedSettingsInternal())
class AccountSettingsUpdater(
val accountSettings: AccountSettings,
)
@ -173,32 +145,40 @@ class AccountSettings(
// Zaps and Reactions
// ---
fun changeDefaultZapType(zapType: LnZapEvent.ZapType) {
if (defaultZapType.value != zapType) {
defaultZapType.tryEmit(zapType)
fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean {
if (syncedSettings.zaps.defaultZapType.value != zapType) {
syncedSettings.zaps.defaultZapType.tryEmit(zapType)
saveAccountSettings()
return true
}
return false
}
fun changeZapAmounts(newAmounts: List<Long>) {
if (zapAmountChoices.value != newAmounts) {
zapAmountChoices.tryEmit(newAmounts.toImmutableList())
fun changeZapAmounts(newAmounts: List<Long>): Boolean {
if (syncedSettings.zaps.zapAmountChoices.value != newAmounts) {
syncedSettings.zaps.zapAmountChoices.tryEmit(newAmounts.toImmutableList())
saveAccountSettings()
return true
}
return false
}
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?) {
fun changeReactionTypes(newTypes: List<String>): Boolean {
if (syncedSettings.reactions.reactionChoices.value != newTypes) {
syncedSettings.reactions.reactionChoices.tryEmit(newTypes.toImmutableList())
saveAccountSettings()
return true
}
return false
}
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?): Boolean {
if (zapPaymentRequest != newServer) {
zapPaymentRequest = newServer
saveAccountSettings()
return true
}
}
fun changeReactionTypes(newTypes: List<String>) {
if (reactionChoices.value != newTypes) {
reactionChoices.tryEmit(newTypes.toImmutableList())
saveAccountSettings()
}
return false
}
// ---
@ -260,22 +240,18 @@ class AccountSettings(
// language services
// ---
fun toggleDontTranslateFrom(languageCode: String) {
if (!dontTranslateFrom.contains(languageCode)) {
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
saveAccountSettings()
} else {
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
saveAccountSettings()
}
syncedSettings.languages.toggleDontTranslateFrom(languageCode)
saveAccountSettings()
}
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language)
fun updateTranslateTo(languageCode: Locale) {
if (translateTo != languageCode.language) {
translateTo = languageCode.language
fun updateTranslateTo(languageCode: Locale): Boolean {
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
saveAccountSettings()
return true
}
return false
}
fun prefer(
@ -283,23 +259,14 @@ class AccountSettings(
target: String,
preference: String,
) {
val key = "$source,$target"
if (key !in languagePreferences) {
languagePreferences = languagePreferences + Pair(key, preference)
saveAccountSettings()
} else {
if (languagePreferences.get(key) == preference) {
languagePreferences = languagePreferences.minus(key)
} else {
languagePreferences = languagePreferences + Pair(key, preference)
}
}
syncedSettings.languages.prefer(source, target, preference)
saveAccountSettings()
}
fun preferenceBetween(
source: String,
target: String,
): String? = languagePreferences["$source,$target"]
): String? = syncedSettings.languages.preferenceBetween(source, target)
// ----
// Backup Lists
@ -382,6 +349,22 @@ class AccountSettings(
}
}
fun updateAppSpecificData(
appSettings: AppSpecificDataEvent?,
newSyncedSettings: AccountSyncedSettingsInternal,
) {
if (appSettings == null || appSettings.content().isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupAppSpecificData?.id != appSettings.id) {
println("AABBCC Update App Specific Data")
backupAppSpecificData = appSettings
syncedSettings.updateFrom(newSyncedSettings)
saveAccountSettings()
}
}
// ----
// Warning dialogs
// ----
@ -407,15 +390,6 @@ class AccountSettings(
}
}
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (showSensitiveContent.value != show) {
showSensitiveContent.update { show }
saveAccountSettings()
return true
}
return false
}
// ---
// donations
// ---
@ -509,16 +483,20 @@ class AccountSettings(
// ---
// filters
// ---
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (syncedSettings.security.updateShowSensitiveContent(show)) {
saveAccountSettings()
return true
}
return false
}
fun updateOptOutOptions(
warnReports: Boolean,
filterSpam: Boolean,
): Boolean =
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
if (syncedSettings.security.updateOptOutOptions(warnReports, filterSpam)) {
saveAccountSettings()
true
} else {
false

View File

@ -0,0 +1,208 @@
/**
* 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 androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.util.Locale
@Stable
class AccountSyncedSettings(
internalSettings: AccountSyncedSettingsInternal,
) {
val reactions = AccountReactionPreferences(MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList()))
val zaps =
AccountZapPreferences(
MutableStateFlow(internalSettings.zaps.zapAmountChoices.toImmutableList()),
MutableStateFlow(internalSettings.zaps.defaultZapType),
)
val languages =
AccountLanguagePreferences(
internalSettings.languages.dontTranslateFrom,
internalSettings.languages.languagePreferences,
internalSettings.languages.translateTo,
)
val security =
AccountSecurityPreferences(
MutableStateFlow(internalSettings.security.showSensitiveContent),
internalSettings.security.warnAboutPostsWithReports,
internalSettings.security.filterSpamFromStrangers,
)
fun toInternal(): AccountSyncedSettingsInternal =
AccountSyncedSettingsInternal(
reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value),
zaps =
AccountZapPreferencesInternal(
zaps.zapAmountChoices.value,
zaps.defaultZapType.value,
),
languages =
AccountLanguagePreferencesInternal(
languages.dontTranslateFrom,
languages.languagePreferences,
languages.translateTo,
),
security =
AccountSecurityPreferencesInternal(
security.showSensitiveContent.value,
security.warnAboutPostsWithReports,
security.filterSpamFromStrangers,
),
)
fun updateFrom(syncedSettingsInternal: AccountSyncedSettingsInternal) {
val newReactionChoices = syncedSettingsInternal.reactions.reactionChoices.toImmutableList()
if (!equalImmutableLists(reactions.reactionChoices.value, newReactionChoices)) {
reactions.reactionChoices.tryEmit(newReactionChoices)
}
val newZapChoices = syncedSettingsInternal.zaps.zapAmountChoices.toImmutableList()
if (!equalImmutableLists(zaps.zapAmountChoices.value, newZapChoices)) {
zaps.zapAmountChoices.tryEmit(newZapChoices)
}
if (zaps.defaultZapType.value != syncedSettingsInternal.zaps.defaultZapType) {
zaps.defaultZapType.tryEmit(syncedSettingsInternal.zaps.defaultZapType)
}
if (languages.dontTranslateFrom != syncedSettingsInternal.languages.dontTranslateFrom) {
languages.dontTranslateFrom = syncedSettingsInternal.languages.dontTranslateFrom
}
if (languages.languagePreferences != syncedSettingsInternal.languages.languagePreferences) {
languages.languagePreferences = syncedSettingsInternal.languages.languagePreferences
}
if (languages.translateTo != syncedSettingsInternal.languages.translateTo) {
languages.translateTo = syncedSettingsInternal.languages.translateTo
}
if (security.showSensitiveContent.value != syncedSettingsInternal.security.showSensitiveContent) {
security.showSensitiveContent.tryEmit(syncedSettingsInternal.security.showSensitiveContent)
}
if (security.filterSpamFromStrangers != syncedSettingsInternal.security.filterSpamFromStrangers) {
security.filterSpamFromStrangers = syncedSettingsInternal.security.filterSpamFromStrangers
}
if (security.warnAboutPostsWithReports != syncedSettingsInternal.security.warnAboutPostsWithReports) {
security.warnAboutPostsWithReports = syncedSettingsInternal.security.warnAboutPostsWithReports
}
}
}
@Stable
class AccountReactionPreferences(
var reactionChoices: MutableStateFlow<ImmutableList<String>>,
)
@Stable
class AccountZapPreferences(
var zapAmountChoices: MutableStateFlow<ImmutableList<Long>>,
val defaultZapType: MutableStateFlow<LnZapEvent.ZapType>,
)
@Stable
class AccountLanguagePreferences(
var dontTranslateFrom: Set<String>,
var languagePreferences: Map<String, String>,
var translateTo: String,
) {
// ---
// language services
// ---
fun toggleDontTranslateFrom(languageCode: String) {
if (!dontTranslateFrom.contains(languageCode)) {
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
} else {
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
}
}
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
fun updateTranslateTo(languageCode: Locale): Boolean {
if (translateTo != languageCode.language) {
translateTo = languageCode.language
return true
}
return false
}
fun prefer(
source: String,
target: String,
preference: String,
) {
val key = "$source,$target"
if (key !in languagePreferences) {
languagePreferences = languagePreferences + Pair(key, preference)
} else {
if (languagePreferences.get(key) == preference) {
languagePreferences = languagePreferences.minus(key)
} else {
languagePreferences = languagePreferences + Pair(key, preference)
}
}
}
fun preferenceBetween(
source: String,
target: String,
): String? = languagePreferences["$source,$target"]
}
@Stable
class AccountSecurityPreferences(
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
) {
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (showSensitiveContent.value != show) {
showSensitiveContent.update { show }
return true
}
return false
}
// ---
// filters
// ---
fun updateOptOutOptions(
warnReports: Boolean,
filterSpam: Boolean,
): Boolean =
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
true
} else {
false
}
}

View File

@ -0,0 +1,76 @@
/**
* 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.core.os.ConfigurationCompat
import com.vitorpamplona.quartz.events.LnZapEvent
import java.util.Locale
val DefaultReactions =
listOf(
"\uD83D\uDE80",
"\uD83E\uDEC2",
"\uD83D\uDC40",
"\uD83D\uDE02",
"\uD83C\uDF89",
"\uD83E\uDD14",
"\uD83D\uDE31",
)
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
}
class AccountSyncedSettingsInternal(
val reactions: AccountReactionPreferencesInternal = AccountReactionPreferencesInternal(),
val zaps: AccountZapPreferencesInternal = AccountZapPreferencesInternal(),
val languages: AccountLanguagePreferencesInternal = AccountLanguagePreferencesInternal(),
val security: AccountSecurityPreferencesInternal = AccountSecurityPreferencesInternal(),
)
class AccountReactionPreferencesInternal(
var reactionChoices: List<String> = DefaultReactions,
)
class AccountZapPreferencesInternal(
var zapAmountChoices: List<Long> = DefaultZapAmounts,
val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
)
class AccountLanguagePreferencesInternal(
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language,
)
class AccountSecurityPreferencesInternal(
val showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
)

View File

@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
@ -1232,6 +1233,13 @@ object LocalCache {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: AppSpecificDataEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
@Suppress("UNUSED_PARAMETER")
fun consume(event: RecommendRelayEvent) {
// // Log.d("RR", event.toJson())
@ -2645,6 +2653,7 @@ object LocalCache {
is AdvertisedRelayListEvent -> consume(event, relay)
is AppDefinitionEvent -> consume(event, relay)
is AppRecommendationEvent -> consume(event, relay)
is AppSpecificDataEvent -> consume(event, relay)
is AudioHeaderEvent -> consume(event, relay)
is AudioTrackEvent -> consume(event, relay)
is BadgeAwardEvent -> consume(event)

View File

@ -33,6 +33,7 @@ import com.vitorpamplona.ammolite.relays.filters.EOSETime
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
@ -78,35 +79,15 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
kinds = listOf(ContactListEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
fun createAccountMetadataFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
fun createAccountRelayListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
kinds =
listOf(
MetadataEvent.KIND,
ContactListEvent.KIND,
StatusEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
@ -138,7 +119,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
PeopleListEvent.KIND,
),
authors = otherAuthors,
limit = 100,
limit = otherAuthors.size * 10,
),
)
}
@ -154,6 +135,18 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
),
)
fun createAccountSettings2Filter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
kinds = listOf(AppSpecificDataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)),
limit = 1,
),
)
fun createAccountReportsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
@ -465,8 +458,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
accountChannel.typedFilters =
listOfNotNull(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettings2Filter(),
createNotificationFilter(),
createNotificationFilter2(),
createGiftWrapsToMeFilter(),
@ -480,9 +472,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
accountChannel.typedFilters =
listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettingsFilter(),
createAccountSettings2Filter(),
).ifEmpty { null }
}

View File

@ -97,9 +97,7 @@ fun SensitivityWarning(
accountViewModel: AccountViewModel,
content: @Composable () -> Unit,
) {
val accountState =
accountViewModel.account.settings.showSensitiveContent
.collectAsStateWithLifecycle()
val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle()
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }

View File

@ -525,16 +525,12 @@ fun ZapVote(
)
return@combinedClickable
} else if (
accountViewModel.account.settings.zapAmountChoices.value.size == 1 &&
pollViewModel.isValidInputVoteAmount(
accountViewModel.account.settings.zapAmountChoices.value
.first(),
)
accountViewModel.zapAmountChoices().size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
) {
accountViewModel.zap(
baseNote,
accountViewModel.account.settings.zapAmountChoices.value
.first() * 1000,
accountViewModel.zapAmountChoices().first() * 1000,
poolOption.option,
"",
context,
@ -667,7 +663,7 @@ fun FilteredZapAmountChoicePopup(
val context = LocalContext.current
// TODO: Move this to the viewModel
val zapPaymentChoices by accountViewModel.account.settings.zapAmountChoices
val zapPaymentChoices by accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle()
val zapMessage = ""

View File

@ -965,9 +965,9 @@ private fun likeClick(
)
return
}
if (accountViewModel.account.settings.reactionChoices.value
.isEmpty()
) {
val choices = accountViewModel.reactionChoices()
if (choices.isEmpty()) {
accountViewModel.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change,
@ -977,9 +977,9 @@ private fun likeClick(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts,
)
} else if (accountViewModel.account.settings.reactionChoices.value.size == 1) {
} else if (choices.size == 1) {
onWantsToSignReaction()
} else if (accountViewModel.account.settings.reactionChoices.value.size > 1) {
} else if (choices.size > 1) {
onMultipleChoices()
}
}
@ -1181,9 +1181,9 @@ fun zapClick(
return
}
if (accountViewModel.account.settings.zapAmountChoices.value
.isEmpty()
) {
val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
accountViewModel.toast(
R.string.error_dialog_zap_error,
R.string.no_zap_amount_setup_long_press_to_change,
@ -1193,11 +1193,10 @@ 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.settings.zapAmountChoices.value.size == 1) {
} else if (choices.size == 1) {
accountViewModel.zap(
baseNote,
accountViewModel.account.settings.zapAmountChoices.value
.first() * 1000,
choices.first() * 1000,
null,
"",
context,
@ -1205,7 +1204,7 @@ fun zapClick(
onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent,
)
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) {
} else if (choices.size > 1) {
onMultipleChoices()
}
}
@ -1408,8 +1407,7 @@ fun ReactionChoicePopup(
) {
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
val reactions by accountViewModel.account.settings.reactionChoices
.collectAsStateWithLifecycle()
val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle()
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
Popup(
@ -1573,7 +1571,7 @@ fun ZapAmountChoicePopup(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {
val zapAmountChoices by
accountViewModel.account.settings.zapAmountChoices
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle()
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)

View File

@ -49,7 +49,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -69,12 +68,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
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
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.firstFullChar
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
@ -94,16 +93,17 @@ import com.vitorpamplona.quartz.events.EmojiUrl
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class UpdateReactionTypeViewModel(
val accountSettings: AccountSettings,
) : ViewModel() {
class UpdateReactionTypeViewModel : ViewModel() {
var account: Account? = null
var nextChoice by mutableStateOf(TextFieldValue(""))
var reactionSet by mutableStateOf(listOf<String>())
fun load() {
this.reactionSet = accountSettings.reactionChoices.value
fun load(myAccount: Account) {
this.account = myAccount
this.reactionSet = myAccount.settings.syncedSettings.reactions.reactionChoices.value
}
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
@ -124,21 +124,24 @@ class UpdateReactionTypeViewModel(
}
fun sendPost() {
accountSettings.changeReactionTypes(reactionSet)
nextChoice = TextFieldValue("")
viewModelScope.launch(Dispatchers.IO) {
account?.changeReactionTypes(reactionSet)
nextChoice = TextFieldValue("")
}
}
fun cancel() {
nextChoice = TextFieldValue("")
}
fun hasChanged(): Boolean = reactionSet != accountSettings.reactionChoices.value
class Factory(
val accountSettings: AccountSettings,
) : ViewModelProvider.Factory {
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel = UpdateReactionTypeViewModel(accountSettings) as UpdateReactionTypeViewModel
}
fun hasChanged(): Boolean =
reactionSet !=
account
?.settings
?.syncedSettings
?.reactions
?.reactionChoices
?.value
}
@OptIn(ExperimentalLayoutApi::class)
@ -148,14 +151,20 @@ fun UpdateReactionTypeDialog(
accountViewModel: AccountViewModel,
nav: INav,
) {
val postViewModel: UpdateReactionTypeViewModel =
viewModel(
key = "UpdateReactionTypeViewModel",
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account.settings),
)
val postViewModel: UpdateReactionTypeViewModel = viewModel()
postViewModel.load(accountViewModel.account)
LaunchedEffect(accountViewModel) { postViewModel.load() }
UpdateReactionTypeDialog(postViewModel, onClose, accountViewModel, nav)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateReactionTypeDialog(
postViewModel: UpdateReactionTypeViewModel,
onClose: () -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
Dialog(
onDismissRequest = { onClose() },
properties =

View File

@ -82,10 +82,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
@ -106,10 +106,12 @@ import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class UpdateZapAmountViewModel : ViewModel() {
var account: Account? = null
class UpdateZapAmountViewModel(
val accountSettings: AccountSettings,
) : ViewModel() {
var nextAmount by mutableStateOf(TextFieldValue(""))
var amountSet by mutableStateOf(listOf<Long>())
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
@ -124,15 +126,23 @@ class UpdateZapAmountViewModel(
updateNIP47(text)
}
fun load() {
this.amountSet = accountSettings.zapAmountChoices.value
fun load(myAccount: Account) {
this.account = myAccount
this.amountSet = myAccount.settings.syncedSettings.zaps.zapAmountChoices.value
this.selectedZapType = myAccount.settings.syncedSettings.zaps.defaultZapType.value
this.walletConnectPubkey =
accountSettings.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
myAccount.settings.zapPaymentRequest
?.pubKeyHex
?.let { TextFieldValue(it) } ?: TextFieldValue("")
this.walletConnectRelay =
accountSettings.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
myAccount.settings.zapPaymentRequest
?.relayUri
?.let { TextFieldValue(it) } ?: TextFieldValue("")
this.walletConnectSecret =
accountSettings.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("")
this.selectedZapType = accountSettings.defaultZapType.value
myAccount.settings.zapPaymentRequest
?.secret
?.let { TextFieldValue(it) } ?: TextFieldValue("")
}
fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
@ -151,37 +161,37 @@ class UpdateZapAmountViewModel(
}
fun sendPost() {
accountSettings.changeZapAmounts(amountSet)
accountSettings.changeDefaultZapType(selectedZapType)
val nip47Update =
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
val pubkeyHex =
try {
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
val pubkeyHex =
try {
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlFormatter.normalize(it) }
val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) }
val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlFormatter.normalize(it) }
val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) }
if (pubkeyHex != null && relayUrl != null) {
accountSettings.changeZapPaymentRequest(
if (pubkeyHex != null && relayUrl != null) {
Nip47WalletConnect.Nip47URI(
pubkeyHex,
relayUrl,
privKeyHex,
),
)
)
} else {
null
}
} else {
accountSettings.changeZapPaymentRequest(null)
null
}
} else {
accountSettings.changeZapPaymentRequest(null)
}
nextAmount = TextFieldValue("")
viewModelScope.launch(Dispatchers.IO) {
account?.updateZapAmounts(amountSet, selectedZapType, nip47Update)
nextAmount = TextFieldValue("")
}
}
fun cancel() {
@ -190,11 +200,23 @@ class UpdateZapAmountViewModel(
fun hasChanged(): Boolean =
(
selectedZapType != accountSettings.defaultZapType.value ||
amountSet != accountSettings.zapAmountChoices.value ||
walletConnectPubkey.text != (accountSettings.zapPaymentRequest?.pubKeyHex ?: "") ||
walletConnectRelay.text != (accountSettings.zapPaymentRequest?.relayUri ?: "") ||
walletConnectSecret.text != (accountSettings.zapPaymentRequest?.secret ?: "")
selectedZapType !=
account
?.settings
?.syncedSettings
?.zaps
?.defaultZapType
?.value ||
amountSet !=
account
?.settings
?.syncedSettings
?.zaps
?.zapAmountChoices
?.value ||
walletConnectPubkey.text != (account?.settings?.zapPaymentRequest?.pubKeyHex ?: "") ||
walletConnectRelay.text != (account?.settings?.zapPaymentRequest?.relayUri ?: "") ||
walletConnectSecret.text != (account?.settings?.zapPaymentRequest?.secret ?: "")
)
fun updateNIP47(uri: String) {
@ -205,20 +227,25 @@ class UpdateZapAmountViewModel(
walletConnectSecret = TextFieldValue(contact.secret ?: "")
}
}
class Factory(
val accountSettings: AccountSettings,
) : ViewModelProvider.Factory {
override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel = UpdateZapAmountViewModel(accountSettings) as UpdateZapAmountViewModel
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateZapAmountDialog(
onClose: () -> Unit,
nip47uri: String? = null,
accountViewModel: AccountViewModel,
) {
val postViewModel: UpdateZapAmountViewModel = viewModel()
postViewModel.load(accountViewModel.account)
UpdateZapAmountDialog(postViewModel, onClose, nip47uri, accountViewModel)
}
@Composable
fun UpdateZapAmountDialog(
postViewModel: UpdateZapAmountViewModel,
onClose: () -> Unit,
nip47uri: String? = null,
accountViewModel: AccountViewModel,
) {
Dialog(
onDismissRequest = { onClose() },
@ -233,12 +260,6 @@ fun UpdateZapAmountDialog(
modifier = Modifier.fillMaxWidth(),
) {
Column {
val postViewModel: UpdateZapAmountViewModel =
viewModel(
key = "UpdateZapAmountViewModel",
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@ -311,7 +332,6 @@ fun UpdateZapAmountContent(
}
LaunchedEffect(accountViewModel, nip47uri) {
postViewModel.load()
if (nip47uri != null) {
try {
postViewModel.updateNIP47(nip47uri)

View File

@ -153,7 +153,7 @@ fun ZapCustomDialog(
}
var selectedZapType by
remember(accountViewModel) { mutableStateOf(accountViewModel.account.settings.defaultZapType.value) }
remember(accountViewModel) { mutableStateOf(accountViewModel.defaultZapType()) }
Dialog(
onDismissRequest = { onClose() },
@ -224,7 +224,7 @@ fun ZapCustomDialog(
label = stringRes(id = R.string.zap_type),
placeholder =
zapTypes
.filter { it.first == accountViewModel.account.settings.defaultZapType.value }
.filter { it.first == accountViewModel.defaultZapType() }
.first()
.second,
options = zapOptions,

View File

@ -315,10 +315,8 @@ fun NoteDropDownMenu(
DropdownMenuItem(
text = { Text(stringRes(R.string.content_warning_hide_all_sensitive_content)) },
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.hideSensitiveContent()
onDismiss()
}
accountViewModel.hideSensitiveContent()
onDismiss()
},
)
}
@ -326,10 +324,8 @@ fun NoteDropDownMenu(
DropdownMenuItem(
text = { Text(stringRes(R.string.content_warning_show_all_sensitive_content)) },
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.disableContentWarnings()
onDismiss()
}
accountViewModel.disableContentWarnings()
onDismiss()
},
)
}
@ -337,10 +333,8 @@ fun NoteDropDownMenu(
DropdownMenuItem(
text = { Text(stringRes(R.string.content_warning_see_warnings)) },
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.seeContentWarnings()
onDismiss()
}
accountViewModel.seeContentWarnings()
onDismiss()
},
)
}
@ -385,7 +379,8 @@ fun WatchBookmarksFollowsAndAccount(
.live()
.bookmarks
.observeAsState()
val showSensitiveContent by accountViewModel.account.settings.showSensitiveContent
val showSensitiveContent by accountViewModel
.showSensitiveContent()
.collectAsStateWithLifecycle()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {

View File

@ -455,9 +455,9 @@ fun customZapClick(
return
}
if (accountViewModel.account.settings.zapAmountChoices.value
.isEmpty()
) {
val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
accountViewModel.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
@ -467,10 +467,8 @@ 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.settings.zapAmountChoices.value.size == 1) {
val amount =
accountViewModel.account.settings.zapAmountChoices.value
.first()
} else if (choices.size == 1) {
val amount = choices.first()
if (amount > 1100) {
accountViewModel.zap(
@ -488,11 +486,9 @@ fun customZapClick(
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
// recommends amounts for a monthly release.
}
} 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 if (choices.size > 1) {
if (choices.any { it > 1100 }) {
onMultipleChoices(choices)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
}

View File

@ -120,6 +120,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@ -328,9 +329,7 @@ class AccountViewModel(
fun reactToOrDelete(note: Note) {
viewModelScope.launch(Dispatchers.IO) {
val reaction =
account.settings.reactionChoices.value
.first()
val reaction = reactionChoices().first()
if (hasReactedTo(note, reaction)) {
deleteReactionTo(note, reaction)
} else {
@ -714,7 +713,7 @@ class AccountViewModel(
onProgress(it)
},
onPayViaIntent = onPayViaIntent,
zapType = zapType ?: account.settings.defaultZapType.value,
zapType = zapType ?: defaultZapType(),
)
}
}
@ -885,24 +884,55 @@ class AccountViewModel(
fun isFollowing(user: HexKey): Boolean = account.isFollowing(user)
fun hideSensitiveContent() {
account.updateShowSensitiveContent(false)
}
fun disableContentWarnings() {
account.updateShowSensitiveContent(true)
}
fun seeContentWarnings() {
account.updateShowSensitiveContent(null)
}
fun markDonatedInThisVersion() {
viewModelScope.launch {
account.markDonatedInThisVersion()
viewModelScope.launch(Dispatchers.IO) {
account.updateShowSensitiveContent(false)
}
}
fun defaultZapType(): LnZapEvent.ZapType = account.settings.defaultZapType.value
fun disableContentWarnings() {
viewModelScope.launch(Dispatchers.IO) {
account.updateShowSensitiveContent(true)
}
}
fun seeContentWarnings() {
viewModelScope.launch(Dispatchers.IO) {
account.updateShowSensitiveContent(null)
}
}
fun markDonatedInThisVersion() {
account.markDonatedInThisVersion()
}
fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom
fun translateTo() = account.settings.syncedSettings.languages.translateTo
fun defaultZapType() = account.settings.syncedSettings.zaps.defaultZapType.value
fun showSensitiveContent(): MutableStateFlow<Boolean?> = account.settings.syncedSettings.security.showSensitiveContent
fun zapAmountChoicesFlow() = account.settings.syncedSettings.zaps.zapAmountChoices
fun zapAmountChoices() = zapAmountChoicesFlow().value
fun reactionChoicesFlow() = account.settings.syncedSettings.reactions.reactionChoices
fun reactionChoices() = reactionChoicesFlow().value
fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers
fun updateOptOutOptions(
warnReports: Boolean,
filterSpam: Boolean,
) {
viewModelScope.launch(Dispatchers.IO) {
if (account.updateOptOutOptions(warnReports, filterSpam)) {
LocalCache.antiSpam.active = filterSpamFromStrangers()
}
}
}
fun unwrap(
event: GiftWrapEvent,
@ -1539,7 +1569,7 @@ class AccountViewModel(
context: Context,
) {
viewModelScope.launch(Dispatchers.IO) {
if (account.settings.defaultZapType.value == LnZapEvent.ZapType.NONZAP) {
if (defaultZapType() == LnZapEvent.ZapType.NONZAP) {
LightningAddressResolver()
.lnAddressInvoice(
lnaddress,
@ -1553,7 +1583,7 @@ class AccountViewModel(
context = context,
)
} else {
account.createZapRequestFor(toUserPubKeyHex, message, account.settings.defaultZapType.value) { zapRequest ->
account.createZapRequestFor(toUserPubKeyHex, message, defaultZapType()) { zapRequest ->
LocalCache.justConsume(zapRequest, null)
LightningAddressResolver()
.lnAddressInvoice(

View File

@ -42,19 +42,25 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.stringRes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NIP47SetupScreen(
accountViewModel: AccountViewModel,
nav: INav,
nip47: String?,
) {
val postViewModel: UpdateZapAmountViewModel =
viewModel(
key = "UpdateZapAmountViewModel",
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
)
val postViewModel: UpdateZapAmountViewModel = viewModel()
postViewModel.load(accountViewModel.account)
NIP47SetupScreen(postViewModel, accountViewModel, nav, nip47)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NIP47SetupScreen(
postViewModel: UpdateZapAmountViewModel,
accountViewModel: AccountViewModel,
nav: INav,
nip47: String?,
) {
Scaffold(
topBar = {
TopAppBar(

View File

@ -157,15 +157,15 @@ fun SecurityFiltersScreen(
Column(Modifier.padding(it).fillMaxHeight()) {
val pagerState = rememberPagerState { 3 }
val coroutineScope = rememberCoroutineScope()
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.filterSpamFromStrangers) }
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.filterSpamFromStrangers) }
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = warnAboutReports,
onCheckedChange = {
warnAboutReports = it
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
accountViewModel.updateOptOutOptions(warnAboutReports, filterSpam)
},
)
@ -177,7 +177,7 @@ fun SecurityFiltersScreen(
checked = filterSpam,
onCheckedChange = {
filterSpam = it
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
accountViewModel.updateOptOutOptions(warnAboutReports, filterSpam)
},
)

View File

@ -234,7 +234,7 @@ private fun TranslationMessage(
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
if (source in accountViewModel.account.settings.dontTranslateFrom) {
if (source in accountViewModel.dontTranslateFrom()) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
@ -255,7 +255,7 @@ private fun TranslationMessage(
}
},
onClick = {
accountViewModel.account.settings.toggleDontTranslateFrom(source)
accountViewModel.account.toggleDontTranslateFrom(source)
langSettingsPopupExpanded = false
},
)
@ -285,7 +285,7 @@ private fun TranslationMessage(
},
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.prefer(source, target, source)
accountViewModel.account.prefer(source, target, source)
langSettingsPopupExpanded = false
}
},
@ -293,7 +293,9 @@ private fun TranslationMessage(
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
if (accountViewModel.account.settings.preferenceBetween(source, target) == target) {
if (accountViewModel.account.settings.syncedSettings.languages
.preferenceBetween(source, target) == target
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
@ -315,7 +317,7 @@ private fun TranslationMessage(
},
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.prefer(source, target, target)
accountViewModel.account.prefer(source, target, target)
langSettingsPopupExpanded = false
}
},
@ -350,7 +352,7 @@ private fun TranslationMessage(
},
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.updateTranslateTo(lang)
accountViewModel.account.updateTranslateTo(lang)
langSettingsPopupExpanded = false
}
},
@ -377,8 +379,8 @@ fun TranslateAndWatchLanguageChanges(
LanguageTranslatorService
.autoTranslate(
content,
accountViewModel.account.settings.dontTranslateFrom,
accountViewModel.account.settings.translateTo,
accountViewModel.dontTranslateFrom(),
accountViewModel.translateTo(),
).addOnCompleteListener { task ->
if (task.isSuccessful && !content.equals(task.result.result, true)) {
if (task.result.sourceLang != null && task.result.targetLang != null) {

View File

@ -0,0 +1,70 @@
/**
* 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.events
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
class AppSpecificDataEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 30078
const val ALT = "Arbitrary app data"
fun createTag(
pubkey: HexKey,
dTag: String,
): ATag = ATag(KIND, pubkey, dTag, null)
fun create(
dTag: String,
description: String,
otherTags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (AppSpecificDataEvent) -> Unit,
) {
val withD =
if (otherTags.any { it.size > 1 && it[0] == "d" && it[1] == dTag }) {
otherTags
} else {
otherTags.filter { it.size > 0 && it[0] != "d" }.toTypedArray() + arrayOf("d", dTag)
}
val newTags =
if (withD.none { it.size > 0 && it[0] == "alt" }) {
withD + arrayOf("alt", ALT)
} else {
withD
}
signer.sign(createdAt, KIND, newTags, description, onReady)
}
}
}

View File

@ -39,26 +39,22 @@ class EventFactory {
content: String,
sig: String,
) = when (kind) {
AdvertisedRelayListEvent.KIND ->
AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND ->
AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AppSpecificDataEvent.KIND -> AppSpecificDataEvent(id, pubKey, createdAt, tags, content, sig)
AudioHeaderEvent.KIND -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.KIND ->
CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.KIND ->
CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.KIND ->
ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.KIND -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
@ -79,23 +75,19 @@ class EventFactory {
}
ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.KIND ->
CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.KIND ->
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND ->
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.KIND ->
FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
@ -105,16 +97,12 @@ class EventFactory {
GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND ->
HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND ->
LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.KIND ->
LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.KIND ->
LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)