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,8 +62,10 @@ internal class DefaultServiceProvider : ServiceProvider {
}
override fun changeInstance(value: String) {
currentInstance = value
reinitialize()
if (currentInstance != value) {
currentInstance = value
reinitialize()
}
}
private fun reinitialize() {

View File

@ -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(

View File

@ -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<ModalDrawerMviModel.Intent, ModalDrawerMviModel.UiState, ModalDrawerMviModel.Effect> by mvi {
@OptIn(FlowPreview::class)
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch(Dispatchers.Main) {

View File

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

View File

@ -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)
}
}

View File

@ -27,6 +27,7 @@ val profileTabModule = module {
identityRepository = get(),
siteRepository = get(),
communityRepository = get(),
apiConfigurationRepository = get(),
)
}
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.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)
},
) {

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.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<LoginBottomSheetMviModel.Intent, LoginBottomSheetMviModel.UiState, LoginBottomSheetMviModel.Effect>,
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<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) {
when (intent) {
LoginBottomSheetMviModel.Intent.Confirm -> submit()

View File

@ -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)
}