mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add custom mute until option.
This commit is contained in:
committed by
Cody Henthorne
parent
7d1897a9d2
commit
121f0c6134
@@ -38,7 +38,6 @@ import org.signal.core.util.orNull
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -119,10 +118,11 @@ private const val REQUEST_CODE_ADD_CONTACT = 2
|
||||
private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
|
||||
private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
|
||||
|
||||
class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
menuId = R.menu.conversation_settings
|
||||
) {
|
||||
class ConversationSettingsFragment :
|
||||
DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
menuId = R.menu.conversation_settings
|
||||
) {
|
||||
|
||||
private val args: ConversationSettingsFragmentArgs by navArgs()
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
@@ -469,9 +469,11 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
}
|
||||
},
|
||||
onMuteClick = {
|
||||
onMuteClick = { view ->
|
||||
if (!state.buttonStripState.isMuted) {
|
||||
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
|
||||
MuteContextMenu.show(view, requireView() as ViewGroup, childFragmentManager, viewLifecycleOwner) { duration ->
|
||||
viewModel.setMuteUntil(duration)
|
||||
}
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(state.recipient.muteUntil.formatMutedUntil(requireContext()))
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object MuteContextMenu {
|
||||
|
||||
@JvmStatic
|
||||
fun show(anchor: View, container: ViewGroup, fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner, action: (Long) -> Unit): SignalContextMenu {
|
||||
fragmentManager.setFragmentResultListener(MuteUntilTimePickerBottomSheet.REQUEST_KEY, lifecycleOwner) { _, bundle ->
|
||||
action(bundle.getLong(MuteUntilTimePickerBottomSheet.RESULT_TIMESTAMP))
|
||||
}
|
||||
|
||||
val context = anchor.context
|
||||
val actionItems = listOf(
|
||||
ActionItem(R.drawable.ic_daytime_24, context.getString(R.string.arrays__mute_for_one_hour)) {
|
||||
action(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))
|
||||
},
|
||||
ActionItem(R.drawable.ic_nighttime_26, context.getString(R.string.arrays__mute_for_eight_hours)) {
|
||||
action(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8))
|
||||
},
|
||||
ActionItem(R.drawable.symbol_calendar_one, context.getString(R.string.arrays__mute_for_one_day)) {
|
||||
action(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1))
|
||||
},
|
||||
ActionItem(R.drawable.symbol_calendar_week, context.getString(R.string.arrays__mute_for_seven_days)) {
|
||||
action(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7))
|
||||
},
|
||||
ActionItem(R.drawable.symbol_calendar_24, context.getString(R.string.MuteDialog__mute_until)) {
|
||||
MuteUntilTimePickerBottomSheet.show(fragmentManager)
|
||||
},
|
||||
ActionItem(R.drawable.symbol_bell_slash_24, context.getString(R.string.arrays__always)) {
|
||||
action(Long.MAX_VALUE)
|
||||
}
|
||||
)
|
||||
|
||||
return SignalContextMenu.Builder(anchor, container)
|
||||
.offsetX(12.dp)
|
||||
.offsetY(12.dp)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
|
||||
.show(actionItems)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat
|
||||
import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.atMidnight
|
||||
import org.thoughtcrime.securesms.util.atUTC
|
||||
import org.thoughtcrime.securesms.util.formatHours
|
||||
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.Locale
|
||||
|
||||
class MuteUntilTimePickerBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.66f
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "mute_until_result"
|
||||
const val RESULT_TIMESTAMP = "timestamp"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
val fragment = MuteUntilTimePickerBottomSheet()
|
||||
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val context = LocalContext.current
|
||||
val now = remember { LocalDateTime.now() }
|
||||
|
||||
val defaultDateTime = remember {
|
||||
if (now.hour < 17) {
|
||||
now.withHour(17).withMinute(0).withSecond(0).withNano(0)
|
||||
} else {
|
||||
val nextMorning = if (now.dayOfWeek == DayOfWeek.FRIDAY || now.dayOfWeek == DayOfWeek.SATURDAY || now.dayOfWeek == DayOfWeek.SUNDAY) {
|
||||
now.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
|
||||
} else {
|
||||
now.plusDays(1)
|
||||
}
|
||||
nextMorning.withHour(8).withMinute(0).withSecond(0).withNano(0)
|
||||
}
|
||||
}
|
||||
|
||||
var selectedDate by remember { mutableLongStateOf(defaultDateTime.toMillis()) }
|
||||
var selectedHour by remember { mutableIntStateOf(defaultDateTime.hour) }
|
||||
var selectedMinute by remember { mutableIntStateOf(defaultDateTime.minute) }
|
||||
|
||||
val dateText = remember(selectedDate) {
|
||||
DateUtils.getDayPrecisionTimeString(context, Locale.getDefault(), selectedDate)
|
||||
}
|
||||
|
||||
val timeText = remember(selectedHour, selectedMinute) {
|
||||
LocalTime.of(selectedHour, selectedMinute).formatHours(context)
|
||||
}
|
||||
|
||||
val zonedDateTime = remember { ZonedDateTime.now() }
|
||||
val timezoneDisclaimer = remember {
|
||||
val zoneOffsetFormatter = DateTimeFormatter.ofPattern("OOOO")
|
||||
val zoneNameFormatter = DateTimeFormatter.ofPattern("zzzz")
|
||||
context.getString(
|
||||
R.string.MuteUntilTimePickerBottomSheet__timezone_disclaimer,
|
||||
zoneOffsetFormatter.format(zonedDateTime),
|
||||
zoneNameFormatter.format(zonedDateTime)
|
||||
)
|
||||
}
|
||||
|
||||
MuteUntilSheetContent(
|
||||
dateText = dateText,
|
||||
timeText = timeText,
|
||||
timezoneDisclaimer = timezoneDisclaimer,
|
||||
onDateClick = {
|
||||
val local = LocalDateTime.now().atMidnight().atUTC().toMillis()
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText(context.getString(R.string.MuteUntilTimePickerBottomSheet__select_date_title))
|
||||
.setSelection(selectedDate)
|
||||
.setCalendarConstraints(CalendarConstraints.Builder().setStart(local).setValidator(DateValidatorPointForward.now()).build())
|
||||
.build()
|
||||
|
||||
datePicker.addOnDismissListener {
|
||||
datePicker.clearOnDismissListeners()
|
||||
datePicker.clearOnPositiveButtonClickListeners()
|
||||
}
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener {
|
||||
selectedDate = it.toLocalDateTime(ZoneOffset.UTC).atZone(ZoneId.systemDefault()).toMillis()
|
||||
}
|
||||
datePicker.show(childFragmentManager, "DATE_PICKER")
|
||||
},
|
||||
onTimeClick = {
|
||||
val timeFormat = if (DateFormat.is24HourFormat(context)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
|
||||
val timePicker = MaterialTimePicker.Builder()
|
||||
.setTimeFormat(timeFormat)
|
||||
.setHour(selectedHour)
|
||||
.setMinute(selectedMinute)
|
||||
.setTitleText(context.getString(R.string.MuteUntilTimePickerBottomSheet__select_time_title))
|
||||
.build()
|
||||
|
||||
timePicker.addOnDismissListener {
|
||||
timePicker.clearOnDismissListeners()
|
||||
timePicker.clearOnPositiveButtonClickListeners()
|
||||
}
|
||||
|
||||
timePicker.addOnPositiveButtonClickListener {
|
||||
selectedHour = timePicker.hour
|
||||
selectedMinute = timePicker.minute
|
||||
}
|
||||
timePicker.show(childFragmentManager, "TIME_PICKER")
|
||||
},
|
||||
onMuteClick = {
|
||||
val timestamp = selectedDate.toLocalDateTime()
|
||||
.withHour(selectedHour)
|
||||
.withMinute(selectedMinute)
|
||||
.withSecond(0)
|
||||
.withNano(0)
|
||||
.toMillis()
|
||||
|
||||
if (timestamp > System.currentTimeMillis()) {
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_TIMESTAMP to timestamp))
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MuteUntilSheetContent(
|
||||
dateText: String,
|
||||
timeText: String,
|
||||
timezoneDisclaimer: String,
|
||||
onDateClick: () -> Unit,
|
||||
onTimeClick: () -> Unit,
|
||||
onMuteClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.MuteUntilTimePickerBottomSheet__dialog_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 18.dp, bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = timezoneDisclaimer,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 56.dp)
|
||||
.align(Alignment.Start)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable(onClick = onDateClick)
|
||||
) {
|
||||
Text(
|
||||
text = dateText,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_expand_down_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable(onClick = onTimeClick)
|
||||
) {
|
||||
Text(
|
||||
text = timeText,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_expand_down_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 18.dp, end = 18.dp, top = 14.dp, bottom = 24.dp)
|
||||
) {
|
||||
Buttons.MediumTonal(
|
||||
onClick = onMuteClick
|
||||
) {
|
||||
Text(stringResource(R.string.MuteUntilTimePickerBottomSheet__mute_notifications))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MuteUntilSheetContentPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
MuteUntilSheetContent(
|
||||
dateText = "Today",
|
||||
timeText = "5:00 PM",
|
||||
timezoneDisclaimer = "All times in (GMT-05:00) Eastern Standard Time",
|
||||
onDateClick = {},
|
||||
onTimeClick = {},
|
||||
onMuteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ object ButtonStripPreference {
|
||||
val onMessageClick: () -> Unit = {},
|
||||
val onVideoClick: () -> Unit = {},
|
||||
val onAudioClick: () -> Unit = {},
|
||||
val onMuteClick: () -> Unit = {},
|
||||
val onMuteClick: (View) -> Unit = {},
|
||||
val onSearchClick: () -> Unit = {}
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
@@ -97,7 +97,7 @@ object ButtonStripPreference {
|
||||
message.setOnClickListener { model.onMessageClick() }
|
||||
videoCall.setOnClickListener { model.onVideoClick() }
|
||||
audioCall.setOnClickListener { model.onAudioClick() }
|
||||
mute.setOnClickListener { model.onMuteClick() }
|
||||
mute.setOnClickListener { model.onMuteClick(it) }
|
||||
search.setOnClickListener { model.onSearchClick() }
|
||||
addToStory.setOnClickListener { model.onAddToStoryClick() }
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
|
||||
) {
|
||||
class SoundsAndNotificationsSettingsFragment :
|
||||
DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
|
||||
) {
|
||||
|
||||
private val mentionLabels: Array<String> by lazy {
|
||||
resources.getStringArray(R.array.SoundsAndNotificationsSettingsFragment__mention_labels)
|
||||
@@ -66,7 +67,7 @@ class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
|
||||
summary = DSLSettingsText.from(muteSummary),
|
||||
onClick = {
|
||||
if (state.muteUntil <= 0) {
|
||||
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
|
||||
MuteDialog.show(requireContext(), childFragmentManager, viewLifecycleOwner, viewModel::setMuteUntil)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(muteSummary)
|
||||
|
||||
Reference in New Issue
Block a user