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 package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
import android.R.attr.version
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
@@ -77,7 +76,10 @@ class UserSuggestionState(
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
val nip05ResolutionFlow = val nip05ResolutionFlow =
searchTerm currentWord
.debounce(300)
.distinctUntilChanged()
.map(::userSearchTermOrNull)
.map { prefix -> .map { prefix ->
if (prefix != null) { if (prefix != null) {
if (prefix.contains('@')) { if (prefix.contains('@')) {
@@ -20,8 +20,10 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
import android.R.attr.identifier
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -40,30 +46,80 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User 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.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton 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.note.creators.userSuggestions.UserSuggestionState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes 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 @Composable
fun ImportFollowListSelectUserScreen( fun ImportFollowListSelectUserScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val viewModel: ImportFollowListSelectUserViewModel =
viewModel(
factory = ImportFollowListSelectUserViewModel.Factory(accountViewModel.account, accountViewModel.nip05Client),
)
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
@@ -79,9 +135,7 @@ fun ImportFollowListSelectUserScreen(
.padding(padding), .padding(padding),
) { ) {
InputSelectUserBody( InputSelectUserBody(
onLookup = { viewModel = viewModel,
nav.nav(Route.ImportFollowsPickFollows(it.pubkeyHex))
},
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@@ -91,22 +145,19 @@ fun ImportFollowListSelectUserScreen(
@Composable @Composable
private fun InputSelectUserBody( private fun InputSelectUserBody(
onLookup: (User) -> Unit, viewModel: ImportFollowListSelectUserViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
var identifier by rememberSaveable { mutableStateOf("") }
val userSuggestions = remember { UserSuggestionState(accountViewModel.account, accountViewModel.nip05Client) }
Column(Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) { Column(Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
ImportHeader() ImportHeader()
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
OutlinedTextField( OutlinedTextField(
value = identifier, value = viewModel.identifier,
onValueChange = { onValueChange = {
identifier = it viewModel.identifier = it
userSuggestions.processCurrentWord(it) viewModel.userSuggestions.processCurrentWord(it)
}, },
label = { Text(stringRes(R.string.profile_to_import_from)) }, label = { Text(stringRes(R.string.profile_to_import_from)) },
placeholder = { Text(stringRes(R.string.name_search_npub1_alice_example_com)) }, placeholder = { Text(stringRes(R.string.name_search_npub1_alice_example_com)) },
@@ -118,11 +169,10 @@ private fun InputSelectUserBody(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
ShowUserSuggestionList( CustomShowUserSuggestionList(
userSuggestions = userSuggestions, viewModel = viewModel,
onSelect = { user -> onSelect = { user ->
onLookup(user) nav.nav(Route.ImportFollowsPickFollows(user.pubkeyHex))
userSuggestions.reset()
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
accountViewModel = accountViewModel, 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()
}
}