Add custom mute until option.

This commit is contained in:
Greyson Parrelli
2026-02-25 10:06:15 -05:00
committed by Cody Henthorne
parent 7d1897a9d2
commit 121f0c6134
13 changed files with 486 additions and 83 deletions

View File

@@ -1,65 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.concurrent.TimeUnit;
public class MuteDialog extends AlertDialog {
protected MuteDialog(Context context) {
super(context);
}
protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
protected MuteDialog(Context context, int theme) {
super(context, theme);
}
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
show(context, listener, null);
}
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, (dialog, which) -> {
final long muteUntil;
switch (which) {
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
case 4: muteUntil = Long.MAX_VALUE; break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
}
listener.onMuted(muteUntil);
});
if (cancelListener != null) {
builder.setOnCancelListener(dialog -> {
cancelListener.run();
dialog.dismiss();
});
}
builder.show();
}
public interface MuteSelectionListener {
public void onMuted(long until);
}
}

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.components.settings.conversation.MuteUntilTimePickerBottomSheet
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
object MuteDialog {
private const val MUTE_UNTIL: Long = -1L
private data class MuteOption(
@DrawableRes val iconRes: Int,
val title: String,
val duration: Long
)
@JvmStatic
fun show(context: Context, fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner, action: MuteSelectionListener) {
fragmentManager.setFragmentResultListener(MuteUntilTimePickerBottomSheet.REQUEST_KEY, lifecycleOwner) { _, bundle ->
action.onMuted(bundle.getLong(MuteUntilTimePickerBottomSheet.RESULT_TIMESTAMP))
}
val options = listOf(
MuteOption(R.drawable.ic_daytime_24, context.getString(R.string.arrays__mute_for_one_hour), 1.hours.inWholeMilliseconds),
MuteOption(R.drawable.ic_nighttime_26, context.getString(R.string.arrays__mute_for_eight_hours), 8.hours.inWholeMilliseconds),
MuteOption(R.drawable.symbol_calendar_one, context.getString(R.string.arrays__mute_for_one_day), 1.days.inWholeMilliseconds),
MuteOption(R.drawable.symbol_calendar_week, context.getString(R.string.arrays__mute_for_seven_days), 7.days.inWholeMilliseconds),
MuteOption(R.drawable.symbol_calendar_24, context.getString(R.string.MuteDialog__mute_until), MUTE_UNTIL),
MuteOption(R.drawable.symbol_bell_slash_24, context.getString(R.string.arrays__always), Long.MAX_VALUE)
)
val adapter = object : BaseAdapter() {
override fun getCount(): Int = options.size
override fun getItem(position: Int): MuteOption = options[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.mute_dialog_item, parent, false)
val option = options[position]
view.findViewById<ImageView>(R.id.mute_dialog_icon).setImageResource(option.iconRes)
view.findViewById<TextView>(R.id.mute_dialog_title).text = option.title
return view
}
}
MaterialAlertDialogBuilder(context)
.setTitle(R.string.MuteDialog_mute_notifications)
.setAdapter(adapter) { _, which ->
val option = options[which]
when (option.duration) {
MUTE_UNTIL -> MuteUntilTimePickerBottomSheet.show(fragmentManager)
Long.MAX_VALUE -> action.onMuted(Long.MAX_VALUE)
else -> action.onMuted(System.currentTimeMillis() + option.duration)
}
}
.show()
}
fun interface MuteSelectionListener {
fun onMuted(until: Long)
}
}

View File

@@ -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()))

View File

@@ -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)
}
}

View File

@@ -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 = {}
)
}
}

View File

@@ -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() }
}

View File

@@ -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)

View File

@@ -3992,7 +3992,7 @@ class ConversationFragment :
}
override fun handleMuteNotifications() {
MuteDialog.show(requireContext(), viewModel::muteConversation)
MuteDialog.show(requireContext(), childFragmentManager, viewLifecycleOwner, viewModel::muteConversation)
}
override fun handleUnmuteNotifications() {

View File

@@ -1177,9 +1177,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
}
private void handleMute(@NonNull Collection<Conversation> conversations) {
MuteDialog.show(requireContext(), until -> {
updateMute(conversations, until);
});
MuteDialog.show(requireContext(), getChildFragmentManager(), getViewLifecycleOwner(), until -> updateMute(conversations, until));
}
private void handleUnmute(@NonNull Collection<Conversation> conversations) {
@@ -1612,7 +1610,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
@Override
public void onMuteAll(@NonNull ChatFolderRecord chatFolder) {
MuteDialog.show(requireContext(), until -> viewModel.onUpdateMute(chatFolder, until));
MuteDialog.show(requireContext(), getChildFragmentManager(), getViewLifecycleOwner(), until -> viewModel.onUpdateMute(chatFolder, until));
}
@Override
@@ -1945,6 +1943,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
void onMultiSelectFinished();
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12.37 11.13c-0.23 0-0.43 0.1-0.57 0.25-0.22 0.25-0.42 0.4-0.64 0.5-0.22 0.11-0.5 0.18-0.86 0.22-0.37 0.05-0.68 0.37-0.68 0.76 0 0.42 0.35 0.77 0.77 0.77h0.98v4.12c0 0.48 0.4 0.88 0.88 0.88s0.88-0.4 0.88-0.88v-5.87c0-0.42-0.34-0.76-0.76-0.76Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M7.5 0.75c0.55 0 1 0.45 1 1v1.88h7V1.75c0-0.55 0.45-1 1-1s1 0.45 1 1v1.88c0.36 0 0.68 0 0.96 0.03 0.47 0.04 0.91 0.12 1.32 0.33 0.64 0.33 1.15 0.84 1.48 1.48 0.2 0.4 0.29 0.85 0.33 1.32 0.04 0.45 0.04 1 0.04 1.67v8.58c0 0.67 0 1.22-0.04 1.67-0.04 0.47-0.12 0.91-0.33 1.32-0.33 0.64-0.84 1.15-1.48 1.48-0.4 0.2-0.85 0.29-1.32 0.33-0.45 0.04-1 0.04-1.67 0.04H7.2c-0.67 0-1.22 0-1.67-0.04-0.47-0.04-0.91-0.12-1.32-0.33-0.64-0.33-1.15-0.84-1.48-1.48-0.2-0.4-0.29-0.85-0.33-1.32-0.04-0.45-0.04-1-0.04-1.67V8.46c0-0.67 0-1.22 0.04-1.67 0.04-0.47 0.12-0.91 0.33-1.32 0.33-0.64 0.84-1.15 1.48-1.48 0.4-0.2 0.85-0.29 1.32-0.33 0.28-0.02 0.6-0.03 0.96-0.03V1.75c0-0.55 0.45-1 1-1ZM5.68 5.41C5.32 5.44 5.14 5.49 5 5.55c-0.3 0.16-0.55 0.4-0.7 0.71-0.07 0.13-0.12 0.3-0.15 0.67-0.02 0.25-0.03 0.55-0.03 0.95h15.74c0-0.4 0-0.7-0.03-0.95-0.03-0.36-0.08-0.54-0.14-0.67-0.16-0.3-0.4-0.55-0.71-0.7-0.13-0.07-0.3-0.12-0.67-0.15-0.37-0.03-0.86-0.04-1.57-0.04h-9.5c-0.71 0-1.2 0-1.57 0.04Zm14.2 4.21H4.13V17c0 0.71 0 1.2 0.03 1.57 0.03 0.36 0.08 0.54 0.14 0.67 0.16 0.3 0.4 0.55 0.71 0.7 0.13 0.07 0.3 0.12 0.67 0.15 0.37 0.03 0.86 0.04 1.57 0.04h9.5c0.71 0 1.2 0 1.57-0.04 0.36-0.03 0.54-0.08 0.67-0.14 0.3-0.16 0.55-0.4 0.7-0.71 0.07-0.13 0.12-0.3 0.15-0.67 0.03-0.37 0.04-0.86 0.04-1.57V9.62Z"/>
</vector>

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7.5 11.25c-0.76 0-1.38 0.62-1.38 1.38C6.13 13.38 6.75 14 7.5 14c0.76 0 1.38-0.62 1.38-1.38 0-0.75-0.62-1.37-1.38-1.37Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M16.5 11.25c-0.76 0-1.38 0.62-1.38 1.38 0 0.75 0.62 1.37 1.38 1.37 0.76 0 1.38-0.62 1.38-1.38 0-0.75-0.62-1.37-1.38-1.37Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12 11.25c-0.76 0-1.38 0.62-1.38 1.38 0 0.75 0.62 1.37 1.38 1.37 0.76 0 1.38-0.62 1.38-1.38 0-0.75-0.62-1.37-1.38-1.37Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M7.5 0.75c0.55 0 1 0.45 1 1v1.88h7V1.75c0-0.55 0.45-1 1-1s1 0.45 1 1v1.88c0.36 0 0.68 0 0.96 0.03 0.47 0.04 0.91 0.12 1.32 0.33 0.64 0.33 1.15 0.84 1.48 1.48 0.2 0.4 0.29 0.85 0.33 1.32 0.04 0.45 0.04 1 0.04 1.67v8.58c0 0.67 0 1.22-0.04 1.67-0.04 0.47-0.12 0.91-0.33 1.32-0.33 0.64-0.84 1.15-1.48 1.48-0.4 0.2-0.85 0.29-1.32 0.33-0.45 0.04-1 0.04-1.67 0.04H7.2c-0.67 0-1.22 0-1.67-0.04-0.47-0.04-0.91-0.12-1.32-0.33-0.64-0.33-1.15-0.84-1.48-1.48-0.2-0.4-0.29-0.85-0.33-1.32-0.04-0.45-0.04-1-0.04-1.67V8.46c0-0.67 0-1.22 0.04-1.67 0.04-0.47 0.12-0.91 0.33-1.32 0.33-0.64 0.84-1.15 1.48-1.48 0.4-0.2 0.85-0.29 1.32-0.33 0.28-0.02 0.6-0.03 0.96-0.03V1.75c0-0.55 0.45-1 1-1Zm-2.49 4.8c-0.3 0.16-0.55 0.4-0.7 0.71-0.07 0.13-0.12 0.3-0.15 0.67-0.02 0.25-0.03 0.55-0.03 0.95h15.74c0-0.4 0-0.7-0.03-0.95-0.03-0.36-0.08-0.54-0.14-0.67-0.16-0.3-0.4-0.55-0.71-0.7-0.13-0.07-0.3-0.12-0.67-0.15-0.37-0.03-0.86-0.04-1.57-0.04h-9.5c-0.71 0-1.2 0-1.57 0.04C5.32 5.44 5.14 5.49 5 5.55ZM4.13 9.63V17c0 0.71 0 1.2 0.03 1.57 0.03 0.36 0.08 0.54 0.14 0.67 0.16 0.3 0.4 0.55 0.71 0.7 0.13 0.07 0.3 0.12 0.67 0.15 0.37 0.03 0.86 0.04 1.57 0.04h9.5c0.71 0 1.2 0 1.57-0.04 0.36-0.03 0.54-0.08 0.67-0.14 0.3-0.16 0.55-0.4 0.7-0.71 0.07-0.13 0.12-0.3 0.15-0.67 0.03-0.37 0.04-0.86 0.04-1.57V9.62H4.13Z"/>
</vector>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/mute_dialog_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
tools:src="@drawable/ic_daytime_24" />
<TextView
android:id="@+id/mute_dialog_title"
style="@style/Signal.Text.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
tools:text="Mute for 1 hour" />
</LinearLayout>

View File

@@ -3212,7 +3212,22 @@
<string name="MessageDisplayHelper_message_encrypted_for_non_existing_session">Message encrypted for non-existing session</string>
<!-- MuteDialog -->
<!-- Title for a dialog where a user chooses how long they'd like to mute notifications for -->
<string name="MuteDialog_mute_notifications">Mute notifications</string>
<!-- Dialog option that, when pressed, will open a time picker to let the user choose how long they'd like to mute notifications for -->
<string name="MuteDialog__mute_until">Mute until…</string>
<!-- MuteUntilTimePickerBottomSheet -->
<!-- Title for a dialog where a user chooses how long they'd like to mute notifications for -->
<string name="MuteUntilTimePickerBottomSheet__dialog_title">Mute notifications until…</string>
<!-- Label for a data selector where the user chooses how long they'd like to mute notifications for -->
<string name="MuteUntilTimePickerBottomSheet__select_date_title">Select date</string>
<!-- Label for a time selector where the user chooses how long they'd like to mute notifications for -->
<string name="MuteUntilTimePickerBottomSheet__select_time_title">Select time</string>
<!-- Label for a button that, when pressed, will confirm the user's choice on how long to mute notifications for -->
<string name="MuteUntilTimePickerBottomSheet__mute_notifications">Mute notifications</string>
<!-- Subtitle in a dialog that describes the timezone the user is picking times in. The first placeholder is a UTC offset, and the second placeholder is a user-friendly name for the timezone, e.g. "All times in (GMT-05:00) Eastern Standard Time" -->
<string name="MuteUntilTimePickerBottomSheet__timezone_disclaimer">All times in (%1$s) %2$s</string>
<!-- KeyCachingService -->
<string name="KeyCachingService_signal_passphrase_cached">Touch to open.</string>