From 5d574d4d764e85d3bce60f9a76fc200a591844e1 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Wed, 24 Jul 2024 17:57:19 +0200 Subject: [PATCH] feat: Include pre-set date search options (#835) When selecting a search date range show the user a dialog with some pre-set options, and a button that allows them to pick a custom date range. --- .../components/search/SearchActivity.kt | 84 +++++++++++---- .../components/search/SearchOperator.kt | 66 +++++++++--- .../search/SearchOperatorViewData.kt | 16 +-- .../layout/search_operator_date_dialog.xml | 101 ++++++++++++++++++ app/src/main/res/values/strings.xml | 8 +- 5 files changed, 231 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/layout/search_operator_date_dialog.xml diff --git a/app/src/main/java/app/pachli/components/search/SearchActivity.kt b/app/src/main/java/app/pachli/components/search/SearchActivity.kt index 01536aa13b..ed99e7378e 100644 --- a/app/src/main/java/app/pachli/components/search/SearchActivity.kt +++ b/app/src/main/java/app/pachli/components/search/SearchActivity.kt @@ -35,7 +35,7 @@ import androidx.lifecycle.repeatOnLifecycle import app.pachli.R import app.pachli.components.compose.ComposeAutoCompleteAdapter import app.pachli.components.search.SearchOperator.DateOperator -import app.pachli.components.search.SearchOperator.DateOperator.DateRange +import app.pachli.components.search.SearchOperator.DateOperator.DateChoice import app.pachli.components.search.SearchOperator.FromOperator import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromAccount import app.pachli.components.search.SearchOperator.FromOperator.FromKind.FromMe @@ -94,6 +94,7 @@ import app.pachli.core.ui.extensions.reduceSwipeSensitivity import app.pachli.core.ui.makeIcon import app.pachli.databinding.ActivitySearchBinding import app.pachli.databinding.SearchOperatorAttachmentDialogBinding +import app.pachli.databinding.SearchOperatorDateDialogBinding import app.pachli.databinding.SearchOperatorFromDialogBinding import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding import com.github.michaelbull.result.get @@ -389,30 +390,71 @@ class SearchActivity : binding.chipDate.toggle() lifecycleScope.launch { - val picker = MaterialDatePicker.Builder.dateRangePicker() - .setTitleText(R.string.search_operator_date_dialog_title) - .setCalendarConstraints( - CalendarConstraints.Builder() - .setValidator(DateValidatorPointBackward.now()) - // Default behaviour is to show two months, with the current month - // at the top. This wastes space, as the user can't select beyond - // the current month, so open one month earlier to show this month - // and the previous month on screen. - .setOpenAt( - LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(), - ) - .build(), + val dialogBinding = SearchOperatorDateDialogBinding.inflate(layoutInflater, null, false) + val choice = viewModel.getOperator()?.choice - ) - .build() - .await(supportFragmentManager, "dateRangePicker") + dialogBinding.radioGroup.check( + when (choice) { + null -> R.id.radioAll + DateChoice.Today -> R.id.radioLastDay + DateChoice.Last7Days -> R.id.radioLast7Days + DateChoice.Last30Days -> R.id.radioLast30Days + DateChoice.Last6Months -> R.id.radioLast6Months + is DateChoice.DateRange -> -1 + }, + ) - picker ?: return@launch + val dialog = AlertDialog.Builder(this@SearchActivity) + .setView(dialogBinding.root) + .setTitle(R.string.search_operator_date_dialog_title) + .create() - val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate() - val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate() + // Wait until the dialog is shown to set up the custom range button click + // listener, as it needs a reference to the dialog to be able to dismiss + // it if appropriate. + dialog.setOnShowListener { + dialogBinding.buttonCustomRange.setOnClickListener { + launch { + val picker = MaterialDatePicker.Builder.dateRangePicker() + .setTitleText(R.string.search_operator_date_dialog_title) + .setCalendarConstraints( + CalendarConstraints.Builder() + .setValidator(DateValidatorPointBackward.now()) + // Default behaviour is to show two months, with the current month + // at the top. This wastes space, as the user can't select beyond + // the current month, so open one month earlier to show this month + // and the previous month on screen. + .setOpenAt( + LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli(), + ) + .build(), + ) + .build() + .await(supportFragmentManager, "dateRangePicker") + + picker ?: return@launch + + val after = Instant.ofEpochMilli(picker.first).atOffset(ZoneOffset.UTC).toLocalDate() + val before = Instant.ofEpochMilli(picker.second).atOffset(ZoneOffset.UTC).toLocalDate() + + viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(DateChoice.DateRange(after, before)))) + dialog.dismiss() + } + } + } + + val button = dialog.await(android.R.string.ok, android.R.string.cancel) - viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(DateRange(after, before)))) + if (button == AlertDialog.BUTTON_POSITIVE) { + val operator = when (dialogBinding.radioGroup.checkedRadioButtonId) { + R.id.radioLastDay -> DateChoice.Today + R.id.radioLast7Days -> DateChoice.Last7Days + R.id.radioLast30Days -> DateChoice.Last30Days + R.id.radioLast6Months -> DateChoice.Last6Months + else -> null + } + viewModel.replaceOperator(SearchOperatorViewData.from(DateOperator(operator))) + } } } } diff --git a/app/src/main/java/app/pachli/components/search/SearchOperator.kt b/app/src/main/java/app/pachli/components/search/SearchOperator.kt index 3360e9e85b..12c44285bb 100644 --- a/app/src/main/java/app/pachli/components/search/SearchOperator.kt +++ b/app/src/main/java/app/pachli/components/search/SearchOperator.kt @@ -19,7 +19,9 @@ package app.pachli.components.search import app.pachli.BuildConfig import app.pachli.util.modernLanguageCode +import java.time.Instant import java.time.LocalDate +import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.util.Locale @@ -111,26 +113,58 @@ sealed interface SearchOperator { } /** The date-range operator. Creates `after:... before:...`. */ - class DateOperator(override val choice: DateRange? = null) : SearchOperator { - /** - * The date range to search. - * - * @param startDate Earliest date to search (inclusive) - * @param endDate Latest date to search (inclusive) - */ - data class DateRange(val startDate: LocalDate, val endDate: LocalDate) { - // This class treats the date range as **inclusive** of the start and - // end dates, Mastodon's search treats the dates as exclusive, so the - // range must be expanded by one day in each direction when creating - // the search string. - fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}" - - companion object { - private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + class DateOperator(override val choice: DateChoice? = null) : SearchOperator { + sealed interface DateChoice { + fun fmt(): String + + data object Today : DateChoice { + override fun fmt(): String { + val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate() + return "after:${now.minusDays(1)}" + } + } + + data object Last7Days : DateChoice { + override fun fmt(): String { + val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate() + return "after:${now.minusDays(7)}" + } + } + + data object Last30Days : DateChoice { + override fun fmt(): String { + val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate() + return "after:${now.minusDays(30)}" + } + } + + data object Last6Months : DateChoice { + override fun fmt(): String { + val now = Instant.now().atOffset(ZoneOffset.UTC).toLocalDate() + return "after:${now.minusMonths(6)}" + } + } + + /** + * The date range to search. + * + * @param startDate Earliest date to search (inclusive) + * @param endDate Latest date to search (inclusive) + */ + data class DateRange(val startDate: LocalDate, val endDate: LocalDate) : DateChoice { + // This class treats the date range as **inclusive** of the start and + // end dates, Mastodon's search treats the dates as exclusive, so the + // range must be expanded by one day in each direction when creating + // the search string. + override fun fmt() = "after:${formatter.format(startDate.minusDays(1))} before:${formatter.format(endDate.plusDays(1))}" } } override fun query() = choice?.fmt() + + companion object { + private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + } } /** The `from:...` operator. */ diff --git a/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt b/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt index cd563ae149..37ca4c71b7 100644 --- a/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt +++ b/app/src/main/java/app/pachli/components/search/SearchOperatorViewData.kt @@ -185,19 +185,23 @@ sealed interface SearchOperatorViewData { data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData { private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - override fun chipLabel(context: Context) = when (operator.choice) { + override fun chipLabel(context: Context) = when (val choice = operator.choice) { null -> context.getString(R.string.search_operator_date_all) - else -> { - if (operator.choice.startDate == operator.choice.endDate) { + DateOperator.DateChoice.Today -> context.getString(R.string.search_operator_date_dialog_today) + DateOperator.DateChoice.Last7Days -> context.getString(R.string.search_operator_date_dialog_last_7_days) + DateOperator.DateChoice.Last30Days -> context.getString(R.string.search_operator_date_dialog_last_30_days) + DateOperator.DateChoice.Last6Months -> context.getString(R.string.search_operator_date_dialog_last_6_months) + is DateOperator.DateChoice.DateRange -> { + if (choice.startDate == choice.endDate) { context.getString( R.string.search_operator_date_checked_same_day, - formatter.format(operator.choice.startDate), + formatter.format(choice.startDate), ) } else { context.getString( R.string.search_operator_date_checked, - formatter.format(operator.choice.startDate), - formatter.format(operator.choice.endDate), + formatter.format(choice.startDate), + formatter.format(choice.endDate), ) } } diff --git a/app/src/main/res/layout/search_operator_date_dialog.xml b/app/src/main/res/layout/search_operator_date_dialog.xml new file mode 100644 index 0000000000..31e0c6684a --- /dev/null +++ b/app/src/main/res/layout/search_operator_date_dialog.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + +