Saves the position of the screen on import follows dialog

This commit is contained in:
Vitor Pamplona
2026-03-10 18:46:18 -04:00
parent 7a3794eabb
commit cc4e81146f
2 changed files with 125 additions and 19 deletions
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
import android.R.attr.version
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
@@ -77,7 +76,10 @@ class UserSuggestionState(
@OptIn(FlowPreview::class)
val nip05ResolutionFlow =
searchTerm
currentWord
.debounce(300)
.distinctUntilChanged()
.map(::userSearchTermOrNull)
.map { prefix ->
if (prefix != null) {
if (prefix.contains('@')) {
@@ -20,8 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
import android.R.attr.identifier
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,9 +32,13 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -40,30 +46,80 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@Stable
class ImportFollowListSelectUserViewModel(
val account: Account,
val nip05Client: INip05Client,
) : ViewModel() {
var identifier by mutableStateOf("")
val userSuggestions = UserSuggestionState(account, nip05Client)
val listState = LazyListState(0, 0)
val searchTerm =
userSuggestions.searchTerm
.onEach {
if (!it.isNullOrBlank()) {
listState.scrollToItem(0, 0)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, userSuggestions.currentWord)
val results =
userSuggestions.results
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
class Factory(
val account: Account,
val nip05Client: INip05Client,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = ImportFollowListSelectUserViewModel(account, nip05Client) as T
}
}
@Composable
fun ImportFollowListSelectUserScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val viewModel: ImportFollowListSelectUserViewModel =
viewModel(
factory = ImportFollowListSelectUserViewModel.Factory(accountViewModel.account, accountViewModel.nip05Client),
)
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
@@ -79,9 +135,7 @@ fun ImportFollowListSelectUserScreen(
.padding(padding),
) {
InputSelectUserBody(
onLookup = {
nav.nav(Route.ImportFollowsPickFollows(it.pubkeyHex))
},
viewModel = viewModel,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -91,22 +145,19 @@ fun ImportFollowListSelectUserScreen(
@Composable
private fun InputSelectUserBody(
onLookup: (User) -> Unit,
viewModel: ImportFollowListSelectUserViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
var identifier by rememberSaveable { mutableStateOf("") }
val userSuggestions = remember { UserSuggestionState(accountViewModel.account, accountViewModel.nip05Client) }
Column(Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
ImportHeader()
Spacer(Modifier.height(20.dp))
OutlinedTextField(
value = identifier,
value = viewModel.identifier,
onValueChange = {
identifier = it
userSuggestions.processCurrentWord(it)
viewModel.identifier = it
viewModel.userSuggestions.processCurrentWord(it)
},
label = { Text(stringRes(R.string.profile_to_import_from)) },
placeholder = { Text(stringRes(R.string.name_search_npub1_alice_example_com)) },
@@ -118,11 +169,10 @@ private fun InputSelectUserBody(
Spacer(Modifier.height(8.dp))
ShowUserSuggestionList(
userSuggestions = userSuggestions,
CustomShowUserSuggestionList(
viewModel = viewModel,
onSelect = { user ->
onLookup(user)
userSuggestions.reset()
nav.nav(Route.ImportFollowsPickFollows(user.pubkeyHex))
},
modifier = Modifier.weight(1f),
accountViewModel = accountViewModel,
@@ -180,3 +230,57 @@ private fun ImportHeader() {
)
}
}
@Composable
private fun CustomShowUserSuggestionList(
viewModel: ImportFollowListSelectUserViewModel,
onSelect: (User) -> Unit,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onEmpty: @Composable () -> Unit = {},
) {
UserSearchDataSourceSubscription(viewModel.userSuggestions, accountViewModel)
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
viewModel.userSuggestions.invalidateData()
}
}
launch(Dispatchers.IO) {
LocalCache.live.deletedEventBundles.collect {
viewModel.userSuggestions.invalidateData()
}
}
}
CustomWatchResponses(viewModel, onSelect, accountViewModel, modifier, onEmpty)
}
@Composable
fun CustomWatchResponses(
viewModel: ImportFollowListSelectUserViewModel,
onSelect: (User) -> Unit,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onEmpty: @Composable () -> Unit = {},
) {
val suggestions by viewModel.results.collectAsStateWithLifecycle()
if (suggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(top = 10.dp),
modifier = modifier,
state = viewModel.listState,
) {
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
UserLine(item, accountViewModel) { onSelect(item) }
HorizontalDivider(
thickness = DividerThickness,
)
}
}
} else {
onEmpty()
}
}