Skip to content

Commit

Permalink
feat: Include pre-set date search options (#835)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nikclayton authored Jul 24, 2024
1 parent bad502e commit 5d574d4
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 44 deletions.
84 changes: 63 additions & 21 deletions app/src/main/java/app/pachli/components/search/SearchActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<DateOperator>()?.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)))
}
}
}
}
Expand Down
66 changes: 50 additions & 16 deletions app/src/main/java/app/pachli/components/search/SearchOperator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,23 @@ sealed interface SearchOperatorViewData<out T : SearchOperator> {
data class DateOperatorViewData(override val operator: DateOperator) : SearchOperatorViewData<DateOperator> {
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),
)
}
}
Expand Down
101 changes: 101 additions & 0 deletions app/src/main/res/layout/search_operator_date_dialog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="4dp"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">

<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<RadioButton
android:id="@+id/radioAll"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_all"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />

<RadioButton
android:id="@+id/radioLastDay"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_today"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />

<RadioButton
android:id="@+id/radioLast7Days"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_7_days"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />

<RadioButton
android:id="@+id/radioLast30Days"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_30_days"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />

<RadioButton
android:id="@+id/radioLast6Months"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:text="@string/search_operator_date_dialog_last_6_months"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:ignore="RtlSymmetry" />

</RadioGroup>

<Button
android:id="@+id/buttonCustomRange"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/search_operator_date_dialog_custom_range"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radioGroup" />
</androidx.constraintlayout.widget.ConstraintLayout>
8 changes: 7 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,13 @@
<string name="search_operator_date_all">Dates ▾</string>
<string name="search_operator_date_checked">%1$s - %2$s</string>
<string name="search_operator_date_checked_same_day">On %1$s</string>
<string name="search_operator_date_dialog_title">Limit to posts between…</string>
<string name="search_operator_date_dialog_title">Limit to posts sent…</string>
<string name="search_operator_date_dialog_all">Any time</string>
<string name="search_operator_date_dialog_today">Today</string>
<string name="search_operator_date_dialog_last_7_days">Last 7 days</string>
<string name="search_operator_date_dialog_last_30_days">Last 30 days</string>
<string name="search_operator_date_dialog_last_6_months">Last 6 months</string>
<string name="search_operator_date_dialog_custom_range">Custom range</string>

<string name="search_operator_language_dialog_title">Limit to posts written in…</string>
<string name="search_operator_language_all">Any language ▾</string>
Expand Down

0 comments on commit 5d574d4

Please sign in to comment.