android: allow to configure custom disappearance interval on chat level (#2460)
This commit is contained in:
@@ -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<Int?>, 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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<Int> {
|
||||
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<CustomTimeUnit> = 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<Int?>,
|
||||
propagateExternalSelectionUpdate: Boolean = false,
|
||||
label: String,
|
||||
dropdownValues: List<Int?>,
|
||||
customPickerTitle: String,
|
||||
customPickerConfirmButtonText: String,
|
||||
customPickerTimeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
||||
onSelected: (Int?) -> Unit
|
||||
) {
|
||||
fun getValues(selectedValue: Int?): List<DropdownSelection> =
|
||||
dropdownValues.map { DropdownSelection.DropdownValue(it) } +
|
||||
(if (dropdownValues.contains(selectedValue)) listOf() else listOf(DropdownSelection.DropdownValue(selectedValue))) +
|
||||
listOf(DropdownSelection.Custom)
|
||||
|
||||
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
|
||||
val values: MutableState<List<DropdownSelection>> = 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1407,4 +1407,6 @@
|
||||
<string name="custom_time_unit_days">days</string>
|
||||
<string name="custom_time_unit_weeks">weeks</string>
|
||||
<string name="custom_time_unit_months">months</string>
|
||||
<string name="custom_time_picker_select">Select</string>
|
||||
<string name="custom_time_picker_custom">custom</string>
|
||||
</resources>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ struct DropdownCustomTimePicker: View {
|
||||
) {
|
||||
if #available(iOS 16.0, *) {
|
||||
customTimePicker()
|
||||
.presentationDetents([.fraction(0.6)])
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
customTimePicker()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user