From e6036449255d85cd98e7bd4a1f0d7983ffa40649 Mon Sep 17 00:00:00 2001 From: Diego Beraldin Date: Thu, 9 Nov 2023 21:41:25 +0100 Subject: [PATCH] feat: quick login implementation (#118) * feat: quick login implementation * chore: update from PR feedback --- .../api/provider/DefaultServiceProvider.kt | 6 +- .../commonui/drawer/ModalDrawerContent.kt | 22 ++++- .../commonui/drawer/ModalDrawerViewModel.kt | 2 + .../domain/identity/di/IdentityModule.kt | 1 + .../DefaultApiConfigurationRepository.kt | 11 +++ .../feature/profile/di/ProfileModule.kt | 1 + .../feature/profile/login/LoginBottomSheet.kt | 31 +++++-- .../login/LoginBottomSheetViewModel.kt | 10 +++ .../diegoberaldin/raccoonforlemmy/App.kt | 80 +++++++++---------- 9 files changed, 113 insertions(+), 51 deletions(-) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/provider/DefaultServiceProvider.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/provider/DefaultServiceProvider.kt index b632ac74e..25f717180 100644 --- a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/provider/DefaultServiceProvider.kt +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/provider/DefaultServiceProvider.kt @@ -62,8 +62,10 @@ internal class DefaultServiceProvider : ServiceProvider { } override fun changeInstance(value: String) { - currentInstance = value - reinitialize() + if (currentInstance != value) { + currentInstance = value + reinitialize() + } } private fun reinitialize() { diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerContent.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerContent.kt index eceb5d5a9..3cc0bfcd0 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerContent.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerContent.kt @@ -18,6 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Bookmarks +import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh @@ -131,7 +132,13 @@ object ModalDrawerContent : Tab { user = uiState.user, instance = uiState.instance, autoLoadImages = uiState.autoLoadImages, - onOpenChangeInstance = { + onOpenChangeInstance = rememberCallback(model) { + // suggests current instance + model.reduce( + ModalDrawerMviModel.Intent.ChangeInstanceName( + uiState.instance.orEmpty() + ) + ) changeInstanceDialogOpen = true }, ) @@ -432,6 +439,19 @@ private fun ChangeInstanceDialog( ) } }, + trailingIcon = { + if (instanceName.isNotEmpty()) { + Icon( + modifier = Modifier.onClick( + rememberCallback { + onChangeInstanceName?.invoke("") + }, + ), + imageVector = Icons.Default.Clear, + contentDescription = null, + ) + } + }, ) Button( diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerViewModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerViewModel.kt index 2b0b561fd..e4ab9b63e 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerViewModel.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/drawer/ModalDrawerViewModel.kt @@ -12,6 +12,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepo import com.github.diegoberaldin.raccoonforlemmy.resources.MR import dev.icerock.moko.resources.desc.desc import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.debounce @@ -34,6 +35,7 @@ class ModalDrawerViewModel( ) : ModalDrawerMviModel, MviModel by mvi { + @OptIn(FlowPreview::class) override fun onStarted() { mvi.onStarted() mvi.scope?.launch(Dispatchers.Main) { diff --git a/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/di/IdentityModule.kt b/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/di/IdentityModule.kt index 4a4dbc014..970f92157 100644 --- a/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/di/IdentityModule.kt +++ b/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/di/IdentityModule.kt @@ -19,6 +19,7 @@ val coreIdentityModule = module { single { DefaultApiConfigurationRepository( serviceProvider = get(named("default")), + keyStore = get(), ) } single { diff --git a/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/repository/DefaultApiConfigurationRepository.kt b/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/repository/DefaultApiConfigurationRepository.kt index 49eb58535..a56e1f898 100644 --- a/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/repository/DefaultApiConfigurationRepository.kt +++ b/domain-identity/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/identity/repository/DefaultApiConfigurationRepository.kt @@ -1,6 +1,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider +import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay @@ -10,12 +11,21 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.isActive +private const val KEY_LAST_INSTANCE = "lastInstance" + internal class DefaultApiConfigurationRepository( private val serviceProvider: ServiceProvider, + private val keyStore: TemporaryKeyStore, ) : ApiConfigurationRepository { private val scope = CoroutineScope(SupervisorJob()) + init { + val instance = keyStore[KEY_LAST_INSTANCE, ""] + .takeIf { it.isNotEmpty() } ?: serviceProvider.currentInstance + changeInstance(instance) + } + override val instance = channelFlow { while (isActive) { val value = serviceProvider.currentInstance @@ -30,5 +40,6 @@ internal class DefaultApiConfigurationRepository( override fun changeInstance(value: String) { serviceProvider.changeInstance(value) + keyStore.save(KEY_LAST_INSTANCE, value) } } diff --git a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/di/ProfileModule.kt b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/di/ProfileModule.kt index cf8f9b6af..9a4626e81 100644 --- a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/di/ProfileModule.kt +++ b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/di/ProfileModule.kt @@ -27,6 +27,7 @@ val profileTabModule = module { identityRepository = get(), siteRepository = get(), communityRepository = get(), + apiConfigurationRepository = get(), ) } factory { diff --git a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheet.kt b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheet.kt index d5b630c5c..afaa8f552 100644 --- a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheet.kt +++ b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheet.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.HelpOutline import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff @@ -53,6 +54,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCo import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.rememberCallback +import com.github.diegoberaldin.raccoonforlemmy.core.utils.rememberCallbackArgs import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getLoginBottomSheetViewModel import com.github.diegoberaldin.raccoonforlemmy.resources.MR import dev.icerock.moko.resources.compose.localized @@ -145,6 +147,7 @@ class LoginBottomSheet : Screen { val passwordFocusRequester = remember { FocusRequester() } val tokenFocusRequester = remember { FocusRequester() } + // instance name TextField( modifier = Modifier.focusRequester(instanceFocusRequester), label = { @@ -163,7 +166,7 @@ class LoginBottomSheet : Screen { autoCorrect = false, imeAction = ImeAction.Next, ), - onValueChange = { value -> + onValueChange = rememberCallbackArgs(model) { value -> model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value)) }, supportingText = { @@ -174,8 +177,24 @@ class LoginBottomSheet : Screen { ) } }, + trailingIcon = { + if (uiState.instanceName.isNotEmpty()) { + Icon( + modifier = Modifier.onClick( + rememberCallback(model) { + model.reduce( + LoginBottomSheetMviModel.Intent.SetInstanceName("") + ) + }, + ), + imageVector = Icons.Default.Clear, + contentDescription = null, + ) + } + }, ) + // user name TextField( modifier = Modifier.focusRequester(usernameFocusRequester), label = { @@ -194,7 +213,7 @@ class LoginBottomSheet : Screen { autoCorrect = false, imeAction = ImeAction.Next, ), - onValueChange = { value -> + onValueChange = rememberCallbackArgs(model) { value -> model.reduce(LoginBottomSheetMviModel.Intent.SetUsername(value)) }, supportingText = { @@ -207,6 +226,7 @@ class LoginBottomSheet : Screen { }, ) + // password var transformation: VisualTransformation by remember { mutableStateOf(PasswordVisualTransformation()) } @@ -227,7 +247,7 @@ class LoginBottomSheet : Screen { keyboardType = KeyboardType.Password, imeAction = ImeAction.Next, ), - onValueChange = { value -> + onValueChange = rememberCallbackArgs(model) { value -> model.reduce(LoginBottomSheetMviModel.Intent.SetPassword(value)) }, visualTransformation = transformation, @@ -262,6 +282,7 @@ class LoginBottomSheet : Screen { }, ) + // TOTP 2FA token TextField( modifier = Modifier.focusRequester(tokenFocusRequester), label = { @@ -282,7 +303,7 @@ class LoginBottomSheet : Screen { keyboardType = KeyboardType.Password, imeAction = ImeAction.Done, ), - onValueChange = { value -> + onValueChange = rememberCallbackArgs(model) { value -> model.reduce(LoginBottomSheetMviModel.Intent.SetTotp2faToken(value)) }, visualTransformation = PasswordVisualTransformation(), @@ -290,7 +311,7 @@ class LoginBottomSheet : Screen { Spacer(modifier = Modifier.height(Spacing.m)) Button( modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { + onClick = rememberCallback(model) { model.reduce(LoginBottomSheetMviModel.Intent.Confirm) }, ) { diff --git a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheetViewModel.kt b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheetViewModel.kt index d1ac9af32..060e9a50f 100644 --- a/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheetViewModel.kt +++ b/feature-profile/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/profile/login/LoginBottomSheetViewModel.kt @@ -3,6 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.login import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase.LoginUseCase import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType @@ -17,6 +18,7 @@ import kotlinx.coroutines.launch class LoginBottomSheetViewModel( private val mvi: DefaultMviModel, private val login: LoginUseCase, + private val apiConfigurationRepository: ApiConfigurationRepository, private val identityRepository: IdentityRepository, private val accountRepository: AccountRepository, private val siteRepository: SiteRepository, @@ -24,6 +26,14 @@ class LoginBottomSheetViewModel( ) : LoginBottomSheetMviModel, MviModel by mvi { + override fun onStarted() { + mvi.onStarted() + val instance = apiConfigurationRepository.instance.value + mvi.updateState { + it.copy(instanceName = instance) + } + } + override fun reduce(intent: LoginBottomSheetMviModel.Intent) { when (intent) { LoginBottomSheetMviModel.Intent.Confirm -> submit() diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt index 7dde05d07..cd61a634c 100644 --- a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/App.kt @@ -85,7 +85,7 @@ fun App() { val currentSettings = settingsRepository.getSettings(accountId) settingsRepository.changeCurrentSettings(currentSettings) val lastActiveAccount = accountRepository.getActive() - val lastInstance = lastActiveAccount?.instance + val lastInstance = lastActiveAccount?.instance?.takeIf { it.isNotEmpty() } if (lastInstance != null) { apiConfigurationRepository.changeInstance(lastInstance) } @@ -134,41 +134,39 @@ fun App() { val uiFontScale by themeRepository.uiFontScale.collectAsState() val navigationCoordinator = remember { getNavigationCoordinator() } LaunchedEffect(navigationCoordinator) { - navigationCoordinator.deepLinkUrl - .debounce(750) - .onEach { url -> - val community = getCommunityFromUrl(url) - val user = getUserFromUrl(url) - val postAndInstance = getPostFromUrl(url) - val newScreen = when { - community != null -> { - CommunityDetailScreen( - community = community, - otherInstance = community.host, - ) - } - - user != null -> { - UserDetailScreen( - user = user, - otherInstance = user.host, - ) - } - - postAndInstance != null -> { - val (post, otherInstance) = postAndInstance - PostDetailScreen( - post = post, - otherInstance = otherInstance, - ) - } - - else -> null + navigationCoordinator.deepLinkUrl.debounce(750).onEach { url -> + val community = getCommunityFromUrl(url) + val user = getUserFromUrl(url) + val postAndInstance = getPostFromUrl(url) + val newScreen = when { + community != null -> { + CommunityDetailScreen( + community = community, + otherInstance = community.host, + ) } - if (newScreen != null) { - navigationCoordinator.getRootNavigator()?.push(newScreen) + + user != null -> { + UserDetailScreen( + user = user, + otherInstance = user.host, + ) } - }.launchIn(this) + + postAndInstance != null -> { + val (post, otherInstance) = postAndInstance + PostDetailScreen( + post = post, + otherInstance = otherInstance, + ) + } + + else -> null + } + if (newScreen != null) { + navigationCoordinator.getRootNavigator()?.push(newScreen) + } + }.launchIn(this) } val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) @@ -224,8 +222,7 @@ fun App() { ) { BottomSheetNavigator( sheetShape = RoundedCornerShape( - topStart = CornerSize.xl, - topEnd = CornerSize.xl + topStart = CornerSize.xl, topEnd = CornerSize.xl ), sheetBackgroundColor = MaterialTheme.colorScheme.background, ) { bottomNavigator -> @@ -240,13 +237,10 @@ fun App() { } }, ) { - Navigator( - screen = MainScreen, - onBackPressed = { - val callback = navigationCoordinator.getCanGoBackCallback() - callback?.let { it() } ?: true - } - ) { navigator -> + Navigator(screen = MainScreen, onBackPressed = { + val callback = navigationCoordinator.getCanGoBackCallback() + callback?.let { it() } ?: true + }) { navigator -> LaunchedEffect(Unit) { navigationCoordinator.setRootNavigator(navigator) }