refactor default alerts

This commit is contained in:
2024-07-15 06:02:25 +03:00
parent 654f47bb94
commit eaf609c475
10 changed files with 366 additions and 235 deletions
@@ -3,7 +3,13 @@ package com.meloda.app.fast.designsystem
import android.content.res.Configuration import android.content.res.Configuration
import android.view.KeyEvent import android.view.KeyEvent
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -77,3 +83,21 @@ fun Modifier.handleEnterKey(
action.invoke() action.invoke()
} else false } else false
} }
@Composable
fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
@@ -37,6 +37,208 @@ import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.parseString import com.meloda.app.fast.common.parseString
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MaterialDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
confirmText: String? = null,
confirmAction: (() -> Unit)? = null,
cancelText: String? = null,
cancelAction: (() -> Unit)? = null,
neutralText: String? = null,
neutralAction: (() -> Unit)? = null,
title: String? = null,
text: String? = null,
itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None,
items: ImmutableList<String> = ImmutableList.empty(),
preSelectedItems: ImmutableList<Int> = ImmutableList.empty(),
onItemClick: ((index: Int) -> Unit)? = null,
properties: DialogProperties = DialogProperties(),
actionInvokeDismiss: ActionInvokeDismiss = ActionInvokeDismiss.IfNoAction,
customContent: (ColumnScope.() -> Unit)? = null
) {
var alertItems by remember {
mutableStateOf(
items.mapIndexed { index, title ->
DialogItem(
title,
preSelectedItems.contains(index)
)
}
)
}
BasicAlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier,
properties = properties
) {
val scrollState = rememberScrollState()
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } }
Surface(
modifier = Modifier.fillMaxWidth(),
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(bottom = 10.dp)) {
if (title != null) {
Spacer(modifier = Modifier.height(20.dp))
Row {
Spacer(modifier = Modifier.width(24.dp))
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.width(20.dp))
}
}
if (canScrollBackward) {
HorizontalDivider(modifier = Modifier.fillMaxWidth())
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = false)
.verticalScroll(scrollState)
) {
Spacer(modifier = Modifier.height(8.dp))
if (text != null && title == null) {
Spacer(modifier = Modifier.height(20.dp))
}
if (text != null) {
Row {
Spacer(modifier = Modifier.width(24.dp))
Text(
modifier = Modifier.weight(1f),
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(20.dp))
}
}
Spacer(modifier = Modifier.height(8.dp))
if (alertItems.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
AlertItems(
selectionType = itemsSelectionType,
items = alertItems,
onItemClick = { index ->
onItemClick?.invoke(index)
if (itemsSelectionType == ItemsSelectionType.None) {
onDismissRequest.invoke()
} else {
val newItems =
alertItems.mapIndexed { itemIndex, item ->
item.copy(isSelected = itemIndex == index)
}
alertItems = newItems
}
},
onItemCheckedChanged = { index ->
val newItems = alertItems.toMutableList()
val oldItem = newItems[index]
newItems[index] =
oldItem.copy(isSelected = !oldItem.isSelected)
alertItems = newItems.toImmutableList()
}
)
Spacer(modifier = Modifier.height(10.dp))
} else {
if (customContent != null) {
Spacer(modifier = Modifier.height(4.dp))
customContent.invoke(this)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
if (canScrollForward) {
HorizontalDivider(modifier = Modifier.fillMaxWidth())
}
Row {
Spacer(modifier = Modifier.width(20.dp))
if (neutralText != null) {
TextButton(
onClick = {
neutralAction?.invoke() ?: kotlin.run {
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
onDismissRequest()
}
}
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
onDismissRequest()
}
}
) {
Text(text = neutralText)
}
}
Spacer(modifier = Modifier.weight(1f))
if (cancelText != null) {
TextButton(
onClick = {
cancelAction?.invoke() ?: kotlin.run {
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
onDismissRequest()
}
}
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
onDismissRequest()
}
}
) {
Text(text = cancelText)
}
}
Spacer(modifier = Modifier.width(2.dp))
if (confirmText != null) {
TextButton(
onClick = {
confirmAction?.invoke() ?: kotlin.run {
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
onDismissRequest()
}
}
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
onDismissRequest()
}
}
) {
Text(text = confirmText)
}
}
Spacer(modifier = Modifier.width(20.dp))
}
}
}
}
}
// TODO: 08.04.2023, Danil Nikolaev: refactor this // TODO: 08.04.2023, Danil Nikolaev: refactor this
@Deprecated("need refactoring") @Deprecated("need refactoring")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -84,7 +286,7 @@ fun MaterialDialog(
) )
} }
AlertAnimation(visible = isVisible) { if (isVisible) {
BasicAlertDialog( BasicAlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = properties properties = properties
@@ -250,21 +452,6 @@ fun MaterialDialog(
} }
} }
@Composable
fun AlertAnimation(
visible: Boolean,
content: @Composable () -> Unit
) {
if (visible) content()
// AnimatedVisibility(
// visible = visible,
// enter = fadeIn(animationSpec = tween(400)) +
// scaleIn(animationSpec = tween(400)),
// exit = fadeOut(animationSpec = tween(150)),
// content = content
// )
}
@Composable @Composable
private fun AlertItems( private fun AlertItems(
selectionType: ItemsSelectionType, selectionType: ItemsSelectionType,
@@ -324,8 +511,14 @@ data class DialogItem(
val isSelected: Boolean val isSelected: Boolean
) )
sealed interface ItemsSelectionType { sealed class ActionInvokeDismiss {
data object Single : ItemsSelectionType data object Never : ActionInvokeDismiss()
data object Multi : ItemsSelectionType data object IfNoAction : ActionInvokeDismiss()
data object None : ItemsSelectionType data object Always : ActionInvokeDismiss()
}
sealed class ItemsSelectionType {
data object Single : ItemsSelectionType()
data object Multi : ItemsSelectionType()
data object None : ItemsSelectionType()
} }
@@ -259,4 +259,7 @@
<string name="notification_channel_long_polling_service_name">Message update service</string> <string name="notification_channel_long_polling_service_name">Message update service</string>
<string name="notification_channel_long_polling_service_description">Message update service notifications</string> <string name="notification_channel_long_polling_service_description">Message update service notifications</string>
</resources> <string name="warning_confirmation">Confirmation</string>
<string name="captcha_exit_warning">Are you sure? Captcha process will be cancelled</string>
<string name="validation_exit_warning">Are you sure? Validation process will be cancelled</string>
</resources>
@@ -41,6 +41,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -50,7 +51,7 @@ import coil.request.ImageRequest
import com.meloda.app.fast.auth.captcha.CaptchaViewModel import com.meloda.app.fast.auth.captcha.CaptchaViewModel
import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl
import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState
import com.meloda.app.fast.common.UiText import com.meloda.app.fast.designsystem.ActionInvokeDismiss
import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.TextFieldErrorText import com.meloda.app.fast.designsystem.TextFieldErrorText
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -89,7 +90,7 @@ fun CaptchaScreen(
onTextFieldDoneAction: () -> Unit = {}, onTextFieldDoneAction: () -> Unit = {},
onDoneButtonClicked: () -> Unit = {} onDoneButtonClicked: () -> Unit = {}
) { ) {
var confirmedExit by rememberSaveable { var confirmedExit by remember {
mutableStateOf(false) mutableStateOf(false)
} }
@@ -111,14 +112,13 @@ fun CaptchaScreen(
if (showExitAlert) { if (showExitAlert) {
MaterialDialog( MaterialDialog(
onDismissAction = { showExitAlert = false }, onDismissRequest = { showExitAlert = false },
title = UiText.Simple("Confirmation"), title = stringResource(id = UiR.string.warning_confirmation),
text = UiText.Simple("Are you sure? Captcha process will be cancelled."), text = stringResource(id = UiR.string.captcha_exit_warning),
confirmText = UiText.Resource(UiR.string.yes), confirmAction = { confirmedExit = true },
confirmAction = { confirmText = stringResource(id = UiR.string.yes),
confirmedExit = true cancelText = stringResource(id = UiR.string.no),
}, actionInvokeDismiss = ActionInvokeDismiss.Always
cancelText = UiText.Resource(UiR.string.no)
) )
} }
@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
@@ -46,7 +47,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.auth.validation.ValidationViewModel import com.meloda.app.fast.auth.validation.ValidationViewModel
import com.meloda.app.fast.auth.validation.ValidationViewModelImpl import com.meloda.app.fast.auth.validation.ValidationViewModelImpl
import com.meloda.app.fast.auth.validation.model.ValidationScreenState import com.meloda.app.fast.auth.validation.model.ValidationScreenState
import com.meloda.app.fast.common.UiText import com.meloda.app.fast.designsystem.ActionInvokeDismiss
import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.TextFieldErrorText import com.meloda.app.fast.designsystem.TextFieldErrorText
import com.meloda.app.fast.designsystem.getString import com.meloda.app.fast.designsystem.getString
@@ -96,7 +97,7 @@ fun ValidationScreen(
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var confirmedExit by rememberSaveable { var confirmedExit by remember {
mutableStateOf(false) mutableStateOf(false)
} }
@@ -118,14 +119,13 @@ fun ValidationScreen(
if (showExitAlert) { if (showExitAlert) {
MaterialDialog( MaterialDialog(
onDismissAction = { showExitAlert = false }, onDismissRequest = { showExitAlert = false },
title = UiText.Simple("Confirmation"), title = stringResource(id = UiR.string.warning_confirmation),
text = UiText.Simple("Are you sure? Authorization process will be cancelled."), text = stringResource(id = UiR.string.validation_exit_warning),
confirmText = UiText.Resource(UiR.string.yes), confirmAction = { confirmedExit = true },
confirmAction = { confirmText = stringResource(id = UiR.string.yes),
confirmedExit = true cancelText = stringResource(id = UiR.string.no),
}, actionInvokeDismiss = ActionInvokeDismiss.Always
cancelText = UiText.Resource(UiR.string.no)
) )
} }
@@ -47,8 +47,7 @@ interface ConversationsViewModel {
fun onPaginationConditionsMet() fun onPaginationConditionsMet()
fun onDeleteDialogDismissed() fun onDeleteDialogDismissed()
fun onDeleteDialogPositiveClick()
fun onDeleteDialogPositiveClick(conversationId: Int)
fun onRefresh() fun onRefresh()
@@ -56,7 +55,8 @@ interface ConversationsViewModel {
fun onConversationItemLongClick(conversation: UiConversation) fun onConversationItemLongClick(conversation: UiConversation)
fun onPinDialogDismissed() fun onPinDialogDismissed()
fun onPinDialogPositiveClick(conversation: UiConversation) fun onPinDialogPositiveClick()
fun onOptionClicked(conversation: UiConversation, option: ConversationOption) fun onOptionClicked(conversation: UiConversation, option: ConversationOption)
fun onErrorConsumed() fun onErrorConsumed()
@@ -104,9 +104,11 @@ class ConversationsViewModelImpl(
emitShowOptions { old -> old.copy(showDeleteDialog = null) } emitShowOptions { old -> old.copy(showDeleteDialog = null) }
} }
override fun onDeleteDialogPositiveClick(conversationId: Int) { override fun onDeleteDialogPositiveClick() {
val conversationId = screenState.value.showOptions.showDeleteDialog ?: return
deleteConversation(conversationId) deleteConversation(conversationId)
hideOptions(conversationId) hideOptions(conversationId)
onDeleteDialogDismissed()
} }
override fun onRefresh() { override fun onRefresh() {
@@ -168,9 +170,11 @@ class ConversationsViewModelImpl(
emitShowOptions { old -> old.copy(showPinDialog = null) } emitShowOptions { old -> old.copy(showPinDialog = null) }
} }
override fun onPinDialogPositiveClick(conversation: UiConversation) { override fun onPinDialogPositiveClick() {
val conversation = screenState.value.showOptions.showPinDialog ?: return
pinConversation(conversation.id, !conversation.isPinned) pinConversation(conversation.id, !conversation.isPinned)
hideOptions(conversation.id) hideOptions(conversation.id)
onPinDialogDismissed()
} }
override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) { override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) {
@@ -222,64 +226,65 @@ class ConversationsViewModelImpl(
private fun loadConversations( private fun loadConversations(
offset: Int = currentOffset.value offset: Int = currentOffset.value
) { ) {
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state -> conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
state.processState( .listenValue { state ->
error = { error -> state.processState(
if (error is State.Error.ApiError) { error = { error ->
when (error.errorCode) { if (error is State.Error.ApiError) {
VkErrorCodes.UserAuthorizationFailed -> { when (error.errorCode) {
baseError.setValue { BaseError.SessionExpired } VkErrorCodes.UserAuthorizationFailed -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> Unit
} }
else -> Unit
} }
} },
}, 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.conversations.isNotEmpty() screenState.value.conversations.isNotEmpty()
imagesToPreload.setValue { imagesToPreload.setValue {
response.mapNotNull { it.extractAvatar().extractUrl() } response.mapNotNull { it.extractAvatar().extractUrl() }
}
conversationsUseCase.storeConversations(response)
val loadedConversations = response.map {
it.asPresentation(
resources,
userSettings.useContactNames.value
)
}
val newState = screenState.value.copy(
isPaginationExhausted = paginationExhausted
)
if (offset == 0) {
conversations.emit(response)
screenState.setValue {
newState.copy(conversations = loadedConversations)
} }
} else { conversationsUseCase.storeConversations(response)
conversations.emit(conversations.value.plus(response))
screenState.setValue { val loadedConversations = response.map {
newState.copy( it.asPresentation(
conversations = newState.conversations.plus(loadedConversations) resources,
userSettings.useContactNames.value
) )
} }
}
}
)
screenState.setValue { old -> val newState = screenState.value.copy(
old.copy( isPaginationExhausted = paginationExhausted
isLoading = offset == 0 && state.isLoading(), )
isPaginating = offset > 0 && state.isLoading() if (offset == 0) {
conversations.emit(response)
screenState.setValue {
newState.copy(conversations = loadedConversations)
}
} else {
conversations.emit(conversations.value.plus(response))
screenState.setValue {
newState.copy(
conversations = newState.conversations.plus(loadedConversations)
)
}
}
}
) )
screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
} }
}
} }
private fun deleteConversation(peerId: Int) { private fun deleteConversation(peerId: Int) {
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
@@ -68,7 +67,6 @@ import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.conversations.ConversationsViewModel import com.meloda.app.fast.conversations.ConversationsViewModel
import com.meloda.app.fast.conversations.ConversationsViewModelImpl import com.meloda.app.fast.conversations.ConversationsViewModelImpl
import com.meloda.app.fast.conversations.model.ConversationOption import com.meloda.app.fast.conversations.model.ConversationOption
@@ -79,6 +77,7 @@ import com.meloda.app.fast.designsystem.LocalHazeState
import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.components.FullScreenLoader import com.meloda.app.fast.designsystem.components.FullScreenLoader
import com.meloda.app.fast.designsystem.isScrollingUp
import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.ui.ErrorView import com.meloda.app.fast.ui.ErrorView
import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.haze
@@ -398,72 +397,30 @@ fun HandleDialogs(
val showOptions = screenState.showOptions val showOptions = screenState.showOptions
if (showOptions.showDeleteDialog != null) { if (showOptions.showDeleteDialog != null) {
val conversationId = showOptions.showDeleteDialog MaterialDialog(
DeleteDialog( onDismissRequest = viewModel::onDeleteDialogDismissed,
conversationId = conversationId, title = stringResource(id = UiR.string.confirm_delete_conversation),
viewModel = viewModel confirmAction = viewModel::onDeleteDialogPositiveClick,
confirmText = stringResource(id = UiR.string.action_delete),
cancelText = stringResource(id = UiR.string.cancel)
) )
} }
showOptions.showPinDialog?.let { conversation -> if (showOptions.showPinDialog != null) {
PinDialog( val conversation = showOptions.showPinDialog
conversation = conversation,
viewModel = viewModel MaterialDialog(
onDismissRequest = viewModel::onPinDialogDismissed,
title = stringResource(
id = if (conversation.isPinned) UiR.string.confirm_unpin_conversation
else UiR.string.confirm_pin_conversation
),
confirmAction = viewModel::onPinDialogPositiveClick,
confirmText = stringResource(
id = if (conversation.isPinned) UiR.string.action_unpin
else UiR.string.action_pin
),
cancelText = stringResource(id = UiR.string.cancel)
) )
} }
} }
@Composable
fun DeleteDialog(
conversationId: Int,
viewModel: ConversationsViewModel
) {
MaterialDialog(
title = UiText.Resource(UiR.string.confirm_delete_conversation),
confirmText = UiText.Resource(UiR.string.action_delete),
confirmAction = { viewModel.onDeleteDialogPositiveClick(conversationId) },
cancelText = UiText.Resource(UiR.string.cancel),
onDismissAction = viewModel::onDeleteDialogDismissed
)
}
@Composable
fun PinDialog(
conversation: UiConversation,
viewModel: ConversationsViewModel
) {
MaterialDialog(
title = UiText.Resource(
if (conversation.isPinned) UiR.string.confirm_unpin_conversation
else UiR.string.confirm_pin_conversation
),
confirmText = UiText.Resource(
if (conversation.isPinned) UiR.string.action_unpin
else UiR.string.action_pin
),
confirmAction = {
viewModel.onPinDialogPositiveClick(conversation)
},
cancelText = UiText.Resource(UiR.string.cancel),
onDismissAction = viewModel::onPinDialogDismissed
)
}
@Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
@@ -32,15 +32,10 @@ interface SettingsViewModel {
val isLongPollBackgroundEnabled: StateFlow<Boolean?> val isLongPollBackgroundEnabled: StateFlow<Boolean?>
fun onLogOutAlertDismissed() fun onLogOutAlertDismissed()
fun onPerformCrashAlertDismissed()
fun onPerformCrashPositiveButtonClicked()
fun onLogOutAlertPositiveClick() fun onLogOutAlertPositiveClick()
fun onLongPollingAlertPositiveClicked() fun onPerformCrashAlertDismissed()
fun onLongPollingAlertDismissed() fun onPerformCrashPositiveButtonClicked()
fun onSettingsItemClicked(key: String) fun onSettingsItemClicked(key: String)
fun onSettingsItemLongClicked(key: String) fun onSettingsItemLongClicked(key: String)
@@ -68,14 +63,6 @@ class SettingsViewModelImpl(
emitShowOptions { old -> old.copy(showLogOut = false) } emitShowOptions { old -> old.copy(showLogOut = false) }
} }
override fun onPerformCrashAlertDismissed() {
emitShowOptions { old -> old.copy(showPerformCrash = false) }
}
override fun onPerformCrashPositiveButtonClicked() {
throw Exception("Test exception")
}
override fun onLogOutAlertPositiveClick() { override fun onLogOutAlertPositiveClick() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountsRepository.storeAccounts( accountsRepository.storeAccounts(
@@ -93,18 +80,12 @@ class SettingsViewModelImpl(
} }
} }
override fun onLongPollingAlertPositiveClicked() { override fun onPerformCrashAlertDismissed() {
screenState.setValue { old -> old.copy(isNeedToRequestNotificationPermission = true) } emitShowOptions { old -> old.copy(showPerformCrash = false) }
} }
override fun onLongPollingAlertDismissed() { override fun onPerformCrashPositiveButtonClicked() {
screenState.setValue { old -> throw Exception("Test exception")
old.copy(
showOptions = old.showOptions.copy(
showLongPollNotifications = false
)
)
}
} }
override fun onSettingsItemClicked(key: String) { override fun onSettingsItemClicked(key: String) {
@@ -3,14 +3,12 @@ package com.meloda.app.fast.settings.model
data class SettingsShowOptions( data class SettingsShowOptions(
val showLogOut: Boolean, val showLogOut: Boolean,
val showPerformCrash: Boolean, val showPerformCrash: Boolean,
val showLongPollNotifications: Boolean
) { ) {
companion object { companion object {
val EMPTY: SettingsShowOptions = SettingsShowOptions( val EMPTY: SettingsShowOptions = SettingsShowOptions(
showLogOut = false, showLogOut = false,
showPerformCrash = false, showPerformCrash = false,
showLongPollNotifications = false
) )
} }
} }
@@ -34,11 +34,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.datastore.SettingsKeys import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.isUsingDarkMode import com.meloda.app.fast.datastore.isUsingDarkMode
import com.meloda.app.fast.designsystem.ActionInvokeDismiss
import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.settings.HapticType import com.meloda.app.fast.settings.HapticType
@@ -118,8 +118,6 @@ fun SettingsRoute(
onLogOutButtonClicked() onLogOutButtonClicked()
}, },
logoutDismissed = viewModel::onLogOutAlertDismissed, logoutDismissed = viewModel::onLogOutAlertDismissed,
longPollingPositiveClick = viewModel::onLongPollingAlertPositiveClicked,
longPollingDismissed = viewModel::onLongPollingAlertDismissed,
screenState = screenState screenState = screenState
) )
} }
@@ -273,8 +271,6 @@ fun HandlePopups(
performCrashDismissed: () -> Unit, performCrashDismissed: () -> Unit,
logoutPositiveClick: () -> Unit, logoutPositiveClick: () -> Unit,
logoutDismissed: () -> Unit, logoutDismissed: () -> Unit,
longPollingPositiveClick: () -> Unit,
longPollingDismissed: () -> Unit,
screenState: SettingsScreenState screenState: SettingsScreenState
) { ) {
val showOptions = screenState.showOptions val showOptions = screenState.showOptions
@@ -290,12 +286,6 @@ fun HandlePopups(
dismiss = logoutDismissed, dismiss = logoutDismissed,
show = showOptions.showLogOut show = showOptions.showLogOut
) )
LongPollingNotificationsPermission(
positiveClick = longPollingPositiveClick,
dismiss = longPollingDismissed,
show = showOptions.showLongPollNotifications
)
} }
@Composable @Composable
@@ -306,12 +296,13 @@ fun PerformCrashDialog(
) { ) {
if (show) { if (show) {
MaterialDialog( MaterialDialog(
title = UiText.Simple("Perform Crash"), onDismissRequest = dismiss,
text = UiText.Simple("App will be crashed. Are you sure?"), title = "Perform crash",
confirmText = UiText.Resource(UiR.string.yes), text = "App will be crashed. Are you sure?",
confirmAction = positiveClick, confirmAction = positiveClick,
cancelText = UiText.Resource(UiR.string.cancel), confirmText = stringResource(id = UiR.string.yes),
onDismissAction = dismiss cancelText = stringResource(id = UiR.string.cancel),
actionInvokeDismiss = ActionInvokeDismiss.Always
) )
} }
} }
@@ -325,41 +316,20 @@ fun LogOutDialog(
if (show) { if (show) {
val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY val isEasterEgg = UserConfig.userId == SettingsKeys.ID_DMITRY
val title = UiText.Resource(
if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.sign_out_confirm_title
)
val positiveText = UiText.Resource(
if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.action_sign_out
)
MaterialDialog( MaterialDialog(
title = title, onDismissRequest = dismiss,
text = UiText.Resource(UiR.string.sign_out_confirm), title = stringResource(
confirmText = positiveText, id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
else UiR.string.sign_out_confirm_title
),
text = stringResource(id = UiR.string.sign_out_confirm),
confirmAction = positiveClick, confirmAction = positiveClick,
cancelText = UiText.Resource(UiR.string.cancel), confirmText = stringResource(
onDismissAction = dismiss id = if (isEasterEgg) UiR.string.easter_egg_log_out_dmitry
) else UiR.string.action_sign_out
} ),
} cancelText = stringResource(id = UiR.string.no),
actionInvokeDismiss = ActionInvokeDismiss.Always
@Composable
fun LongPollingNotificationsPermission(
positiveClick: () -> Unit,
dismiss: () -> Unit,
show: Boolean
) {
if (show) {
MaterialDialog(
title = UiText.Resource(UiR.string.warning),
text = UiText.Simple("Long polling in background required notifications permission on Android 13 and up"),
confirmText = UiText.Simple("Grant"),
confirmAction = positiveClick,
cancelText = UiText.Resource(UiR.string.cancel),
onDismissAction = dismiss
) )
} }
} }