Saves Contact List locally to avoid losing follows.

pull/147/head
Vitor Pamplona 2023-02-21 15:48:23 -05:00
rodzic e8b09a9ba3
commit 5ab3ce84d3
12 zmienionych plików z 139 dodań i 79 usunięć

Wyświetl plik

@ -8,6 +8,9 @@ import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import java.util.Locale
import nostr.postr.Persona
import nostr.postr.events.ContactListEvent
import nostr.postr.events.Event
import nostr.postr.events.Event.Companion.getRefinedEvent
import nostr.postr.toHex
class LocalPreferences(context: Context) {
@ -24,6 +27,7 @@ class LocalPreferences(context: Context) {
remove("dontTranslateFrom")
remove("translateTo")
remove("zapAmounts")
remove("latestContactList")
}.apply()
}
@ -37,6 +41,7 @@ class LocalPreferences(context: Context) {
account.dontTranslateFrom.let { putStringSet("dontTranslateFrom", it) }
account.translateTo.let { putString("translateTo", it) }
account.zapAmountChoices.let { putString("zapAmounts", gson.toJson(it)) }
account.latestContactList.let { putString("latestContactList", Event.gson.toJson(it)) }
}.apply()
}
@ -59,6 +64,15 @@ class LocalPreferences(context: Context) {
object : TypeToken<List<Long>>() {}.type
) ?: listOf(500L, 1000L, 5000L)
val latestContactList = try {
getString("latestContactList", null)?.let {
Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent
}
} catch (e: Throwable) {
e.printStackTrace()
null
}
if (pubKey != null) {
return Account(
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
@ -67,7 +81,8 @@ class LocalPreferences(context: Context) {
localRelays,
dontTranslateFrom,
translateTo,
zapAmountChoices
zapAmountChoices,
latestContactList
)
} else {
return null

Wyświetl plik

@ -57,7 +57,8 @@ class Account(
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L)
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
var latestContactList: ContactListEvent? = null
) {
var transientHiddenUsers: Set<String> = setOf()
@ -80,18 +81,23 @@ class Account(
fun sendNewRelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
if (!isWriteable()) return
val lastestContactList = userProfile().latestContactList
val event = if (lastestContactList != null) {
ContactListEvent.create(
lastestContactList.follows,
val contactList = latestContactList
if (contactList != null && contactList.follows.size > 0) {
val event = ContactListEvent.create(
contactList.follows,
relays,
loggedIn.privKey!!)
} else {
ContactListEvent.create(listOf(), relays, loggedIn.privKey!!)
}
Client.send(event)
LocalCache.consume(event)
Client.send(event)
LocalCache.consume(event)
} else {
val event = ContactListEvent.create(listOf(), relays, loggedIn.privKey!!)
// Keep this local to avoid erasing a good contact list.
// Client.send(event)
LocalCache.consume(event)
}
}
fun sendNewUserMetadata(toString: String) {
@ -208,10 +214,11 @@ class Account(
fun follow(user: User) {
if (!isWriteable()) return
val lastestContactList = userProfile().latestContactList
val event = if (lastestContactList != null) {
val contactList = latestContactList
val event = if (contactList != null && contactList.follows.size > 0) {
ContactListEvent.create(
lastestContactList.follows.plus(Contact(user.pubkeyHex, null)),
contactList.follows.plus(Contact(user.pubkeyHex, null)),
userProfile().relays,
loggedIn.privKey!!)
} else {
@ -230,12 +237,14 @@ class Account(
fun unfollow(user: User) {
if (!isWriteable()) return
val lastestContactList = userProfile().latestContactList
if (lastestContactList != null) {
val contactList = latestContactList
if (contactList != null && contactList.follows.size > 0) {
val event = ContactListEvent.create(
lastestContactList.follows.filter { it.pubKeyHex != user.pubkeyHex },
contactList.follows.filter { it.pubKeyHex != user.pubkeyHex },
userProfile().relays,
loggedIn.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@ -309,28 +318,35 @@ class Account(
fun joinChannel(idHex: String) {
followingChannels = followingChannels + idHex
invalidateData(live)
live.invalidateData()
saveable.invalidateData()
}
fun leaveChannel(idHex: String) {
followingChannels = followingChannels - idHex
invalidateData(live)
live.invalidateData()
saveable.invalidateData()
}
fun hideUser(pubkeyHex: String) {
hiddenUsers = hiddenUsers + pubkeyHex
invalidateData(live)
live.invalidateData()
saveable.invalidateData()
}
fun showUser(pubkeyHex: String) {
hiddenUsers = hiddenUsers - pubkeyHex
transientHiddenUsers = transientHiddenUsers - pubkeyHex
invalidateData(live)
live.invalidateData()
saveable.invalidateData()
}
fun changeZapAmounts(newAmounts: List<Long>) {
zapAmountChoices = newAmounts
invalidateData(live)
live.invalidateData()
saveable.invalidateData()
}
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
@ -384,12 +400,23 @@ class Account(
fun addDontTranslateFrom(languageCode: String) {
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
invalidateData(liveLanguages)
liveLanguages.invalidateData()
saveable.invalidateData()
}
fun updateTranslateTo(languageCode: String) {
translateTo = languageCode
invalidateData(liveLanguages)
liveLanguages.invalidateData()
saveable.invalidateData()
}
private fun updateContactListTo(newContactList: ContactListEvent?) {
if ((newContactList?.follows?.size ?: 0) > 0 && latestContactList != newContactList) {
latestContactList = newContactList
saveable.invalidateData()
}
}
fun activeRelays(): Array<Relay>? {
@ -415,11 +442,28 @@ class Account(
}
init {
latestContactList?.let {
println("Loading saved contacts ${it.toJson()}")
if (userProfile().latestContactList == null) {
LocalCache.consume(it)
}
}
// Observes relays to restart connections
userProfile().live().relays.observeForever {
GlobalScope.launch(Dispatchers.IO) {
reconnectIfRelaysHaveChanged()
}
}
// saves contact list for the next time.
userProfile().live().follows.observeForever {
GlobalScope.launch(Dispatchers.IO) {
updateContactListTo(userProfile().latestContactList)
}
}
// imports transient blocks due to spam.
LocalCache.antiSpam.liveSpam.observeForever {
GlobalScope.launch(Dispatchers.IO) {
it.cache.spamMessages.snapshot().values.forEach {
@ -437,26 +481,7 @@ class Account(
// Observers line up here.
val live: AccountLiveData = AccountLiveData(this)
val liveLanguages: AccountLiveData = AccountLiveData(this)
var handlerWaiting = AtomicBoolean()
@Synchronized
private fun invalidateData(live: AccountLiveData) {
if (handlerWaiting.getAndSet(true)) return
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
try {
delay(100)
live.refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
}
}
}
val saveable: AccountLiveData = AccountLiveData(this)
fun isHidden(user: User) = user in hiddenUsers()
@ -496,10 +521,32 @@ class Account(
fun saveRelayList(value: List<RelaySetupInfo>) {
localRelays = value.toSet()
sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } )
saveable.invalidateData()
}
}
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
var handlerWaiting = AtomicBoolean()
@Synchronized
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
try {
delay(100)
refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
}
}
}
fun refresh() {
postValue(AccountState(account))
}

Wyświetl plik

@ -1,7 +1,5 @@
package com.vitorpamplona.amethyst.ui
import android.content.ComponentCallbacks2
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import androidx.activity.ComponentActivity
@ -16,24 +14,13 @@ import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.util.DebugLogger
import com.vitorpamplona.amethyst.EncryptedStorage
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import fr.acinq.secp256k1.Hex
import nostr.postr.Persona
import nostr.postr.bechToBytes
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -64,11 +51,11 @@ class MainActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
val accountViewModel: AccountStateViewModel = viewModel {
val accountStateViewModel: AccountStateViewModel = viewModel {
AccountStateViewModel(LocalPreferences(applicationContext))
}
AccountScreen(accountViewModel, startingPage)
AccountScreen(accountStateViewModel, startingPage)
}
}
}
@ -78,6 +65,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
// Only starts after login
ServiceManager.start()
}

Wyświetl plik

@ -38,8 +38,6 @@ class NewChannelViewModel: ViewModel() {
channelDescription.value.text,
channelPicture.value.text
)
LocalPreferences(context).saveToEncryptedStorage(account)
} else
account.sendChangeChannel(
channelName.value.text,

Wyświetl plik

@ -25,7 +25,6 @@ class NewRelayListViewModel: ViewModel() {
fun create(ctx: Context) {
relays.let {
account.saveRelayList(it.value)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
clear(ctx)

Wyświetl plik

@ -540,7 +540,6 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
SaveButton(
onPost = {
postViewModel.sendPost()
LocalPreferences(ctx).saveToEncryptedStorage(account)
onClose()
},
isActive = postViewModel.amounts.text.isNotBlank()

Wyświetl plik

@ -75,7 +75,6 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
} else if (userFollows.isFollowing(baseUser)) {
UnfollowButton { account.unfollow(baseUser) }

Wyświetl plik

@ -102,7 +102,6 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
if (account.isHidden(baseAuthor)) {
ShowUserButton {
account.showUser(baseAuthor.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
} else if (userFollows.isFollowing(baseAuthor)) {
UnfollowButton { account.unfollow(baseAuthor) }

Wyświetl plik

@ -1,16 +1,15 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import fr.acinq.secp256k1.Hex
import java.util.regex.Pattern
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -70,9 +69,35 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences): Vie
scope.launch {
ServiceManager.start(account)
}
GlobalScope.launch(Dispatchers.Main) {
account.saveable.observeForever(saveListener)
}
}
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
GlobalScope.launch(Dispatchers.IO) {
localPreferences.saveToEncryptedStorage(it.account)
}
}
fun logOff() {
val state = accountContent.value
when (state) {
is AccountState.LoggedIn -> {
GlobalScope.launch(Dispatchers.Main) {
state.account.saveable.removeObserver(saveListener)
}
}
is AccountState.LoggedInViewOnly -> {
GlobalScope.launch(Dispatchers.Main) {
state.account.saveable.removeObserver(saveListener)
}
}
else -> {}
}
_accountContent.update { AccountState.LoggedOff }
localPreferences.clearEncryptedStorage()

Wyświetl plik

@ -11,6 +11,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountState
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ReportEvent
@ -67,21 +69,17 @@ class AccountViewModel(private val account: Account): ViewModel() {
fun hide(user: User, ctx: Context) {
account.hideUser(user.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
fun show(user: User, ctx: Context) {
account.showUser(user.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
fun translateTo(lang: Locale, ctx: Context) {
account.updateTranslateTo(lang.language)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
fun dontTranslateFrom(lang: String, ctx: Context) {
account.addDontTranslateFrom(lang)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
}

Wyświetl plik

@ -306,13 +306,10 @@ private fun EditButton(account: Account, channel: Channel) {
@Composable
private fun JoinButton(account: Account, channel: Channel, navController: NavController) {
val context = LocalContext.current.applicationContext
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.joinChannel(channel.idHex)
LocalPreferences(context).saveToEncryptedStorage(account)
navController.navigate(Route.Message.route)
},
shape = RoundedCornerShape(20.dp),
@ -328,13 +325,10 @@ private fun JoinButton(account: Account, channel: Channel, navController: NavCon
@Composable
private fun LeaveButton(account: Account, channel: Channel, navController: NavController) {
val context = LocalContext.current.applicationContext
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.leaveChannel(channel.idHex)
LocalPreferences(context).saveToEncryptedStorage(account)
navController.navigate(Route.Message.route)
},
shape = RoundedCornerShape(20.dp),

Wyświetl plik

@ -323,7 +323,6 @@ private fun ProfileHeader(
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
} else if (accountUser.isFollowing(baseUser)) {
UnfollowButton { account.unfollow(baseUser) }