diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt new file mode 100644 index 000000000..18f3455e3 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt @@ -0,0 +1,106 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import com.sd.lib.compose.wheel_picker.* + +@Composable +actual fun CustomTimePicker( + selection: MutableState, + timeUnitsLimits: List +) { + fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List { + val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) + val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() + return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) + } + + val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) + val selectedUnit: MutableState = remember { mutableStateOf(unit) } + val selectedDuration = remember { mutableStateOf(duration) } + val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } + val isTriggered = remember { mutableStateOf(false) } + + LaunchedEffect(selectedUnit.value) { + // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue + // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), + // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition + if (isTriggered.value) { + val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue + if (maxValue != null && selectedDuration.value > maxValue) { + selectedDuration.value = maxValue + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + } else { + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + } else { + isTriggered.value = true + } + } + + LaunchedEffect(selectedDuration.value) { + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(0.dp) + ) { + Column(Modifier.weight(1f)) { + val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) + FVerticalWheelPicker( + count = selectedUnitValues.value.count(), + state = durationPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + selectedUnitValues.value[index].toString(), + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(durationPickerState) { + snapshotFlow { durationPickerState.currentIndex } + .collect { + selectedDuration.value = selectedUnitValues.value[it] + } + } + } + Column(Modifier.weight(1f)) { + val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) + FVerticalWheelPicker( + count = timeUnitsLimits.count(), + state = unitPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + timeUnitsLimits[index].timeUnit.text, + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(unitPickerState) { + snapshotFlow { unitPickerState.currentIndex } + .collect { + selectedUnit.value = timeUnitsLimits[it].timeUnit + } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index f1079d2f5..456e2a538 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -59,14 +59,6 @@ fun SendMsgView( ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - if (showCustomDisappearingMessageDialog.value) { - CustomDisappearingMessageDialog( - sendMessage = sendMessage, - setShowDialog = { showCustomDisappearingMessageDialog.value = it }, - customDisappearingMessageTimePref = customDisappearingMessageTimePref - ) - } - Box(Modifier.padding(vertical = 8.dp)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } @@ -203,6 +195,11 @@ fun SendMsgView( DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } + CustomDisappearingMessageDialog( + showCustomDisappearingMessageDialog, + sendMessage = sendMessage, + customDisappearingMessageTimePref = customDisappearingMessageTimePref + ) } else { SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) } @@ -220,93 +217,43 @@ expect fun VoiceButtonWithoutPermissionByPlatform() @Composable private fun CustomDisappearingMessageDialog( + showMenu: MutableState, sendMessage: (Int?) -> Unit, - setShowDialog: (Boolean) -> Unit, customDisappearingMessageTimePref: SharedPreference? ) { - val showCustomTimePicker = remember { mutableStateOf(false) } - - if (showCustomTimePicker.value) { - val selectedDisappearingMessageTime = remember { - mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) - } - CustomTimePickerDialog( - selectedDisappearingMessageTime, - title = generalGetString(MR.strings.delete_after), - confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), - confirmButtonAction = { ttl -> - sendMessage(ttl) - customDisappearingMessageTimePref?.set?.invoke(ttl) - setShowDialog(false) - }, - cancel = { setShowDialog(false) } + DefaultDropdownMenu(showMenu) { + Text( + generalGetString(MR.strings.send_disappearing_message), + Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + color = MaterialTheme.colors.secondary ) - } else { - @Composable - fun ChoiceButton( - text: String, - onClick: () -> Unit - ) { - TextButton(onClick) { - Text( - text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } - DefaultDialog(onDismissRequest = { setShowDialog(false) }) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)), - contentColor = LocalContentColor.current - ) { - Box( - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - generalGetString(MR.strings.send_disappearing_message), - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { setShowDialog(false) } - ) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { - sendMessage(30) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) { - sendMessage(60) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { - sendMessage(300) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) { - showCustomTimePicker.value = true - } - } - } - } + ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { + sendMessage(30) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) { + sendMessage(60) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { + sendMessage(300) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) { + showMenu.value = false + val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) + showCustomTimePickerDialog( + selectedDisappearingMessageTime, + title = generalGetString(MR.strings.delete_after), + confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), + confirmButtonAction = { ttl -> + sendMessage(ttl) + customDisappearingMessageTimePref?.set?.invoke(ttl) + }, + cancel = { showMenu.value = false } + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 549f5f2f5..e0f31f65c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -651,6 +651,23 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo } } +@Composable +fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) { + val finalColor = if (color == Color.Unspecified) { + MenuTextColor + } else color + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor + ) + } +} + fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) { AlertManager.shared.showAlertDialog( title = generalGetString(cancelAction.alert.titleId), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt index f13edd618..3c44cbb4d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt @@ -1,116 +1,21 @@ package chat.simplex.common.views.helpers -import androidx.compose.foundation.clickable +import SectionItemView import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.CustomTimeUnit import chat.simplex.common.model.timeText import chat.simplex.res.MR -import com.sd.lib.compose.wheel_picker.* @Composable -fun CustomTimePicker( +expect fun CustomTimePicker( selection: MutableState, timeUnitsLimits: List = TimeUnitLimits.defaultUnitsLimits -) { - fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List { - val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) - val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() - return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) - } - - val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) - val selectedUnit: MutableState = remember { mutableStateOf(unit) } - val selectedDuration = remember { mutableStateOf(duration) } - val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } - val isTriggered = remember { mutableStateOf(false) } - - LaunchedEffect(selectedUnit.value) { - // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue - // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), - // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition - if (isTriggered.value) { - val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue - if (maxValue != null && selectedDuration.value > maxValue) { - selectedDuration.value = maxValue - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - } else { - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - } else { - isTriggered.value = true - } - } - - LaunchedEffect(selectedDuration.value) { - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - - Row( - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - horizontalArrangement = Arrangement.spacedBy(0.dp) - ) { - Column(Modifier.weight(1f)) { - val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) - FVerticalWheelPicker( - count = selectedUnitValues.value.count(), - state = durationPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - selectedUnitValues.value[index].toString(), - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(durationPickerState) { - snapshotFlow { durationPickerState.currentIndex } - .collect { - selectedDuration.value = selectedUnitValues.value[it] - } - } - } - Column(Modifier.weight(1f)) { - val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) - FVerticalWheelPicker( - count = timeUnitsLimits.count(), - state = unitPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - timeUnitsLimits[index].timeUnit.text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(unitPickerState) { - snapshotFlow { unitPickerState.currentIndex } - .collect { - selectedUnit.value = timeUnitsLimits[it].timeUnit - } - } - } - } -} +) data class TimeUnitLimits( val timeUnit: CustomTimeUnit, @@ -141,8 +46,7 @@ data class TimeUnitLimits( } } -@Composable -fun CustomTimePickerDialog( +fun showCustomTimePickerDialog( selection: MutableState, timeUnitsLimits: List = TimeUnitLimits.defaultUnitsLimits, title: String, @@ -150,53 +54,26 @@ fun CustomTimePickerDialog( confirmButtonAction: (Int) -> Unit, cancel: () -> Unit ) { - DefaultDialog(onDismissRequest = cancel) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)), - contentColor = LocalContentColor.current - ) { - Box( - contentAlignment = Alignment.Center + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + onDismissRequest = cancel + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CustomTimePicker( + selection, + timeUnitsLimits + ) + SectionItemView({ + AlertManager.shared.hideAlert() + confirmButtonAction(selection.value) + } ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - title, - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { cancel() } - ) - } - - CustomTimePicker( - selection, - timeUnitsLimits - ) - - TextButton(onClick = { confirmButtonAction(selection.value) }) { - Text( - confirmButtonText, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } + Text( + confirmButtonText, + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) } } } @@ -220,7 +97,6 @@ fun DropdownCustomTimePickerSettingRow( val dropdownSelection: MutableState = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) } val values: MutableState> = remember { mutableStateOf(getValues(selection.value)) } - val showCustomTimePicker = remember { mutableStateOf(false) } fun updateValue(selectedValue: Int?) { values.value = getValues(selectedValue) @@ -247,28 +123,22 @@ fun DropdownCustomTimePickerSettingRow( onSelected = { sel: DropdownSelection -> when (sel) { is DropdownSelection.DropdownValue -> updateValue(sel.value) - DropdownSelection.Custom -> showCustomTimePicker.value = true + DropdownSelection.Custom -> { + val selectedCustomTime = mutableStateOf(selection.value ?: 86400) + showCustomTimePickerDialog( + selectedCustomTime, + timeUnitsLimits = customPickerTimeUnitsLimits, + title = customPickerTitle, + confirmButtonText = customPickerConfirmButtonText, + confirmButtonAction = ::updateValue, + cancel = { + dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) + } + ) + } } } ) - - if (showCustomTimePicker.value) { - val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) } - CustomTimePickerDialog( - selectedCustomTime, - timeUnitsLimits = customPickerTimeUnitsLimits, - title = customPickerTitle, - confirmButtonText = customPickerConfirmButtonText, - confirmButtonAction = { time -> - updateValue(time) - showCustomTimePicker.value = false - }, - cancel = { - dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) - showCustomTimePicker.value = false - } - ) - } } private sealed class DropdownSelection { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt new file mode 100644 index 000000000..03c8e51c5 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt @@ -0,0 +1,80 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING + +@Composable +actual fun CustomTimePicker( + selection: MutableState, + timeUnitsLimits: List +) { + val unit = remember { + var res: CustomTimeUnit = CustomTimeUnit.Second + val found = timeUnitsLimits.asReversed().any { + if (selection.value >= it.minValue * it.timeUnit.toSeconds && selection.value <= it.maxValue * it.timeUnit.toSeconds) { + res = it.timeUnit + selection.value = (selection.value / it.timeUnit.toSeconds).coerceIn(it.minValue, it.maxValue) * it.timeUnit.toSeconds + true + } else { + false + } + } + if (!found) { + // If custom interval doesn't fit in any category, set it to 1 second interval + selection.value = 1 + } + mutableStateOf(res) + } + val values = remember(unit.value) { + val limit = timeUnitsLimits.first { it.timeUnit == unit.value } + val res = ArrayList>() + for (i in limit.minValue..limit.maxValue) { + val seconds = i * limit.timeUnit.toSeconds + val desc = i.toString() + res.add(seconds to desc) + } + if (res.none { it.first == selection.value }) { + // Doesn't fit into min..max, put it equal to the closest value + selection.value = selection.value.coerceIn(res.first().first, res.last().first) + //selection.value = res.last { it.first <= selection.value }.first + } + res + } + val units = remember { + val res = ArrayList>() + for (unit in timeUnitsLimits) { + res.add(unit.timeUnit to unit.timeUnit.text) + } + res + } + + Row( + Modifier.padding(bottom = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ExposedDropDownSetting( + values, + selection, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { selection.value = it } + ) + Spacer(Modifier.width(DEFAULT_PADDING)) + ExposedDropDownSetting( + units, + unit, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { + selection.value = selection.value / unit.value.toSeconds * it.toSeconds + unit.value = it + } + ) + } +}