diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt index 2ab935b37d..f825eb2c68 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt @@ -42,6 +42,9 @@ class SignUpActivity : AppCompatActivity() { onSelected = signUpViewModel::onSelected, onNavigateUp = { finish() + }, + onBottomReached = { + signUpViewModel.onBottomReached() } ) } diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt index e27a6b90fa..89ce7317bf 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt @@ -1,23 +1,39 @@ package net.pantasystem.milktea.auth -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.distinctUntilChanged import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse import net.pantasystem.milktea.auth.viewmodel.SignUpUiState import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.ui.isScrolledToTheEnd import net.pantasystem.milktea.model.instance.InstanceInfoType @Composable @@ -27,8 +43,20 @@ fun SignUpScreen( onInputKeyword: (String) -> Unit, onNextButtonClicked: (InstanceInfoType) -> Unit, onSelected: (InstanceInfosResponse.InstanceInfo) -> Unit, - onNavigateUp: () -> Unit + onNavigateUp: () -> Unit, + onBottomReached: () -> Unit, ) { + + val listState = rememberLazyListState() + LaunchedEffect(Unit) { + snapshotFlow { + listState.isScrolledToTheEnd() + }.distinctUntilChanged().collect { + if (it) { + onBottomReached() + } + } + } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -78,7 +106,8 @@ fun SignUpScreen( LazyColumn( modifier = Modifier .fillMaxWidth() - .weight(1f) + .weight(1f), + state = listState, ) { items(uiState.filteredInfos) { instance -> MisskeyInstanceInfoCard( @@ -103,10 +132,11 @@ fun SignUpScreen( Button( shape = RoundedCornerShape(32.dp), onClick = { - when(val content = uiState.instanceInfo.content) { + when (val content = uiState.instanceInfo.content) { is StateContent.Exist -> { onNextButtonClicked(content.rawContent) } + is StateContent.NotExist -> Unit } }, diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt new file mode 100644 index 0000000000..8497482aad --- /dev/null +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt @@ -0,0 +1,86 @@ +package net.pantasystem.milktea.auth.suggestions + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder +import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.common.PageableState +import net.pantasystem.milktea.common.paginator.EntityConverter +import net.pantasystem.milktea.common.paginator.PaginationState +import net.pantasystem.milktea.common.paginator.PreviousLoader +import net.pantasystem.milktea.common.paginator.PreviousPagingController +import net.pantasystem.milktea.common.paginator.StateLocker +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError +import javax.inject.Inject + +class InstanceSuggestionsPagingModel @Inject constructor( + private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder, +) : StateLocker, + PaginationState, + PreviousLoader, + EntityConverter { + + private var _offset = 0 + private var _name: String = "" + private val _state = + MutableStateFlow>>(PageableState.Loading.Init()) + + private var _job: Job? = null + + override suspend fun convertAll(list: List): List { + return list + } + + override val state: Flow>> + get() = _state + + override fun getState(): PageableState> { + return _state.value + } + + override fun setState(state: PageableState>) { + _state.value = state + } + + override suspend fun loadPrevious(): Result> = + runCancellableCatching { + instancesInfoAPIBuilder.build().getInstances( + offset = _offset, + name = _name, + ).throwIfHasError().body()!!.also { + _offset += it.size + } + } + + suspend fun setQueryName(name: String) { + _job?.cancel() + mutex.withLock { + _name = name + + } + setState(PageableState.Loading.Init()) + } + + override val mutex: Mutex = Mutex() + + private val previousPagingController = PreviousPagingController( + this, + this, + this, + this + ) + + fun onLoadNext(scope: CoroutineScope) { + _job?.cancel() + _job = scope.launch { + previousPagingController.loadPrevious() + } + + } +} \ No newline at end of file diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt index 4cff4062b0..81a8963da0 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel import net.pantasystem.milktea.common.* import net.pantasystem.milktea.model.instance.InstanceInfoService import net.pantasystem.milktea.model.instance.InstanceInfoType @@ -14,34 +14,37 @@ import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( - private val instancesInfosAPIBuilder: InstanceInfoAPIBuilder, private val instanceInfoService: InstanceInfoService, + private val instancePagingModel: InstanceSuggestionsPagingModel, loggerFactory: Logger.Factory, ) : ViewModel() { - - private val logger by lazy { - loggerFactory.create("SignUpViewModel") - } private var _keyword = MutableStateFlow("") val keyword = _keyword.asStateFlow() - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - private val instancesInfosResponse = keyword.flatMapLatest { name -> - suspend { - requireNotNull( - instancesInfosAPIBuilder.build().getInstances( - name = name - ).throwIfHasError() - .body() - ).distinctBy { - it.url - } - }.asFlow() - }.catch { - logger.error("インスタンス情報の取得に失敗", it) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) +// @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +// private val instancesInfosResponse = keyword.flatMapLatest { name -> +// suspend { +// requireNotNull( +// instancesInfosAPIBuilder.build().getInstances( +// name = name +// ).throwIfHasError() +// .body() +// ).distinctBy { +// it.url +// } +// }.asFlow() +// }.catch { +// logger.error("インスタンス情報の取得に失敗", it) +// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + private val instancesInfosResponse = instancePagingModel.state.map { + (it.content as? StateContent.Exist)?.rawContent + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + emptyList(), + ) private var _selectedInstanceUrl = MutableStateFlow("misskey.io") @@ -83,13 +86,25 @@ class SignUpViewModel @Inject constructor( SignUpUiState() ) + init { + instancePagingModel.onLoadNext(viewModelScope) + } + fun onInputKeyword(value: String) { - _keyword.value = value + viewModelScope.launch { + _keyword.value = value + instancePagingModel.setQueryName(value) + instancePagingModel.onLoadNext(this) + } } fun onSelected(instancesInfosResponse: InstanceInfosResponse.InstanceInfo) { _selectedInstanceUrl.value = instancesInfosResponse.url } + + fun onBottomReached() { + instancePagingModel.onLoadNext(viewModelScope) + } } data class SignUpUiState( diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt index 0570d193a6..fb53b6c264 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,10 +26,10 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus -import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder import net.pantasystem.milktea.api.misskey.MisskeyAPIServiceBuilder import net.pantasystem.milktea.api.misskey.auth.UserKey import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent @@ -61,7 +60,7 @@ class AppAuthViewModel @Inject constructor( private val getAccessToken: GetAccessToken, private val clientIdRepository: ClientIdRepository, private val syncMetaExecutor: SyncMetaExecutor, - private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder, + private val instanceSuggestionsPagingModel: InstanceSuggestionsPagingModel, ) : ViewModel() { private val logger = loggerFactory.create("AppAuthViewModel") @@ -93,21 +92,29 @@ class AppAuthViewModel @Inject constructor( // private val instances = instanceInfoRepository.observeAll() // .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - private val misskeyInstances = instanceDomain.flatMapLatest { name -> - suspend { - requireNotNull( - instancesInfoAPIBuilder.build().getInstances( - name = name - ).throwIfHasError() - .body() - ).distinctBy { - it.url - } - }.asFlow() - }.catch { - logger.error("インスタンス情報の取得に失敗", it) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) +// @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +// private val misskeyInstances = instanceDomain.flatMapLatest { name -> +// suspend { +// requireNotNull( +// instancesInfoAPIBuilder.build().getInstances( +// name = name +// ).throwIfHasError() +// .body() +// ).distinctBy { +// it.url +// } +// }.asFlow() +// }.catch { +// logger.error("インスタンス情報の取得に失敗", it) +// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) +// + private val misskeyInstances = instanceSuggestionsPagingModel.state.map { + (it.content as? StateContent.Exist)?.rawContent ?: emptyList() + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + emptyList() + ) private val isPrivacyPolicyAgreement = MutableStateFlow(false) private val isTermsOfServiceAgreement = MutableStateFlow(false) @@ -263,7 +270,7 @@ class AppAuthViewModel @Inject constructor( }, waiting4ApproveState = waiting4Approve, clientId = "clientId: ${clientIdRepository.getOrCreate().clientId}", - misskeyInstanceInfosResponse = misskeyInstances ?: emptyList(), + misskeyInstanceInfosResponse = misskeyInstances, ) }.stateIn( viewModelScope, @@ -278,6 +285,10 @@ class AppAuthViewModel @Inject constructor( init { + instanceDomain.onEach { + instanceSuggestionsPagingModel.setQueryName(it) + instanceSuggestionsPagingModel.onLoadNext(viewModelScope) + }.launchIn(viewModelScope) waiting4UserApprove.mapNotNull { (it.content as? StateContent.Exist)?.rawContent