feat: quick login implementation (#118)

* feat: quick login implementation

* chore: update from PR feedback
This commit is contained in:
Diego Beraldin 2023-11-09 21:41:25 +01:00 committed by GitHub
parent 5d032fd583
commit e603644925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 113 additions and 51 deletions

View File

@ -62,9 +62,11 @@ internal class DefaultServiceProvider : ServiceProvider {
} }
override fun changeInstance(value: String) { override fun changeInstance(value: String) {
if (currentInstance != value) {
currentInstance = value currentInstance = value
reinitialize() reinitialize()
} }
}
private fun reinitialize() { private fun reinitialize() {
val client = HttpClient(factory) { val client = HttpClient(factory) {

View File

@ -18,6 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Bookmarks 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.icons.filled.ManageAccounts
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
@ -131,7 +132,13 @@ object ModalDrawerContent : Tab {
user = uiState.user, user = uiState.user,
instance = uiState.instance, instance = uiState.instance,
autoLoadImages = uiState.autoLoadImages, autoLoadImages = uiState.autoLoadImages,
onOpenChangeInstance = { onOpenChangeInstance = rememberCallback(model) {
// suggests current instance
model.reduce(
ModalDrawerMviModel.Intent.ChangeInstanceName(
uiState.instance.orEmpty()
)
)
changeInstanceDialogOpen = true 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( Button(

View File

@ -12,6 +12,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepo
import com.github.diegoberaldin.raccoonforlemmy.resources.MR import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.desc.desc
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -34,6 +35,7 @@ class ModalDrawerViewModel(
) : ModalDrawerMviModel, ) : ModalDrawerMviModel,
MviModel<ModalDrawerMviModel.Intent, ModalDrawerMviModel.UiState, ModalDrawerMviModel.Effect> by mvi { MviModel<ModalDrawerMviModel.Intent, ModalDrawerMviModel.UiState, ModalDrawerMviModel.Effect> by mvi {
@OptIn(FlowPreview::class)
override fun onStarted() { override fun onStarted() {
mvi.onStarted() mvi.onStarted()
mvi.scope?.launch(Dispatchers.Main) { mvi.scope?.launch(Dispatchers.Main) {

View File

@ -19,6 +19,7 @@ val coreIdentityModule = module {
single<ApiConfigurationRepository> { single<ApiConfigurationRepository> {
DefaultApiConfigurationRepository( DefaultApiConfigurationRepository(
serviceProvider = get(named("default")), serviceProvider = get(named("default")),
keyStore = get(),
) )
} }
single<IdentityRepository> { single<IdentityRepository> {

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository package com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -10,12 +11,21 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
private const val KEY_LAST_INSTANCE = "lastInstance"
internal class DefaultApiConfigurationRepository( internal class DefaultApiConfigurationRepository(
private val serviceProvider: ServiceProvider, private val serviceProvider: ServiceProvider,
private val keyStore: TemporaryKeyStore,
) : ApiConfigurationRepository { ) : ApiConfigurationRepository {
private val scope = CoroutineScope(SupervisorJob()) private val scope = CoroutineScope(SupervisorJob())
init {
val instance = keyStore[KEY_LAST_INSTANCE, ""]
.takeIf { it.isNotEmpty() } ?: serviceProvider.currentInstance
changeInstance(instance)
}
override val instance = channelFlow { override val instance = channelFlow {
while (isActive) { while (isActive) {
val value = serviceProvider.currentInstance val value = serviceProvider.currentInstance
@ -30,5 +40,6 @@ internal class DefaultApiConfigurationRepository(
override fun changeInstance(value: String) { override fun changeInstance(value: String) {
serviceProvider.changeInstance(value) serviceProvider.changeInstance(value)
keyStore.save(KEY_LAST_INSTANCE, value)
} }
} }

View File

@ -27,6 +27,7 @@ val profileTabModule = module {
identityRepository = get(), identityRepository = get(),
siteRepository = get(), siteRepository = get(),
communityRepository = get(), communityRepository = get(),
apiConfigurationRepository = get(),
) )
} }
factory<ProfileLoggedMviModel> { factory<ProfileLoggedMviModel> {

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons 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.HelpOutline
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff 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.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.rememberCallback 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.feature.profile.di.getLoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.localized import dev.icerock.moko.resources.compose.localized
@ -145,6 +147,7 @@ class LoginBottomSheet : Screen {
val passwordFocusRequester = remember { FocusRequester() } val passwordFocusRequester = remember { FocusRequester() }
val tokenFocusRequester = remember { FocusRequester() } val tokenFocusRequester = remember { FocusRequester() }
// instance name
TextField( TextField(
modifier = Modifier.focusRequester(instanceFocusRequester), modifier = Modifier.focusRequester(instanceFocusRequester),
label = { label = {
@ -163,7 +166,7 @@ class LoginBottomSheet : Screen {
autoCorrect = false, autoCorrect = false,
imeAction = ImeAction.Next, imeAction = ImeAction.Next,
), ),
onValueChange = { value -> onValueChange = rememberCallbackArgs(model) { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value)) model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value))
}, },
supportingText = { 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( TextField(
modifier = Modifier.focusRequester(usernameFocusRequester), modifier = Modifier.focusRequester(usernameFocusRequester),
label = { label = {
@ -194,7 +213,7 @@ class LoginBottomSheet : Screen {
autoCorrect = false, autoCorrect = false,
imeAction = ImeAction.Next, imeAction = ImeAction.Next,
), ),
onValueChange = { value -> onValueChange = rememberCallbackArgs(model) { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetUsername(value)) model.reduce(LoginBottomSheetMviModel.Intent.SetUsername(value))
}, },
supportingText = { supportingText = {
@ -207,6 +226,7 @@ class LoginBottomSheet : Screen {
}, },
) )
// password
var transformation: VisualTransformation by remember { var transformation: VisualTransformation by remember {
mutableStateOf(PasswordVisualTransformation()) mutableStateOf(PasswordVisualTransformation())
} }
@ -227,7 +247,7 @@ class LoginBottomSheet : Screen {
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next, imeAction = ImeAction.Next,
), ),
onValueChange = { value -> onValueChange = rememberCallbackArgs(model) { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetPassword(value)) model.reduce(LoginBottomSheetMviModel.Intent.SetPassword(value))
}, },
visualTransformation = transformation, visualTransformation = transformation,
@ -262,6 +282,7 @@ class LoginBottomSheet : Screen {
}, },
) )
// TOTP 2FA token
TextField( TextField(
modifier = Modifier.focusRequester(tokenFocusRequester), modifier = Modifier.focusRequester(tokenFocusRequester),
label = { label = {
@ -282,7 +303,7 @@ class LoginBottomSheet : Screen {
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
onValueChange = { value -> onValueChange = rememberCallbackArgs(model) { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetTotp2faToken(value)) model.reduce(LoginBottomSheetMviModel.Intent.SetTotp2faToken(value))
}, },
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
@ -290,7 +311,7 @@ class LoginBottomSheet : Screen {
Spacer(modifier = Modifier.height(Spacing.m)) Spacer(modifier = Modifier.height(Spacing.m))
Button( Button(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = { onClick = rememberCallback(model) {
model.reduce(LoginBottomSheetMviModel.Intent.Confirm) model.reduce(LoginBottomSheetMviModel.Intent.Confirm)
}, },
) { ) {

View File

@ -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.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository 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.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase.LoginUseCase import com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase.LoginUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType
@ -17,6 +18,7 @@ import kotlinx.coroutines.launch
class LoginBottomSheetViewModel( class LoginBottomSheetViewModel(
private val mvi: DefaultMviModel<LoginBottomSheetMviModel.Intent, LoginBottomSheetMviModel.UiState, LoginBottomSheetMviModel.Effect>, private val mvi: DefaultMviModel<LoginBottomSheetMviModel.Intent, LoginBottomSheetMviModel.UiState, LoginBottomSheetMviModel.Effect>,
private val login: LoginUseCase, private val login: LoginUseCase,
private val apiConfigurationRepository: ApiConfigurationRepository,
private val identityRepository: IdentityRepository, private val identityRepository: IdentityRepository,
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
private val siteRepository: SiteRepository, private val siteRepository: SiteRepository,
@ -24,6 +26,14 @@ class LoginBottomSheetViewModel(
) : LoginBottomSheetMviModel, ) : LoginBottomSheetMviModel,
MviModel<LoginBottomSheetMviModel.Intent, LoginBottomSheetMviModel.UiState, LoginBottomSheetMviModel.Effect> by mvi { MviModel<LoginBottomSheetMviModel.Intent, LoginBottomSheetMviModel.UiState, LoginBottomSheetMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
val instance = apiConfigurationRepository.instance.value
mvi.updateState {
it.copy(instanceName = instance)
}
}
override fun reduce(intent: LoginBottomSheetMviModel.Intent) { override fun reduce(intent: LoginBottomSheetMviModel.Intent) {
when (intent) { when (intent) {
LoginBottomSheetMviModel.Intent.Confirm -> submit() LoginBottomSheetMviModel.Intent.Confirm -> submit()

View File

@ -85,7 +85,7 @@ fun App() {
val currentSettings = settingsRepository.getSettings(accountId) val currentSettings = settingsRepository.getSettings(accountId)
settingsRepository.changeCurrentSettings(currentSettings) settingsRepository.changeCurrentSettings(currentSettings)
val lastActiveAccount = accountRepository.getActive() val lastActiveAccount = accountRepository.getActive()
val lastInstance = lastActiveAccount?.instance val lastInstance = lastActiveAccount?.instance?.takeIf { it.isNotEmpty() }
if (lastInstance != null) { if (lastInstance != null) {
apiConfigurationRepository.changeInstance(lastInstance) apiConfigurationRepository.changeInstance(lastInstance)
} }
@ -134,9 +134,7 @@ fun App() {
val uiFontScale by themeRepository.uiFontScale.collectAsState() val uiFontScale by themeRepository.uiFontScale.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() } val navigationCoordinator = remember { getNavigationCoordinator() }
LaunchedEffect(navigationCoordinator) { LaunchedEffect(navigationCoordinator) {
navigationCoordinator.deepLinkUrl navigationCoordinator.deepLinkUrl.debounce(750).onEach { url ->
.debounce(750)
.onEach { url ->
val community = getCommunityFromUrl(url) val community = getCommunityFromUrl(url)
val user = getUserFromUrl(url) val user = getUserFromUrl(url)
val postAndInstance = getPostFromUrl(url) val postAndInstance = getPostFromUrl(url)
@ -224,8 +222,7 @@ fun App() {
) { ) {
BottomSheetNavigator( BottomSheetNavigator(
sheetShape = RoundedCornerShape( sheetShape = RoundedCornerShape(
topStart = CornerSize.xl, topStart = CornerSize.xl, topEnd = CornerSize.xl
topEnd = CornerSize.xl
), ),
sheetBackgroundColor = MaterialTheme.colorScheme.background, sheetBackgroundColor = MaterialTheme.colorScheme.background,
) { bottomNavigator -> ) { bottomNavigator ->
@ -240,13 +237,10 @@ fun App() {
} }
}, },
) { ) {
Navigator( Navigator(screen = MainScreen, onBackPressed = {
screen = MainScreen,
onBackPressed = {
val callback = navigationCoordinator.getCanGoBackCallback() val callback = navigationCoordinator.getCanGoBackCallback()
callback?.let { it() } ?: true callback?.let { it() } ?: true
} }) { navigator ->
) { navigator ->
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
navigationCoordinator.setRootNavigator(navigator) navigationCoordinator.setRootNavigator(navigator)
} }