diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt index a7799d262d..da832679a6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt @@ -186,7 +186,15 @@ private fun TimedMessagesFeatureSection( ) if (featuresAllowed.timedMessagesAllowed) { val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) } - TimedMessagesTTLPicker(ttl, onTTLUpdated) + DropdownCustomTimePickerSettingRow( + selection = ttl, + propagateExternalSelectionUpdate = true, // for Reset + label = generalGetString(R.string.delete_after), + dropdownValues = TimedMessagesPreference.ttlValues, + customPickerTitle = generalGetString(R.string.delete_after), + customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select), + onSelected = onTTLUpdated + ) } else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) { InfoRow(generalGetString(R.string.delete_after), timeText(pref.contactPreference.ttl)) } @@ -206,18 +214,6 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool } } -@Composable -fun TimedMessagesTTLPicker(selection: MutableState, onSelected: (Int?) -> Unit) { - val ttlValues = TimedMessagesPreference.ttlValues - val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value) - ExposedDropDownSettingRow( - generalGetString(R.string.delete_after), - values.map { it to timeText(it) }, - selection, - onSelected = onSelected - ) -} - private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(R.string.save_preferences_question), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 94087754f1..c976a5ab80 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -161,41 +161,51 @@ fun SendMsgView( val disabled = !cs.sendEnabled() || (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || cs.endLiveDisabled - val showSendLiveMessageMenuButton = - cs.liveMessage == null && !cs.editing && + val showDropdown = rememberSaveable { mutableStateOf(false) } + + @Composable + fun MenuItems(): List<@Composable () -> Unit> { + val menuItems = mutableListOf<@Composable () -> Unit>() + + if (cs.liveMessage == null && !cs.editing) { + if ( cs.preview !is ComposePreview.VoicePreview && cs.contextItem is ComposeContextItem.NoContextItem && sendLiveMessage != null && updateLiveMessage != null - val showSendDisappearingMessageMenuButton = - cs.liveMessage == null && !cs.editing && - timedMessageAllowed + ) { + menuItems.add { + ItemAction( + generalGetString(R.string.send_live_message), + BoltFilled, + onClick = { + startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) + showDropdown.value = false + } + ) + } + } + if (timedMessageAllowed) { + menuItems.add { + ItemAction( + generalGetString(R.string.disappearing_message), + painterResource(R.drawable.ic_timer), + onClick = { + showCustomDisappearingMessageDialog.value = true + showDropdown.value = false + } + ) + } + } + } - if (showSendLiveMessageMenuButton || showSendDisappearingMessageMenuButton) { - val showDropdown = rememberSaveable { mutableStateOf(false) } + return menuItems + } + + val menuItems = MenuItems() + if (menuItems.isNotEmpty()) { SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true } - DefaultDropdownMenu( - showDropdown, - ) { - if (showSendLiveMessageMenuButton && sendLiveMessage != null && updateLiveMessage != null) { - ItemAction( - generalGetString(R.string.send_live_message), - BoltFilled, - onClick = { - startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) - showDropdown.value = false - } - ) - } - if (showSendDisappearingMessageMenuButton) { - ItemAction( - generalGetString(R.string.disappearing_message), - painterResource(R.drawable.ic_timer), - onClick = { - showCustomDisappearingMessageDialog.value = true - showDropdown.value = false - } - ) - } + DefaultDropdownMenu(showDropdown) { + menuItems.forEach { composable -> composable() } } } else { SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt index e99a0883ad..06692e41b5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.TimedMessagesTTLPicker import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon @@ -141,7 +140,15 @@ private fun FeatureSection( } if (timedOn) { val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) } - TimedMessagesTTLPicker(ttl, onTTLUpdated) + DropdownCustomTimePickerSettingRow( + selection = ttl, + propagateExternalSelectionUpdate = true, // for Reset + label = generalGetString(R.string.delete_after), + dropdownValues = TimedMessagesPreference.ttlValues.filterNotNull(), // TODO in 5.2 - allow "off" + customPickerTitle = generalGetString(R.string.delete_after), + customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select), + onSelected = onTTLUpdated + ) } } else { InfoRow( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt index 7fa045dd15..335d633ae9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import chat.simplex.app.R -import chat.simplex.app.model.CustomTimeUnit +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.DEFAULT_PADDING import com.sd.lib.compose.wheel_picker.* @@ -24,23 +24,31 @@ fun CustomTimePicker( ) { fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List { val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) - val unitValues = (unitLimits.minValue..unitLimits.maxValue).toList() - return unitValues + if (unitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) + 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) { - 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) + // 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 { - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - selection.value = selectedUnit.value.toSeconds * selectedDuration.value + isTriggered.value = true } } @@ -191,3 +199,92 @@ fun CustomTimePickerDialog( } } } + +@Composable +fun DropdownCustomTimePickerSettingRow( + selection: MutableState, + propagateExternalSelectionUpdate: Boolean = false, + label: String, + dropdownValues: List, + customPickerTitle: String, + customPickerConfirmButtonText: String, + customPickerTimeUnitsLimits: List = TimeUnitLimits.defaultUnitsLimits, + onSelected: (Int?) -> Unit +) { + fun getValues(selectedValue: Int?): List = + dropdownValues.map { DropdownSelection.DropdownValue(it) } + + (if (dropdownValues.contains(selectedValue)) listOf() else listOf(DropdownSelection.DropdownValue(selectedValue))) + + listOf(DropdownSelection.Custom) + + 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) + dropdownSelection.value = DropdownSelection.DropdownValue(selectedValue) + onSelected(selectedValue) + } + + if (propagateExternalSelectionUpdate) { + LaunchedEffect(selection.value) { + values.value = getValues(selection.value) + dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) + } + } + + ExposedDropDownSettingRow( + label, + values.value.map { sel: DropdownSelection -> + when (sel) { + is DropdownSelection.DropdownValue -> sel to timeText(sel.value) + DropdownSelection.Custom -> sel to generalGetString(R.string.custom_time_picker_custom) + } + }, + dropdownSelection, + onSelected = { sel: DropdownSelection -> + when (sel) { + is DropdownSelection.DropdownValue -> updateValue(sel.value) + DropdownSelection.Custom -> showCustomTimePicker.value = true + } + } + ) + + 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 { + data class DropdownValue(val value: Int?): DropdownSelection() + object Custom: DropdownSelection() + + override fun equals(other: Any?): Boolean = + other is DropdownSelection && + when (other) { + is DropdownValue -> this is DropdownValue && this.value == other.value + is Custom -> this is Custom + } + + override fun hashCode(): Int = + // DO NOT REMOVE the as? cast as it will turn them into recursive hashCode calls + // https://youtrack.jetbrains.com/issue/KT-31239 + when (this) { + is DropdownValue -> (this as? DropdownValue).hashCode() + is Custom -> (this as? Custom).hashCode() + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt index 8e59df5f4f..d7af17673a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @Composable diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 11df772d51..690e841378 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -1407,4 +1407,6 @@ days weeks months + Select + custom diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index d610dec105..fb31eec456 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -183,7 +183,7 @@ struct SendMessageView: View { .sheet(isPresented: $showCustomTimePicker, onDismiss: { selectedDisappearingMessageTime = customDisappearingMessageTimeDefault.get() }) { if #available(iOS 16.0, *) { disappearingMessageCustomTimePicker() - .presentationDetents([.fraction(0.6)]) + .presentationDetents([.medium]) } else { disappearingMessageCustomTimePicker() } diff --git a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift index c044107069..3044c4d436 100644 --- a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift +++ b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift @@ -189,7 +189,7 @@ struct DropdownCustomTimePicker: View { ) { if #available(iOS 16.0, *) { customTimePicker() - .presentationDetents([.fraction(0.6)]) + .presentationDetents([.medium]) } else { customTimePicker() }