Merge branch 'vitorpamplona:main' into kmp-completeness

This commit is contained in:
KotlinGeekDev
2026-03-04 02:02:12 +01:00
committed by GitHub
3 changed files with 112 additions and 55 deletions
@@ -97,15 +97,16 @@ object MediaSaverToDisk {
onSuccess: () -> Any?, onSuccess: () -> Any?,
onError: (Throwable) -> Any?, onError: (Throwable) -> Any?,
) { ) {
val client = okHttpClient(url)
val request =
Request
.Builder()
.get()
.url(url)
.build()
try { try {
val client = okHttpClient(url)
val request =
Request
.Builder()
.get()
.url(url)
.build()
client.newCall(request).executeAsync().use { response -> client.newCall(request).executeAsync().use { response ->
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
check(response.isSuccessful) check(response.isSuccessful)
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.DefaultChannels import com.vitorpamplona.amethyst.model.DefaultChannels
import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.model.DefaultDMRelayList
@@ -75,6 +76,18 @@ import java.util.regex.Pattern
val EMAIL_PATTERN: Pattern = Pattern.compile(".+@.+\\.[a-z]+") val EMAIL_PATTERN: Pattern = Pattern.compile(".+@.+\\.[a-z]+")
sealed class AccountState {
object Loading : AccountState()
object LoggedOff : AccountState()
@Stable
class LoggedIn(
val account: Account,
var route: Route? = null,
) : AccountState()
}
@Stable @Stable
class AccountSessionManager( class AccountSessionManager(
val accountsCache: AccountCacheState, val accountsCache: AccountCacheState,
@@ -175,15 +188,6 @@ class AccountSessionManager(
} }
} }
private fun prepareLogoutOrSwitch() =
when (val state = _accountContent.value) {
is AccountState.LoggedIn -> {
state.currentViewModelStore.viewModelStore.clear()
}
else -> {}
}
fun login( fun login(
key: String, key: String,
password: String, password: String,
@@ -256,9 +260,6 @@ class AccountSessionManager(
onError: (String) -> Unit, onError: (String) -> Unit,
) { ) {
try { try {
if (_accountContent.value is AccountState.LoggedIn) {
prepareLogoutOrSwitch()
}
loginAndStartUI(key, transientAccount, loginWithExternalSigner, packageName) loginAndStartUI(key, transientAccount, loginWithExternalSigner, packageName)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -269,10 +270,6 @@ class AccountSessionManager(
fun newKey(name: String? = null) { fun newKey(name: String? = null) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
if (_accountContent.value is AccountState.LoggedIn) {
prepareLogoutOrSwitch()
}
_accountContent.update { AccountState.Loading } _accountContent.update { AccountState.Loading }
val accountSettings = createNewAccount(name) val accountSettings = createNewAccount(name)
@@ -342,7 +339,6 @@ class AccountSessionManager(
accountInfo: AccountInfo, accountInfo: AccountInfo,
route: Route? = null, route: Route? = null,
) { ) {
prepareLogoutOrSwitch()
localPreferences.switchToAccount(accountInfo) localPreferences.switchToAccount(accountInfo)
loginWithDefaultAccount(route) loginWithDefaultAccount(route)
} }
@@ -364,7 +360,6 @@ class AccountSessionManager(
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
if (accountInfo.npub == currentAccountNPub()) { if (accountInfo.npub == currentAccountNPub()) {
// log off and relogin with the 0 account // log off and relogin with the 0 account
prepareLogoutOrSwitch()
localPreferences.deleteAccount(accountInfo) localPreferences.deleteAccount(accountInfo)
accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey())
loginWithDefaultAccount() loginWithDefaultAccount()
@@ -20,49 +20,110 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import com.vitorpamplona.amethyst.model.Account import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.components.getActivity
sealed class AccountState {
object Loading : AccountState()
object LoggedOff : AccountState()
@Stable
class LoggedIn(
val account: Account,
var route: Route? = null,
) : AccountState() {
val currentViewModelStore = AccountCentricViewModelStore()
}
}
/**
* Creates a new scope for the given ViewModel type.
*/
@Composable @Composable
fun SetAccountCentricViewModelStore( fun SetAccountCentricViewModelStore(
state: AccountState.LoggedIn, state: AccountState.LoggedIn,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val activity = LocalContext.current.getActivity()
val vmStore: StoreOwnerRegistry = viewModel(viewModelStoreOwner = activity)
vmStore.checkAttached(activity)
val owner = vmStore.getOwner(state)
val observer = remember { CompositionObserver(vmStore, state) }
CompositionLocalProvider( CompositionLocalProvider(
LocalViewModelStoreOwner provides state.currentViewModelStore, LocalViewModelStoreOwner provides owner,
) { content = content,
content() )
}
/**
* This class is responsible for notifying the [StoreOwnerRegistry] when a composable is detached so
* that the viewmodel can be cleared.
*/
class CompositionObserver(
private val vmStore: StoreOwnerRegistry,
private val key: Any,
) : RememberObserver {
override fun onRemembered() {}
override fun onForgotten() = vmStore.composableDetached(key)
override fun onAbandoned() = vmStore.composableDetached(key)
}
/**
* Registry for [ViewModelStoreOwner]s that are scoped to a particular composition.
* This ViewModel is registered with the Activity's lifecycle and will clear the viewmodels.
*/
class StoreOwnerRegistry : ViewModel() {
private var isActivityRegistered: Boolean = false
private var isChangingConfigurations: Boolean = false
private val map = mutableMapOf<Any, ViewModelStoreOwner>()
override fun onCleared() {
map.values.forEach { it.viewModelStore.clear() }
super.onCleared()
} }
// moved this clearing activity to the viewmodel account fun getOwner(key: Any): ViewModelStoreOwner = map[key] ?: ScopedViewModelStoreOwner().also { map[key] = it }
// because the new composable might run before the onDispose.
// DisposableEffect(key1 = state) { fun composableDetached(key: Any) {
// onDispose { // TODO: This prevents the viewmodel from being cleared when the Composable is detached due
// state.currentViewModelStore.viewModelStore.clear() // to a configuration change. We need to make sure that the viewmodel is cleared when the
// } // Composition is recreated without the Composable. E.g. by observing the Composition
// } if (isChangingConfigurations) return
map.remove(key)?.also { owner -> owner.viewModelStore.clear() }
}
fun checkAttached(activity: ComponentActivity) {
if (!isActivityRegistered) {
isActivityRegistered = true
activity.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
isChangingConfigurations = false
}
override fun onStop(owner: LifecycleOwner) {
if (activity.isChangingConfigurations) {
isChangingConfigurations = true
}
}
override fun onDestroy(owner: LifecycleOwner) {
isActivityRegistered = false
owner.lifecycle.removeObserver(this)
}
},
)
}
}
} }
class AccountCentricViewModelStore : ViewModelStoreOwner { /**
override val viewModelStore = ViewModelStore() * Simple ViewModelStoreOwner that can be used to create a new scope.
*/
class ScopedViewModelStoreOwner : ViewModelStoreOwner {
override val viewModelStore: ViewModelStore = ViewModelStore()
} }