@@ -19,7 +19,6 @@ import com.flashcardsopensourceapp.data.local.model.EffortLevel
1919import com.flashcardsopensourceapp.data.local.model.ReviewFilter
2020import com.flashcardsopensourceapp.data.local.model.decodeDeckFilterDefinitionJson
2121import com.flashcardsopensourceapp.data.local.model.normalizeTagKey
22- import com.flashcardsopensourceapp.data.local.model.normalizeTags
2322import com.flashcardsopensourceapp.data.local.notifications.CurrentReviewNotificationCard
2423import com.flashcardsopensourceapp.data.local.notifications.ReviewNotificationMode
2524import com.flashcardsopensourceapp.data.local.notifications.ReviewNotificationsReconcileTrigger
@@ -138,9 +137,24 @@ class ReviewNotificationsManager(
138137 val selectedReviewFilter = reviewPreferencesStore.loadSelectedReviewFilter(
139138 workspaceId = workspace.workspaceId
140139 )
140+ val reviewNotificationFilterPlan = loadReviewNotificationFilterPlan(
141+ workspaceId = workspace.workspaceId,
142+ selectedReviewFilter = selectedReviewFilter
143+ )
144+ val resolvedReviewFilter = when (reviewNotificationFilterPlan) {
145+ is ReviewNotificationFilterPlan .Schedule -> reviewNotificationFilterPlan.reviewFilter
146+ ReviewNotificationFilterPlan .SuppressScheduledPayloads -> {
147+ reviewNotificationsStore.saveScheduledPayloads(
148+ workspaceId = workspace.workspaceId,
149+ payloads = emptyList()
150+ )
151+ return
152+ }
153+ }
154+
141155 val currentCard = loadCurrentReviewNotificationCard(
142156 workspaceId = workspace.workspaceId,
143- reviewFilter = selectedReviewFilter ,
157+ reviewFilter = resolvedReviewFilter ,
144158 nowMillis = nowMillis
145159 )
146160
@@ -172,7 +186,7 @@ class ReviewNotificationsManager(
172186 }
173187 }
174188 } else {
175- val persistedReviewFilter = makePersistedReviewFilter(reviewFilter = selectedReviewFilter )
189+ val persistedReviewFilter = makePersistedReviewFilter(reviewFilter = resolvedReviewFilter )
176190 val fallbackFrontText = reviewTextProvider(context = context).notificationFallbackFrontText
177191 when (settings.selectedMode) {
178192 ReviewNotificationMode .DAILY -> buildFallbackDailyReminderPayloads(
@@ -327,6 +341,40 @@ class ReviewNotificationsManager(
327341 }
328342 }
329343
344+ private suspend fun loadReviewNotificationFilterPlan (
345+ workspaceId : String ,
346+ selectedReviewFilter : ReviewFilter
347+ ): ReviewNotificationFilterPlan {
348+ val selectedDeckFilterDefinition = when (selectedReviewFilter) {
349+ is ReviewFilter .Deck -> loadCurrentWorkspaceDeckFilterDefinitionOrNull(
350+ workspaceId = workspaceId,
351+ deckId = selectedReviewFilter.deckId
352+ )
353+
354+ ReviewFilter .AllCards ,
355+ is ReviewFilter .Effort ,
356+ is ReviewFilter .Tag -> null
357+ }
358+
359+ return resolveReviewNotificationFilterPlan(
360+ selectedReviewFilter = selectedReviewFilter,
361+ activeReviewTagNames = loadActiveReviewTagNames(workspaceId = workspaceId),
362+ selectedDeckFilterDefinition = selectedDeckFilterDefinition
363+ )
364+ }
365+
366+ private suspend fun loadCurrentWorkspaceDeckFilterDefinitionOrNull (
367+ workspaceId : String ,
368+ deckId : String
369+ ): DeckFilterDefinition ? {
370+ val deck = database.deckDao().loadDeck(deckId = deckId) ? : return null
371+ if (deck.workspaceId != workspaceId || deck.deletedAtMillis != null ) {
372+ return null
373+ }
374+
375+ return decodeDeckFilterDefinitionJson(filterDefinitionJson = deck.filterDefinitionJson)
376+ }
377+
330378 private suspend fun loadCurrentAllCardsReviewNotificationCard (
331379 workspaceId : String ,
332380 nowMillis : Long
@@ -349,7 +397,7 @@ class ReviewNotificationsManager(
349397 nowMillis : Long
350398 ): CurrentReviewNotificationCard ? {
351399 val deck = database.deckDao().loadDeck(deckId = deckId)
352- if (deck == null || deck.deletedAtMillis != null ) {
400+ if (deck == null || deck.workspaceId != workspaceId || deck. deletedAtMillis != null ) {
353401 return loadCurrentAllCardsReviewNotificationCard(
354402 workspaceId = workspaceId,
355403 nowMillis = nowMillis
@@ -395,11 +443,11 @@ class ReviewNotificationsManager(
395443 tag : String ,
396444 nowMillis : Long
397445 ): CurrentReviewNotificationCard ? {
398- val hasTag = database.tagDao().hasTag (
446+ val exactTagNames = loadExactStoredReviewTagNames (
399447 workspaceId = workspaceId,
400- tagName = tag
448+ requestedTagNames = listOf ( tag)
401449 )
402- if (hasTag. not ()) {
450+ if (exactTagNames.isEmpty ()) {
403451 return loadCurrentAllCardsReviewNotificationCard(
404452 workspaceId = workspaceId,
405453 nowMillis = nowMillis
@@ -409,7 +457,7 @@ class ReviewNotificationsManager(
409457 val card = database.cardDao().loadTopReviewCardByAnyTags(
410458 workspaceId = workspaceId,
411459 nowMillis = nowMillis,
412- normalizedTagNames = listOf (normalizeTagKey(tag = tag))
460+ tagNames = exactTagNames
413461 ) ? : return null
414462
415463 return CurrentReviewNotificationCard (
@@ -424,34 +472,36 @@ class ReviewNotificationsManager(
424472 nowMillis : Long ,
425473 filterDefinition : DeckFilterDefinition
426474 ): com.flashcardsopensourceapp.data.local.database.CardEntity ? {
427- val normalizedTagNames = normalizeTags(
428- values = filterDefinition.tags,
429- referenceTags = emptyList()
430- ).map { tag ->
431- normalizeTagKey(tag = tag)
475+ val exactTagNames = loadExactStoredReviewTagNames(
476+ workspaceId = workspaceId,
477+ requestedTagNames = filterDefinition.tags
478+ )
479+ val hasTagPredicate = filterDefinition.tags.isNotEmpty()
480+ if (hasTagPredicate && exactTagNames.isEmpty()) {
481+ return null
432482 }
433483
434484 return when {
435- filterDefinition.effortLevels.isEmpty() && normalizedTagNames.isEmpty () -> {
485+ filterDefinition.effortLevels.isEmpty() && hasTagPredicate. not () -> {
436486 database.cardDao().loadTopReviewCard(
437487 workspaceId = workspaceId,
438488 nowMillis = nowMillis
439489 )
440490 }
441491
442- filterDefinition.effortLevels.isNotEmpty() && normalizedTagNames.isEmpty () -> {
492+ filterDefinition.effortLevels.isNotEmpty() && hasTagPredicate. not () -> {
443493 database.cardDao().loadTopReviewCardByEffortLevels(
444494 workspaceId = workspaceId,
445495 nowMillis = nowMillis,
446496 effortLevels = filterDefinition.effortLevels
447497 )
448498 }
449499
450- filterDefinition.effortLevels.isEmpty() && normalizedTagNames.isNotEmpty() -> {
500+ filterDefinition.effortLevels.isEmpty() -> {
451501 database.cardDao().loadTopReviewCardByAnyTags(
452502 workspaceId = workspaceId,
453503 nowMillis = nowMillis,
454- normalizedTagNames = normalizedTagNames
504+ tagNames = exactTagNames
455505 )
456506 }
457507
@@ -460,13 +510,102 @@ class ReviewNotificationsManager(
460510 workspaceId = workspaceId,
461511 nowMillis = nowMillis,
462512 effortLevels = filterDefinition.effortLevels,
463- normalizedTagNames = normalizedTagNames
513+ tagNames = exactTagNames
514+ )
515+ }
516+ }
517+ }
518+
519+ private suspend fun loadExactStoredReviewTagNames (
520+ workspaceId : String ,
521+ requestedTagNames : List <String >
522+ ): List <String > {
523+ return resolveExactStoredReviewTagNames(
524+ requestedTagNames = requestedTagNames,
525+ storedTagNames = loadActiveReviewTagNames(workspaceId = workspaceId)
526+ )
527+ }
528+
529+ private suspend fun loadActiveReviewTagNames (workspaceId : String ): List <String > {
530+ return database.tagDao().loadReviewTagNames(workspaceId = workspaceId)
531+ }
532+ }
533+
534+ internal sealed interface ReviewNotificationFilterPlan {
535+ data class Schedule (
536+ val reviewFilter : ReviewFilter
537+ ) : ReviewNotificationFilterPlan
538+
539+ data object SuppressScheduledPayloads : ReviewNotificationFilterPlan
540+ }
541+
542+ internal fun resolveReviewNotificationFilterPlan (
543+ selectedReviewFilter : ReviewFilter ,
544+ activeReviewTagNames : List <String >,
545+ selectedDeckFilterDefinition : DeckFilterDefinition ?
546+ ): ReviewNotificationFilterPlan {
547+ return when (selectedReviewFilter) {
548+ ReviewFilter .AllCards -> ReviewNotificationFilterPlan .Schedule (reviewFilter = ReviewFilter .AllCards )
549+ is ReviewFilter .Effort -> ReviewNotificationFilterPlan .Schedule (reviewFilter = selectedReviewFilter)
550+ is ReviewFilter .Tag -> {
551+ val exactTagName = resolveExactStoredReviewTagNames(
552+ requestedTagNames = listOf (selectedReviewFilter.tag),
553+ storedTagNames = activeReviewTagNames
554+ ).firstOrNull()
555+ val resolvedReviewFilter = exactTagName?.let { tagName ->
556+ ReviewFilter .Tag (tag = tagName)
557+ } ? : ReviewFilter .AllCards
558+
559+ ReviewNotificationFilterPlan .Schedule (reviewFilter = resolvedReviewFilter)
560+ }
561+
562+ is ReviewFilter .Deck -> {
563+ if (selectedDeckFilterDefinition == null ) {
564+ return ReviewNotificationFilterPlan .Schedule (reviewFilter = ReviewFilter .AllCards )
565+ }
566+
567+ if (hasImpossibleStoredTagDeckPredicate(
568+ filterDefinition = selectedDeckFilterDefinition,
569+ storedTagNames = activeReviewTagNames
464570 )
571+ ) {
572+ ReviewNotificationFilterPlan .SuppressScheduledPayloads
573+ } else {
574+ ReviewNotificationFilterPlan .Schedule (reviewFilter = selectedReviewFilter)
465575 }
466576 }
467577 }
468578}
469579
580+ internal fun hasImpossibleStoredTagDeckPredicate (
581+ filterDefinition : DeckFilterDefinition ,
582+ storedTagNames : List <String >
583+ ): Boolean {
584+ return filterDefinition.tags.isNotEmpty() && resolveExactStoredReviewTagNames(
585+ requestedTagNames = filterDefinition.tags,
586+ storedTagNames = storedTagNames
587+ ).isEmpty()
588+ }
589+
590+ internal fun resolveExactStoredReviewTagNames (
591+ requestedTagNames : List <String >,
592+ storedTagNames : List <String >
593+ ): List <String > {
594+ val requestedTagKeys: List <String > = requestedTagNames.map { tagName ->
595+ normalizeTagKey(tag = tagName)
596+ }.filter { tagKey ->
597+ tagKey.isNotEmpty()
598+ }.distinct()
599+ if (requestedTagKeys.isEmpty()) {
600+ return emptyList()
601+ }
602+
603+ val requestedTagKeySet: Set <String > = requestedTagKeys.toSet()
604+ return storedTagNames.filter { storedTagName ->
605+ requestedTagKeySet.contains(normalizeTagKey(tag = storedTagName))
606+ }.distinct()
607+ }
608+
470609fun hasNotificationPermission (context : Context ): Boolean {
471610 return ContextCompat .checkSelfPermission(
472611 context,
0 commit comments