Merge pull request #153 from binwiederhier/search

Search within a topic
This commit is contained in:
Philipp C. Heckel 2026-01-27 12:07:23 -05:00 committed by GitHub
commit 6b901b0559
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 213 additions and 118 deletions

View file

@ -574,6 +574,19 @@ interface NotificationDao {
@Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC")
fun listFlow(subscriptionId: Long): Flow<List<Notification>>
@Query("""
SELECT * FROM notification
WHERE subscriptionId = :subscriptionId
AND deleted != 1
AND (
title LIKE '%' || :query || '%' COLLATE NOCASE
OR message LIKE '%' || :query || '%' COLLATE NOCASE
OR tags LIKE '%' || :query || '%' COLLATE NOCASE
)
ORDER BY timestamp DESC
""")
fun listFlowFiltered(subscriptionId: Long, query: String): Flow<List<Notification>>
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
fun listDeletedWithAttachments(): List<Notification>

View file

@ -120,6 +120,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
return notificationDao.listFlow(subscriptionId).asLiveData()
}
fun getNotificationsFilteredLiveData(subscriptionId: Long, query: String): LiveData<List<Notification>> {
return notificationDao.listFlowFiltered(subscriptionId, query).asLiveData()
}
fun getNotification(notificationId: String): Notification? {
return notificationDao.get(notificationId)
}

View file

@ -9,12 +9,15 @@ import android.text.Html
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@ -61,6 +64,7 @@ import androidx.core.net.toUri
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import android.widget.ImageButton
import com.google.android.material.color.MaterialColors
class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSettingsListener, PublishFragment.PublishListener {
private val viewModel by viewModels<DetailViewModel> {
@ -78,7 +82,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
private var subscriptionBaseUrl: String = "" // Set in onCreate()
private var subscriptionTopic: String = "" // Set in onCreate()
private var subscriptionDisplayName: String = "" // Set in onCreate() & updated by options menu!
private var subscriptionInstant: Boolean = false // Set in onCreate() & updated by options menu!
private var subscriptionMutedUntil: Long = 0L // Set in onCreate() & updated by options menu!
// UI elements
@ -92,6 +95,12 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
private lateinit var messageBarPublishButton: FloatingActionButton
private lateinit var messageBarExpandButton: ImageButton
// Search state
private var searchView: SearchView? = null
private var isSearchActive: Boolean = false
private lateinit var toolbar: com.google.android.material.appbar.MaterialToolbar
private var toolbarTextColor: Int = 0
// Action mode stuff
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
@ -140,13 +149,18 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
val dynamicColors = repository.getDynamicColorsEnabled()
val darkMode = isDarkThemeOn(this)
val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode)
val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode)
toolbarLayout.setBackgroundColor(statusBarColor)
val toolbar = toolbarLayout.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.toolbar)
// Set collapse icon (back arrow when search is expanded) with proper tint
toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode)
val collapseIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp)?.mutate()
collapseIcon?.setTint(toolbarTextColor)
toolbar = toolbarLayout.findViewById(R.id.toolbar)
toolbar.setTitleTextColor(toolbarTextColor)
toolbar.setNavigationIconTint(toolbarTextColor)
toolbar.overflowIcon?.setTint(toolbarTextColor)
toolbar.collapseIcon = collapseIcon
setSupportActionBar(toolbar)
// Set system status bar appearance
@ -157,7 +171,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
val detailContentLayout = findViewById<View>(R.id.detail_content_layout)
if (repository.getDynamicColorsEnabled()) {
detailContentLayout.setBackgroundColor(
com.google.android.material.color.MaterialColors.getColor(
MaterialColors.getColor(
this,
android.R.attr.colorBackground,
ContextCompat.getColor(this, R.color.detail_activity_background)
@ -270,7 +284,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
subscriptionTopic = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return
subscriptionDisplayName = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return
subscriptionInstant = intent.getBooleanExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, false)
subscriptionMutedUntil = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, 0L)
// Set title
@ -280,9 +293,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
// Set "how to instructions"
val howToExample: TextView = findViewById(R.id.detail_how_to_example)
howToExample.linksClickable = true
val howToText = getString(R.string.detail_how_to_example, topicUrl)
howToExample.linksClickable = true
howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
// Swipe to refresh
@ -307,20 +319,36 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
insets
}
viewModel.list(subscriptionId).observe(this) {
it?.let {
// Observe filtered notifications (filtered by search query)
val noSearchResultsText: TextView = findViewById(R.id.detail_no_notifications_text)
val howToIntro: View = findViewById(R.id.detail_how_to_intro)
val howToLink: View = findViewById(R.id.detail_how_to_link)
viewModel.listFiltered(subscriptionId).observe(this) {
it?.let { notifications ->
// Show list view
adapter.submitList(it as MutableList<Notification>)
if (it.isEmpty()) {
adapter.submitList(notifications.toMutableList())
if (notifications.isEmpty()) {
mainListContainer.visibility = View.GONE
noEntriesText.visibility = View.VISIBLE
// Show different text based on whether we're searching or not
if (isSearchActive && viewModel.hasSearchQuery()) {
noSearchResultsText.text = getString(R.string.detail_no_search_results)
howToIntro.visibility = View.GONE
howToExample.visibility = View.GONE
howToLink.visibility = View.GONE
} else {
noSearchResultsText.text = getString(R.string.detail_no_notifications_text)
howToIntro.visibility = View.VISIBLE
howToExample.visibility = View.VISIBLE
howToLink.visibility = if (BuildConfig.PAYMENT_LINKS_AVAILABLE) View.VISIBLE else View.GONE
}
} else {
mainListContainer.visibility = View.VISIBLE
noEntriesText.visibility = View.GONE
}
// Cancel notifications that still have popups
maybeCancelNotificationPopups(it)
maybeCancelNotificationPopups(notifications)
}
}
@ -363,8 +391,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
}
// Observe connection details and update menu item visibility
repository.getConnectionDetailsLiveData().observe(this) { details ->
showHideConnectionErrorMenuItem(details)
repository.getConnectionDetailsLiveData().observe(this) {
showHideConnectionErrorMenuItem()
}
// Mark this subscription as "open" so we don't receive notifications for it
@ -507,14 +535,10 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
// Update buttons (this is for when we return from the preferences screen)
lifecycleScope.launch(Dispatchers.IO) {
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
subscriptionInstant = subscription.instant
subscriptionMutedUntil = subscription.mutedUntil
subscriptionDisplayName = displayName(appBaseUrl, subscription)
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
showHideCopyMenuItems(subscription.baseUrl)
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
showHideMenuItems()
updateTitle(subscriptionDisplayName)
}
}
@ -547,17 +571,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
menuInflater.inflate(R.menu.menu_detail_action_bar, menu)
this.menu = menu
// Tint menu icons based on theme
val toolbarTextColor = Colors.toolbarTextColor(this, repository.getDynamicColorsEnabled(), isDarkThemeOn(this))
for (i in 0 until menu.size) {
menu[i].icon?.setTint(toolbarTextColor)
}
// Show and hide buttons
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
showHideCopyMenuItems(subscriptionBaseUrl)
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
setupSearchView()
showHideMenuItems()
// Regularly check if "notification muted" time has passed
// NOTE: This is done here, because then we know that we've initialized the menu items.
@ -566,6 +581,82 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
return true
}
private fun setupSearchView() {
val searchItem = menu.findItem(R.id.detail_menu_search)
searchView = searchItem?.actionView as? SearchView
searchView?.let { sv ->
sv.queryHint = getString(R.string.detail_menu_search_hint)
sv.maxWidth = Integer.MAX_VALUE // Make SearchView expand fully
// Tint SearchView icons and text
val searchIcon = sv.findViewById<ImageView>(androidx.appcompat.R.id.search_button)
val closeIcon = sv.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchEditText = sv.findViewById<EditText>(androidx.appcompat.R.id.search_src_text)
searchIcon?.setColorFilter(toolbarTextColor)
closeIcon?.setColorFilter(toolbarTextColor)
searchEditText?.setTextColor(toolbarTextColor)
searchEditText?.setHintTextColor(toolbarTextColor.and(0x80FFFFFF.toInt())) // 50% alpha
// Handle query text changes
sv.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.setSearchQuery(newText ?: "")
return true
}
})
// Handle expand/collapse
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
isSearchActive = true
showHideMenuItems()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewModel.setSearchQuery("")
isSearchActive = false
showHideMenuItems()
return true
}
})
}
}
private fun showHideMenuItems() {
if (!this::menu.isInitialized) return
runOnUiThread {
// Tint menu icons based on theme
for (i in 0 until menu.size) {
menu[i].icon?.setTint(toolbarTextColor)
}
// Ensure collapse icon is tinted (back arrow when search is expanded)
toolbar.collapseIcon?.setTint(toolbarTextColor)
}
// Show/hide menu items based on state
showHideMutedUntilMenuItems()
showHideCopyMenuItems()
showHideConnectionErrorMenuItem()
showHideOtherMenuItems()
}
private fun showHideOtherMenuItems() {
if (!this::menu.isInitialized) return
runOnUiThread {
menu.findItem(R.id.detail_menu_settings)?.isVisible = !isSearchActive
menu.findItem(R.id.detail_menu_clear)?.isVisible = !isSearchActive
menu.findItem(R.id.detail_menu_test)?.isVisible = !isSearchActive
menu.findItem(R.id.detail_menu_unsubscribe)?.isVisible = !isSearchActive
}
}
private fun startNotificationMutedChecker() {
// FIXME This is awful and has to go.
@ -578,7 +669,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
if (mutedUntilExpired) {
val newSubscription = subscription.copy(mutedUntil = 0L)
repository.updateSubscription(newSubscription)
showHideMutedUntilMenuItems(0L)
subscriptionMutedUntil = 0L
showHideMutedUntilMenuItems()
}
delay(60_000)
}
@ -603,14 +695,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
onMutedUntilClick(enable = true)
true
}
R.id.detail_menu_enable_instant -> {
onInstantEnableClick(enable = true)
true
}
R.id.detail_menu_disable_instant -> {
onInstantEnableClick(enable = false)
true
}
R.id.detail_menu_connection_error -> {
onConnectionErrorClick()
true
@ -693,7 +777,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
val newSubscription = subscription?.copy(mutedUntil = mutedUntilTimestamp)
newSubscription?.let { repository.updateSubscription(newSubscription) }
subscriptionMutedUntil = mutedUntilTimestamp
showHideMutedUntilMenuItems(mutedUntilTimestamp)
showHideMutedUntilMenuItems()
runOnUiThread {
when (mutedUntilTimestamp) {
0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show()
@ -744,86 +828,44 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
}
}
private fun onInstantEnableClick(enable: Boolean) {
Log.d(TAG, "Toggling instant delivery setting for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
lifecycleScope.launch(Dispatchers.IO) {
val subscription = repository.getSubscription(subscriptionId)
val newSubscription = subscription?.copy(instant = enable)
newSubscription?.let { repository.updateSubscription(newSubscription) }
showHideInstantMenuItems(enable)
runOnUiThread {
if (enable) {
Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_enabled), Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_disabled), Toast.LENGTH_SHORT)
.show()
private fun showHideMutedUntilMenuItems() {
if (!this::menu.isInitialized) return
runOnUiThread {
val notificationsEnabledItem = menu.findItem(R.id.detail_menu_notifications_enabled)
val notificationsDisabledUntilItem = menu.findItem(R.id.detail_menu_notifications_disabled_until)
val notificationsDisabledForeverItem = menu.findItem(R.id.detail_menu_notifications_disabled_forever)
if (isSearchActive) {
notificationsEnabledItem?.isVisible = false
notificationsDisabledUntilItem?.isVisible = false
notificationsDisabledForeverItem?.isVisible = false
} else {
notificationsEnabledItem?.isVisible = subscriptionMutedUntil == 0L
notificationsDisabledForeverItem?.isVisible = subscriptionMutedUntil == 1L
notificationsDisabledUntilItem?.isVisible = subscriptionMutedUntil > 1L
if (subscriptionMutedUntil > 1L) {
val formattedDate = formatDateShort(subscriptionMutedUntil)
notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate)
}
}
}
}
private fun showHideInstantMenuItems(enable: Boolean) {
if (!this::menu.isInitialized) {
return
}
subscriptionInstant = enable
runOnUiThread {
val appBaseUrl = getString(R.string.app_base_url)
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
if (allowToggleInstant) {
enableInstantItem?.isVisible = !subscriptionInstant
disableInstantItem?.isVisible = subscriptionInstant
} else {
enableInstantItem?.isVisible = false
disableInstantItem?.isVisible = false
}
}
}
private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) {
if (!this::menu.isInitialized) {
return
}
subscriptionMutedUntil = mutedUntilTimestamp
runOnUiThread {
val notificationsEnabledItem = menu.findItem(R.id.detail_menu_notifications_enabled)
val notificationsDisabledUntilItem = menu.findItem(R.id.detail_menu_notifications_disabled_until)
val notificationsDisabledForeverItem = menu.findItem(R.id.detail_menu_notifications_disabled_forever)
notificationsEnabledItem?.isVisible = subscriptionMutedUntil == 0L
notificationsDisabledForeverItem?.isVisible = subscriptionMutedUntil == 1L
notificationsDisabledUntilItem?.isVisible = subscriptionMutedUntil > 1L
if (subscriptionMutedUntil > 1L) {
val formattedDate = formatDateShort(subscriptionMutedUntil)
notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate)
}
}
}
private fun showHideCopyMenuItems(subscriptionBaseUrl: String) {
if (!this::menu.isInitialized) {
return
}
private fun showHideCopyMenuItems() {
if (!this::menu.isInitialized) return
runOnUiThread {
// Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463
val copyUrlItem = menu.findItem(R.id.detail_menu_copy_url)
copyUrlItem?.isVisible = appBaseUrl != subscriptionBaseUrl || BuildConfig.PAYMENT_LINKS_AVAILABLE
copyUrlItem?.isVisible = !isSearchActive && (appBaseUrl != subscriptionBaseUrl || BuildConfig.PAYMENT_LINKS_AVAILABLE)
}
}
private fun showHideConnectionErrorMenuItem(details: Map<String, io.heckel.ntfy.db.ConnectionDetails>) {
if (!this::menu.isInitialized) {
return
}
private fun showHideConnectionErrorMenuItem() {
if (!this::menu.isInitialized) return
runOnUiThread {
val connectionErrorItem = menu.findItem(R.id.detail_menu_connection_error)
// Only show if there's an error for this subscription's base URL
val hasError = details[subscriptionBaseUrl]?.hasError() == true
connectionErrorItem?.isVisible = hasError
val connectionErrorItem = menu.findItem(R.id.detail_menu_connection_error)
val hasError = repository.getConnectionDetails()[subscriptionBaseUrl]?.hasError() == true
connectionErrorItem?.isVisible = !isSearchActive && hasError
}
}

View file

@ -1,8 +1,10 @@
package io.heckel.ntfy.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
@ -10,10 +12,28 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DetailViewModel(private val repository: Repository) : ViewModel() {
private val searchQuery = MutableLiveData("")
fun setSearchQuery(query: String) {
searchQuery.value = query
}
fun hasSearchQuery(): Boolean = !searchQuery.value.isNullOrBlank()
fun list(subscriptionId: Long): LiveData<List<Notification>> {
return repository.getNotificationsLiveData(subscriptionId)
}
fun listFiltered(subscriptionId: Long): LiveData<List<Notification>> {
return searchQuery.switchMap { query ->
if (query.isNullOrBlank()) {
repository.getNotificationsLiveData(subscriptionId)
} else {
repository.getNotificationsFilteredLiveData(subscriptionId, query)
}
}
}
fun markAsDeleted(notificationId: String) = viewModelScope.launch(Dispatchers.IO) {
repository.markAsDeleted(notificationId)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View file

@ -1,6 +1,12 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/detail_menu_search"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/detail_menu_search"
app:showAsAction="always|collapseActionView"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
<item
android:id="@+id/detail_menu_connection_error"
android:icon="@drawable/ic_warning_white_24dp"
@ -22,16 +28,6 @@
android:icon="@drawable/ic_notifications_off_white_outline_24dp"
android:title="@string/detail_menu_notifications_disabled_forever"
app:showAsAction="ifRoom" />
<item
android:id="@+id/detail_menu_enable_instant"
android:icon="@drawable/ic_bolt_outline_white_24dp"
android:title="@string/detail_menu_enable_instant"
app:showAsAction="ifRoom" />
<item
android:id="@+id/detail_menu_disable_instant"
android:icon="@drawable/ic_bolt_white_24dp"
android:title="@string/detail_menu_disable_instant"
app:showAsAction="ifRoom" />
<item
android:id="@+id/detail_menu_settings"
android:title="@string/detail_menu_settings" />

View file

@ -141,6 +141,7 @@
<string name="detail_how_to_example"><![CDATA[ Example (using curl):<br/><tt>$ curl -d \"Hi\" %1$s</tt> ]]></string>
<string name="detail_how_to_link">Detailed instructions available on ntfy.sh, and in the docs.
</string>
<string name="detail_no_search_results">Your search returned no results</string>
<string name="detail_clear_dialog_message">Delete all of the notifications in this topic?
</string>
<string name="detail_clear_dialog_permanently_delete">Delete permanently</string>
@ -203,6 +204,8 @@
<string name="detail_menu_clear">Clear all notifications</string>
<string name="detail_menu_settings">Subscription settings</string>
<string name="detail_menu_unsubscribe">Unsubscribe</string>
<string name="detail_menu_search">Search notifications</string>
<string name="detail_menu_search_hint">Search in notifications</string>
<!-- Detail activity: Action mode -->
<string name="detail_action_mode_menu_delete">Delete</string>

View file

@ -49,6 +49,7 @@
<item name="switchPreferenceCompatStyle">@style/MaterialSwitch</item>
<item name="windowActionModeOverlay">true</item>
<item name="searchViewStyle">@style/AppSearchViewStyle</item>
<item name="actionModeStyle">@style/ActionMode</item>
<item name="actionModeCloseButtonStyle">@style/ActionModeCloseButtonStyle</item>
@ -57,6 +58,11 @@
<style name="OverflowButtonStyle" parent="Widget.AppCompat.ActionButton.Overflow" />
<!-- SearchView style without the hint icon -->
<style name="AppSearchViewStyle" parent="Widget.AppCompat.SearchView.ActionBar">
<item name="searchHintIcon">@null</item>
</style>
<style name="DangerText" parent="@android:style/TextAppearance">
<item name="android:textColor">?attr/colorError</item>
</style>

View file

@ -0,0 +1,2 @@
Features:
* Search within a topic (#141, ntfy-android#153, thanks to @Copephobia and @StoyanYonkov for reporting, and to @Fearup for sponsoring)