add confirmation dialog to chat creation

This commit introduces a confirmation dialog before creating a new chat. The dialog displays the final chat title, which is now dynamically generated based on the user's input or the names of the selected participants.

Key changes:
- Added a confirmation dialog that appears when the user clicks the "create chat" button.
- Implemented logic to generate a provisional chat title from participants' names if no title is explicitly set.
- Refactored `CreateChatViewModel` by removing the interface and simplifying the implementation.
- Added new string resources for the confirmation dialog.
This commit is contained in:
2025-12-02 05:31:09 +03:00
parent 6f55251fb7
commit 018151ad18
7 changed files with 128 additions and 71 deletions
@@ -286,4 +286,7 @@
<string name="week_short">Н</string> <string name="week_short">Н</string>
<string name="day_short">Д</string> <string name="day_short">Д</string>
<string name="time_now">Сейчас</string> <string name="time_now">Сейчас</string>
<string name="confirm_chat_create_with_title">Вы действительно хотите создать чат «%s»?</string>
<string name="confirm_chat_create_empty_with_title">Вы действительно хотите создать чат «%s» только с собой?</string>
</resources> </resources>
+3
View File
@@ -363,4 +363,7 @@
<string name="week_short">W</string> <string name="week_short">W</string>
<string name="day_short">D</string> <string name="day_short">D</string>
<string name="time_now">Now</string> <string name="time_now">Now</string>
<string name="confirm_chat_create_with_title">Are you sure you want to create chat «%s»?</string>
<string name="confirm_chat_create_empty_with_title">Are you sure you want to create chat «%s» only with yourself?</string>
</resources> </resources>
@@ -17,74 +17,67 @@ import dev.meloda.fast.domain.GetLocalUserByIdUseCase
import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.domain.util.asPresentation
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.network.VkErrorCode
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.api.UiFriend
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface CreateChatViewModel { class CreateChatViewModel(
val screenState: StateFlow<CreateChatScreenState>
val baseError: StateFlow<BaseError?>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
val isChatCreated: StateFlow<Long?>
fun onPaginationConditionsMet()
fun onRefresh()
fun onErrorConsumed()
fun toggleFriendSelection(userId: Long)
fun onTitleTextInputChanged(newTitle: String)
fun onCreateChatButtonClicked()
fun onNavigatedBack()
}
class CreateChatViewModelImpl(
private val friendsUseCase: FriendsUseCase, private val friendsUseCase: FriendsUseCase,
private val messagesUseCase: MessagesUseCase, private val messagesUseCase: MessagesUseCase,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
private val applicationContext: Context, private val applicationContext: Context,
private val getLocalUserByIdUseCase: GetLocalUserByIdUseCase, private val getLocalUserByIdUseCase: GetLocalUserByIdUseCase,
private val userSettings: UserSettings private val userSettings: UserSettings
) : CreateChatViewModel, ViewModel() { ) : ViewModel() {
override val screenState = MutableStateFlow(CreateChatScreenState.EMPTY) private val _screenState = MutableStateFlow(CreateChatScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null) val screenState: StateFlow<CreateChatScreenState> = _screenState.asStateFlow()
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
override val isChatCreated = MutableStateFlow<Long?>(null) private val _baseError = MutableStateFlow<BaseError?>(null)
val baseError: StateFlow<BaseError?> = _baseError.asStateFlow()
private val currentOffset = MutableStateFlow(0)
private val _canPaginate = MutableStateFlow(false)
val canPaginate: StateFlow<Boolean> = _canPaginate.asStateFlow()
private val _isChatCreated = MutableStateFlow<Long?>(null)
val isChatCreated: StateFlow<Long?> = _isChatCreated.asStateFlow()
private val _finalChatTitle = MutableStateFlow("")
val finalChatTitle: StateFlow<String> = _finalChatTitle.asStateFlow()
private val useContactNames: Boolean = userSettings.useContactNames.value private val useContactNames: Boolean = userSettings.useContactNames.value
private var accountUser: VkUser? = null
init { init {
loadFriends() fetchAccountUser()
fetchUsers()
} }
override fun onPaginationConditionsMet() { fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.friends.size } currentOffset.update { screenState.value.friends.size }
loadFriends() fetchUsers()
} }
override fun onRefresh() { fun onRefresh() {
onErrorConsumed() onErrorConsumed()
loadFriends(offset = 0) fetchUsers(offset = 0)
} }
override fun onErrorConsumed() { fun onErrorConsumed() {
baseError.setValue { null } _baseError.setValue { null }
} }
override fun toggleFriendSelection(userId: Long) { fun toggleFriendSelection(userId: Long) {
val newSelectionList = screenState.value.selectedFriendsIds.toMutableList() val newSelectionList = screenState.value.selectedFriendsIds.toMutableList()
if (newSelectionList.contains(userId)) { if (newSelectionList.contains(userId)) {
@@ -93,26 +86,69 @@ class CreateChatViewModelImpl(
newSelectionList.add(userId) newSelectionList.add(userId)
} }
screenState.setValue { old -> _screenState.setValue { old ->
old.copy(selectedFriendsIds = newSelectionList) old.copy(selectedFriendsIds = newSelectionList)
} }
refreshFinalTitle()
}
fun onTitleTextInputChanged(newTitle: String) {
_screenState.setValue { old -> old.copy(chatTitle = newTitle) }
refreshFinalTitle()
}
fun onCreateChatButtonClicked() {
_screenState.setValue { old -> old.copy(showConfirmDialog = true) }
}
fun onNavigatedBack() {
viewModelScope.launch(Dispatchers.Main) {
_isChatCreated.emit(null)
}
} }
override fun onTitleTextInputChanged(newTitle: String) { fun onConfirmDialogDismissed() {
screenState.setValue { old -> old.copy(chatTitle = newTitle) } _screenState.setValue { old -> old.copy(showConfirmDialog = false) }
} }
override fun onCreateChatButtonClicked() { fun onConfirmDialogConfirmed() {
_screenState.setValue { old -> old.copy(showConfirmDialog = false) }
createChat() createChat()
} }
override fun onNavigatedBack() { private fun fetchAccountUser() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch {
isChatCreated.emit(null) accountUser = getLocalUserByIdUseCase.proceed(UserConfig.userId)
if (accountUser != null) {
_finalChatTitle.setValue { accountUser?.firstName.orEmpty() }
}
} }
} }
private fun loadFriends( private fun refreshFinalTitle() {
if (screenState.value.chatTitle.trim().isNotEmpty()) {
_finalChatTitle.setValue { screenState.value.chatTitle.trim() }
} else {
val accountAsFriend = accountUser?.asPresentation(useContactNames)
val accountList = accountAsFriend?.let(::listOf) ?: emptyList()
val selectedFriends = screenState.value.selectedFriendsIds
.take(3)
.takeIf { it.isNotEmpty() }
?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } }
val finalTitle =
(accountList + selectedFriends.orEmpty()).joinToString(transform = UiFriend::firstName)
.plus(if (screenState.value.selectedFriendsIds.size > 3) ", ..." else "")
_finalChatTitle.setValue { finalTitle }
}
}
private fun fetchUsers(
offset: Int = currentOffset.value offset: Int = currentOffset.value
) { ) {
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
@@ -121,13 +157,13 @@ class CreateChatViewModelImpl(
error = ::handleError, error = ::handleError,
success = { response -> success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient } _canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient && val paginationExhausted = !itemsCountSufficient &&
screenState.value.friends.isNotEmpty() screenState.value.friends.isNotEmpty()
val imagesToPreload = val imagesToPreload =
response.mapNotNull { it.photo100.takeIf { !it.isNullOrEmpty() } } response.mapNotNull { it.photo100.takeIf { p -> !p.isNullOrEmpty() } }
imagesToPreload.forEach { url -> imagesToPreload.forEach { url ->
imageLoader.enqueue( imageLoader.enqueue(
@@ -147,11 +183,11 @@ class CreateChatViewModelImpl(
isPaginationExhausted = paginationExhausted isPaginationExhausted = paginationExhausted
) )
if (offset == 0) { if (offset == 0) {
screenState.setValue { _screenState.setValue {
newState.copy(friends = loadedFriends) newState.copy(friends = loadedFriends)
} }
} else { } else {
screenState.setValue { _screenState.setValue {
newState.copy( newState.copy(
friends = newState.friends.plus(loadedFriends) friends = newState.friends.plus(loadedFriends)
) )
@@ -160,7 +196,7 @@ class CreateChatViewModelImpl(
} }
) )
screenState.setValue { old -> _screenState.setValue { old ->
old.copy( old.copy(
isLoading = offset == 0 && state.isLoading(), isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading() isPaginating = offset > 0 && state.isLoading()
@@ -171,27 +207,19 @@ class CreateChatViewModelImpl(
private fun createChat() { private fun createChat() {
viewModelScope.launch { viewModelScope.launch {
val title = screenState.value.chatTitle.takeUnless(String::isBlank)
val accountAsFriend =
getLocalUserByIdUseCase.proceed(UserConfig.userId)?.asPresentation(useContactNames)
val accountList = accountAsFriend?.let(::listOf) ?: emptyList()
val selectedFriends = screenState.value.selectedFriendsIds val selectedFriends = screenState.value.selectedFriendsIds
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } } ?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } }
messagesUseCase.createChat( messagesUseCase.createChat(
userIds = selectedFriends?.map { it.userId }, userIds = selectedFriends?.map { it.userId },
title = title title = finalChatTitle.value
?: (accountList + selectedFriends.orEmpty()).joinToString(transform = UiFriend::firstName)
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = ::handleError, error = ::handleError,
success = { response -> success = { response ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isChatCreated.emit(2_000_000_000 + response) _isChatCreated.emit(2_000_000_000 + response)
} }
} }
) )
@@ -204,11 +232,11 @@ class CreateChatViewModelImpl(
is State.Error.ApiError -> { is State.Error.ApiError -> {
when (error.errorCode) { when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> { VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired } _baseError.setValue { BaseError.SessionExpired }
} }
else -> { else -> {
baseError.setValue { _baseError.setValue {
BaseError.SimpleError(message = error.errorMessage) BaseError.SimpleError(message = error.errorMessage)
} }
} }
@@ -216,19 +244,19 @@ class CreateChatViewModelImpl(
} }
State.Error.ConnectionError -> { State.Error.ConnectionError -> {
baseError.setValue { _baseError.setValue {
BaseError.SimpleError(message = "Connection error") BaseError.SimpleError(message = "Connection error")
} }
} }
State.Error.InternalError -> { State.Error.InternalError -> {
baseError.setValue { _baseError.setValue {
BaseError.SimpleError(message = "Internal error") BaseError.SimpleError(message = "Internal error")
} }
} }
State.Error.UnknownError -> { State.Error.UnknownError -> {
baseError.setValue { _baseError.setValue {
BaseError.SimpleError(message = "Unknown error") BaseError.SimpleError(message = "Unknown error")
} }
} }
@@ -1,9 +1,9 @@
package dev.meloda.fast.conversations.di package dev.meloda.fast.conversations.di
import dev.meloda.fast.conversations.CreateChatViewModelImpl import dev.meloda.fast.conversations.CreateChatViewModel
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val createChatModule = module { val createChatModule = module {
viewModelOf(::CreateChatViewModelImpl) viewModelOf(::CreateChatViewModel)
} }
@@ -10,7 +10,8 @@ data class CreateChatScreenState(
val isPaginationExhausted: Boolean, val isPaginationExhausted: Boolean,
val friends: List<UiFriend>, val friends: List<UiFriend>,
val selectedFriendsIds: List<Long>, val selectedFriendsIds: List<Long>,
val chatTitle: String val chatTitle: String,
val showConfirmDialog: Boolean
) { ) {
companion object { companion object {
val EMPTY: CreateChatScreenState = CreateChatScreenState( val EMPTY: CreateChatScreenState = CreateChatScreenState(
@@ -19,7 +20,8 @@ data class CreateChatScreenState(
isPaginationExhausted = false, isPaginationExhausted = false,
friends = emptyList(), friends = emptyList(),
selectedFriendsIds = emptyList(), selectedFriendsIds = emptyList(),
chatTitle = "" chatTitle = "",
showConfirmDialog = false
) )
} }
} }
@@ -6,7 +6,6 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import dev.meloda.fast.conversations.CreateChatViewModel import dev.meloda.fast.conversations.CreateChatViewModel
import dev.meloda.fast.conversations.CreateChatViewModelImpl
import dev.meloda.fast.conversations.presentation.CreateChatRoute import dev.meloda.fast.conversations.presentation.CreateChatRoute
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@@ -20,7 +19,7 @@ fun NavGraphBuilder.createChatScreen(
) { ) {
composable<CreateChat> { composable<CreateChat> {
val context = LocalContext.current val context = LocalContext.current
val viewModel: CreateChatViewModel = koinViewModel<CreateChatViewModelImpl>( val viewModel: CreateChatViewModel = koinViewModel(
viewModelStoreOwner = context as AppCompatActivity viewModelStoreOwner = context as AppCompatActivity
) )
@@ -64,6 +64,7 @@ import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.FullScreenContainedLoader
import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.components.IconButton
import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.components.VkErrorView
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
@@ -89,6 +90,27 @@ fun CreateChatRoute(
} }
} }
if (screenState.showConfirmDialog) {
MaterialDialog(
onDismissRequest = viewModel::onConfirmDialogDismissed,
title = stringResource(R.string.confirm),
text = when {
screenState.selectedFriendsIds.isEmpty() -> stringResource(
R.string.confirm_chat_create_empty_with_title,
viewModel.finalChatTitle.value
)
else -> stringResource(
R.string.confirm_chat_create_with_title,
viewModel.finalChatTitle.value
)
},
confirmAction = viewModel::onConfirmDialogConfirmed,
confirmText = stringResource(R.string.action_create),
cancelText = stringResource(R.string.cancel)
)
}
CreateChatScreen( CreateChatScreen(
screenState = screenState, screenState = screenState,
baseError = baseError, baseError = baseError,