Merge branch 'vitorpamplona:main' into kmp-completeness
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
+13
-18
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user