diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt index fcfbcb4..008d40d 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt @@ -13,12 +13,7 @@ import com.avinal.memos.db.entity.toDomain import com.avinal.memos.parser.TaskParser import kotlin.time.Clock import kotlinx.coroutines.Dispatchers -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.atTime -import kotlinx.datetime.toInstant -import kotlinx.datetime.todayIn class TaskCheckWorker( private val appContext: Context, @@ -27,11 +22,7 @@ class TaskCheckWorker( override suspend fun doWork(): Result { val prefs = appContext.getSharedPreferences("task_notifications", Context.MODE_PRIVATE) - val notificationsEnabled = prefs.getBoolean("enabled", true) - if (!notificationsEnabled) return Result.success() - val scheduledIds = prefs.getStringSet("scheduled_ids", emptySet()) ?: emptySet() - val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) val nowMillis = Clock.System.now().toEpochMilliseconds() val db = Room.databaseBuilder( @@ -48,51 +39,26 @@ class TaskCheckWorker( val allTasks = memos.flatMap { memo -> TaskParser.extractTasks(memo.id, memo.content) } val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager val tz = TimeZone.currentSystemDefault() - val newScheduledIds = scheduledIds.toMutableSet() - allTasks.forEach { task -> - if (task.isCompleted || task.dueDate == null || task.id in scheduledIds) return@forEach - - // Schedule reminder as duration before due - if (task.reminder != null) { - val dueInstant = task.dueDate.atTime(task.dueTime ?: LocalTime(8, 0)).toInstant(tz) - val offsetMs = when (task.reminder.unit) { - com.avinal.memos.domain.ReminderUnit.MIN -> task.reminder.value * 60_000L - com.avinal.memos.domain.ReminderUnit.HR -> task.reminder.value * 3_600_000L - com.avinal.memos.domain.ReminderUnit.DAY -> task.reminder.value * 86_400_000L - com.avinal.memos.domain.ReminderUnit.WEEK -> task.reminder.value * 604_800_000L - } - val reminderMs = dueInstant.toEpochMilliseconds() - offsetMs - if (reminderMs > nowMillis) { - scheduleAlarm(alarmManager, "${task.id}_remind", task.text, "reminder: ${task.reminder}", reminderMs) - } - } - - if (task.dueTime != null) { - val alarmInstant = task.dueDate.atTime(task.dueTime).toInstant(tz) - val alarmMs = alarmInstant.toEpochMilliseconds() - if (alarmMs > nowMillis) { - scheduleAlarm(alarmManager, task.id, task.text, "due at ${task.dueTime}", alarmMs) - newScheduledIds.add(task.id) - } - } else { - val morning = task.dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() - val evening = task.dueDate.atTime(LocalTime(20, 0)).toInstant(tz).toEpochMilliseconds() - - if (morning > nowMillis) { - scheduleAlarm(alarmManager, "${task.id}_am", task.text, "due today", morning) - } - if (evening > nowMillis) { - scheduleAlarm(alarmManager, "${task.id}_pm", task.text, "reminder: still due today", evening) - } - newScheduledIds.add(task.id) - } + android.util.Log.d("TaskCheckWorker", "Found ${memos.size} memos, ${allTasks.size} tasks, scheduled=${scheduledIds.size}") + allTasks.forEach { t -> + android.util.Log.d("TaskCheckWorker", "Task: ${t.text}, date=${t.dueDate}, time=${t.dueTime}, reminder=${t.reminder}, completed=${t.isCompleted}, id=${t.id}") } - // Clean up IDs for completed/removed tasks + val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, scheduledIds) + android.util.Log.d("TaskCheckWorker", "Computed ${alarms.size} alarms") + + alarms.forEach { alarm -> + android.util.Log.d("TaskCheckWorker", "Scheduling: ${alarm.taskText} at ${alarm.triggerAtMillis} (${alarm.label})") + scheduleAlarm(alarmManager, alarm.taskId, alarm.taskText, alarm.label, alarm.triggerAtMillis) + } + + val newScheduledIds = scheduledIds.toMutableSet() + alarms.forEach { newScheduledIds.add(it.taskId) } + val activeTaskIds = allTasks.filter { !it.isCompleted }.map { it.id }.toSet() val cleaned = newScheduledIds.filter { id -> - val baseId = id.removeSuffix("_am").removeSuffix("_pm") + val baseId = id.removeSuffix("_am").removeSuffix("_pm").removeSuffix("_remind") baseId in activeTaskIds }.toSet() diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/WorkManagerSetup.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/WorkManagerSetup.kt index 2ca147e..cffd39b 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/WorkManagerSetup.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/WorkManagerSetup.kt @@ -2,11 +2,24 @@ package com.avinal.memos.notifications import android.content.Context import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import java.util.concurrent.TimeUnit +fun runTaskCheckNow(context: Context) { + WorkManager.getInstance(context).enqueue( + OneTimeWorkRequestBuilder().build() + ) +} + fun scheduleTaskChecker(context: Context) { + // Run once immediately on app launch + WorkManager.getInstance(context).enqueue( + OneTimeWorkRequestBuilder().build() + ) + + // Then every 15 minutes val request = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "task_check", diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt new file mode 100644 index 0000000..18192c2 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt @@ -0,0 +1,8 @@ +package com.avinal.memos.util + +import com.avinal.memos.notifications.runTaskCheckNow + +actual fun triggerReminderCheck() { + val ctx = appContext ?: return + runTaskCheckNow(ctx) +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt new file mode 100644 index 0000000..75157ab --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt @@ -0,0 +1,70 @@ +package com.avinal.memos.notifications + +import com.avinal.memos.domain.ReminderUnit +import com.avinal.memos.domain.Task +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atTime +import kotlinx.datetime.toInstant +import kotlinx.datetime.todayIn + +data class ScheduledAlarm( + val taskId: String, + val taskText: String, + val label: String, + val triggerAtMillis: Long, +) + +object ReminderScheduler { + + fun computeAlarms( + tasks: List, + nowMillis: Long, + timeZone: TimeZone, + alreadyScheduledIds: Set, + ): List { + val alarms = mutableListOf() + + val today = kotlin.time.Clock.System.todayIn(timeZone) + + tasks.forEach { task -> + if (task.isCompleted) return@forEach + val effectiveDate = task.dueDate ?: if (task.dueTime != null) today else return@forEach + + // Explicit reminder: fire at dueDateTime - duration + if (task.reminder != null) { + val dueInstant = effectiveDate.atTime(task.dueTime ?: LocalTime(8, 0)).toInstant(timeZone) + val offsetMs = when (task.reminder.unit) { + ReminderUnit.MIN -> task.reminder.value * 60_000L + ReminderUnit.HR -> task.reminder.value * 3_600_000L + ReminderUnit.DAY -> task.reminder.value * 86_400_000L + ReminderUnit.WEEK -> task.reminder.value * 604_800_000L + } + val reminderMs = dueInstant.toEpochMilliseconds() - offsetMs + if (reminderMs > nowMillis) { + alarms.add(ScheduledAlarm("${task.id}_remind", task.text, "reminder: ${task.reminder}", reminderMs)) + } + } + + // Due time alarm + if (task.dueTime != null) { + val alarmMs = effectiveDate.atTime(task.dueTime).toInstant(timeZone).toEpochMilliseconds() + if (alarmMs > nowMillis) { + alarms.add(ScheduledAlarm(task.id, task.text, "due at ${task.dueTime}", alarmMs)) + } + } else { + // Default: 8am and 8pm on due date + val morning = effectiveDate.atTime(LocalTime(8, 0)).toInstant(timeZone).toEpochMilliseconds() + val evening = effectiveDate.atTime(LocalTime(20, 0)).toInstant(timeZone).toEpochMilliseconds() + if (morning > nowMillis) { + alarms.add(ScheduledAlarm("${task.id}_am", task.text, "due today", morning)) + } + if (evening > nowMillis) { + alarms.add(ScheduledAlarm("${task.id}_pm", task.text, "reminder: still due today", evening)) + } + } + } + + return alarms + } +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/settings/SettingsScreen.kt index be34ae6..6fa9bab 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/settings/SettingsScreen.kt @@ -165,6 +165,13 @@ fun SettingsScreen( ) } Text("get notified when tasks are due or overdue", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + "check reminders now", + fontSize = 13.sp, color = accent, + modifier = Modifier.clickable { + com.avinal.memos.util.triggerReminderCheck() + }.padding(vertical = 4.dp), + ) Spacer(Modifier.height(24.dp)) SectionHeader("backup") diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt index c34b339..554f684 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt @@ -92,11 +92,52 @@ fun TaskDetailSheet( } if (showReminderPicker) { - val options = listOf(ReminderDuration(15, ReminderUnit.MIN), ReminderDuration(30, ReminderUnit.MIN), ReminderDuration(1, ReminderUnit.HR), ReminderDuration(2, ReminderUnit.HR), ReminderDuration(1, ReminderUnit.DAY), ReminderDuration(2, ReminderUnit.DAY), ReminderDuration(1, ReminderUnit.WEEK)) - AlertDialog(onDismissRequest = { showReminderPicker = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null, - text = { Column { options.forEach { d -> Text(d.toString(), fontSize = 15.sp, color = if (currentTask.reminder == d) accent else textColor, fontWeight = if (currentTask.reminder == d) FontWeight.SemiBold else FontWeight.Normal, - modifier = Modifier.fillMaxWidth().clickable { doUpdate(currentTask.copy(reminder = d)); showReminderPicker = false }.padding(vertical = 8.dp)) - }; if (currentTask.reminder != null) { Text("no reminder", fontSize = 15.sp, color = subtleColor, modifier = Modifier.fillMaxWidth().clickable { doUpdate(currentTask.copy(reminder = null)); showReminderPicker = false }.padding(vertical = 8.dp)) } } }, confirmButton = {}) + var reminderValue by remember { mutableStateOf(currentTask.reminder?.value?.toString() ?: "30") } + var reminderUnit by remember { mutableStateOf(currentTask.reminder?.unit ?: ReminderUnit.MIN) } + + AlertDialog( + onDismissRequest = { showReminderPicker = false }, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + title = null, + text = { + Column { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextField( + value = reminderValue, + onValueChange = { reminderValue = it.filter { c -> c.isDigit() } }, + modifier = Modifier.width(72.dp), + singleLine = true, + textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor), + colors = TextFieldDefaults.colors(focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedIndicatorColor = accent, unfocusedIndicatorColor = subtleColor.copy(alpha = 0.3f), cursorColor = accent), + ) + ReminderUnit.entries.forEach { unit -> + val sel = reminderUnit == unit + Text( + unit.suffix, fontSize = 14.sp, + color = if (sel) accent else textColor, + fontWeight = if (sel) FontWeight.SemiBold else FontWeight.Normal, + modifier = Modifier.clickable { reminderUnit = unit }.padding(horizontal = 6.dp, vertical = 4.dp), + ) + } + } + Spacer(Modifier.height(12.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if (currentTask.reminder != null) { + Text("clear", fontSize = 14.sp, color = subtleColor, modifier = Modifier.clickable { showReminderPicker = false; doUpdate(currentTask.copy(reminder = null)) }) + } else { + Spacer(Modifier.width(1.dp)) + } + Text("save", fontSize = 14.sp, color = accent, fontWeight = FontWeight.SemiBold, + modifier = Modifier.clickable { + showReminderPicker = false + val v = reminderValue.toIntOrNull() + if (v != null && v > 0) doUpdate(currentTask.copy(reminder = ReminderDuration(v, reminderUnit))) + }) + } + } + }, + confirmButton = {}, + ) } if (showPriorityPicker) { diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt new file mode 100644 index 0000000..7e78e07 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt @@ -0,0 +1,3 @@ +package com.avinal.memos.util + +expect fun triggerReminderCheck() diff --git a/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt b/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt new file mode 100644 index 0000000..2e022d0 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt @@ -0,0 +1,199 @@ +package com.avinal.memos + +import com.avinal.memos.domain.ReminderDuration +import com.avinal.memos.domain.ReminderUnit +import com.avinal.memos.domain.Task +import com.avinal.memos.notifications.ReminderScheduler +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atTime +import kotlinx.datetime.toInstant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ReminderSchedulerTest { + + private val tz = TimeZone.UTC + private val dueDate = LocalDate(2026, 6, 15) + // nowMillis = 2026-06-15 00:00 UTC + private val nowMillis = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds() + + private fun task( + id: String = "t1", + completed: Boolean = false, + date: LocalDate? = dueDate, + time: LocalTime? = null, + reminder: ReminderDuration? = null, + priority: Int? = null, + ) = Task( + id = id, memoId = "m1", lineIndex = 0, text = "Test", + isCompleted = completed, dueDate = date, dueTime = time, + reminder = reminder, priority = priority, + ) + + @Test + fun completedTaskProducesNoAlarms() { + val alarms = ReminderScheduler.computeAlarms(listOf(task(completed = true)), nowMillis, tz, emptySet()) + assertTrue(alarms.isEmpty()) + } + + @Test + fun taskWithNoDateProducesNoAlarms() { + val alarms = ReminderScheduler.computeAlarms(listOf(task(date = null)), nowMillis, tz, emptySet()) + assertTrue(alarms.isEmpty()) + } + + @Test + fun alreadyScheduledTaskStillRecomputed() { + // Alarms are always recomputed — AlarmManager deduplicates via PendingIntent + val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, setOf("t1")) + assertTrue(alarms.isNotEmpty()) + } + + @Test + fun taskWithDueTimeProducesOneAlarm() { + val t = task(time = LocalTime(15, 0)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + assertEquals(1, alarms.size) + assertEquals("t1", alarms[0].taskId) + assertTrue(alarms[0].label.contains("due at")) + } + + @Test + fun taskWithDueTimeAlarmAtCorrectTime() { + val t = task(time = LocalTime(15, 0)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + val expected = dueDate.atTime(LocalTime(15, 0)).toInstant(tz).toEpochMilliseconds() + assertEquals(expected, alarms[0].triggerAtMillis) + } + + @Test + fun taskWithoutTimeProducesTwoAlarms() { + val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet()) + assertEquals(2, alarms.size) + assertTrue(alarms.any { it.taskId == "t1_am" }) + assertTrue(alarms.any { it.taskId == "t1_pm" }) + } + + @Test + fun defaultAlarmsAt8amAnd8pm() { + val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet()) + val am = alarms.first { it.taskId == "t1_am" } + val pm = alarms.first { it.taskId == "t1_pm" } + val expected8am = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() + val expected8pm = dueDate.atTime(LocalTime(20, 0)).toInstant(tz).toEpochMilliseconds() + assertEquals(expected8am, am.triggerAtMillis) + assertEquals(expected8pm, pm.triggerAtMillis) + } + + @Test + fun reminderDurationOffset30min() { + val t = task(time = LocalTime(15, 0), reminder = ReminderDuration(30, ReminderUnit.MIN)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + val reminder = alarms.first { it.taskId.endsWith("_remind") } + val expected = dueDate.atTime(LocalTime(15, 0)).toInstant(tz).toEpochMilliseconds() - 30 * 60_000L + assertEquals(expected, reminder.triggerAtMillis) + } + + @Test + fun reminderDurationOffset1hr() { + val t = task(time = LocalTime(10, 0), reminder = ReminderDuration(1, ReminderUnit.HR)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + val reminder = alarms.first { it.taskId.endsWith("_remind") } + val expected = dueDate.atTime(LocalTime(10, 0)).toInstant(tz).toEpochMilliseconds() - 3_600_000L + assertEquals(expected, reminder.triggerAtMillis) + } + + @Test + fun reminderDurationOffset1day() { + val t = task(reminder = ReminderDuration(1, ReminderUnit.DAY)) + // Set now to 2 days before due date so the 1-day reminder is in the future + val earlyNow = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds() - 2 * 86_400_000L + val alarms = ReminderScheduler.computeAlarms(listOf(t), earlyNow, tz, emptySet()) + val reminder = alarms.first { it.taskId.endsWith("_remind") } + val expected = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() - 86_400_000L + assertEquals(expected, reminder.triggerAtMillis) + } + + @Test + fun reminderDurationOffset1week() { + val t = task(reminder = ReminderDuration(1, ReminderUnit.WEEK)) + val earlyNow = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds() - 8 * 86_400_000L + val alarms = ReminderScheduler.computeAlarms(listOf(t), earlyNow, tz, emptySet()) + val reminder = alarms.first { it.taskId.endsWith("_remind") } + val expected = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() - 604_800_000L + assertEquals(expected, reminder.triggerAtMillis) + } + + @Test + fun reminderWithDueTimeProducesBothAlarms() { + val t = task(time = LocalTime(14, 0), reminder = ReminderDuration(30, ReminderUnit.MIN)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + // Should have: reminder alarm + due time alarm + assertEquals(2, alarms.size) + assertTrue(alarms.any { it.taskId.endsWith("_remind") }) + assertTrue(alarms.any { it.taskId == "t1" }) + } + + @Test + fun pastAlarmsNotScheduled() { + // now is after the due time + val lateNow = dueDate.atTime(LocalTime(23, 0)).toInstant(tz).toEpochMilliseconds() + val t = task(time = LocalTime(10, 0)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), lateNow, tz, emptySet()) + assertTrue(alarms.isEmpty()) + } + + @Test + fun pastReminderNotScheduledButDueTimeStillIs() { + // now is after the reminder time but before due time + val t = task(time = LocalTime(15, 0), reminder = ReminderDuration(2, ReminderUnit.HR)) + val midNow = dueDate.atTime(LocalTime(14, 0)).toInstant(tz).toEpochMilliseconds() + val alarms = ReminderScheduler.computeAlarms(listOf(t), midNow, tz, emptySet()) + // Reminder at 13:00 is past, due at 15:00 is future + assertEquals(1, alarms.size) + assertEquals("t1", alarms[0].taskId) + } + + @Test + fun multipleTasksProduceCorrectAlarms() { + val tasks = listOf( + task(id = "a", time = LocalTime(9, 0)), + task(id = "b", time = LocalTime(17, 0)), + task(id = "c", completed = true), + task(id = "d", date = null), + ) + val alarms = ReminderScheduler.computeAlarms(tasks, nowMillis, tz, emptySet()) + assertEquals(2, alarms.size) + assertTrue(alarms.any { it.taskId == "a" }) + assertTrue(alarms.any { it.taskId == "b" }) + } + + @Test + fun alarmLabelsCorrect() { + val t = task(time = LocalTime(14, 0), reminder = ReminderDuration(30, ReminderUnit.MIN)) + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + val reminder = alarms.first { it.taskId.endsWith("_remind") } + assertTrue(reminder.label.contains("reminder")) + val due = alarms.first { it.taskId == "t1" } + assertTrue(due.label.contains("due at")) + } + + @Test + fun defaultAlarmLabelsCorrect() { + val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet()) + val am = alarms.first { it.taskId == "t1_am" } + val pm = alarms.first { it.taskId == "t1_pm" } + assertEquals("due today", am.label) + assertTrue(pm.label.contains("still due")) + } + + @Test + fun taskTextPreservedInAlarm() { + val t = task().copy(text = "Buy groceries") + val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet()) + assertTrue(alarms.all { it.taskText == "Buy groceries" }) + } +} diff --git a/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt b/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt new file mode 100644 index 0000000..e2817c5 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt @@ -0,0 +1,5 @@ +package com.avinal.memos.util + +actual fun triggerReminderCheck() { + // TODO: iOS notification check +}