Compare commits

..
12 changed files with 71 additions and 266 deletions

No files matched your search

+5 -4
View File
@@ -7,10 +7,11 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
### Added
- A "Description in reminders" setting under Notifications sets how much of a
task's description a reminder shows: none, a short preview of up to three
lines (the new default), or all of it as before (#156).
### Fixed
- With 3-button navigation, the navigation bar no longer covers the last home
search results, and in landscape it no longer covers the home screen, the
task list or the home top bar's buttons. The task details and editor no
longer leave an empty gap between the open keyboard and the text (#152).
## [1.3.1] - 2026-11-09
@@ -54,9 +54,6 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) {
TimeFormatPref.TWENTY_FOUR_HOUR -> true
}
/** How much of a task's description a due reminder shows when expanded. */
enum class ReminderDescription { HIDDEN, SHORT, FULL }
const val DEFAULT_SNOOZE_MINUTES = 10
/** Done starts folded away, to keep the open tasks in focus. */ val DEFAULT_COLLAPSED_SECTIONS: Set<TaskSection> = setOf(TaskSection.COMPLETED)
@@ -106,7 +103,6 @@ data class Settings(
val sortOrder: TaskSortOrder = TaskSortOrder.DUE,
/** How long a reminder's "Snooze" action puts it off. */
val snoozeMinutes: Int = DEFAULT_SNOOZE_MINUTES,
val reminderDescription: ReminderDescription = ReminderDescription.SHORT,
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
/** The first day of the week; `null` follows the locale. */
val weekStart: DayOfWeek? = null,
@@ -175,9 +171,6 @@ class SettingsPrefs @Inject constructor(
sortOrder = p[SORT_ORDER]?.let { runCatching { TaskSortOrder.valueOf(it) }.getOrNull() }
?: TaskSortOrder.DUE,
snoozeMinutes = p[SNOOZE_MINUTES]?.takeIf { it > 0 } ?: DEFAULT_SNOOZE_MINUTES,
reminderDescription = p[REMINDER_DESCRIPTION]
?.let { runCatching { ReminderDescription.valueOf(it) }.getOrNull() }
?: ReminderDescription.SHORT,
timeFormat = p[TIME_FORMAT]?.let { runCatching { TimeFormatPref.valueOf(it) }.getOrNull() }
?: TimeFormatPref.AUTO,
autofocusTitle = p[AUTOFOCUS_TITLE] ?: true,
@@ -206,9 +199,6 @@ class SettingsPrefs @Inject constructor(
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
suspend fun setReminderDescription(pref: ReminderDescription) =
dataStore.edit { it[REMINDER_DESCRIPTION] = pref.name }
suspend fun setSyncIntervalMinutes(minutes: Int) = dataStore.edit { it[SYNC_INTERVAL] = minutes }
suspend fun setPushEnabled(enabled: Boolean) = dataStore.edit { it[PUSH_ENABLED] = enabled }
@@ -355,7 +345,6 @@ class SettingsPrefs @Inject constructor(
val DEFAULT_EDIT_FIELDS = stringSetPreferencesKey("default_edit_fields")
val SORT_ORDER = stringPreferencesKey("sort_order")
val SNOOZE_MINUTES = intPreferencesKey("snooze_minutes")
val REMINDER_DESCRIPTION = stringPreferencesKey("reminder_description")
val TIME_FORMAT = stringPreferencesKey("time_format")
val WEEK_START = stringPreferencesKey("week_start")
val AUTOFOCUS_TITLE = booleanPreferencesKey("autofocus_title")
@@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat
import androidx.core.content.edit
import de.jeanlucmakiola.agendula.MainActivity
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
import de.jeanlucmakiola.agendula.data.prefs.Settings
import de.jeanlucmakiola.agendula.domain.readableDescription
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
@@ -87,11 +86,7 @@ class TaskNotifier @Inject constructor(
),
)
}
val description = reminderBody(
task.description?.readableDescription(),
settings.reminderDescription,
lineChars = (NOTIFICATION_LINE_CHARS / context.resources.configuration.fontScale).toInt(),
)
val description = task.description?.readableDescription()?.trim()?.takeIf { it.isNotBlank() }
val tapIntent = PendingIntent.getActivity(
context,
task.taskId.toInt(),
@@ -282,96 +277,3 @@ internal fun expandedText(dueLine: String?, description: String?): String? = whe
dueLine == null -> description
else -> "$dueLine\n\n$description"
}
/** Rough characters per line of an expanded notification on a phone, at the default font size. */
private const val NOTIFICATION_LINE_CHARS = 40
/** The description a reminder shows, as [mode] asks; null when there is none to show. */
internal fun reminderBody(
description: String?,
mode: ReminderDescription,
lineChars: Int = NOTIFICATION_LINE_CHARS,
): String? {
val text = description?.trim()?.takeIf { it.isNotBlank() } ?: return null
return when (mode) {
ReminderDescription.HIDDEN -> null
ReminderDescription.SHORT -> shortDescription(text, lineChars = lineChars)
ReminderDescription.FULL -> text
}
}
/**
* [description] cut to about [maxLines] lines of an expanded notification,
* ending in an ellipsis when anything was left out. Blank lines are dropped,
* and a long line counts as the lines it wraps to.
*/
internal fun shortDescription(
description: String,
maxLines: Int = 3,
lineChars: Int = NOTIFICATION_LINE_CHARS,
): String {
val perLine = lineChars.coerceAtLeast(2)
val lines = description.lines().map { it.trimEnd() }.filter { it.isNotBlank() }
val kept = mutableListOf<String>()
var rowsLeft = maxLines
for ((index, line) in lines.withIndex()) {
val width = displayWidth(line)
val rows = (width + perLine - 1) / perLine
val last = index == lines.lastIndex
// The ellipsis after a line that ends the budget needs a cell of its own.
val fits = rows < rowsLeft || (rows == rowsLeft && (last || width < rows * perLine))
if (fits) {
kept += line
rowsLeft -= rows
if (rowsLeft == 0 && !last) return ellipsize(kept)
continue
}
if (rowsLeft > 0) kept += cutAtWord(line, rowsLeft * perLine - 1)
return ellipsize(kept)
}
return kept.joinToString("\n")
}
private fun ellipsize(lines: List<String>): String =
lines.joinToString("\n").trimEnd { it.isWhitespace() || it in ".,;:" } + "\u2026"
/** [line] cut to at most [max] cells, at a word break when there is one, never inside a character. */
private fun cutAtWord(line: String, max: Int): String {
var end = 0
var width = 0
while (end < line.length) {
val codePoint = line.codePointAt(end)
width += cellsOf(codePoint)
if (width > max) break
end += Character.charCount(codePoint)
}
if (end >= line.length || line[end].isWhitespace()) return line.substring(0, end)
val cut = line.substring(0, end)
val space = cut.indexOfLast { it.isWhitespace() }
return if (space > end / 2) cut.substring(0, space) else cut
}
/** How many cells [text] takes, wide characters (CJK, emoji) counting two. */
private fun displayWidth(text: String): Int {
var width = 0
var i = 0
while (i < text.length) {
val codePoint = text.codePointAt(i)
width += cellsOf(codePoint)
i += Character.charCount(codePoint)
}
return width
}
private fun cellsOf(codePoint: Int): Int = when {
Character.isSupplementaryCodePoint(codePoint) -> 2
Character.UnicodeScript.of(codePoint) in WIDE_SCRIPTS -> 2
else -> 1
}
private val WIDE_SCRIPTS = setOf(
Character.UnicodeScript.HAN,
Character.UnicodeScript.HIRAGANA,
Character.UnicodeScript.KATAKANA,
Character.UnicodeScript.HANGUL,
)
@@ -0,0 +1,27 @@
package de.jeanlucmakiola.agendula.ui.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
/**
* Pads by the start/end of a scaffold's [inner] padding and consumes it, so a
* side navigation bar or cutout (3-button nav in landscape) is cleared once.
*/
@Composable
fun Modifier.sideInsetsPadding(inner: PaddingValues): Modifier {
val layoutDirection = LocalLayoutDirection.current
val sides = PaddingValues(
start = inner.calculateStartPadding(layoutDirection),
top = 0.dp,
end = inner.calculateEndPadding(layoutDirection),
bottom = 0.dp,
)
return this.padding(sides).consumeWindowInsets(sides)
}
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -268,6 +269,9 @@ private fun DetailBody(
modifier = Modifier
.fillMaxSize()
.padding(inner)
// Consume the bar insets so imePadding adds only what the keyboard
// covers beyond them.
.consumeWindowInsets(inner)
// Shrink the scroll viewport by the keyboard so the focused add-subtask
// field scrolls into view above the IME instead of being hidden.
.imePadding()
@@ -57,6 +57,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -219,7 +220,9 @@ fun TaskEditScreen(
viewModel = viewModel,
scroll = scroll,
onTitleBottom = { titleBottom = it },
modifier = Modifier.padding(inner),
// Consumed so EditContent's imePadding adds only what the keyboard
// covers beyond the bars.
modifier = Modifier.padding(inner).consumeWindowInsets(inner),
)
}
@@ -29,7 +29,16 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@@ -99,6 +108,7 @@ import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
import de.jeanlucmakiola.agendula.ui.common.ShapedActionButton
import de.jeanlucmakiola.agendula.ui.common.SyncPullBox
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
import de.jeanlucmakiola.agendula.ui.common.sideInsetsPadding
import de.jeanlucmakiola.agendula.ui.tasklist.priorityLabel
import de.jeanlucmakiola.floret.components.positionOf
import de.jeanlucmakiola.floret.time.formatDateTimeCompact
@@ -180,7 +190,10 @@ fun ListsScreen(
available = syncAvailable && !searchActive,
syncing = syncing,
onSync = viewModel::syncAll,
modifier = Modifier.fillMaxSize().padding(top = inner.calculateTopPadding()),
modifier = Modifier
.fillMaxSize()
.padding(top = inner.calculateTopPadding())
.sideInsetsPadding(inner),
) {
when (val s = state) {
ListsUiState.Loading -> Unit // brief; avoids a flash before first emission
@@ -513,7 +526,10 @@ private fun HomeTopBar(
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.windowInsetsPadding(
WindowInsets.systemBars.union(WindowInsets.displayCutout)
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
)
.height(72.dp)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -672,10 +688,14 @@ private fun SearchResults(
)
}
} else {
// The last result scrolls clear of the navigation bar and the keyboard.
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(results, key = { it.occurrenceKey }) { task ->
UpcomingRow(task = task, onClick = { onOpenTask(task) })
}
item(key = "bottom-inset") {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars.union(WindowInsets.ime)))
}
}
}
}
@@ -39,7 +39,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
import de.jeanlucmakiola.agendula.data.prefs.SNOOZE_PRESETS
import de.jeanlucmakiola.agendula.data.reminders.TaskNotifier
import de.jeanlucmakiola.agendula.domain.TaskReminderOrder
@@ -114,7 +113,6 @@ internal fun NotificationsScreen(
var showOffset by remember { mutableStateOf(false) }
var showAllDay by remember { mutableStateOf(false) }
var showSnooze by remember { mutableStateOf(false) }
var showDescription by remember { mutableStateOf(false) }
var showAllDayTime by remember { mutableStateOf(false) }
var perListExpanded by remember { mutableStateOf(false) }
var expandedLists by remember { mutableStateOf(emptySet<Long>()) }
@@ -180,14 +178,8 @@ internal fun NotificationsScreen(
R.string.settings_allday_reminder_time_summary,
formatMinuteOfDay(state.settings.allDayReminderMinuteOfDay, LocalUse24HourFormat.current),
),
position = Position.Middle,
onClick = { showAllDayTime = true },
)
GroupedRow(
title = stringResource(R.string.settings_reminder_description),
summary = reminderDescriptionLabel(state.settings.reminderDescription),
position = Position.Bottom,
onClick = { showDescription = true },
onClick = { showAllDayTime = true },
)
Spacer(Modifier.height(24.dp))
@@ -326,17 +318,6 @@ internal fun NotificationsScreen(
onDismiss = { showSnooze = false },
)
}
if (showDescription) {
OptionPicker(
title = stringResource(R.string.settings_reminder_description),
options = ReminderDescription.entries,
selected = state.settings.reminderDescription,
label = { reminderDescriptionLabel(it) },
summary = { reminderDescriptionSummary(it) },
onSelect = viewModel::setReminderDescription,
onDismiss = { showDescription = false },
)
}
if (showAllDayTime) {
val minute = state.settings.allDayReminderMinuteOfDay
TimePickerAlert(
@@ -373,24 +354,6 @@ private fun snoozeLabel(minutes: Int): String =
if (minutes % 60 == 0) pluralStringResource(R.plurals.duration_hours, minutes / 60, minutes / 60)
else pluralStringResource(R.plurals.duration_minutes, minutes, minutes)
@Composable
private fun reminderDescriptionLabel(pref: ReminderDescription): String = stringResource(
when (pref) {
ReminderDescription.HIDDEN -> R.string.settings_reminder_description_hidden
ReminderDescription.SHORT -> R.string.settings_reminder_description_short
ReminderDescription.FULL -> R.string.settings_reminder_description_full
},
)
@Composable
private fun reminderDescriptionSummary(pref: ReminderDescription): String = stringResource(
when (pref) {
ReminderDescription.HIDDEN -> R.string.settings_reminder_description_hidden_summary
ReminderDescription.SHORT -> R.string.settings_reminder_description_short_summary
ReminderDescription.FULL -> R.string.settings_reminder_description_full_summary
},
)
/** Which list's override picker is open, and for which kind of task. */
private data class OverrideTarget(val listId: Long, val allDay: Boolean)
@@ -6,7 +6,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import de.jeanlucmakiola.agendula.data.appname.LauncherName
import de.jeanlucmakiola.agendula.data.appname.LauncherNameManager
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
import de.jeanlucmakiola.agendula.data.prefs.Settings
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
@@ -260,7 +259,6 @@ class SettingsViewModel @Inject constructor(
fun setTimeFormat(pref: TimeFormatPref) = viewModelScope.launch { prefs.setTimeFormat(pref) }
fun setWeekStart(day: DayOfWeek?) = viewModelScope.launch { prefs.setWeekStart(day) }
fun setSnoozeMinutes(minutes: Int) = viewModelScope.launch { prefs.setSnoozeMinutes(minutes) }
fun setReminderDescription(pref: ReminderDescription) = viewModelScope.launch { prefs.setReminderDescription(pref) }
fun setShowAddSubtaskRow(show: Boolean) = viewModelScope.launch { prefs.setShowAddSubtaskRow(show) }
fun setBottomAddBar(enabled: Boolean) = viewModelScope.launch { prefs.setBottomAddBar(enabled) }
fun setDefaultStartToday(enabled: Boolean) = viewModelScope.launch { prefs.setDefaultStartToday(enabled) }
@@ -115,6 +115,7 @@ import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
import de.jeanlucmakiola.agendula.ui.common.SyncPullBox
import de.jeanlucmakiola.agendula.ui.common.ignoringEdgeSwipes
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
import de.jeanlucmakiola.agendula.ui.common.sideInsetsPadding
import de.jeanlucmakiola.agendula.ui.lists.ListEditorSheet
import de.jeanlucmakiola.agendula.ui.lists.listWriteFailureMessage
import de.jeanlucmakiola.agendula.ui.lists.ListWriteFailure
@@ -250,7 +251,7 @@ fun TaskListScreen(
}
},
) { inner ->
Box(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize().sideInsetsPadding(inner)) {
SyncPullBox(
available = syncAvailable,
syncing = syncing,
@@ -260,7 +261,10 @@ fun TaskListScreen(
) {
when (val s = state) {
TaskListUiState.Loading -> Unit // brief; avoids a flash before first emission
TaskListUiState.Failure -> CenteredMessage(stringResource(R.string.task_list_failure), inner)
TaskListUiState.Failure -> CenteredMessage(
stringResource(R.string.task_list_failure),
PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding()),
)
is TaskListUiState.Content -> TaskListBody(
s, filter, inner, onOpenTask, onRequestDelete, viewModel,
onClearCompleted = if (listId != null && list?.isReadOnly != true) ({ confirmClear = true }) else null,
-7
View File
@@ -470,13 +470,6 @@
<string name="settings_exact_alarms_blocked">Blocked — tap to allow exact reminders</string>
<string name="settings_reliable_delivery">Reliable delivery</string>
<string name="settings_snooze_duration">Snooze duration</string>
<string name="settings_reminder_description">Description in reminders</string>
<string name="settings_reminder_description_hidden">Hidden</string>
<string name="settings_reminder_description_short">Short</string>
<string name="settings_reminder_description_full">Full</string>
<string name="settings_reminder_description_hidden_summary">Only the title and due time</string>
<string name="settings_reminder_description_short_summary">A short preview of the description</string>
<string name="settings_reminder_description_full_summary">The whole description</string>
<string name="settings_reliable_delivery_hint">Android may delay reminders to save battery. Exempt Agendula so they arrive on time.</string>
<string name="settings_reliable_delivery_exempt">Exempt from battery optimisation — reminders arrive on time.</string>
<string name="settings_default_list">Default list</string>
@@ -1,99 +0,0 @@
package de.jeanlucmakiola.agendula.data.reminders
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
import org.junit.jupiter.api.Test
class ShortDescriptionTest {
@Test
fun `short text is left alone`() {
assertThat(shortDescription("Buy oat milk")).isEqualTo("Buy oat milk")
}
@Test
fun `three short lines fit`() {
assertThat(shortDescription("one\ntwo\nthree")).isEqualTo("one\ntwo\nthree")
}
@Test
fun `lines past the third are cut with an ellipsis`() {
assertThat(shortDescription("one\ntwo\nthree\nfour\nfive")).isEqualTo("one\ntwo\nthree…")
}
@Test
fun `blank lines take no room`() {
assertThat(shortDescription("one\n\n\ntwo\n \nthree")).isEqualTo("one\ntwo\nthree")
}
@Test
fun `a long paragraph is cut at a word within the line budget`() {
val text = List(40) { "word" }.joinToString(" ")
val short = shortDescription(text, maxLines = 3, lineChars = 40)
assertThat(short).endsWith("word…")
assertThat(short.length).isAtMost(3 * 40)
}
@Test
fun `a wrapping line counts as the lines it takes`() {
val long = "a".repeat(70)
val short = shortDescription("$long\nsecond\nthird", maxLines = 3, lineChars = 40)
assertThat(short).isEqualTo("$long\nsecond…")
}
@Test
fun `a word with no break is cut hard`() {
val short = shortDescription("x".repeat(200), maxLines = 3, lineChars = 40)
assertThat(short).isEqualTo("x".repeat(119) + "…")
}
@Test
fun `trailing punctuation gives way to the ellipsis`() {
assertThat(shortDescription("one.\ntwo,\nthree.\nfour")).isEqualTo("one.\ntwo,\nthree…")
}
@Test
fun `a line that fills the budget exactly leaves room for the ellipsis`() {
val full = "a".repeat(120)
val short = shortDescription("$full\nmore", maxLines = 3, lineChars = 40)
assertThat(short).isEqualTo("a".repeat(119) + "…")
}
@Test
fun `a cut never splits an emoji`() {
val short = shortDescription("x".repeat(118) + "😀" + "y".repeat(10), maxLines = 3, lineChars = 40)
assertThat(short).isEqualTo("x".repeat(118) + "…")
}
@Test
fun `a word that ends right at the cut is kept`() {
val short = shortDescription("aaaa bbbb cccc", maxLines = 1, lineChars = 10)
assertThat(short).isEqualTo("aaaa bbbb…")
}
@Test
fun `tabs count as word breaks`() {
val short = shortDescription("Meeting\tagenda\tnotes", maxLines = 1, lineChars = 12)
assertThat(short).isEqualTo("Meeting…")
}
@Test
fun `no space is left before the ellipsis`() {
assertThat(shortDescription("see list :\nmore", maxLines = 1, lineChars = 40)).isEqualTo("see list…")
}
@Test
fun `wide characters take two cells`() {
val short = shortDescription("中".repeat(30), maxLines = 1, lineChars = 20)
assertThat(short).isEqualTo("中".repeat(9) + "…")
}
@Test
fun `the setting picks what the reminder shows`() {
val long = List(60) { "word" }.joinToString(" ")
assertThat(reminderBody(long, ReminderDescription.HIDDEN)).isNull()
assertThat(reminderBody(long, ReminderDescription.FULL)).isEqualTo(long)
assertThat(reminderBody(long, ReminderDescription.SHORT)).isEqualTo(shortDescription(long))
assertThat(reminderBody(" ", ReminderDescription.FULL)).isNull()
}
}