Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -943,19 +943,8 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
): SnoozeResult? {
val now = clock.currentTimeMillis()
return snoozeEvents(context, { event ->
// Pinned events are excluded from batch snooze
!event.isPinned &&
// Search query filter (existing behavior)
(searchQuery?.let { query ->
event.title.contains(query, ignoreCase = true) ||
event.desc.contains(query, ignoreCase = true)
} ?: true) &&
// FilterState filters
(filterState?.let { filter ->
filter.matchesCalendar(event) &&
filter.matchesStatus(event) &&
filter.matchesTime(event, now)
} ?: true)
FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
Comment thread
williscool marked this conversation as resolved.
}, snoozeDelay, isChange, onlySnoozeVisible)
}

Expand Down Expand Up @@ -1122,12 +1111,17 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}

fun muteAllVisibleEvents(context: Context) {

fun muteAllVisibleEvents(
context: Context,
searchQuery: String? = null,
filterState: FilterState? = null
) {
val now = clock.currentTimeMillis()
getEventsStorage(context).use {
db ->
val eventsToMute = db.events.filter {
event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isTask
event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isTask &&
FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}

if (eventsToMute.isNotEmpty()) {
Expand All @@ -1146,10 +1140,16 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}

fun pinAllVisibleEvents(context: Context) {
fun pinAllVisibleEvents(
context: Context,
searchQuery: String? = null,
filterState: FilterState? = null
) {
val now = clock.currentTimeMillis()
getEventsStorage(context).use { db ->
val eventsToPin = db.events.filter {
event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isPinned
event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isPinned &&
FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}
if (eventsToPin.isNotEmpty()) {
val pinnedEvents = eventsToPin.map { it.isPinned = true; it }
Expand All @@ -1158,9 +1158,16 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}

fun unpinAllVisibleEvents(context: Context) {
fun unpinAllVisibleEvents(
context: Context,
searchQuery: String? = null,
filterState: FilterState? = null
) {
val now = clock.currentTimeMillis()
getEventsStorage(context).use { db ->
val eventsToUnpin = db.events.filter { event -> event.isPinned }
val eventsToUnpin = db.events.filter { event -> event.isPinned &&
FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}
if (eventsToUnpin.isNotEmpty()) {
val unpinnedEvents = eventsToUnpin.map { it.isPinned = false; it }
db.updateEvents(unpinnedEvents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ data class FilterState(
) {

companion object {
/**
* Checks if an event matches the given search query and filter state.
* Handles nullable parameters: null searchQuery or filterState means "match all".
*/
fun matchesSearchAndFilters(
event: EventAlertRecord,
searchQuery: String?,
filterState: FilterState?,
now: Long
): Boolean {
if (searchQuery != null &&
!event.title.contains(searchQuery, ignoreCase = true) &&
!event.desc.contains(searchQuery, ignoreCase = true)) {
return false
}
return filterState?.matchesEvent(event, now) ?: true
}

private const val BUNDLE_CALENDAR_IDS = "filter_calendar_ids"
private const val BUNDLE_CALENDAR_NULL = "filter_calendar_null"
private const val BUNDLE_STATUS_FILTERS = "filter_status"
Expand Down Expand Up @@ -232,7 +250,12 @@ data class FilterState(
if (selectedCalendarIds == null) return true // No filter = show all
return event.calendarId in selectedCalendarIds // Empty set = show none
}


/** Check if an event matches all active filters */
fun matchesEvent(event: EventAlertRecord, now: Long): Boolean {
return filterEvents(listOf(event), now).isNotEmpty()
}

/**
* Filter events by specified filter types.
* @param events List of events to filter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,14 @@ class MainActivityModern : MainActivityBase() {
// Custom quiet hours is deprecated
menu.findItem(R.id.action_custom_quiet_interval)?.isVisible = false

val hasFilters = filterState.hasActiveFilters() || !currentFragment?.getSearchQuery().isNullOrEmpty()

// Show mute all only for fragments that support it (Active events)
val muteAllMenuItem = menu.findItem(R.id.action_mute_all)
val supportsMuteAll = currentFragment?.supportsMuteAll() == true
muteAllMenuItem?.isVisible = supportsMuteAll && settings.enableNotificationMute
muteAllMenuItem?.isEnabled = currentFragment?.anyForMuteAll() == true
muteAllMenuItem?.title = getString(if (hasFilters) R.string.mute_all_filtered else R.string.mute_all)

// Show dismiss all only for fragments that support it (Active events)
val dismissAllMenuItem = menu.findItem(R.id.action_dismiss_all)
Expand All @@ -233,9 +236,11 @@ class MainActivityModern : MainActivityBase() {
val pinAllMenuItem = menu.findItem(R.id.action_pin_all)
pinAllMenuItem?.isVisible = supportsPinAll
pinAllMenuItem?.isEnabled = currentFragment?.anyForPinAll() == true
pinAllMenuItem?.title = getString(if (hasFilters) R.string.pin_all_filtered else R.string.pin_all)
val unpinAllMenuItem = menu.findItem(R.id.action_unpin_all)
unpinAllMenuItem?.isVisible = supportsPinAll
unpinAllMenuItem?.isEnabled = currentFragment?.anyForUnpinAll() == true
unpinAllMenuItem?.title = getString(if (hasFilters) R.string.unpin_all_filtered else R.string.unpin_all)

// Show snooze all only for fragments that support it (Active events)
val snoozeAllMenuItem = menu.findItem(R.id.action_snooze_all)
Expand Down Expand Up @@ -390,8 +395,9 @@ class MainActivityModern : MainActivityBase() {
}

private fun onMuteAll() {
val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
.setMessage(R.string.mute_all_events_question)
.setMessage(if (hasFilters) R.string.mute_all_filtered_events_question else R.string.mute_all_events_question)
Comment thread
cursor[bot] marked this conversation as resolved.
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doMuteAll()
Expand All @@ -402,14 +408,17 @@ class MainActivityModern : MainActivityBase() {
}

private fun doMuteAll() {
ApplicationController.muteAllVisibleEvents(this)
val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
val currentFilterState = getCurrentFilterState()
ApplicationController.muteAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onMuteAllComplete()
invalidateOptionsMenu()
}

private fun onPinAll() {
val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
.setMessage(R.string.pin_all_events_question)
.setMessage(if (hasFilters) R.string.pin_all_filtered_events_question else R.string.pin_all_events_question)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doPinAll()
Expand All @@ -420,14 +429,17 @@ class MainActivityModern : MainActivityBase() {
}

private fun doPinAll() {
ApplicationController.pinAllVisibleEvents(this)
val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
val currentFilterState = getCurrentFilterState()
ApplicationController.pinAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onPinAllComplete()
invalidateOptionsMenu()
}

private fun onUnpinAll() {
val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
.setMessage(R.string.unpin_all_events_question)
.setMessage(if (hasFilters) R.string.unpin_all_filtered_events_question else R.string.unpin_all_events_question)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doUnpinAll()
Expand All @@ -438,7 +450,9 @@ class MainActivityModern : MainActivityBase() {
}

private fun doUnpinAll() {
ApplicationController.unpinAllVisibleEvents(this)
Comment thread
cursor[bot] marked this conversation as resolved.
val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
val currentFilterState = getCurrentFilterState()
ApplicationController.unpinAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onPinAllComplete()
invalidateOptionsMenu()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,14 +527,16 @@ open class ViewEventActivityNoRecents : AppCompatActivity() {
}

private fun togglePinForEvent(pinned: Boolean) {
getEventsStorage(this).use { db ->
val current = db.getEvent(event.eventId, event.instanceStartTime)
if (current != null) {
current.isPinned = pinned
db.updateEvent(current)
event.isPinned = pinned
background {
getEventsStorage(this).use { db ->
val current = db.getEvent(event.eventId, event.instanceStartTime)
if (current != null) {
current.isPinned = pinned
db.updateEvent(current)
}
}
}
event.isPinned = pinned
}

private fun formatPreset(preset: Long): String {
Expand Down
14 changes: 10 additions & 4 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -394,16 +394,22 @@

<string name="un_mute_notification">Un-Mute</string>
<string name="mute_notification">Mute</string>
<string name="mute_all">Mute all visible</string>
<string name="mute_all_events_question">Mute all visible events? (Tasks excluded)</string>
<string name="mute_all">Mute all</string>
<string name="mute_all_filtered">Mute all filtered</string>
<string name="mute_all_events_question">Mute all events? (Tasks excluded)</string>
<string name="mute_all_filtered_events_question">Mute all filtered events? (Tasks excluded)</string>

<string name="notification_is_pinned">Notification is pinned</string>
<string name="pin_notification">Pin</string>
<string name="unpin_notification">Unpin</string>
<string name="pin_all">Pin all visible</string>
<string name="pin_all">Pin all</string>
<string name="pin_all_filtered">Pin all filtered</string>
<string name="unpin_all">Unpin all</string>
<string name="pin_all_events_question">Pin all visible events?\nPinned events are excluded from batch snooze.</string>
<string name="unpin_all_filtered">Unpin all filtered</string>
<string name="pin_all_events_question">Pin all events?\nPinned events are excluded from batch snooze.</string>
<string name="pin_all_filtered_events_question">Pin all filtered events?\nPinned events are excluded from batch snooze.</string>
<string name="unpin_all_events_question">Unpin all pinned events?</string>
<string name="unpin_all_filtered_events_question">Unpin all filtered pinned events?</string>
<string name="filter_status_pinned">Pinned</string>
<string name="filter_status_unpinned">Unpinned</string>
<plurals name="pinned_events_excluded">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,15 @@ class ApplicationControllerCoreRobolectricTest {

private fun createTestEvent(
eventId: Long = 1L,
calendarId: Long = 1L,
instanceStartTime: Long = baseTime,
snoozedUntil: Long = 0L,
isMuted: Boolean = false,
isTask: Boolean = false,
lastStatusChangeTime: Long = baseTime
): EventAlertRecord {
val event = EventAlertRecord(
calendarId = 1L,
calendarId = calendarId,
eventId = eventId,
isAllDay = false,
isRepeating = false,
Expand Down Expand Up @@ -462,6 +463,87 @@ class ApplicationControllerCoreRobolectricTest {
assertFalse("No events should be pinned after unpin all", events.any { it.isPinned })
}

// === filter-aware muteAllVisibleEvents tests ===

@Test
fun testMuteAllVisibleEvents_respectsSearchQuery() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { title = "Meeting with Alice" })
mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { title = "Lunch with Bob" })

ApplicationController.muteAllVisibleEvents(context, searchQuery = "Alice")

val events = mockEventsStorage.events
assertTrue("Matching event should be muted", events.find { it.eventId == 1L }?.isMuted == true)
assertFalse("Non-matching event should NOT be muted", events.find { it.eventId == 2L }?.isMuted == true)
}

@Test
fun testMuteAllVisibleEvents_respectsFilterState() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L))
mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L))

val filter = FilterState(selectedCalendarIds = setOf(10L))
ApplicationController.muteAllVisibleEvents(context, filterState = filter)

val events = mockEventsStorage.events
assertTrue("Filtered-in event should be muted", events.find { it.eventId == 1L }?.isMuted == true)
assertFalse("Filtered-out event should NOT be muted", events.find { it.eventId == 2L }?.isMuted == true)
}

// === filter-aware pinAllVisibleEvents tests ===

@Test
fun testPinAllVisibleEvents_respectsSearchQuery() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { title = "Meeting with Alice" })
mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { title = "Lunch with Bob" })

ApplicationController.pinAllVisibleEvents(context, searchQuery = "Alice")

val events = mockEventsStorage.events
assertTrue("Matching event should be pinned", events.find { it.eventId == 1L }?.isPinned == true)
assertFalse("Non-matching event should NOT be pinned", events.find { it.eventId == 2L }?.isPinned == true)
}

@Test
fun testPinAllVisibleEvents_respectsFilterState() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L))
mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L))

val filter = FilterState(selectedCalendarIds = setOf(10L))
ApplicationController.pinAllVisibleEvents(context, filterState = filter)

val events = mockEventsStorage.events
assertTrue("Filtered-in event should be pinned", events.find { it.eventId == 1L }?.isPinned == true)
assertFalse("Filtered-out event should NOT be pinned", events.find { it.eventId == 2L }?.isPinned == true)
}

// === filter-aware unpinAllVisibleEvents tests ===

@Test
fun testUnpinAllVisibleEvents_respectsSearchQuery() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { isPinned = true; title = "Meeting with Alice" })
mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { isPinned = true; title = "Lunch with Bob" })

ApplicationController.unpinAllVisibleEvents(context, searchQuery = "Alice")

val events = mockEventsStorage.events
assertFalse("Matching event should be unpinned", events.find { it.eventId == 1L }?.isPinned == true)
assertTrue("Non-matching event should stay pinned", events.find { it.eventId == 2L }?.isPinned == true)
}

@Test
fun testUnpinAllVisibleEvents_respectsFilterState() {
mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L).apply { isPinned = true })
mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L).apply { isPinned = true })

val filter = FilterState(selectedCalendarIds = setOf(10L))
ApplicationController.unpinAllVisibleEvents(context, filterState = filter)

val events = mockEventsStorage.events
assertFalse("Filtered-in event should be unpinned", events.find { it.eventId == 1L }?.isPinned == true)
assertTrue("Filtered-out event should stay pinned", events.find { it.eventId == 2L }?.isPinned == true)
}

// === onEventAlarm tests ===

@Test
Expand Down
Loading