mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 21:31:57 +01:00
Uses NIP-78 to start syncing some data between instances of Amethyst
This commit is contained in:
parent
bb430605bc
commit
a3b0436d91
@ -105,7 +105,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] Video Events (NIP-71)
|
||||
- [x] Moderated Communities (NIP-72)
|
||||
- [ ] Zap Goals (NIP-75)
|
||||
- [ ] Arbitrary Custom App Data (NIP-78)
|
||||
- [x] Arbitrary Custom App Data (NIP-78)
|
||||
- [x] Highlights (NIP-84)
|
||||
- [x] Notify Request (NIP-88/Draft)
|
||||
- [x] Recommended Application Handlers (NIP-89)
|
||||
|
@ -26,7 +26,12 @@ import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.AccountLanguagePreferencesInternal
|
||||
import com.vitorpamplona.amethyst.model.AccountReactionPreferencesInternal
|
||||
import com.vitorpamplona.amethyst.model.AccountSecurityPreferencesInternal
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
|
||||
import com.vitorpamplona.amethyst.model.AccountZapPreferencesInternal
|
||||
import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
@ -45,6 +50,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
@ -53,11 +59,9 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -104,6 +108,7 @@ private object PrefKeys {
|
||||
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_MUTE_LIST = "latestMuteList"
|
||||
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
|
||||
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
|
||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
||||
@ -310,19 +315,7 @@ object LocalPreferences {
|
||||
}
|
||||
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(settings.localRelays))
|
||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, settings.dontTranslateFrom)
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
||||
putString(
|
||||
PrefKeys.LANGUAGE_PREFS,
|
||||
Event.mapper.writeValueAsString(settings.languagePreferences),
|
||||
)
|
||||
putString(PrefKeys.TRANSLATE_TO, settings.translateTo)
|
||||
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(settings.zapAmountChoices.value))
|
||||
putString(
|
||||
PrefKeys.REACTION_CHOICES,
|
||||
Event.mapper.writeValueAsString(settings.reactionChoices.value),
|
||||
)
|
||||
putString(PrefKeys.DEFAULT_ZAPTYPE, settings.defaultZapType.value.name)
|
||||
|
||||
putString(
|
||||
PrefKeys.DEFAULT_FILE_SERVER,
|
||||
Event.mapper.writeValueAsString(settings.defaultFileServer),
|
||||
@ -404,6 +397,15 @@ object LocalPreferences {
|
||||
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupAppSpecificData != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_APP_SPECIFIC_DATA,
|
||||
Event.mapper.writeValueAsString(settings.backupAppSpecificData),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
}
|
||||
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
|
||||
@ -414,9 +416,6 @@ object LocalPreferences {
|
||||
|
||||
putString(PrefKeys.TOR_SETTINGS, Event.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
||||
|
||||
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, settings.warnAboutPostsWithReports)
|
||||
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, settings.filterSpamFromStrangers)
|
||||
|
||||
val regularMap =
|
||||
settings.lastReadPerRoute.value.mapValues {
|
||||
it.value.value
|
||||
@ -428,12 +427,6 @@ object LocalPreferences {
|
||||
)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
|
||||
|
||||
if (settings.showSensitiveContent.value == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, settings.showSensitiveContent.value!!)
|
||||
}
|
||||
|
||||
putString(
|
||||
PrefKeys.PENDING_ATTESTATIONS,
|
||||
Event.mapper.writeValueAsString(settings.pendingAttestations.value),
|
||||
@ -510,9 +503,6 @@ object LocalPreferences {
|
||||
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
|
||||
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||
|
||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||
val defaultHomeFollowList =
|
||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||
val defaultStoriesFollowList =
|
||||
@ -528,13 +518,12 @@ object LocalPreferences {
|
||||
} ?: LnZapEvent.ZapType.PUBLIC
|
||||
|
||||
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()
|
||||
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||
|
||||
val defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
|
||||
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||
val defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
|
||||
|
||||
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
|
||||
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
|
||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||
|
||||
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
|
||||
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
|
||||
@ -543,6 +532,54 @@ object LocalPreferences {
|
||||
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
|
||||
val syncedSettings =
|
||||
if (latestAppSpecificData != null) {
|
||||
null
|
||||
} else {
|
||||
// previous version. Delete this when ready.
|
||||
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||
|
||||
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
|
||||
|
||||
val showSensitiveContent =
|
||||
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
|
||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||
|
||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||
|
||||
AccountSyncedSettingsInternal(
|
||||
reactions =
|
||||
AccountReactionPreferencesInternal(
|
||||
reactionChoices = reactionChoices,
|
||||
),
|
||||
zaps =
|
||||
AccountZapPreferencesInternal(
|
||||
zapAmountChoices = zapAmountChoices,
|
||||
defaultZapType = defaultZapType,
|
||||
),
|
||||
languages =
|
||||
AccountLanguagePreferencesInternal(
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
translateTo = translateTo,
|
||||
),
|
||||
security =
|
||||
AccountSecurityPreferencesInternal(
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
@ -571,15 +608,6 @@ object LocalPreferences {
|
||||
parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
||||
}
|
||||
|
||||
val showSensitiveContent =
|
||||
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
|
||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||
|
||||
val lastReadPerRoute =
|
||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
@ -594,12 +622,6 @@ object LocalPreferences {
|
||||
externalSignerPackageName = externalSignerPackageName,
|
||||
localRelays = localRelays,
|
||||
localRelayServers = localRelayServers,
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
translateTo = translateTo,
|
||||
zapAmountChoices = MutableStateFlow(zapAmountChoices.toImmutableList()),
|
||||
reactionChoices = MutableStateFlow(reactionChoices.toImmutableList()),
|
||||
defaultZapType = MutableStateFlow(defaultZapType),
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
@ -616,10 +638,9 @@ object LocalPreferences {
|
||||
backupSearchRelayList = latestSearchRelayList,
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||
backupMuteList = latestMuteList,
|
||||
backupAppSpecificData = latestAppSpecificData,
|
||||
backupSyncedSettings = syncedSettings,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
showSensitiveContent = MutableStateFlow(showSensitiveContent),
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||
|
@ -115,7 +115,12 @@ class ServiceManager(
|
||||
HttpClientManager.setDefaultProxy(null)
|
||||
}
|
||||
|
||||
LocalCache.antiSpam.active = account?.settings?.filterSpamFromStrangers ?: true
|
||||
// Convert this into a flow
|
||||
LocalCache.antiSpam.active = account
|
||||
?.settings
|
||||
?.syncedSettings
|
||||
?.security
|
||||
?.filterSpamFromStrangers ?: true
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance
|
||||
.imageLoaderBuilder()
|
||||
|
@ -27,6 +27,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
@ -44,9 +45,11 @@ import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@ -128,7 +131,9 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@ -138,6 +143,10 @@ class Account(
|
||||
val signer: NostrSigner = settings.createSigner(),
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
|
||||
}
|
||||
|
||||
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
|
||||
|
||||
data class PaymentRequest(
|
||||
@ -959,7 +968,7 @@ class Account(
|
||||
getBlockListNote().flow().metadata.stateFlow,
|
||||
getMuteListNote().flow().metadata.stateFlow,
|
||||
transientHiddenUsers,
|
||||
settings.showSensitiveContent,
|
||||
settings.syncedSettings.security.showSensitiveContent,
|
||||
) { blockList, muteList, transientHiddenUsers, showSensitiveContent ->
|
||||
checkNotInMainThread()
|
||||
emit(assembleLiveHiddenUsers(blockList.note, muteList.note, transientHiddenUsers, showSensitiveContent))
|
||||
@ -972,7 +981,7 @@ class Account(
|
||||
getBlockListNote(),
|
||||
getMuteListNote(),
|
||||
transientHiddenUsers.value,
|
||||
settings.showSensitiveContent.value,
|
||||
settings.syncedSettings.security.showSensitiveContent.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
@ -1025,14 +1034,83 @@ class Account(
|
||||
fun updateOptOutOptions(
|
||||
warnReports: Boolean,
|
||||
filterSpam: Boolean,
|
||||
) {
|
||||
): Boolean {
|
||||
if (settings.updateOptOutOptions(warnReports, filterSpam)) {
|
||||
LocalCache.antiSpam.active = settings.filterSpamFromStrangers
|
||||
if (!settings.filterSpamFromStrangers) {
|
||||
if (!settings.syncedSettings.security.filterSpamFromStrangers) {
|
||||
transientHiddenUsers.update {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
sendNewAppSpecificData()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?) {
|
||||
if (settings.updateShowSensitiveContent(show)) {
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeReactionTypes(reactionSet: List<String>) {
|
||||
if (settings.changeReactionTypes(reactionSet)) {
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateZapAmounts(
|
||||
amountSet: List<Long>,
|
||||
selectedZapType: LnZapEvent.ZapType,
|
||||
nip47Update: Nip47WalletConnect.Nip47URI?,
|
||||
) {
|
||||
var changed = false
|
||||
|
||||
if (settings.changeZapAmounts(amountSet)) changed = true
|
||||
if (settings.changeDefaultZapType(selectedZapType)) changed = true
|
||||
if (settings.changeZapPaymentRequest(nip47Update)) changed = true
|
||||
|
||||
if (changed) {
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDontTranslateFrom(languageCode: String) {
|
||||
settings.toggleDontTranslateFrom(languageCode)
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
|
||||
fun updateTranslateTo(languageCode: Locale) {
|
||||
if (settings.updateTranslateTo(languageCode)) {
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
}
|
||||
|
||||
fun prefer(
|
||||
source: String,
|
||||
target: String,
|
||||
preference: String,
|
||||
) {
|
||||
settings.prefer(source, target, preference)
|
||||
sendNewAppSpecificData()
|
||||
}
|
||||
|
||||
private fun sendNewAppSpecificData() {
|
||||
sendNewAppSpecificData(settings.syncedSettings.toInternal())
|
||||
}
|
||||
|
||||
private fun sendNewAppSpecificData(toInternal: AccountSyncedSettingsInternal) {
|
||||
signer.nip44Encrypt(Event.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted ->
|
||||
AppSpecificDataEvent.create(
|
||||
dTag = APP_SPECIFIC_DATA_D_TAG,
|
||||
description = encrypted,
|
||||
otherTags = emptyArray(),
|
||||
signer = signer,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2788,6 +2866,13 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppSpecificDataNote(): AddressableNote {
|
||||
val aTag = AppSpecificDataEvent.createTag(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG)
|
||||
return LocalCache.getOrCreateAddressableNote(aTag)
|
||||
}
|
||||
|
||||
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlockListNote(): AddressableNote {
|
||||
val aTag =
|
||||
ATag(
|
||||
@ -3118,7 +3203,7 @@ class Account(
|
||||
return true
|
||||
}
|
||||
|
||||
if (!settings.warnAboutPostsWithReports) {
|
||||
if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
|
||||
return !isHidden(user) &&
|
||||
// if user hasn't hided this author
|
||||
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
|
||||
@ -3131,7 +3216,7 @@ class Account(
|
||||
}
|
||||
|
||||
private fun isAcceptableDirect(note: Note): Boolean {
|
||||
if (!settings.warnAboutPostsWithReports) {
|
||||
if (!settings.syncedSettings.security.warnAboutPostsWithReports) {
|
||||
return !note.hasReportsBy(userProfile())
|
||||
}
|
||||
return !note.hasReportsBy(userProfile()) &&
|
||||
@ -3355,8 +3440,6 @@ class Account(
|
||||
(event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true)
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?) = settings.updateShowSensitiveContent(show)
|
||||
|
||||
fun markAsRead(
|
||||
route: String,
|
||||
timestampInSecs: Long,
|
||||
@ -3516,7 +3599,7 @@ class Account(
|
||||
}
|
||||
|
||||
settings.backupPrivateHomeRelayList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved search relay list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.verifyAndConsume(event, null)
|
||||
@ -3524,6 +3607,24 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
settings.backupAppSpecificData?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
LocalCache.verifyAndConsume(event, null)
|
||||
signer.decrypt(event.content, event.pubKey) { decrypted ->
|
||||
try {
|
||||
val syncedSettings = Event.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.syncedSettings.updateFrom(syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings.backupMuteList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved mute list ${it.toJson()}")
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) }
|
||||
@ -3597,6 +3698,28 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
|
||||
getAppSpecificDataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${userProfile().toBestDisplayName()}")
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
signer.decrypt(it.content, it.pubKey) { decrypted ->
|
||||
val syncedSettings =
|
||||
try {
|
||||
Event.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
|
||||
settings.updateAppSpecificData(it, syncedSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
LocalCache.antiSpam.flowSpam.collect {
|
||||
it.cache.spamMessages.snapshot().values.forEach { spammer ->
|
||||
|
@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
@ -34,6 +32,7 @@ import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@ -44,8 +43,6 @@ import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@ -61,17 +58,6 @@ val DefaultChannels =
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||
)
|
||||
|
||||
val DefaultReactions =
|
||||
persistentListOf(
|
||||
"\uD83D\uDE80",
|
||||
"\uD83E\uDEC2",
|
||||
"\uD83D\uDC40",
|
||||
"\uD83D\uDE02",
|
||||
"\uD83C\uDF89",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
)
|
||||
|
||||
val DefaultNIP65List =
|
||||
listOf(
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
@ -93,17 +79,6 @@ val DefaultSearchRelayList =
|
||||
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
|
||||
)
|
||||
|
||||
val DefaultZapAmounts = persistentListOf(100L, 500L, 1000L)
|
||||
|
||||
fun getLanguagesSpokenByUser(): Set<String> {
|
||||
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
|
||||
val codedList = mutableSetOf<String>()
|
||||
for (i in 0 until languageList.size()) {
|
||||
languageList.get(i)?.let { codedList.add(it.language) }
|
||||
}
|
||||
return codedList
|
||||
}
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val GLOBAL_FOLLOWS = " Global "
|
||||
|
||||
@ -117,12 +92,6 @@ class AccountSettings(
|
||||
var externalSignerPackageName: String? = null,
|
||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||
var localRelayServers: Set<String> = setOf(),
|
||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
var zapAmountChoices: MutableStateFlow<ImmutableList<Long>> = MutableStateFlow(DefaultZapAmounts),
|
||||
var reactionChoices: MutableStateFlow<ImmutableList<String>> = MutableStateFlow(DefaultReactions),
|
||||
val defaultZapType: MutableStateFlow<LnZapEvent.ZapType> = MutableStateFlow(LnZapEvent.ZapType.PUBLIC),
|
||||
var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0],
|
||||
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
|
||||
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
@ -139,16 +108,19 @@ class AccountSettings(
|
||||
var backupSearchRelayList: SearchRelayListEvent? = null,
|
||||
var backupMuteList: MuteListEvent? = null,
|
||||
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
||||
var backupAppSpecificData: AppSpecificDataEvent? = null,
|
||||
backupSyncedSettings: AccountSyncedSettingsInternal? = null, // only exist for migration purposes
|
||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
||||
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
var filterSpamFromStrangers: Boolean = true,
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
) {
|
||||
val saveable = MutableStateFlow(AccountSettingsUpdater(this))
|
||||
|
||||
val syncedSettings: AccountSyncedSettings =
|
||||
backupSyncedSettings?.let { AccountSyncedSettings(it) }
|
||||
?: AccountSyncedSettings(AccountSyncedSettingsInternal())
|
||||
|
||||
class AccountSettingsUpdater(
|
||||
val accountSettings: AccountSettings,
|
||||
)
|
||||
@ -173,32 +145,40 @@ class AccountSettings(
|
||||
// Zaps and Reactions
|
||||
// ---
|
||||
|
||||
fun changeDefaultZapType(zapType: LnZapEvent.ZapType) {
|
||||
if (defaultZapType.value != zapType) {
|
||||
defaultZapType.tryEmit(zapType)
|
||||
fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean {
|
||||
if (syncedSettings.zaps.defaultZapType.value != zapType) {
|
||||
syncedSettings.zaps.defaultZapType.tryEmit(zapType)
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||
if (zapAmountChoices.value != newAmounts) {
|
||||
zapAmountChoices.tryEmit(newAmounts.toImmutableList())
|
||||
fun changeZapAmounts(newAmounts: List<Long>): Boolean {
|
||||
if (syncedSettings.zaps.zapAmountChoices.value != newAmounts) {
|
||||
syncedSettings.zaps.zapAmountChoices.tryEmit(newAmounts.toImmutableList())
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?) {
|
||||
fun changeReactionTypes(newTypes: List<String>): Boolean {
|
||||
if (syncedSettings.reactions.reactionChoices.value != newTypes) {
|
||||
syncedSettings.reactions.reactionChoices.tryEmit(newTypes.toImmutableList())
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?): Boolean {
|
||||
if (zapPaymentRequest != newServer) {
|
||||
zapPaymentRequest = newServer
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun changeReactionTypes(newTypes: List<String>) {
|
||||
if (reactionChoices.value != newTypes) {
|
||||
reactionChoices.tryEmit(newTypes.toImmutableList())
|
||||
saveAccountSettings()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---
|
||||
@ -260,22 +240,18 @@ class AccountSettings(
|
||||
// language services
|
||||
// ---
|
||||
fun toggleDontTranslateFrom(languageCode: String) {
|
||||
if (!dontTranslateFrom.contains(languageCode)) {
|
||||
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||
saveAccountSettings()
|
||||
} else {
|
||||
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
|
||||
saveAccountSettings()
|
||||
}
|
||||
syncedSettings.languages.toggleDontTranslateFrom(languageCode)
|
||||
saveAccountSettings()
|
||||
}
|
||||
|
||||
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
|
||||
fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language)
|
||||
|
||||
fun updateTranslateTo(languageCode: Locale) {
|
||||
if (translateTo != languageCode.language) {
|
||||
translateTo = languageCode.language
|
||||
fun updateTranslateTo(languageCode: Locale): Boolean {
|
||||
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun prefer(
|
||||
@ -283,23 +259,14 @@ class AccountSettings(
|
||||
target: String,
|
||||
preference: String,
|
||||
) {
|
||||
val key = "$source,$target"
|
||||
if (key !in languagePreferences) {
|
||||
languagePreferences = languagePreferences + Pair(key, preference)
|
||||
saveAccountSettings()
|
||||
} else {
|
||||
if (languagePreferences.get(key) == preference) {
|
||||
languagePreferences = languagePreferences.minus(key)
|
||||
} else {
|
||||
languagePreferences = languagePreferences + Pair(key, preference)
|
||||
}
|
||||
}
|
||||
syncedSettings.languages.prefer(source, target, preference)
|
||||
saveAccountSettings()
|
||||
}
|
||||
|
||||
fun preferenceBetween(
|
||||
source: String,
|
||||
target: String,
|
||||
): String? = languagePreferences["$source,$target"]
|
||||
): String? = syncedSettings.languages.preferenceBetween(source, target)
|
||||
|
||||
// ----
|
||||
// Backup Lists
|
||||
@ -382,6 +349,22 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAppSpecificData(
|
||||
appSettings: AppSpecificDataEvent?,
|
||||
newSyncedSettings: AccountSyncedSettingsInternal,
|
||||
) {
|
||||
if (appSettings == null || appSettings.content().isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupAppSpecificData?.id != appSettings.id) {
|
||||
println("AABBCC Update App Specific Data")
|
||||
backupAppSpecificData = appSettings
|
||||
syncedSettings.updateFrom(newSyncedSettings)
|
||||
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// Warning dialogs
|
||||
// ----
|
||||
@ -407,15 +390,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?): Boolean {
|
||||
if (showSensitiveContent.value != show) {
|
||||
showSensitiveContent.update { show }
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---
|
||||
// donations
|
||||
// ---
|
||||
@ -509,16 +483,20 @@ class AccountSettings(
|
||||
// ---
|
||||
// filters
|
||||
// ---
|
||||
fun updateShowSensitiveContent(show: Boolean?): Boolean {
|
||||
if (syncedSettings.security.updateShowSensitiveContent(show)) {
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateOptOutOptions(
|
||||
warnReports: Boolean,
|
||||
filterSpam: Boolean,
|
||||
): Boolean =
|
||||
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) {
|
||||
warnAboutPostsWithReports = warnReports
|
||||
filterSpamFromStrangers = filterSpam
|
||||
|
||||
if (syncedSettings.security.updateOptOutOptions(warnReports, filterSpam)) {
|
||||
saveAccountSettings()
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.AppRecommendationEvent
|
||||
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
@ -1232,6 +1233,13 @@ object LocalCache {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: AppSpecificDataEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun consume(event: RecommendRelayEvent) {
|
||||
// // Log.d("RR", event.toJson())
|
||||
@ -2645,6 +2653,7 @@ object LocalCache {
|
||||
is AdvertisedRelayListEvent -> consume(event, relay)
|
||||
is AppDefinitionEvent -> consume(event, relay)
|
||||
is AppRecommendationEvent -> consume(event, relay)
|
||||
is AppSpecificDataEvent -> consume(event, relay)
|
||||
is AudioHeaderEvent -> consume(event, relay)
|
||||
is AudioTrackEvent -> consume(event, relay)
|
||||
is BadgeAwardEvent -> consume(event)
|
||||
|
@ -33,6 +33,7 @@ import com.vitorpamplona.ammolite.relays.filters.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
@ -78,35 +79,15 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
|
||||
|
||||
fun createAccountContactListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountMetadataFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountRelayListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
StatusEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
@ -138,7 +119,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
PeopleListEvent.KIND,
|
||||
),
|
||||
authors = otherAuthors,
|
||||
limit = 100,
|
||||
limit = otherAuthors.size * 10,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -154,6 +135,18 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountSettings2Filter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds = listOf(AppSpecificDataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
|
||||
fun createAccountReportsFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
@ -465,8 +458,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
accountChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createAccountSettings2Filter(),
|
||||
createNotificationFilter(),
|
||||
createNotificationFilter2(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
@ -480,9 +472,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
accountChannel.typedFilters =
|
||||
listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createAccountSettingsFilter(),
|
||||
createAccountSettings2Filter(),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
|
||||
|
@ -97,9 +97,7 @@ fun SensitivityWarning(
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val accountState =
|
||||
accountViewModel.account.settings.showSensitiveContent
|
||||
.collectAsStateWithLifecycle()
|
||||
val accountState = accountViewModel.showSensitiveContent().collectAsStateWithLifecycle()
|
||||
|
||||
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
|
||||
|
||||
|
@ -525,16 +525,12 @@ fun ZapVote(
|
||||
)
|
||||
return@combinedClickable
|
||||
} else if (
|
||||
accountViewModel.account.settings.zapAmountChoices.value.size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first(),
|
||||
)
|
||||
accountViewModel.zapAmountChoices().size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
|
||||
) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first() * 1000,
|
||||
accountViewModel.zapAmountChoices().first() * 1000,
|
||||
poolOption.option,
|
||||
"",
|
||||
context,
|
||||
@ -667,7 +663,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
val context = LocalContext.current
|
||||
|
||||
// TODO: Move this to the viewModel
|
||||
val zapPaymentChoices by accountViewModel.account.settings.zapAmountChoices
|
||||
val zapPaymentChoices by accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
val zapMessage = ""
|
||||
|
@ -965,9 +965,9 @@ private fun likeClick(
|
||||
)
|
||||
return
|
||||
}
|
||||
if (accountViewModel.account.settings.reactionChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
|
||||
val choices = accountViewModel.reactionChoices()
|
||||
if (choices.isEmpty()) {
|
||||
accountViewModel.toast(
|
||||
R.string.no_reactions_setup,
|
||||
R.string.no_reaction_type_setup_long_press_to_change,
|
||||
@ -977,9 +977,9 @@ private fun likeClick(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_like_posts,
|
||||
)
|
||||
} else if (accountViewModel.account.settings.reactionChoices.value.size == 1) {
|
||||
} else if (choices.size == 1) {
|
||||
onWantsToSignReaction()
|
||||
} else if (accountViewModel.account.settings.reactionChoices.value.size > 1) {
|
||||
} else if (choices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
@ -1181,9 +1181,9 @@ fun zapClick(
|
||||
return
|
||||
}
|
||||
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
val choices = accountViewModel.zapAmountChoices()
|
||||
|
||||
if (choices.isEmpty()) {
|
||||
accountViewModel.toast(
|
||||
R.string.error_dialog_zap_error,
|
||||
R.string.no_zap_amount_setup_long_press_to_change,
|
||||
@ -1193,11 +1193,10 @@ fun zapClick(
|
||||
R.string.error_dialog_zap_error,
|
||||
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
|
||||
)
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size == 1) {
|
||||
} else if (choices.size == 1) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first() * 1000,
|
||||
choices.first() * 1000,
|
||||
null,
|
||||
"",
|
||||
context,
|
||||
@ -1205,7 +1204,7 @@ fun zapClick(
|
||||
onProgress = { onZappingProgress(it) },
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
)
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) {
|
||||
} else if (choices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
@ -1408,8 +1407,7 @@ fun ReactionChoicePopup(
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
val reactions by accountViewModel.account.settings.reactionChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle()
|
||||
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
|
||||
|
||||
Popup(
|
||||
@ -1573,7 +1571,7 @@ fun ZapAmountChoicePopup(
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val zapAmountChoices by
|
||||
accountViewModel.account.settings.zapAmountChoices
|
||||
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
|
@ -49,7 +49,6 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@ -69,12 +68,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
@ -94,16 +93,17 @@ import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdateReactionTypeViewModel(
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModel() {
|
||||
class UpdateReactionTypeViewModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||
var reactionSet by mutableStateOf(listOf<String>())
|
||||
|
||||
fun load() {
|
||||
this.reactionSet = accountSettings.reactionChoices.value
|
||||
fun load(myAccount: Account) {
|
||||
this.account = myAccount
|
||||
this.reactionSet = myAccount.settings.syncedSettings.reactions.reactionChoices.value
|
||||
}
|
||||
|
||||
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
@ -124,21 +124,24 @@ class UpdateReactionTypeViewModel(
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
accountSettings.changeReactionTypes(reactionSet)
|
||||
nextChoice = TextFieldValue("")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account?.changeReactionTypes(reactionSet)
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun hasChanged(): Boolean = reactionSet != accountSettings.reactionChoices.value
|
||||
|
||||
class Factory(
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel = UpdateReactionTypeViewModel(accountSettings) as UpdateReactionTypeViewModel
|
||||
}
|
||||
fun hasChanged(): Boolean =
|
||||
reactionSet !=
|
||||
account
|
||||
?.settings
|
||||
?.syncedSettings
|
||||
?.reactions
|
||||
?.reactionChoices
|
||||
?.value
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@ -148,14 +151,20 @@ fun UpdateReactionTypeDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val postViewModel: UpdateReactionTypeViewModel =
|
||||
viewModel(
|
||||
key = "UpdateReactionTypeViewModel",
|
||||
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
val postViewModel: UpdateReactionTypeViewModel = viewModel()
|
||||
postViewModel.load(accountViewModel.account)
|
||||
|
||||
LaunchedEffect(accountViewModel) { postViewModel.load() }
|
||||
UpdateReactionTypeDialog(postViewModel, onClose, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UpdateReactionTypeDialog(
|
||||
postViewModel: UpdateReactionTypeViewModel,
|
||||
onClose: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
|
@ -82,10 +82,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
@ -106,10 +106,12 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdateZapAmountViewModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
|
||||
class UpdateZapAmountViewModel(
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModel() {
|
||||
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||
var amountSet by mutableStateOf(listOf<Long>())
|
||||
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
||||
@ -124,15 +126,23 @@ class UpdateZapAmountViewModel(
|
||||
updateNIP47(text)
|
||||
}
|
||||
|
||||
fun load() {
|
||||
this.amountSet = accountSettings.zapAmountChoices.value
|
||||
fun load(myAccount: Account) {
|
||||
this.account = myAccount
|
||||
this.amountSet = myAccount.settings.syncedSettings.zaps.zapAmountChoices.value
|
||||
this.selectedZapType = myAccount.settings.syncedSettings.zaps.defaultZapType.value
|
||||
|
||||
this.walletConnectPubkey =
|
||||
accountSettings.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
myAccount.settings.zapPaymentRequest
|
||||
?.pubKeyHex
|
||||
?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.walletConnectRelay =
|
||||
accountSettings.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
myAccount.settings.zapPaymentRequest
|
||||
?.relayUri
|
||||
?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.walletConnectSecret =
|
||||
accountSettings.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.selectedZapType = accountSettings.defaultZapType.value
|
||||
myAccount.settings.zapPaymentRequest
|
||||
?.secret
|
||||
?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
}
|
||||
|
||||
fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
@ -151,37 +161,37 @@ class UpdateZapAmountViewModel(
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
accountSettings.changeZapAmounts(amountSet)
|
||||
accountSettings.changeDefaultZapType(selectedZapType)
|
||||
val nip47Update =
|
||||
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
|
||||
val pubkeyHex =
|
||||
try {
|
||||
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
|
||||
val pubkeyHex =
|
||||
try {
|
||||
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlFormatter.normalize(it) }
|
||||
val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) }
|
||||
|
||||
val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlFormatter.normalize(it) }
|
||||
val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) }
|
||||
|
||||
if (pubkeyHex != null && relayUrl != null) {
|
||||
accountSettings.changeZapPaymentRequest(
|
||||
if (pubkeyHex != null && relayUrl != null) {
|
||||
Nip47WalletConnect.Nip47URI(
|
||||
pubkeyHex,
|
||||
relayUrl,
|
||||
privKeyHex,
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
accountSettings.changeZapPaymentRequest(null)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
accountSettings.changeZapPaymentRequest(null)
|
||||
}
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account?.updateZapAmounts(amountSet, selectedZapType, nip47Update)
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
@ -190,11 +200,23 @@ class UpdateZapAmountViewModel(
|
||||
|
||||
fun hasChanged(): Boolean =
|
||||
(
|
||||
selectedZapType != accountSettings.defaultZapType.value ||
|
||||
amountSet != accountSettings.zapAmountChoices.value ||
|
||||
walletConnectPubkey.text != (accountSettings.zapPaymentRequest?.pubKeyHex ?: "") ||
|
||||
walletConnectRelay.text != (accountSettings.zapPaymentRequest?.relayUri ?: "") ||
|
||||
walletConnectSecret.text != (accountSettings.zapPaymentRequest?.secret ?: "")
|
||||
selectedZapType !=
|
||||
account
|
||||
?.settings
|
||||
?.syncedSettings
|
||||
?.zaps
|
||||
?.defaultZapType
|
||||
?.value ||
|
||||
amountSet !=
|
||||
account
|
||||
?.settings
|
||||
?.syncedSettings
|
||||
?.zaps
|
||||
?.zapAmountChoices
|
||||
?.value ||
|
||||
walletConnectPubkey.text != (account?.settings?.zapPaymentRequest?.pubKeyHex ?: "") ||
|
||||
walletConnectRelay.text != (account?.settings?.zapPaymentRequest?.relayUri ?: "") ||
|
||||
walletConnectSecret.text != (account?.settings?.zapPaymentRequest?.secret ?: "")
|
||||
)
|
||||
|
||||
fun updateNIP47(uri: String) {
|
||||
@ -205,20 +227,25 @@ class UpdateZapAmountViewModel(
|
||||
walletConnectSecret = TextFieldValue(contact.secret ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
val accountSettings: AccountSettings,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel = UpdateZapAmountViewModel(accountSettings) as UpdateZapAmountViewModel
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UpdateZapAmountDialog(
|
||||
onClose: () -> Unit,
|
||||
nip47uri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val postViewModel: UpdateZapAmountViewModel = viewModel()
|
||||
postViewModel.load(accountViewModel.account)
|
||||
UpdateZapAmountDialog(postViewModel, onClose, nip47uri, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateZapAmountDialog(
|
||||
postViewModel: UpdateZapAmountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nip47uri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@ -233,12 +260,6 @@ fun UpdateZapAmountDialog(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column {
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@ -311,7 +332,6 @@ fun UpdateZapAmountContent(
|
||||
}
|
||||
|
||||
LaunchedEffect(accountViewModel, nip47uri) {
|
||||
postViewModel.load()
|
||||
if (nip47uri != null) {
|
||||
try {
|
||||
postViewModel.updateNIP47(nip47uri)
|
||||
|
@ -153,7 +153,7 @@ fun ZapCustomDialog(
|
||||
}
|
||||
|
||||
var selectedZapType by
|
||||
remember(accountViewModel) { mutableStateOf(accountViewModel.account.settings.defaultZapType.value) }
|
||||
remember(accountViewModel) { mutableStateOf(accountViewModel.defaultZapType()) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@ -224,7 +224,7 @@ fun ZapCustomDialog(
|
||||
label = stringRes(id = R.string.zap_type),
|
||||
placeholder =
|
||||
zapTypes
|
||||
.filter { it.first == accountViewModel.account.settings.defaultZapType.value }
|
||||
.filter { it.first == accountViewModel.defaultZapType() }
|
||||
.first()
|
||||
.second,
|
||||
options = zapOptions,
|
||||
|
@ -315,10 +315,8 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.content_warning_hide_all_sensitive_content)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.hideSensitiveContent()
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.hideSensitiveContent()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -326,10 +324,8 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.content_warning_show_all_sensitive_content)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.disableContentWarnings()
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.disableContentWarnings()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -337,10 +333,8 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.content_warning_see_warnings)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.seeContentWarnings()
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.seeContentWarnings()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -385,7 +379,8 @@ fun WatchBookmarksFollowsAndAccount(
|
||||
.live()
|
||||
.bookmarks
|
||||
.observeAsState()
|
||||
val showSensitiveContent by accountViewModel.account.settings.showSensitiveContent
|
||||
val showSensitiveContent by accountViewModel
|
||||
.showSensitiveContent()
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
|
||||
|
@ -455,9 +455,9 @@ fun customZapClick(
|
||||
return
|
||||
}
|
||||
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.isEmpty()
|
||||
) {
|
||||
val choices = accountViewModel.zapAmountChoices()
|
||||
|
||||
if (choices.isEmpty()) {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
|
||||
@ -467,10 +467,8 @@ fun customZapClick(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
)
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size == 1) {
|
||||
val amount =
|
||||
accountViewModel.account.settings.zapAmountChoices.value
|
||||
.first()
|
||||
} else if (choices.size == 1) {
|
||||
val amount = choices.first()
|
||||
|
||||
if (amount > 1100) {
|
||||
accountViewModel.zap(
|
||||
@ -488,11 +486,9 @@ fun customZapClick(
|
||||
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
|
||||
// recommends amounts for a monthly release.
|
||||
}
|
||||
} else if (accountViewModel.account.settings.zapAmountChoices.value.size > 1) {
|
||||
if (accountViewModel.account.settings.zapAmountChoices.value
|
||||
.any { it > 1100 }
|
||||
) {
|
||||
onMultipleChoices(accountViewModel.account.settings.zapAmountChoices.value)
|
||||
} else if (choices.size > 1) {
|
||||
if (choices.any { it > 1100 }) {
|
||||
onMultipleChoices(choices)
|
||||
} else {
|
||||
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
@ -328,9 +329,7 @@ class AccountViewModel(
|
||||
|
||||
fun reactToOrDelete(note: Note) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val reaction =
|
||||
account.settings.reactionChoices.value
|
||||
.first()
|
||||
val reaction = reactionChoices().first()
|
||||
if (hasReactedTo(note, reaction)) {
|
||||
deleteReactionTo(note, reaction)
|
||||
} else {
|
||||
@ -714,7 +713,7 @@ class AccountViewModel(
|
||||
onProgress(it)
|
||||
},
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
zapType = zapType ?: account.settings.defaultZapType.value,
|
||||
zapType = zapType ?: defaultZapType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -885,24 +884,55 @@ class AccountViewModel(
|
||||
fun isFollowing(user: HexKey): Boolean = account.isFollowing(user)
|
||||
|
||||
fun hideSensitiveContent() {
|
||||
account.updateShowSensitiveContent(false)
|
||||
}
|
||||
|
||||
fun disableContentWarnings() {
|
||||
account.updateShowSensitiveContent(true)
|
||||
}
|
||||
|
||||
fun seeContentWarnings() {
|
||||
account.updateShowSensitiveContent(null)
|
||||
}
|
||||
|
||||
fun markDonatedInThisVersion() {
|
||||
viewModelScope.launch {
|
||||
account.markDonatedInThisVersion()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.updateShowSensitiveContent(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultZapType(): LnZapEvent.ZapType = account.settings.defaultZapType.value
|
||||
fun disableContentWarnings() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.updateShowSensitiveContent(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun seeContentWarnings() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.updateShowSensitiveContent(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun markDonatedInThisVersion() {
|
||||
account.markDonatedInThisVersion()
|
||||
}
|
||||
|
||||
fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom
|
||||
|
||||
fun translateTo() = account.settings.syncedSettings.languages.translateTo
|
||||
|
||||
fun defaultZapType() = account.settings.syncedSettings.zaps.defaultZapType.value
|
||||
|
||||
fun showSensitiveContent(): MutableStateFlow<Boolean?> = account.settings.syncedSettings.security.showSensitiveContent
|
||||
|
||||
fun zapAmountChoicesFlow() = account.settings.syncedSettings.zaps.zapAmountChoices
|
||||
|
||||
fun zapAmountChoices() = zapAmountChoicesFlow().value
|
||||
|
||||
fun reactionChoicesFlow() = account.settings.syncedSettings.reactions.reactionChoices
|
||||
|
||||
fun reactionChoices() = reactionChoicesFlow().value
|
||||
|
||||
fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers
|
||||
|
||||
fun updateOptOutOptions(
|
||||
warnReports: Boolean,
|
||||
filterSpam: Boolean,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (account.updateOptOutOptions(warnReports, filterSpam)) {
|
||||
LocalCache.antiSpam.active = filterSpamFromStrangers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrap(
|
||||
event: GiftWrapEvent,
|
||||
@ -1539,7 +1569,7 @@ class AccountViewModel(
|
||||
context: Context,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (account.settings.defaultZapType.value == LnZapEvent.ZapType.NONZAP) {
|
||||
if (defaultZapType() == LnZapEvent.ZapType.NONZAP) {
|
||||
LightningAddressResolver()
|
||||
.lnAddressInvoice(
|
||||
lnaddress,
|
||||
@ -1553,7 +1583,7 @@ class AccountViewModel(
|
||||
context = context,
|
||||
)
|
||||
} else {
|
||||
account.createZapRequestFor(toUserPubKeyHex, message, account.settings.defaultZapType.value) { zapRequest ->
|
||||
account.createZapRequestFor(toUserPubKeyHex, message, defaultZapType()) { zapRequest ->
|
||||
LocalCache.justConsume(zapRequest, null)
|
||||
LightningAddressResolver()
|
||||
.lnAddressInvoice(
|
||||
|
@ -42,19 +42,25 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NIP47SetupScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
nip47: String?,
|
||||
) {
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
val postViewModel: UpdateZapAmountViewModel = viewModel()
|
||||
postViewModel.load(accountViewModel.account)
|
||||
NIP47SetupScreen(postViewModel, accountViewModel, nav, nip47)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NIP47SetupScreen(
|
||||
postViewModel: UpdateZapAmountViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
nip47: String?,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
@ -157,15 +157,15 @@ fun SecurityFiltersScreen(
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
val pagerState = rememberPagerState { 3 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.warnAboutPostsWithReports) }
|
||||
var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.filterSpamFromStrangers) }
|
||||
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.warnAboutPostsWithReports) }
|
||||
var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.filterSpamFromStrangers) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = warnAboutReports,
|
||||
onCheckedChange = {
|
||||
warnAboutReports = it
|
||||
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
|
||||
accountViewModel.updateOptOutOptions(warnAboutReports, filterSpam)
|
||||
},
|
||||
)
|
||||
|
||||
@ -177,7 +177,7 @@ fun SecurityFiltersScreen(
|
||||
checked = filterSpam,
|
||||
onCheckedChange = {
|
||||
filterSpam = it
|
||||
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
|
||||
accountViewModel.updateOptOutOptions(warnAboutReports, filterSpam)
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -234,7 +234,7 @@ private fun TranslationMessage(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (source in accountViewModel.account.settings.dontTranslateFrom) {
|
||||
if (source in accountViewModel.dontTranslateFrom()) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -255,7 +255,7 @@ private fun TranslationMessage(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
accountViewModel.account.settings.toggleDontTranslateFrom(source)
|
||||
accountViewModel.account.toggleDontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
},
|
||||
)
|
||||
@ -285,7 +285,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.settings.prefer(source, target, source)
|
||||
accountViewModel.account.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -293,7 +293,9 @@ private fun TranslationMessage(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (accountViewModel.account.settings.preferenceBetween(source, target) == target) {
|
||||
if (accountViewModel.account.settings.syncedSettings.languages
|
||||
.preferenceBetween(source, target) == target
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
@ -315,7 +317,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.settings.prefer(source, target, target)
|
||||
accountViewModel.account.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -350,7 +352,7 @@ private fun TranslationMessage(
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.settings.updateTranslateTo(lang)
|
||||
accountViewModel.account.updateTranslateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
@ -377,8 +379,8 @@ fun TranslateAndWatchLanguageChanges(
|
||||
LanguageTranslatorService
|
||||
.autoTranslate(
|
||||
content,
|
||||
accountViewModel.account.settings.dontTranslateFrom,
|
||||
accountViewModel.account.settings.translateTo,
|
||||
accountViewModel.dontTranslateFrom(),
|
||||
accountViewModel.translateTo(),
|
||||
).addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -39,26 +39,22 @@ class EventFactory {
|
||||
content: String,
|
||||
sig: String,
|
||||
) = when (kind) {
|
||||
AdvertisedRelayListEvent.KIND ->
|
||||
AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AppRecommendationEvent.KIND ->
|
||||
AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AppSpecificDataEvent.KIND -> AppSpecificDataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AudioHeaderEvent.KIND -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarDateSlotEvent.KIND ->
|
||||
CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarTimeSlotEvent.KIND ->
|
||||
CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelHideMessageEvent.KIND ->
|
||||
ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelMetadataEvent.KIND -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@ -79,23 +75,19 @@ class EventFactory {
|
||||
}
|
||||
ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityDefinitionEvent.KIND ->
|
||||
CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityPostApprovalEvent.KIND ->
|
||||
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackSelectionEvent.KIND ->
|
||||
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageHeaderEvent.KIND ->
|
||||
FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@ -105,16 +97,12 @@ class EventFactory {
|
||||
GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HTTPAuthorizationEvent.KIND ->
|
||||
HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesChatMessageEvent.KIND ->
|
||||
LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentRequestEvent.KIND ->
|
||||
LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentResponseEvent.KIND ->
|
||||
LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
Loading…
x
Reference in New Issue
Block a user