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] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72) - [x] Moderated Communities (NIP-72)
- [ ] Zap Goals (NIP-75) - [ ] Zap Goals (NIP-75)
- [ ] Arbitrary Custom App Data (NIP-78) - [x] Arbitrary Custom App Data (NIP-78)
- [x] Highlights (NIP-84) - [x] Highlights (NIP-84)
- [x] Notify Request (NIP-88/Draft) - [x] Notify Request (NIP-88/Draft)
- [x] Recommended Application Handlers (NIP-89) - [x] Recommended Application Handlers (NIP-89)

View File

@@ -26,7 +26,12 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue 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.AccountSettings
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
import com.vitorpamplona.amethyst.model.AccountZapPreferencesInternal
import com.vitorpamplona.amethyst.model.DefaultReactions import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS 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.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event 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.MuteListEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.SearchRelayListEvent import com.vitorpamplona.quartz.events.SearchRelayListEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -104,6 +108,7 @@ private object PrefKeys {
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList" const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
const val LATEST_MUTE_LIST = "latestMuteList" const val LATEST_MUTE_LIST = "latestMuteList"
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList" 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_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later 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()) } settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(settings.localRelays)) 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( putString(
PrefKeys.DEFAULT_FILE_SERVER, PrefKeys.DEFAULT_FILE_SERVER,
Event.mapper.writeValueAsString(settings.defaultFileServer), Event.mapper.writeValueAsString(settings.defaultFileServer),
@@ -404,6 +397,15 @@ object LocalPreferences {
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST) 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_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog) putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
@@ -414,9 +416,6 @@ object LocalPreferences {
putString(PrefKeys.TOR_SETTINGS, Event.mapper.writeValueAsString(settings.torSettings.toSettings())) 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 = val regularMap =
settings.lastReadPerRoute.value.mapValues { settings.lastReadPerRoute.value.mapValues {
it.value.value it.value.value
@@ -428,12 +427,6 @@ object LocalPreferences {
) )
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value) 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( putString(
PrefKeys.PENDING_ATTESTATIONS, PrefKeys.PENDING_ATTESTATIONS,
Event.mapper.writeValueAsString(settings.pendingAttestations.value), Event.mapper.writeValueAsString(settings.pendingAttestations.value),
@@ -510,9 +503,6 @@ object LocalPreferences {
getString(PrefKeys.SIGNER_PACKAGE_NAME, null) getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else 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 = val defaultHomeFollowList =
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
val defaultStoriesFollowList = val defaultStoriesFollowList =
@@ -528,13 +518,12 @@ object LocalPreferences {
} ?: LnZapEvent.ZapType.PUBLIC } ?: LnZapEvent.ZapType.PUBLIC
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet() 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 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 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 latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST) val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
@@ -543,6 +532,54 @@ object LocalPreferences {
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST) val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST) val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_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 hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
@@ -571,15 +608,6 @@ object LocalPreferences {
parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings() 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 = val lastReadPerRoute =
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues { parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
MutableStateFlow(it.value) MutableStateFlow(it.value)
@@ -594,12 +622,6 @@ object LocalPreferences {
externalSignerPackageName = externalSignerPackageName, externalSignerPackageName = externalSignerPackageName,
localRelays = localRelays, localRelays = localRelays,
localRelayServers = localRelayServers, localRelayServers = localRelayServers,
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
zapAmountChoices = MutableStateFlow(zapAmountChoices.toImmutableList()),
reactionChoices = MutableStateFlow(reactionChoices.toImmutableList()),
defaultZapType = MutableStateFlow(defaultZapType),
defaultFileServer = defaultFileServer, defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList), defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList), defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
@@ -616,10 +638,9 @@ object LocalPreferences {
backupSearchRelayList = latestSearchRelayList, backupSearchRelayList = latestSearchRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList, backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList, backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupSyncedSettings = syncedSettings,
torSettings = TorSettingsFlow.build(torSettings), torSettings = TorSettingsFlow.build(torSettings),
showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = MutableStateFlow(lastReadPerRoute), lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations), pendingAttestations = MutableStateFlow(pendingAttestations),

View File

@@ -115,7 +115,12 @@ class ServiceManager(
HttpClientManager.setDefaultProxy(null) 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 { Coil.setImageLoader {
Amethyst.instance Amethyst.instance
.imageLoaderBuilder() .imageLoaderBuilder()

View File

@@ -27,6 +27,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource 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.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
@@ -128,7 +131,9 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale
import java.util.UUID import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -138,6 +143,10 @@ class Account(
val signer: NostrSigner = settings.createSigner(), val signer: NostrSigner = settings.createSigner(),
val scope: CoroutineScope, val scope: CoroutineScope,
) { ) {
companion object {
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
}
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()) var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
data class PaymentRequest( data class PaymentRequest(
@@ -959,7 +968,7 @@ class Account(
getBlockListNote().flow().metadata.stateFlow, getBlockListNote().flow().metadata.stateFlow,
getMuteListNote().flow().metadata.stateFlow, getMuteListNote().flow().metadata.stateFlow,
transientHiddenUsers, transientHiddenUsers,
settings.showSensitiveContent, settings.syncedSettings.security.showSensitiveContent,
) { blockList, muteList, transientHiddenUsers, showSensitiveContent -> ) { blockList, muteList, transientHiddenUsers, showSensitiveContent ->
checkNotInMainThread() checkNotInMainThread()
emit(assembleLiveHiddenUsers(blockList.note, muteList.note, transientHiddenUsers, showSensitiveContent)) emit(assembleLiveHiddenUsers(blockList.note, muteList.note, transientHiddenUsers, showSensitiveContent))
@@ -972,7 +981,7 @@ class Account(
getBlockListNote(), getBlockListNote(),
getMuteListNote(), getMuteListNote(),
transientHiddenUsers.value, transientHiddenUsers.value,
settings.showSensitiveContent.value, settings.syncedSettings.security.showSensitiveContent.value,
) )
}, },
) )
@@ -1025,14 +1034,83 @@ class Account(
fun updateOptOutOptions( fun updateOptOutOptions(
warnReports: Boolean, warnReports: Boolean,
filterSpam: Boolean, filterSpam: Boolean,
) { ): Boolean {
if (settings.updateOptOutOptions(warnReports, filterSpam)) { if (settings.updateOptOutOptions(warnReports, filterSpam)) {
LocalCache.antiSpam.active = settings.filterSpamFromStrangers if (!settings.syncedSettings.security.filterSpamFromStrangers) {
if (!settings.filterSpamFromStrangers) {
transientHiddenUsers.update { transientHiddenUsers.update {
emptySet() 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 { fun getBlockListNote(): AddressableNote {
val aTag = val aTag =
ATag( ATag(
@@ -3118,7 +3203,7 @@ class Account(
return true return true
} }
if (!settings.warnAboutPostsWithReports) { if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
return !isHidden(user) && return !isHidden(user) &&
// if user hasn't hided this author // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
@@ -3131,7 +3216,7 @@ class Account(
} }
private fun isAcceptableDirect(note: Note): Boolean { private fun isAcceptableDirect(note: Note): Boolean {
if (!settings.warnAboutPostsWithReports) { if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
return !note.hasReportsBy(userProfile()) return !note.hasReportsBy(userProfile())
} }
return !note.hasReportsBy(userProfile()) && return !note.hasReportsBy(userProfile()) &&
@@ -3355,8 +3440,6 @@ class Account(
(event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true) (event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true)
} }
fun updateShowSensitiveContent(show: Boolean?) = settings.updateShowSensitiveContent(show)
fun markAsRead( fun markAsRead(
route: String, route: String,
timestampInSecs: Long, timestampInSecs: Long,
@@ -3516,7 +3599,7 @@ class Account(
} }
settings.backupPrivateHomeRelayList?.let { event -> 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) { GlobalScope.launch(Dispatchers.IO) {
event.privateTags(signer) { event.privateTags(signer) {
LocalCache.verifyAndConsume(event, null) 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 { settings.backupMuteList?.let {
Log.d("AccountRegisterObservers", "Loading saved mute list ${it.toJson()}") Log.d("AccountRegisterObservers", "Loading saved mute list ${it.toJson()}")
GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) } 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) { scope.launch(Dispatchers.Default) {
LocalCache.antiSpam.flowSpam.collect { LocalCache.antiSpam.flowSpam.collect {
it.cache.spamMessages.snapshot().values.forEach { spammer -> it.cache.spamMessages.snapshot().values.forEach { spammer ->

View File

@@ -20,9 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import android.content.res.Resources
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow 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.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.LnZapEvent 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.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -61,17 +58,6 @@ val DefaultChannels =
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
) )
val DefaultReactions =
persistentListOf(
"\uD83D\uDE80",
"\uD83E\uDEC2",
"\uD83D\uDC40",
"\uD83D\uDE02",
"\uD83C\uDF89",
"\uD83E\uDD14",
"\uD83D\uDE31",
)
val DefaultNIP65List = val DefaultNIP65List =
listOf( listOf(
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH), AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
@@ -93,17 +79,6 @@ val DefaultSearchRelayList =
RelayUrlFormatter.normalize("wss://relay.noswhere.com"), 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. // This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val GLOBAL_FOLLOWS = " Global " val GLOBAL_FOLLOWS = " Global "
@@ -117,12 +92,6 @@ class AccountSettings(
var externalSignerPackageName: String? = null, var externalSignerPackageName: String? = null,
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(), var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
var localRelayServers: Set<String> = setOf(), 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], var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0],
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS), val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS), val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
@@ -139,16 +108,19 @@ class AccountSettings(
var backupSearchRelayList: SearchRelayListEvent? = null, var backupSearchRelayList: SearchRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null, var backupMuteList: MuteListEvent? = null,
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null, var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
var backupAppSpecificData: AppSpecificDataEvent? = null,
backupSyncedSettings: AccountSyncedSettingsInternal? = null, // only exist for migration purposes
val torSettings: TorSettingsFlow = TorSettingsFlow(), 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()), val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()), var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()), val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
) { ) {
val saveable = MutableStateFlow(AccountSettingsUpdater(this)) val saveable = MutableStateFlow(AccountSettingsUpdater(this))
val syncedSettings: AccountSyncedSettings =
backupSyncedSettings?.let { AccountSyncedSettings(it) }
?: AccountSyncedSettings(AccountSyncedSettingsInternal())
class AccountSettingsUpdater( class AccountSettingsUpdater(
val accountSettings: AccountSettings, val accountSettings: AccountSettings,
) )
@@ -173,32 +145,40 @@ class AccountSettings(
// Zaps and Reactions // Zaps and Reactions
// --- // ---
fun changeDefaultZapType(zapType: LnZapEvent.ZapType) { fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean {
if (defaultZapType.value != zapType) { if (syncedSettings.zaps.defaultZapType.value != zapType) {
defaultZapType.tryEmit(zapType) syncedSettings.zaps.defaultZapType.tryEmit(zapType)
saveAccountSettings() saveAccountSettings()
return true
} }
return false
} }
fun changeZapAmounts(newAmounts: List<Long>) { fun changeZapAmounts(newAmounts: List<Long>): Boolean {
if (zapAmountChoices.value != newAmounts) { if (syncedSettings.zaps.zapAmountChoices.value != newAmounts) {
zapAmountChoices.tryEmit(newAmounts.toImmutableList()) syncedSettings.zaps.zapAmountChoices.tryEmit(newAmounts.toImmutableList())
saveAccountSettings() 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) { if (zapPaymentRequest != newServer) {
zapPaymentRequest = newServer zapPaymentRequest = newServer
saveAccountSettings() saveAccountSettings()
return true
} }
} return false
fun changeReactionTypes(newTypes: List<String>) {
if (reactionChoices.value != newTypes) {
reactionChoices.tryEmit(newTypes.toImmutableList())
saveAccountSettings()
}
} }
// --- // ---
@@ -260,22 +240,18 @@ class AccountSettings(
// language services // language services
// --- // ---
fun toggleDontTranslateFrom(languageCode: String) { fun toggleDontTranslateFrom(languageCode: String) {
if (!dontTranslateFrom.contains(languageCode)) { syncedSettings.languages.toggleDontTranslateFrom(languageCode)
dontTranslateFrom = dontTranslateFrom.plus(languageCode) saveAccountSettings()
saveAccountSettings()
} else {
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
saveAccountSettings()
}
} }
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language) fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language)
fun updateTranslateTo(languageCode: Locale) { fun updateTranslateTo(languageCode: Locale): Boolean {
if (translateTo != languageCode.language) { if (syncedSettings.languages.updateTranslateTo(languageCode)) {
translateTo = languageCode.language
saveAccountSettings() saveAccountSettings()
return true
} }
return false
} }
fun prefer( fun prefer(
@@ -283,23 +259,14 @@ class AccountSettings(
target: String, target: String,
preference: String, preference: String,
) { ) {
val key = "$source,$target" syncedSettings.languages.prefer(source, target, preference)
if (key !in languagePreferences) { saveAccountSettings()
languagePreferences = languagePreferences + Pair(key, preference)
saveAccountSettings()
} else {
if (languagePreferences.get(key) == preference) {
languagePreferences = languagePreferences.minus(key)
} else {
languagePreferences = languagePreferences + Pair(key, preference)
}
}
} }
fun preferenceBetween( fun preferenceBetween(
source: String, source: String,
target: String, target: String,
): String? = languagePreferences["$source,$target"] ): String? = syncedSettings.languages.preferenceBetween(source, target)
// ---- // ----
// Backup Lists // 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 // 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 // donations
// --- // ---
@@ -509,16 +483,20 @@ class AccountSettings(
// --- // ---
// filters // filters
// --- // ---
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (syncedSettings.security.updateShowSensitiveContent(show)) {
saveAccountSettings()
return true
}
return false
}
fun updateOptOutOptions( fun updateOptOutOptions(
warnReports: Boolean, warnReports: Boolean,
filterSpam: Boolean, filterSpam: Boolean,
): Boolean = ): Boolean =
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) { if (syncedSettings.security.updateOptOutOptions(warnReports, filterSpam)) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
saveAccountSettings() saveAccountSettings()
true true
} else { } else {
false 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.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AppRecommendationEvent import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent
@@ -1232,6 +1233,13 @@ object LocalCache {
consumeBaseReplaceable(event, relay) consumeBaseReplaceable(event, relay)
} }
fun consume(
event: AppSpecificDataEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun consume(event: RecommendRelayEvent) { fun consume(event: RecommendRelayEvent) {
// // Log.d("RR", event.toJson()) // // Log.d("RR", event.toJson())
@@ -2645,6 +2653,7 @@ object LocalCache {
is AdvertisedRelayListEvent -> consume(event, relay) is AdvertisedRelayListEvent -> consume(event, relay)
is AppDefinitionEvent -> consume(event, relay) is AppDefinitionEvent -> consume(event, relay)
is AppRecommendationEvent -> consume(event, relay) is AppRecommendationEvent -> consume(event, relay)
is AppSpecificDataEvent -> consume(event, relay)
is AudioHeaderEvent -> consume(event, relay) is AudioHeaderEvent -> consume(event, relay)
is AudioTrackEvent -> consume(event, relay) is AudioTrackEvent -> consume(event, relay)
is BadgeAwardEvent -> consume(event) 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.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent
@@ -78,35 +79,15 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
val latestEOSEs = EOSEAccount() val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>() 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 = 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( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
SincePerRelayFilter( SincePerRelayFilter(
kinds = kinds =
listOf( listOf(
MetadataEvent.KIND,
ContactListEvent.KIND,
StatusEvent.KIND, StatusEvent.KIND,
AdvertisedRelayListEvent.KIND, AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND, ChatMessageRelayListEvent.KIND,
@@ -138,7 +119,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
PeopleListEvent.KIND, PeopleListEvent.KIND,
), ),
authors = otherAuthors, 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 = fun createAccountReportsFilter(): TypedFilter =
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
@@ -465,8 +458,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
accountChannel.typedFilters = accountChannel.typedFilters =
listOfNotNull( listOfNotNull(
createAccountMetadataFilter(), createAccountMetadataFilter(),
createAccountContactListFilter(), createAccountSettings2Filter(),
createAccountRelayListFilter(),
createNotificationFilter(), createNotificationFilter(),
createNotificationFilter2(), createNotificationFilter2(),
createGiftWrapsToMeFilter(), createGiftWrapsToMeFilter(),
@@ -480,9 +472,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
accountChannel.typedFilters = accountChannel.typedFilters =
listOf( listOf(
createAccountMetadataFilter(), createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettingsFilter(), createAccountSettingsFilter(),
createAccountSettings2Filter(),
).ifEmpty { null } ).ifEmpty { null }
} }

View File

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

View File

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

View File

@@ -965,9 +965,9 @@ private fun likeClick(
) )
return return
} }
if (accountViewModel.account.settings.reactionChoices.value
.isEmpty() val choices = accountViewModel.reactionChoices()
) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toast(
R.string.no_reactions_setup, R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change, R.string.no_reaction_type_setup_long_press_to_change,
@@ -977,9 +977,9 @@ private fun likeClick(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts, 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() onWantsToSignReaction()
} else if (accountViewModel.account.settings.reactionChoices.value.size > 1) { } else if (choices.size > 1) {
onMultipleChoices() onMultipleChoices()
} }
} }
@@ -1181,9 +1181,9 @@ fun zapClick(
return return
} }
if (accountViewModel.account.settings.zapAmountChoices.value val choices = accountViewModel.zapAmountChoices()
.isEmpty()
) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toast(
R.string.error_dialog_zap_error, R.string.error_dialog_zap_error,
R.string.no_zap_amount_setup_long_press_to_change, R.string.no_zap_amount_setup_long_press_to_change,
@@ -1193,11 +1193,10 @@ fun zapClick(
R.string.error_dialog_zap_error, R.string.error_dialog_zap_error,
R.string.login_with_a_private_key_to_be_able_to_send_zaps, 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( accountViewModel.zap(
baseNote, baseNote,
accountViewModel.account.settings.zapAmountChoices.value choices.first() * 1000,
.first() * 1000,
null, null,
"", "",
context, context,
@@ -1205,7 +1204,7 @@ fun zapClick(
onProgress = { onZappingProgress(it) }, onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent, onPayViaIntent = onPayViaIntent,
) )
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) { } else if (choices.size > 1) {
onMultipleChoices() onMultipleChoices()
} }
} }
@@ -1408,8 +1407,7 @@ fun ReactionChoicePopup(
) { ) {
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
val reactions by accountViewModel.account.settings.reactionChoices val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle()
.collectAsStateWithLifecycle()
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() } val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
Popup( Popup(
@@ -1573,7 +1571,7 @@ fun ZapAmountChoicePopup(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
val zapAmountChoices by val zapAmountChoices by
accountViewModel.account.settings.zapAmountChoices accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R 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.model.AddressableNote
import com.vitorpamplona.amethyst.service.firstFullChar import com.vitorpamplona.amethyst.service.firstFullChar
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class UpdateReactionTypeViewModel( class UpdateReactionTypeViewModel : ViewModel() {
val accountSettings: AccountSettings, var account: Account? = null
) : ViewModel() {
var nextChoice by mutableStateOf(TextFieldValue("")) var nextChoice by mutableStateOf(TextFieldValue(""))
var reactionSet by mutableStateOf(listOf<String>()) var reactionSet by mutableStateOf(listOf<String>())
fun load() { fun load(myAccount: Account) {
this.reactionSet = accountSettings.reactionChoices.value this.account = myAccount
this.reactionSet = myAccount.settings.syncedSettings.reactions.reactionChoices.value
} }
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 } fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
@@ -124,21 +124,24 @@ class UpdateReactionTypeViewModel(
} }
fun sendPost() { fun sendPost() {
accountSettings.changeReactionTypes(reactionSet) viewModelScope.launch(Dispatchers.IO) {
nextChoice = TextFieldValue("") account?.changeReactionTypes(reactionSet)
nextChoice = TextFieldValue("")
}
} }
fun cancel() { fun cancel() {
nextChoice = TextFieldValue("") nextChoice = TextFieldValue("")
} }
fun hasChanged(): Boolean = reactionSet != accountSettings.reactionChoices.value fun hasChanged(): Boolean =
reactionSet !=
class Factory( account
val accountSettings: AccountSettings, ?.settings
) : ViewModelProvider.Factory { ?.syncedSettings
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel = UpdateReactionTypeViewModel(accountSettings) as UpdateReactionTypeViewModel ?.reactions
} ?.reactionChoices
?.value
} }
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@@ -148,14 +151,20 @@ fun UpdateReactionTypeDialog(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val postViewModel: UpdateReactionTypeViewModel = val postViewModel: UpdateReactionTypeViewModel = viewModel()
viewModel( postViewModel.load(accountViewModel.account)
key = "UpdateReactionTypeViewModel",
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account.settings),
)
LaunchedEffect(accountViewModel) { postViewModel.load() } UpdateReactionTypeDialog(postViewModel, onClose, accountViewModel, nav)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateReactionTypeDialog(
postViewModel: UpdateReactionTypeViewModel,
onClose: () -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
Dialog( Dialog(
onDismissRequest = { onClose() }, onDismissRequest = { onClose() },
properties = properties =

View File

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

View File

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

View File

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

View File

@@ -455,9 +455,9 @@ fun customZapClick(
return return
} }
if (accountViewModel.account.settings.zapAmountChoices.value val choices = accountViewModel.zapAmountChoices()
.isEmpty()
) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toast(
stringRes(context, R.string.error_dialog_zap_error), stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change), 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.error_dialog_zap_error),
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps), stringRes(context, 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) {
val amount = val amount = choices.first()
accountViewModel.account.settings.zapAmountChoices.value
.first()
if (amount > 1100) { if (amount > 1100) {
accountViewModel.zap( accountViewModel.zap(
@@ -488,11 +486,9 @@ fun customZapClick(
onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
// recommends amounts for a monthly release. // recommends amounts for a monthly release.
} }
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) { } else if (choices.size > 1) {
if (accountViewModel.account.settings.zapAmountChoices.value if (choices.any { it > 1100 }) {
.any { it > 1100 } onMultipleChoices(choices)
) {
onMultipleChoices(accountViewModel.account.settings.zapAmountChoices.value)
} else { } else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) 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.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@@ -328,9 +329,7 @@ class AccountViewModel(
fun reactToOrDelete(note: Note) { fun reactToOrDelete(note: Note) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val reaction = val reaction = reactionChoices().first()
account.settings.reactionChoices.value
.first()
if (hasReactedTo(note, reaction)) { if (hasReactedTo(note, reaction)) {
deleteReactionTo(note, reaction) deleteReactionTo(note, reaction)
} else { } else {
@@ -714,7 +713,7 @@ class AccountViewModel(
onProgress(it) onProgress(it)
}, },
onPayViaIntent = onPayViaIntent, 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 isFollowing(user: HexKey): Boolean = account.isFollowing(user)
fun hideSensitiveContent() { fun hideSensitiveContent() {
account.updateShowSensitiveContent(false) viewModelScope.launch(Dispatchers.IO) {
} account.updateShowSensitiveContent(false)
fun disableContentWarnings() {
account.updateShowSensitiveContent(true)
}
fun seeContentWarnings() {
account.updateShowSensitiveContent(null)
}
fun markDonatedInThisVersion() {
viewModelScope.launch {
account.markDonatedInThisVersion()
} }
} }
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( fun unwrap(
event: GiftWrapEvent, event: GiftWrapEvent,
@@ -1539,7 +1569,7 @@ class AccountViewModel(
context: Context, context: Context,
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (account.settings.defaultZapType.value == LnZapEvent.ZapType.NONZAP) { if (defaultZapType() == LnZapEvent.ZapType.NONZAP) {
LightningAddressResolver() LightningAddressResolver()
.lnAddressInvoice( .lnAddressInvoice(
lnaddress, lnaddress,
@@ -1553,7 +1583,7 @@ class AccountViewModel(
context = context, context = context,
) )
} else { } else {
account.createZapRequestFor(toUserPubKeyHex, message, account.settings.defaultZapType.value) { zapRequest -> account.createZapRequestFor(toUserPubKeyHex, message, defaultZapType()) { zapRequest ->
LocalCache.justConsume(zapRequest, null) LocalCache.justConsume(zapRequest, null)
LightningAddressResolver() LightningAddressResolver()
.lnAddressInvoice( .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.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NIP47SetupScreen( fun NIP47SetupScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
nip47: String?, nip47: String?,
) { ) {
val postViewModel: UpdateZapAmountViewModel = val postViewModel: UpdateZapAmountViewModel = viewModel()
viewModel( postViewModel.load(accountViewModel.account)
key = "UpdateZapAmountViewModel", NIP47SetupScreen(postViewModel, accountViewModel, nav, nip47)
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings), }
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NIP47SetupScreen(
postViewModel: UpdateZapAmountViewModel,
accountViewModel: AccountViewModel,
nav: INav,
nip47: String?,
) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(

View File

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

View File

@@ -234,7 +234,7 @@ private fun TranslationMessage(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (source in accountViewModel.account.settings.dontTranslateFrom) { if (source in accountViewModel.dontTranslateFrom()) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = null, contentDescription = null,
@@ -255,7 +255,7 @@ private fun TranslationMessage(
} }
}, },
onClick = { onClick = {
accountViewModel.account.settings.toggleDontTranslateFrom(source) accountViewModel.account.toggleDontTranslateFrom(source)
langSettingsPopupExpanded = false langSettingsPopupExpanded = false
}, },
) )
@@ -285,7 +285,7 @@ private fun TranslationMessage(
}, },
onClick = { onClick = {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.prefer(source, target, source) accountViewModel.account.prefer(source, target, source)
langSettingsPopupExpanded = false langSettingsPopupExpanded = false
} }
}, },
@@ -293,7 +293,9 @@ private fun TranslationMessage(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (accountViewModel.account.settings.preferenceBetween(source, target) == target) { if (accountViewModel.account.settings.syncedSettings.languages
.preferenceBetween(source, target) == target
) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = null, contentDescription = null,
@@ -315,7 +317,7 @@ private fun TranslationMessage(
}, },
onClick = { onClick = {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.prefer(source, target, target) accountViewModel.account.prefer(source, target, target)
langSettingsPopupExpanded = false langSettingsPopupExpanded = false
} }
}, },
@@ -350,7 +352,7 @@ private fun TranslationMessage(
}, },
onClick = { onClick = {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.account.settings.updateTranslateTo(lang) accountViewModel.account.updateTranslateTo(lang)
langSettingsPopupExpanded = false langSettingsPopupExpanded = false
} }
}, },
@@ -377,8 +379,8 @@ fun TranslateAndWatchLanguageChanges(
LanguageTranslatorService LanguageTranslatorService
.autoTranslate( .autoTranslate(
content, content,
accountViewModel.account.settings.dontTranslateFrom, accountViewModel.dontTranslateFrom(),
accountViewModel.account.settings.translateTo, accountViewModel.translateTo(),
).addOnCompleteListener { task -> ).addOnCompleteListener { task ->
if (task.isSuccessful && !content.equals(task.result.result, true)) { if (task.isSuccessful && !content.equals(task.result.result, true)) {
if (task.result.sourceLang != null && task.result.targetLang != null) { 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, content: String,
sig: String, sig: String,
) = when (kind) { ) = when (kind) {
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND -> AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
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) AudioHeaderEvent.KIND -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig) AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig) CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig) ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.KIND -> ChannelMetadataEvent(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) ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig) ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig) CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig) DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig) FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig) ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig) FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig) FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.KIND -> GiftWrapEvent(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) GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND ->
LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.KIND ->
LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig) LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)