fix(alarm): unfreeze snooze modal and add per-minute snooze countdown
The snooze button was fire-and-forget without error handling: if
posponerAlarma threw (e.g. native scheduleAlarm returns false on a
device without exact-alarm permission), _dismissScreen never ran. The
stuck modal also kept _alarmaSonandoActiva true, which made the next
ring get ignored. _posponer/_detener now dismiss in a finally and stop
audio defensively.
Add a native, AlarmManager-driven snooze countdown notification that
re-posts every minute ("Rings in N min", 3->2->1) while the engine is
dead. scheduleSpec drives scheduleSnoozeCountdown for snoozes (instead
of the 30-min pre-notice). Localized text and button labels travel
Dart->Kotlin as {minutes} templates, same pattern as preNoticeTemplate.
Notification actions: "snooze again" (snoozeAgain, anchored to now)
reports back via the existing snoozed event; "stop" (cancelSnooze)
records a handled occurrence for cold-start reconciliation and emits a
new snoozeCancelled event handled in EstadoAlarmas.
Adds snoozeCountdown/snoozeAgainAction keys across all 13 locales and a
test for the snoozeCancelled event. Kotlin changes are static-reviewed
only; no Android build environment available here.
This commit is contained in:
@@ -281,6 +281,10 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
/// The native scheduler already re-registered setAlarmClock, so this only
|
||||
/// persists the canonical state — it MUST NOT call android.programar again.
|
||||
Future<void> _alRecibirEventoNativo(EventoAlarmaAndroid evento) async {
|
||||
if (evento.accion == EventoAlarmaAndroid.accionSnoozeCancelled) {
|
||||
await _registrarCancelacionSnoozeNativa(evento);
|
||||
return;
|
||||
}
|
||||
if (evento.accion != EventoAlarmaAndroid.accionSnoozed) return;
|
||||
if (evento.alarmaId.isEmpty || evento.snoozeUntilMillis <= 0) return;
|
||||
final hasta = DateTime.fromMillisecondsSinceEpoch(evento.snoozeUntilMillis);
|
||||
@@ -304,6 +308,30 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a native snooze cancellation ("Detener" on the countdown
|
||||
/// notification). The native scheduler already advanced to the next normal
|
||||
/// occurrence, so this only clears the snooze in the canonical config and
|
||||
/// MUST NOT call android.programar again (would double-schedule).
|
||||
Future<void> _registrarCancelacionSnoozeNativa(
|
||||
EventoAlarmaAndroid evento,
|
||||
) async {
|
||||
if (evento.alarmaId.isEmpty) return;
|
||||
final origen =
|
||||
evento.occurrenceAtMillis > 0
|
||||
? DateTime.fromMillisecondsSinceEpoch(evento.occurrenceAtMillis)
|
||||
: DateTime.now();
|
||||
debugPrint(
|
||||
'[PluriWave][alarmas] snooze cancelado nativo id=${evento.alarmaId} origen=${origen.toIso8601String()}',
|
||||
);
|
||||
try {
|
||||
final config = await servicio.completarEjecucion(evento.alarmaId, origen);
|
||||
_aplicar(config);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] cancelar snooze nativo ERROR $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sincronizarEjecucionesGestionadasPorAndroid() async {
|
||||
try {
|
||||
final ejecuciones = await android.obtenerEjecucionesNativasGestionadas();
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "يبدأ خلال {minutes} دقيقة",
|
||||
"snoozeCountdown": "يرنّ خلال {minutes} دقيقة",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "غفوة مرة أخرى",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "{minutes} মিনিটে শুরু হবে",
|
||||
"snoozeCountdown": "{minutes} মিনিটে বাজবে",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "আবার স্নুজ করুন",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Startet in {minutes} Min.",
|
||||
"snoozeCountdown": "Klingelt in {minutes} Min.",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Erneut schlummern",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Starts in {minutes} min",
|
||||
"snoozeCountdown": "Rings in {minutes} min",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Snooze again",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -592,6 +592,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Empieza en {minutes} min",
|
||||
"snoozeCountdown": "Suena en {minutes} min",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Posponer otra vez",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Démarre dans {minutes} min",
|
||||
"snoozeCountdown": "Sonne dans {minutes} min",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Reporter encore",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "{minutes} मिनट में शुरू होगा",
|
||||
"snoozeCountdown": "{minutes} मिनट में बजेगा",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "फिर से स्नूज़ करें",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Mulai dalam {minutes} menit",
|
||||
"snoozeCountdown": "Berbunyi dalam {minutes} mnt",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Tunda lagi",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Inizia tra {minutes} min",
|
||||
"snoozeCountdown": "Suona tra {minutes} min",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Posponi di nuovo",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "{minutes}分後に開始",
|
||||
"snoozeCountdown": "{minutes}分後に鳴ります",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "もう一度スヌーズ",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Começa em {minutes} min",
|
||||
"snoozeCountdown": "Toca em {minutes} min",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Adiar novamente",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "Начнётся через {minutes} мин",
|
||||
"snoozeCountdown": "Прозвонит через {minutes} мин",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "Отложить снова",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -629,6 +629,15 @@
|
||||
}
|
||||
},
|
||||
"preNoticeCountdown": "{minutes}分钟后开始",
|
||||
"snoozeCountdown": "{minutes}分钟后响铃",
|
||||
"@snoozeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"snoozeAgainAction": "再次小睡",
|
||||
"@preNoticeCountdown": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
|
||||
@@ -2312,6 +2312,18 @@ abstract class AppLocalizations {
|
||||
/// **'Empieza en {minutes} min'**
|
||||
String preNoticeCountdown(int minutes);
|
||||
|
||||
/// No description provided for @snoozeCountdown.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Suena en {minutes} min'**
|
||||
String snoozeCountdown(int minutes);
|
||||
|
||||
/// No description provided for @snoozeAgainAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Posponer otra vez'**
|
||||
String get snoozeAgainAction;
|
||||
|
||||
/// No description provided for @eqDeviceEditTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -1253,6 +1253,14 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
return 'يبدأ خلال $minutes دقيقة';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'يرنّ خلال $minutes دقيقة';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'غفوة مرة أخرى';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'تعديل الجهاز';
|
||||
|
||||
|
||||
@@ -1260,6 +1260,14 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
return '$minutes মিনিটে শুরু হবে';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return '$minutes মিনিটে বাজবে';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'আবার স্নুজ করুন';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'ডিভাইস সম্পাদনা করুন';
|
||||
|
||||
|
||||
@@ -1270,6 +1270,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Startet in $minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Klingelt in $minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Erneut schlummern';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Gerät bearbeiten';
|
||||
|
||||
|
||||
@@ -1256,6 +1256,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Starts in $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Rings in $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Snooze again';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Edit device';
|
||||
|
||||
|
||||
@@ -1265,6 +1265,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Empieza en $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Suena en $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Posponer otra vez';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Editar dispositivo';
|
||||
|
||||
|
||||
@@ -1275,6 +1275,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Démarre dans $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Sonne dans $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Reporter encore';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Modifier l\'appareil';
|
||||
|
||||
|
||||
@@ -1259,6 +1259,14 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
return '$minutes मिनट में शुरू होगा';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return '$minutes मिनट में बजेगा';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'फिर से स्नूज़ करें';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'डिवाइस संपादित करें';
|
||||
|
||||
|
||||
@@ -1264,6 +1264,14 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
return 'Mulai dalam $minutes menit';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Berbunyi dalam $minutes mnt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Tunda lagi';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Edit perangkat';
|
||||
|
||||
|
||||
@@ -1270,6 +1270,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Inizia tra $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Suona tra $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Posponi di nuovo';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Modifica dispositivo';
|
||||
|
||||
|
||||
@@ -1220,6 +1220,14 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '$minutes分後に開始';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return '$minutes分後に鳴ります';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'もう一度スヌーズ';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'デバイスを編集';
|
||||
|
||||
|
||||
@@ -1262,6 +1262,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Começa em $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Toca em $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Adiar novamente';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Editar dispositivo';
|
||||
|
||||
|
||||
@@ -1266,6 +1266,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Начнётся через $minutes мин';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return 'Прозвонит через $minutes мин';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => 'Отложить снова';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => 'Изменить устройство';
|
||||
|
||||
|
||||
@@ -1213,6 +1213,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '$minutes分钟后开始';
|
||||
}
|
||||
|
||||
@override
|
||||
String snoozeCountdown(int minutes) {
|
||||
return '$minutes分钟后响铃';
|
||||
}
|
||||
|
||||
@override
|
||||
String get snoozeAgainAction => '再次小睡';
|
||||
|
||||
@override
|
||||
String get eqDeviceEditTitle => '编辑设备';
|
||||
|
||||
|
||||
@@ -154,10 +154,17 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
Future<void> _detener() async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
await _liberarAudioLocal();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||
if (mounted) _dismissScreen();
|
||||
await _silenciarAudio(radio);
|
||||
// Dismiss is run from finally so a failing reschedule/teardown can never
|
||||
// leave the ringing screen stuck open (which would also block the next
|
||||
// ring via the _alarmaSonandoActiva guard in app.dart).
|
||||
try {
|
||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] finalizar ejecucion fallo: $e');
|
||||
} finally {
|
||||
if (mounted) _dismissScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// Flutter-first snooze (S2-R1): tears down local audio, then routes
|
||||
@@ -166,10 +173,33 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
Future<void> _posponer(int minutos) async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
await _liberarAudioLocal();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||
if (mounted) _dismissScreen();
|
||||
await _silenciarAudio(radio);
|
||||
// See _detener: the screen MUST close even if posponerAlarma throws
|
||||
// (e.g. native scheduleAlarm returns false on a device without exact
|
||||
// alarm permission). Otherwise the modal freezes and the snooze never
|
||||
// re-rings because _alarmaSonandoActiva stays true.
|
||||
try {
|
||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] posponer alarma fallo: $e');
|
||||
} finally {
|
||||
if (mounted) _dismissScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops both audio sources (local fallback player and the radio handler)
|
||||
/// without letting either failure abort the caller's dismiss flow.
|
||||
Future<void> _silenciarAudio(EstadoRadio radio) async {
|
||||
try {
|
||||
await _liberarAudioLocal();
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] liberar audio local fallo: $e');
|
||||
}
|
||||
try {
|
||||
await radio.audio.pausar();
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] pausar radio fallo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the alarm screen safely in both live-app and dead-app states.
|
||||
|
||||
@@ -23,6 +23,10 @@ class EventoAlarmaAndroid {
|
||||
/// (notification "Posponer" while the app may be backgrounded/killed).
|
||||
static const accionSnoozed = 'snoozed';
|
||||
|
||||
/// Action reported when a pending snooze was cancelled natively from the
|
||||
/// countdown notification ("Detener" while the app may be killed).
|
||||
static const accionSnoozeCancelled = 'snoozeCancelled';
|
||||
|
||||
final String alarmaId;
|
||||
final String titulo;
|
||||
final String accion;
|
||||
@@ -183,6 +187,16 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
);
|
||||
}
|
||||
|
||||
/// Same sentinel strategy as [_preNoticeTemplate], but for the snooze
|
||||
/// countdown notification re-posted every minute while an alarm is snoozed.
|
||||
static String _snoozeCountdownTemplate(AppLocalizations l10n) {
|
||||
const sentinel = 42424242;
|
||||
return l10n.snoozeCountdown(sentinel).replaceFirst(
|
||||
sentinel.toString(),
|
||||
'{minutes}',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
|
||||
|
||||
@@ -203,6 +217,9 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
'id': alarma.id,
|
||||
'title': localizedAlarmName(_textos, alarma.nombre),
|
||||
'preNoticeTemplate': _preNoticeTemplate(_textos),
|
||||
'snoozeCountdownTemplate': _snoozeCountdownTemplate(_textos),
|
||||
'snoozeAgainLabel': _textos.snoozeAgainAction,
|
||||
'snoozeStopLabel': _textos.stopAlarmAction,
|
||||
'triggerAtMillis': proxima.millisecondsSinceEpoch,
|
||||
'preNoticeAtMillis':
|
||||
alarma.snoozeHasta == null
|
||||
|
||||
Reference in New Issue
Block a user