From 9c0431ab97ba9154a6212fbfb9f7d6b718241a51 Mon Sep 17 00:00:00 2001 From: Antoan Angelov Date: Fri, 20 Mar 2026 23:24:57 -0400 Subject: [PATCH] Sort messages in chronologically ascending order This change introduces a new boolean preference, which is false by default. If true, messages in DetailActivity will be sorted in chronologically ascending order, as in newest items will show at the bottom. If false (default value), the existing behavior will be retained, where the newest items are shown at the top. --- .../main/java/io/heckel/ntfy/db/Repository.kt | 11 ++++++++++ .../java/io/heckel/ntfy/ui/DetailActivity.kt | 16 ++++++++++++--- .../io/heckel/ntfy/ui/SettingsActivity.kt | 20 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 +++ app/src/main/res/values/values.xml | 1 + app/src/main/res/xml/main_preferences.xml | 4 ++++ 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index a6716c742..b6c4992a7 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -410,6 +410,16 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) } } + fun getMessageSortAscending(): Boolean { + return sharedPrefs.getBoolean(SHARED_PREFS_MESSAGE_SORT_ASCENDING, false) + } + + fun setMessageSortAscending(ascending: Boolean) { + sharedPrefs.edit { + putBoolean(SHARED_PREFS_MESSAGE_SORT_ASCENDING, ascending) + } + } + fun getBatteryOptimizationsRemindTime(): Long { return sharedPrefs.getLong(SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME, BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS) } @@ -634,6 +644,7 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority" const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs" const val SHARED_PREFS_MESSAGE_BAR_ENABLED = "MessageBarEnabled" + const val SHARED_PREFS_MESSAGE_SORT_ASCENDING = "MessageSortAscending" const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner) const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime" diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index b2554a875..96d83eae4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -325,8 +325,18 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet val howToLink: View = findViewById(R.id.detail_how_to_link) viewModel.listFiltered(subscriptionId).observe(this) { it?.let { notifications -> + // Sort ascending if preference is set (DB returns descending by default) + val sortedNotifications = if (repository.getMessageSortAscending()) { + notifications.sortedBy { n -> n.timestamp } + } else { + notifications.sortedByDescending { n -> n.timestamp } + } // Show list view - adapter.submitList(notifications.toMutableList()) + adapter.submitList(sortedNotifications.toMutableList()) { + if (repository.getMessageSortAscending() && sortedNotifications.isNotEmpty()) { + mainList.scrollToPosition(sortedNotifications.size - 1) + } + } if (notifications.isEmpty()) { mainListContainer.visibility = View.GONE noEntriesText.visibility = View.VISIBLE @@ -375,10 +385,10 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet val itemTouchHelper = ItemTouchHelper(itemTouchCallback) itemTouchHelper.attachToRecyclerView(mainList) - // Scroll up when new notification is added + // Scroll to top when new notification is added (descending order only; ascending is handled in submitList callback) adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (positionStart == 0) { + if (!repository.getMessageSortAscending() && positionStart == 0) { Log.d(TAG, "$itemCount item(s) inserted at 0, scrolling to the top") mainList.scrollToPosition(positionStart) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 8ebf2f874..7025ef0bb 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -481,6 +481,26 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + // Message sort order + val messageSortAscendingPrefId = context?.getString(R.string.settings_general_message_sort_ascending_key) ?: return + val messageSortAscending: SwitchPreferenceCompat? = findPreference(messageSortAscendingPrefId) + messageSortAscending?.isChecked = repository.getMessageSortAscending() + messageSortAscending?.preferenceDataStore = object : PreferenceDataStore() { + override fun putBoolean(key: String?, value: Boolean) { + repository.setMessageSortAscending(value) + } + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + return repository.getMessageSortAscending() + } + } + messageSortAscending?.summaryProvider = Preference.SummaryProvider { pref -> + if (pref.isChecked) { + getString(R.string.settings_general_message_sort_ascending_summary_enabled) + } else { + getString(R.string.settings_general_message_sort_ascending_summary_disabled) + } + } + // Default Base URL val appBaseUrl = getString(R.string.app_base_url) val defaultBaseUrlPrefId = context?.getString(R.string.settings_general_default_base_url_key) ?: return diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 986a33e4e..b34918444 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,6 +402,9 @@ Show message bar Message bar shown at bottom of topic view Publish button shown at bottom of topic view + Sort messages oldest first + Messages sorted oldest to newest + Messages sorted newest to oldest Backup & Restore Back up to file Export config, notifications, and users diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 0b45baf59..7045790de 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -25,6 +25,7 @@ Language DynamicColors MessageBarEnabled + MessageSortAscending Backup Restore BroadcastEnabled diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 1ef47d3ed..a6a105b23 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -62,6 +62,10 @@ app:key="@string/settings_general_message_bar_key" app:title="@string/settings_general_message_bar_title" app:defaultValue="true"/> +