From 40ac5cb977c23011b754c681cab148a1bf64a1dd Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:36:10 -0700 Subject: [PATCH 1/6] refactor: move history filters into overflow menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the expandable filter bar (#298) and folds its actions into the existing three-dots overflow on the activity screen. Pending toggle is now binary (Show/Hide pending); the obscure pending-only mode is dropped. Date filter opens a bottom sheet with presets (All time, Last 7/30 days, This month, Custom range). Active filter state surfaces as a subtitle under the toolbar title (e.g. "Mar 15 - Apr 20 · Pending shown") instead of duplicating state in inline buttons. Migrates legacy SharedPreferences key `filter_state` (Int) to `hide_pending` (Boolean) once on first launch. Bottom sheet uses setFragmentResult so selections survive rotation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/history/DateFilterBottomSheet.kt | 119 ++++++++++ .../history/PaymentsHistoryActivity.kt | 214 ++++++++---------- app/src/main/res/drawable/ic_calendar.xml | 10 + app/src/main/res/layout/activity_history.xml | 137 +++-------- .../res/layout/bottom_sheet_date_filter.xml | 154 +++++++++++++ .../main/res/menu/menu_activity_history.xml | 10 + app/src/main/res/menu/menu_filter_date.xml | 9 - app/src/main/res/menu/menu_filter_status.xml | 14 -- .../main/res/values-es/strings_history.xml | 10 - .../main/res/values-ja/strings_history.xml | 10 - .../main/res/values-ko/strings_history.xml | 10 - .../main/res/values-pt/strings_history.xml | 10 - app/src/main/res/values/strings_history.xml | 23 +- 13 files changed, 435 insertions(+), 295 deletions(-) create mode 100644 app/src/main/java/com/electricdreams/numo/feature/history/DateFilterBottomSheet.kt create mode 100644 app/src/main/res/drawable/ic_calendar.xml create mode 100644 app/src/main/res/layout/bottom_sheet_date_filter.xml delete mode 100644 app/src/main/res/menu/menu_filter_date.xml delete mode 100644 app/src/main/res/menu/menu_filter_status.xml diff --git a/app/src/main/java/com/electricdreams/numo/feature/history/DateFilterBottomSheet.kt b/app/src/main/java/com/electricdreams/numo/feature/history/DateFilterBottomSheet.kt new file mode 100644 index 000000000..050fcfbb5 --- /dev/null +++ b/app/src/main/java/com/electricdreams/numo/feature/history/DateFilterBottomSheet.kt @@ -0,0 +1,119 @@ +package com.electricdreams.numo.feature.history + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult +import com.electricdreams.numo.R +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import java.util.Calendar + +class DateFilterBottomSheet : BottomSheetDialogFragment() { + + override fun getTheme(): Int = R.style.Theme_Numo_BottomSheet + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.bottom_sheet_date_filter, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val currentStart = arguments?.getLong(ARG_CURRENT_START, 0L) ?: 0L + val currentEnd = arguments?.getLong(ARG_CURRENT_END, 0L) ?: 0L + if (currentStart == 0L && currentEnd == 0L) { + view.findViewById(R.id.check_all_time).visibility = View.VISIBLE + } + + view.findViewById(R.id.row_all_time).setOnClickListener { send(0L, 0L) } + view.findViewById(R.id.row_last_7).setOnClickListener { + val (s, e) = preset(daysBack = 7) + send(s, e) + } + view.findViewById(R.id.row_last_30).setOnClickListener { + val (s, e) = preset(daysBack = 30) + send(s, e) + } + view.findViewById(R.id.row_this_month).setOnClickListener { + send(startOfThisMonthLocal(), System.currentTimeMillis()) + } + view.findViewById(R.id.row_custom).setOnClickListener { + send(SENTINEL_CUSTOM, 0L) + } + + setupBottomSheetBehavior() + } + + private fun setupBottomSheetBehavior() { + dialog?.setOnShowListener { dialogInterface -> + val bottomSheetDialog = dialogInterface as BottomSheetDialog + val bottomSheet = bottomSheetDialog.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) ?: return@setOnShowListener + bottomSheet.setBackgroundColor( + ContextCompat.getColor(requireContext(), R.color.color_bg_white) + ) + BottomSheetBehavior.from(bottomSheet).apply { + state = BottomSheetBehavior.STATE_EXPANDED + skipCollapsed = true + isDraggable = true + } + } + } + + private fun send(start: Long, end: Long) { + setFragmentResult( + RESULT_KEY, + bundleOf(RESULT_START to start, RESULT_END to end) + ) + dismiss() + } + + private fun preset(daysBack: Int): Pair { + val now = System.currentTimeMillis() + val start = Calendar.getInstance().apply { + timeInMillis = now + add(Calendar.DAY_OF_YEAR, -daysBack) + set(Calendar.HOUR_OF_DAY, 0) + clear(Calendar.MINUTE) + clear(Calendar.SECOND) + clear(Calendar.MILLISECOND) + }.timeInMillis + return start to now + } + + private fun startOfThisMonthLocal(): Long = Calendar.getInstance().apply { + set(Calendar.DAY_OF_MONTH, 1) + set(Calendar.HOUR_OF_DAY, 0) + clear(Calendar.MINUTE) + clear(Calendar.SECOND) + clear(Calendar.MILLISECOND) + }.timeInMillis + + companion object { + const val TAG = "DateFilterBottomSheet" + const val RESULT_KEY = "date_filter_result" + const val RESULT_START = "start" + const val RESULT_END = "end" + const val SENTINEL_CUSTOM = -1L + + private const val ARG_CURRENT_START = "current_start" + private const val ARG_CURRENT_END = "current_end" + + fun newInstance(currentStart: Long, currentEnd: Long): DateFilterBottomSheet = + DateFilterBottomSheet().apply { + arguments = bundleOf( + ARG_CURRENT_START to currentStart, + ARG_CURRENT_END to currentEnd, + ) + } + } +} diff --git a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt index 8753fc9fa..f370a22b3 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt @@ -11,6 +11,7 @@ import android.os.Bundle import com.electricdreams.numo.util.createProgressDialog import com.electricdreams.numo.util.startActivityForResultCompat import android.view.View +import android.widget.TextView import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -66,8 +67,6 @@ class PaymentsHistoryActivity : AppCompatActivity() { } } - private var isFiltersExpanded = false - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityHistoryBinding.inflate(layoutInflater) @@ -95,7 +94,9 @@ class PaymentsHistoryActivity : AppCompatActivity() { binding.historyRecyclerView.adapter = adapter binding.historyRecyclerView.layoutManager = LinearLayoutManager(this) - setupFilterBar() + migrateLegacyFilterStateIfNeeded() + registerDateFilterResultListener() + updateToolbarSubtitle() // Load and display history loadHistory() @@ -333,75 +334,80 @@ class PaymentsHistoryActivity : AppCompatActivity() { .show() } - private fun setupFilterBar() { + private fun migrateLegacyFilterStateIfNeeded() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - updateFilterButtonTexts() + if (!prefs.contains(LEGACY_KEY_FILTER_STATE) || prefs.contains(KEY_HIDE_PENDING)) return + val legacy = prefs.getInt(LEGACY_KEY_FILTER_STATE, LEGACY_FILTER_PAID) + val hidePending = legacy == LEGACY_FILTER_PAID + prefs.edit() + .putBoolean(KEY_HIDE_PENDING, hidePending) + .remove(LEGACY_KEY_FILTER_STATE) + .apply() + } - binding.filterHeader?.setOnClickListener { - toggleFilters() - } + private fun togglePending() { + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + val current = prefs.getBoolean(KEY_HIDE_PENDING, true) + prefs.edit().putBoolean(KEY_HIDE_PENDING, !current).apply() + updateToolbarSubtitle() + loadHistory() + } - binding.btnFilterStatus?.setOnClickListener { view -> - val popup = PopupMenu(this, view) - popup.menuInflater.inflate(R.menu.menu_filter_status, popup.menu) - - val filterState = prefs.getInt(KEY_FILTER_STATE, FILTER_PAID) - when (filterState) { - FILTER_PAID -> popup.menu.findItem(R.id.filter_status_paid)?.isChecked = true - FILTER_PENDING -> popup.menu.findItem(R.id.filter_status_pending)?.isChecked = true - FILTER_ALL -> popup.menu.findItem(R.id.filter_status_all)?.isChecked = true - } + private fun showDateFilterSheet() { + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + val currentStart = prefs.getLong(KEY_FILTER_DATE_START, 0L) + val currentEnd = prefs.getLong(KEY_FILTER_DATE_END, 0L) + DateFilterBottomSheet + .newInstance(currentStart, currentEnd) + .show(supportFragmentManager, DateFilterBottomSheet.TAG) + } - popup.setOnMenuItemClickListener { item -> - val newState = when (item.itemId) { - R.id.filter_status_paid -> FILTER_PAID - R.id.filter_status_pending -> FILTER_PENDING - R.id.filter_status_all -> FILTER_ALL - else -> FILTER_PAID - } - prefs.edit().putInt(KEY_FILTER_STATE, newState).apply() - updateFilterButtonTexts() - loadHistory() - true - } - popup.show() - } - - binding.btnFilterDate?.setOnClickListener { view -> - val popup = PopupMenu(this, view) - popup.menuInflater.inflate(R.menu.menu_filter_date, popup.menu) - - popup.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.filter_date_all -> { - prefs.edit() - .putLong(KEY_FILTER_DATE_START, 0L) - .putLong(KEY_FILTER_DATE_END, 0L) - .apply() - updateFilterButtonTexts() - loadHistory() - true - } - R.id.filter_date_custom -> { - showDateRangePicker() - true - } - else -> false - } + private fun registerDateFilterResultListener() { + supportFragmentManager.setFragmentResultListener( + DateFilterBottomSheet.RESULT_KEY, + this + ) { _, bundle -> + val start = bundle.getLong(DateFilterBottomSheet.RESULT_START, 0L) + val end = bundle.getLong(DateFilterBottomSheet.RESULT_END, 0L) + if (start == DateFilterBottomSheet.SENTINEL_CUSTOM) { + showDateRangePicker() + return@setFragmentResultListener } - popup.show() + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + prefs.edit() + .putLong(KEY_FILTER_DATE_START, start) + .putLong(KEY_FILTER_DATE_END, end) + .apply() + updateToolbarSubtitle() + loadHistory() } } - private fun toggleFilters() { - isFiltersExpanded = !isFiltersExpanded - - if (isFiltersExpanded) { - binding.filtersContainer?.visibility = View.VISIBLE - binding.filterExpandIcon?.animate()?.rotation(180f)?.setDuration(200)?.start() + private fun updateToolbarSubtitle() { + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + val hidePending = prefs.getBoolean(KEY_HIDE_PENDING, true) + val start = prefs.getLong(KEY_FILTER_DATE_START, 0L) + val end = prefs.getLong(KEY_FILTER_DATE_END, 0L) + + val parts = mutableListOf() + if (start > 0L && end > 0L) { + val format = SimpleDateFormat("MMM d", Locale.getDefault()) + parts += getString( + R.string.history_filter_date_range_format, + format.format(Date(start)), + format.format(Date(end)) + ) + } + if (!hidePending) { + parts += getString(R.string.history_subtitle_pending_shown) + } + + val subtitle = findViewById(R.id.toolbar_subtitle) + if (parts.isEmpty()) { + subtitle.visibility = View.GONE } else { - binding.filtersContainer?.visibility = View.GONE - binding.filterExpandIcon?.animate()?.rotation(0f)?.setDuration(200)?.start() + subtitle.text = parts.joinToString(getString(R.string.history_subtitle_separator)) + subtitle.visibility = View.VISIBLE } } @@ -457,64 +463,36 @@ class PaymentsHistoryActivity : AppCompatActivity() { .putLong(KEY_FILTER_DATE_START, selection.first) .putLong(KEY_FILTER_DATE_END, selection.second) .apply() - updateFilterButtonTexts() + updateToolbarSubtitle() loadHistory() } picker.show(supportFragmentManager, "DATE_RANGE_PICKER") } - private fun updateFilterButtonTexts() { - val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - - val filterState = prefs.getInt(KEY_FILTER_STATE, FILTER_PAID) - val statusText = when (filterState) { - FILTER_PAID -> getString(R.string.history_menu_filter_paid) - FILTER_PENDING -> getString(R.string.history_menu_filter_pending) - FILTER_ALL -> getString(R.string.history_menu_filter_all) - else -> getString(R.string.history_menu_filter_paid) - } - binding.btnFilterStatus?.text = getString(R.string.history_filter_status_format, statusText) - - val start = prefs.getLong(KEY_FILTER_DATE_START, 0L) - val end = prefs.getLong(KEY_FILTER_DATE_END, 0L) - - var dateText = "" - if (start > 0 && end > 0) { - val format = SimpleDateFormat("MMM d", Locale.getDefault()) - val startStr = format.format(Date(start)) - val endStr = format.format(Date(end)) - dateText = getString(R.string.history_filter_date_range_format, startStr, endStr) - binding.btnFilterDate?.text = getString(R.string.history_filter_date_format, dateText) - } else { - dateText = getString(R.string.history_filter_date_all) - binding.btnFilterDate?.text = getString(R.string.history_filter_date_format, dateText) - } - - // Update the main header text to reflect active filters if any - if (filterState == FILTER_ALL && start == 0L && end == 0L) { - binding.filterHeaderText?.text = getString(R.string.history_menu_filter) - binding.filterHeaderText?.setTextColor(getColor(R.color.color_text_secondary)) - binding.filterExpandIcon?.setColorFilter(getColor(R.color.color_text_secondary)) - } else { - val activeFilters = mutableListOf() - if (filterState != FILTER_ALL) activeFilters.add(statusText) - if (start > 0 || end > 0) activeFilters.add(dateText) - - val summary = activeFilters.joinToString(", ") - binding.filterHeaderText?.text = summary - - // Highlight the text to indicate active filters - binding.filterHeaderText?.setTextColor(getColor(R.color.color_text_primary)) - binding.filterExpandIcon?.setColorFilter(getColor(R.color.color_text_primary)) - } - } - private fun showOverflowMenu(anchor: View) { + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) val popup = PopupMenu(this, anchor, android.view.Gravity.END) popup.menuInflater.inflate(R.menu.menu_activity_history, popup.menu) + val hidePending = prefs.getBoolean(KEY_HIDE_PENDING, true) + popup.menu.findItem(R.id.menu_toggle_pending)?.apply { + title = getString( + if (hidePending) R.string.history_overflow_show_pending + else R.string.history_overflow_hide_pending + ) + setIcon(if (hidePending) R.drawable.ic_visibility else R.drawable.ic_visibility_off) + } + popup.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { + R.id.menu_toggle_pending -> { + togglePending() + true + } + R.id.menu_filter_date -> { + showDateFilterSheet() + true + } R.id.menu_export_activity -> { val dateStr = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) csvExportLauncher.launch("numo_activity_export_$dateStr.csv") @@ -528,7 +506,7 @@ class PaymentsHistoryActivity : AppCompatActivity() { private fun loadHistory() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - val filterState = prefs.getInt(KEY_FILTER_STATE, FILTER_PAID) // Hide pending by default + val hidePending = prefs.getBoolean(KEY_HIDE_PENDING, true) val filterStart = prefs.getLong(KEY_FILTER_DATE_START, 0L) val filterEnd = prefs.getLong(KEY_FILTER_DATE_END, 0L) @@ -541,11 +519,8 @@ class PaymentsHistoryActivity : AppCompatActivity() { var filteredList = (paymentHistory + withdrawHistory) .sortedByDescending { it.date.time } - // Apply Status Filter - if (filterState == FILTER_PAID) { + if (hidePending) { filteredList = filteredList.filterNot { it.isPending() } - } else if (filterState == FILTER_PENDING) { - filteredList = filteredList.filter { it.isPending() } } // Apply Date Filter @@ -554,7 +529,7 @@ class PaymentsHistoryActivity : AppCompatActivity() { val endOfDay = filterEnd + 86400000L - 1L filteredList = filteredList.filter { it.date.time in filterStart..endOfDay } } - + currentHistoryList = filteredList adapter.setEntries(currentHistoryList) @@ -589,12 +564,11 @@ class PaymentsHistoryActivity : AppCompatActivity() { companion object { private const val PREFS_NAME = "PaymentHistory" private const val KEY_HISTORY = "history" - private const val KEY_FILTER_STATE = "filter_state" + private const val KEY_HIDE_PENDING = "hide_pending" private const val KEY_FILTER_DATE_START = "filter_date_start" private const val KEY_FILTER_DATE_END = "filter_date_end" - private const val FILTER_ALL = 0 - private const val FILTER_PAID = 1 - private const val FILTER_PENDING = 2 + private const val LEGACY_KEY_FILTER_STATE = "filter_state" + private const val LEGACY_FILTER_PAID = 1 private const val REQUEST_TRANSACTION_DETAIL = 1001 private const val REQUEST_RESUME_PAYMENT = 1002 diff --git a/app/src/main/res/drawable/ic_calendar.xml b/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 000000000..1253df687 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml index fa1ec6832..4872dfd82 100644 --- a/app/src/main/res/layout/activity_history.xml +++ b/app/src/main/res/layout/activity_history.xml @@ -31,15 +31,38 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintEnd_toStartOf="@id/overflow_button" + app:layout_constraintStart_toEndOf="@id/back_button" + app:layout_constraintTop_toTopOf="parent"> + + + + + + - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/balance_section" /> + app:layout_constraintTop_toBottomOf="@id/balance_section" /> diff --git a/app/src/main/res/layout/bottom_sheet_date_filter.xml b/app/src/main/res/layout/bottom_sheet_date_filter.xml new file mode 100644 index 000000000..06d0c61fd --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_date_filter.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_activity_history.xml b/app/src/main/res/menu/menu_activity_history.xml index 1e1e320e5..72038394f 100644 --- a/app/src/main/res/menu/menu_activity_history.xml +++ b/app/src/main/res/menu/menu_activity_history.xml @@ -1,6 +1,16 @@ + + + + diff --git a/app/src/main/res/menu/menu_filter_date.xml b/app/src/main/res/menu/menu_filter_date.xml deleted file mode 100644 index 4728bf903..000000000 --- a/app/src/main/res/menu/menu_filter_date.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/app/src/main/res/menu/menu_filter_status.xml b/app/src/main/res/menu/menu_filter_status.xml deleted file mode 100644 index 377e2dba5..000000000 --- a/app/src/main/res/menu/menu_filter_status.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values-es/strings_history.xml b/app/src/main/res/values-es/strings_history.xml index 2d6b00310..f14ce2f6d 100644 --- a/app/src/main/res/values-es/strings_history.xml +++ b/app/src/main/res/values-es/strings_history.xml @@ -75,22 +75,12 @@ Saldo total Exportar actividad - Filtrar transacciones - Completado - Pendiente - Todas Todo - Hoy - Semana - Mes Actividad exportada a CSV Error al exportar actividad Recibo - Rango personalizado... - Estado: %1$s - Fecha: %1$s %1$s - %2$s Seleccionar rango diff --git a/app/src/main/res/values-ja/strings_history.xml b/app/src/main/res/values-ja/strings_history.xml index a3dbc2052..2d7e085ff 100644 --- a/app/src/main/res/values-ja/strings_history.xml +++ b/app/src/main/res/values-ja/strings_history.xml @@ -57,18 +57,8 @@ ラベルを保存 トークンがクリップボードにコピーされました アクティビティをエクスポート - トランザクションのフィルタリング - 完了のみ - 保留中のみ - すべて表示 全期間 - 今日 - - チップ追加 (%1$d%%) - カスタム範囲... - ステータス: %1$s - 日付: %1$s %1$s - %2$s 日付範囲を選択 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings_history.xml b/app/src/main/res/values-ko/strings_history.xml index 46c1e344c..7532b936e 100644 --- a/app/src/main/res/values-ko/strings_history.xml +++ b/app/src/main/res/values-ko/strings_history.xml @@ -57,18 +57,8 @@ 라벨 저장 토큰이 클립보드에 복사되었습니다 활동 내보내기 - 거래 필터링 - 완료됨 - 대기 중 - 전체 전체 기간 - 오늘 - 이번 주 - 이번 달 팁 추가됨 (%1$d%%) - 사용자 지정 범위... - 상태: %1$s - 날짜: %1$s %1$s - %2$s 날짜 범위 선택 \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings_history.xml b/app/src/main/res/values-pt/strings_history.xml index 49c157a0f..003346f08 100644 --- a/app/src/main/res/values-pt/strings_history.xml +++ b/app/src/main/res/values-pt/strings_history.xml @@ -75,22 +75,12 @@ Saldo total Exportar atividade - Filtrar transações - Concluído - Pendente - Todas Todo - Hoje - Semana - Mês Atividade exportada para CSV Erro ao exportar atividade Recibo - Intervalo personalizado... - Status: %1$s - Data: %1$s %1$s - %2$s Selecionar período diff --git a/app/src/main/res/values/strings_history.xml b/app/src/main/res/values/strings_history.xml index 8e51e2438..d25a39a30 100644 --- a/app/src/main/res/values/strings_history.xml +++ b/app/src/main/res/values/strings_history.xml @@ -75,19 +75,28 @@ Total Balance Export Activity - Filter Transactions - Completed - Pending - All All Time - Custom Range... - Status: %1$s - Date: %1$s %1$s - %2$s Select Date Range Activity exported to CSV Error exporting activity + + Show pending + Hide pending + Filter by date + + + Filter by date + Last 7 days + Last 30 days + This month + Custom range… + + + Pending shown + · + Receipt From 8993c73c112f64afd269ebc88b21d7bed6002804 Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:44:18 -0700 Subject: [PATCH 2/6] fix(history): theme date picker, move filter indicator out of toolbar - Apply ThemeOverlay.Numo.MaterialCalendar to MaterialDatePicker so it uses the brand accent on a light surface in dialog mode, instead of the default fullscreen with the dark calendar grid. - Move the active-filter caption out of the toolbar (which forced the title to shift vertically when it appeared) into the balance section under the sat amount. Title no longer moves; the list reflows below the indicator instead, which reads as content rather than chrome jitter. - Tap the indicator to clear all filters (pending hidden, dates reset). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../history/PaymentsHistoryActivity.kt | 32 ++++++++--- app/src/main/res/layout/activity_history.xml | 56 +++++++++---------- app/src/main/res/values/themes.xml | 11 ++++ 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt index f370a22b3..9b47adedf 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt @@ -96,7 +96,10 @@ class PaymentsHistoryActivity : AppCompatActivity() { migrateLegacyFilterStateIfNeeded() registerDateFilterResultListener() - updateToolbarSubtitle() + findViewById(R.id.active_filter_indicator).setOnClickListener { + clearAllFilters() + } + updateActiveFilterIndicator() // Load and display history loadHistory() @@ -349,7 +352,17 @@ class PaymentsHistoryActivity : AppCompatActivity() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) val current = prefs.getBoolean(KEY_HIDE_PENDING, true) prefs.edit().putBoolean(KEY_HIDE_PENDING, !current).apply() - updateToolbarSubtitle() + updateActiveFilterIndicator() + loadHistory() + } + + private fun clearAllFilters() { + getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit() + .putBoolean(KEY_HIDE_PENDING, true) + .putLong(KEY_FILTER_DATE_START, 0L) + .putLong(KEY_FILTER_DATE_END, 0L) + .apply() + updateActiveFilterIndicator() loadHistory() } @@ -378,12 +391,12 @@ class PaymentsHistoryActivity : AppCompatActivity() { .putLong(KEY_FILTER_DATE_START, start) .putLong(KEY_FILTER_DATE_END, end) .apply() - updateToolbarSubtitle() + updateActiveFilterIndicator() loadHistory() } } - private fun updateToolbarSubtitle() { + private fun updateActiveFilterIndicator() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) val hidePending = prefs.getBoolean(KEY_HIDE_PENDING, true) val start = prefs.getLong(KEY_FILTER_DATE_START, 0L) @@ -402,12 +415,12 @@ class PaymentsHistoryActivity : AppCompatActivity() { parts += getString(R.string.history_subtitle_pending_shown) } - val subtitle = findViewById(R.id.toolbar_subtitle) + val indicator = findViewById(R.id.active_filter_indicator) if (parts.isEmpty()) { - subtitle.visibility = View.GONE + indicator.visibility = View.GONE } else { - subtitle.text = parts.joinToString(getString(R.string.history_subtitle_separator)) - subtitle.visibility = View.VISIBLE + indicator.text = parts.joinToString(getString(R.string.history_subtitle_separator)) + indicator.visibility = View.VISIBLE } } @@ -440,6 +453,7 @@ class PaymentsHistoryActivity : AppCompatActivity() { .setValidator(DateValidatorPointBackward.now()) val builder = MaterialDatePicker.Builder.dateRangePicker() + .setTheme(R.style.ThemeOverlay_Numo_MaterialCalendar) .setTitleText(R.string.history_filter_date_picker_title) .setCalendarConstraints(constraintsBuilder.build()) @@ -463,7 +477,7 @@ class PaymentsHistoryActivity : AppCompatActivity() { .putLong(KEY_FILTER_DATE_START, selection.first) .putLong(KEY_FILTER_DATE_END, selection.second) .apply() - updateToolbarSubtitle() + updateActiveFilterIndicator() loadHistory() } picker.show(supportFragmentManager, "DATE_RANGE_PICKER") diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml index 4872dfd82..67dca70fc 100644 --- a/app/src/main/res/layout/activity_history.xml +++ b/app/src/main/res/layout/activity_history.xml @@ -31,38 +31,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index fe2b9a41b..b8be553e8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -149,4 +149,15 @@ 0dp 0dp + + + From 2f6dd8b17716e5f3348ab795132041430cf0213a Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:45:22 -0700 Subject: [PATCH 3/6] fix(history): make active filter indicator a chip with close icon Replaces the bare caption with a Material Chip (outlined, brand-divider stroke). The close icon makes the dismiss affordance explicit instead of relying on a hidden ripple. Tapping the chip body or the close icon both clear all filters. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../history/PaymentsHistoryActivity.kt | 9 ++++--- app/src/main/res/layout/activity_history.xml | 27 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt index 9b47adedf..b63ee5d03 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivity.kt @@ -11,8 +11,8 @@ import android.os.Bundle import com.electricdreams.numo.util.createProgressDialog import com.electricdreams.numo.util.startActivityForResultCompat import android.view.View -import android.widget.TextView import android.widget.Toast +import com.google.android.material.chip.Chip import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog @@ -96,8 +96,9 @@ class PaymentsHistoryActivity : AppCompatActivity() { migrateLegacyFilterStateIfNeeded() registerDateFilterResultListener() - findViewById(R.id.active_filter_indicator).setOnClickListener { - clearAllFilters() + findViewById(R.id.active_filter_indicator).apply { + setOnClickListener { clearAllFilters() } + setOnCloseIconClickListener { clearAllFilters() } } updateActiveFilterIndicator() @@ -415,7 +416,7 @@ class PaymentsHistoryActivity : AppCompatActivity() { parts += getString(R.string.history_subtitle_pending_shown) } - val indicator = findViewById(R.id.active_filter_indicator) + val indicator = findViewById(R.id.active_filter_indicator) if (parts.isEmpty()) { indicator.visibility = View.GONE } else { diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml index 67dca70fc..5873e8c28 100644 --- a/app/src/main/res/layout/activity_history.xml +++ b/app/src/main/res/layout/activity_history.xml @@ -96,21 +96,26 @@ android:textSize="16sp" tools:text="₿21,305" /> - - + From ff25cc5c99fbf0055d92c26e06cc08973c83a2eb Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:53:29 -0700 Subject: [PATCH 4/6] fix(history): clarify filter copy on the activity screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targeted copy pass on strings touched in the recent filter refactor: - history_subtitle_pending_shown: "Pending shown" → "Including pending" (active voice; reads as an active filter, not a debug status). - history_filter_date_range_format: en dash for date ranges. - history_filter_date_all: "All Time" → "All time" (sentence case for consistency with the rest of the bottom sheet). - history_filter_date_picker_title: "Select Date Range" → "Choose dates" (sentence case + tighter). - history_menu_export_activity: "Export Activity" → "Export to CSV" (matches the existing success toast and is more specific about the outcome). Scope intentionally limited to strings we introduced or actively use in the filter refactor. Pre-existing copy elsewhere on the screen (toolbar title, balance label, empty state, dialogs, row titles) left untouched. Translations in es/ja/ko/pt are now slightly out of sync semantically (notably for export and date picker title); a follow-up translator pass will reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src/main/res/values/strings_history.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values/strings_history.xml b/app/src/main/res/values/strings_history.xml index d25a39a30..4edd95103 100644 --- a/app/src/main/res/values/strings_history.xml +++ b/app/src/main/res/values/strings_history.xml @@ -74,10 +74,10 @@ Total Balance - Export Activity - All Time - %1$s - %2$s - Select Date Range + Export to CSV + All time + %1$s – %2$s + Choose dates Activity exported to CSV Error exporting activity @@ -94,7 +94,7 @@ Custom range… - Pending shown + Including pending · From d1a8093cf476cf10d7cda4fc67f518d14a264f11 Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:00:01 -0700 Subject: [PATCH 5/6] fix(history): preserve spaces around the active-filter chip separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android XML strips leading/trailing whitespace from string resources unless the value is wrapped in double quotes. The previous separator " · " was being compiled down to "·", producing chip text like "Apr 25·Including pending" with no breathing room between segments. Wrapping the value in quotes preserves the spaces verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src/main/res/values/strings_history.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings_history.xml b/app/src/main/res/values/strings_history.xml index 4edd95103..10079ba5c 100644 --- a/app/src/main/res/values/strings_history.xml +++ b/app/src/main/res/values/strings_history.xml @@ -95,7 +95,7 @@ Including pending - · + " · " Receipt From 31ae660bd8d74b36ce3ec6eed38d8243943cdb3e Mon Sep 17 00:00:00 2001 From: Erik <78821053+swedishfrenchpress@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:21:34 -0700 Subject: [PATCH 6/6] test(history): repurpose obsolete FILTER_PENDING test for migration The "pending only" filter mode was removed in this PR. The test that asserted the legacy filter_state=2 still produced a pending-only list now fails as designed. Repurposed it to assert the migration path we shipped: legacy filter_state=2 maps to hide_pending=false (show all transactions), the legacy key is removed from prefs, and the new key is written. This gives regression coverage on the one-time migration code in migrateLegacyFilterStateIfNeeded() rather than just deleting the test. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../history/PaymentsHistoryActivityTest.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivityTest.kt b/app/src/test/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivityTest.kt index ccc9b70ad..8ac9ba3db 100644 --- a/app/src/test/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivityTest.kt +++ b/app/src/test/java/com/electricdreams/numo/feature/history/PaymentsHistoryActivityTest.kt @@ -218,12 +218,12 @@ class PaymentsHistoryActivityTest { } @Test - fun `loadHistory shows only pending transactions when FILTER_PENDING is set`() { - // Change preference to show only pending transactions + fun `legacy FILTER_PENDING preference migrates to show-all behavior`() { + // Pre-refactor users on the now-removed "pending only" mode (filter_state=2) + // should see all transactions after upgrade, since pending-only is gone. val prefs = context.getSharedPreferences("PaymentHistory", Context.MODE_PRIVATE) - prefs.edit().putInt("filter_state", 2).apply() // 2 = FILTER_PENDING + prefs.edit().putInt("filter_state", 2).apply() - // Add one pending and one completed transaction val pendingId = PaymentsHistoryActivity.addPendingPayment( context = context, amount = 100L, entryUnit = "sat", enteredAmount = 100L, bitcoinPrice = null, paymentRequest = null, formattedAmount = null @@ -238,16 +238,19 @@ class PaymentsHistoryActivityTest { paymentType = PaymentHistoryEntry.TYPE_CASHU, mintUrl = null ) - // Launch the activity val controller = Robolectric.buildActivity(PaymentsHistoryActivity::class.java).setup() val activity = controller.get() val recyclerView = activity.findViewById(R.id.history_recycler_view) val adapter = recyclerView.adapter - // Expected size = 2 (1 Header + 1 Pending Transaction) + // 1 month header + 2 transactions (both pending and completed visible) assertNotNull("Adapter should not be null", adapter) - assertEquals(2, adapter!!.itemCount) + assertEquals(3, adapter!!.itemCount) + + // Migration should have removed the legacy key and written the new one. + assertFalse(prefs.contains("filter_state")) + assertFalse(prefs.getBoolean("hide_pending", true)) } @Test