diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 5b80ad3a7..f2401e983 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -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>() {}.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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c07a0c031..05d071c94 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -57,7 +57,8 @@ class Account( var localRelays: Set = Constants.defaultRelays.toSet(), var dontTranslateFrom: Set = getLanguagesSpokenByUser(), var translateTo: String = Locale.getDefault().language, - var zapAmountChoices: List = listOf(500L, 1000L, 5000L) + var zapAmountChoices: List = listOf(500L, 1000L, 5000L), + var latestContactList: ContactListEvent? = null ) { var transientHiddenUsers: Set = setOf() @@ -80,18 +81,23 @@ class Account( fun sendNewRelayList(relays: Map) { 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) { 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? { @@ -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) { 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(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)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 20d6b04ce..3b2b81287 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt index 51e4eb6f6..8e5992d3b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt @@ -38,8 +38,6 @@ class NewChannelViewModel: ViewModel() { channelDescription.value.text, channelPicture.value.text ) - - LocalPreferences(context).saveToEncryptedStorage(account) } else account.sendChangeChannel( channelName.value.text, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index 23dfe2ed4..f88773b74 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -25,7 +25,6 @@ class NewRelayListViewModel: ViewModel() { fun create(ctx: Context) { relays.let { account.saveRelayList(it.value) - LocalPreferences(ctx).saveToEncryptedStorage(account) } clear(ctx) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index b4f01f147..f214581aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -540,7 +540,6 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) { SaveButton( onPost = { postViewModel.sendPost() - LocalPreferences(ctx).saveToEncryptedStorage(account) onClose() }, isActive = postViewModel.amounts.text.isNotBlank() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 4b5223842..b592d9dcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 42d6293c7..a4d86e30f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -102,7 +102,6 @@ fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewMode if (account.isHidden(baseAuthor)) { ShowUserButton { account.showUser(baseAuthor.pubkeyHex) - LocalPreferences(ctx).saveToEncryptedStorage(account) } } else if (userFollows.isFollowing(baseAuthor)) { UnfollowButton { account.unfollow(baseAuthor) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 29ca3f7dc..d630ac5e0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 8a72785e8..09812ac45 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 80cac64a5..b016ec5f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index d8eb5c46f..55d1419de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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) }