feat: notifications say what changed and alert once: a grouped notice names the tracks, grows without re-alerting, and a phone pile lists its titles (M542 #6146-#6151)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m33s
release / android (push) Failing after 1m42s
release / integration (push) Failing after 4m42s
release / Build signed APK (releases and dev) (push) Successful in 4m55s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m33s
release / android (push) Failing after 1m42s
release / integration (push) Failing after 4m42s
release / Build signed APK (releases and dev) (push) Successful in 4m55s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
- An update to a grouped notice keeps its arrival time and email state, so the phone and the email no longer go off again each time it grows. - Notices carry the names of what changed (newest ten), shown as three and "and N more": tracks still missing after the scan's move/replacement fold, duplicate groups new to a sweep, songs merged automatically, and the track and listener a playback error was for. - Duplicates found counts only the groups new to that sweep; playback errors count reports, not the unresolved total. - A scan failure says what stopped and links to Admin > Library. - Android: a pile of more than three alerts lists the newest five titles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+29
-10
@@ -30,8 +30,9 @@ internal data class CatchUp(
|
||||
* - The mark moves to the newest notice in the page, announced or not, so a
|
||||
* kind with the phone off is never announced later.
|
||||
*
|
||||
* A coalesced notice (tracks missing, now 5) moves its time forward when its
|
||||
* count grows, so it is announced again with the new count.
|
||||
* A coalesced notice (tracks missing, now 5) keeps the time it arrived when
|
||||
* its count grows (M542), so it is announced once: the inbox shows it grown,
|
||||
* the phone does not buzz for it again.
|
||||
*/
|
||||
internal fun planCatchUp(
|
||||
page: List<CachedNotificationEntity>,
|
||||
@@ -51,18 +52,32 @@ internal fun planCatchUp(
|
||||
internal sealed interface Announcement {
|
||||
data class Each(val items: List<CachedNotificationEntity>) : Announcement
|
||||
|
||||
/** Coming back from a day offline is one line, not forty buzzes. */
|
||||
data class Pile(val count: Int, val channel: ShadeChannel) : Announcement
|
||||
/**
|
||||
* Coming back from a day offline is one line, not forty buzzes. [lines]
|
||||
* are the newest notices' titles, so the pile says what it holds (M542).
|
||||
*/
|
||||
data class Pile(
|
||||
val count: Int,
|
||||
val channel: ShadeChannel,
|
||||
val lines: List<String> = emptyList(),
|
||||
) : Announcement
|
||||
}
|
||||
|
||||
internal fun announcementFor(fresh: List<CachedNotificationEntity>): Announcement? = when {
|
||||
fresh.isEmpty() -> null
|
||||
fresh.size <= INDIVIDUAL_LIMIT -> Announcement.Each(fresh)
|
||||
/** How many titles an expanded pile lists, newest first. */
|
||||
internal const val PILE_LINES = 5
|
||||
|
||||
internal fun announcementFor(fresh: List<CachedNotificationEntity>): Announcement? {
|
||||
if (fresh.isEmpty()) return null
|
||||
if (fresh.size <= INDIVIDUAL_LIMIT) return Announcement.Each(fresh)
|
||||
// A pile goes where its notices would: library health only when every
|
||||
// one of them is, so a listener's own news is never filed under it.
|
||||
fresh.all { shadeChannelFor(it.kind) == ShadeChannel.LIBRARY_HEALTH } ->
|
||||
Announcement.Pile(fresh.size, ShadeChannel.LIBRARY_HEALTH)
|
||||
else -> Announcement.Pile(fresh.size, ShadeChannel.YOUR_REQUESTS)
|
||||
val channel = if (fresh.all { shadeChannelFor(it.kind) == ShadeChannel.LIBRARY_HEALTH }) {
|
||||
ShadeChannel.LIBRARY_HEALTH
|
||||
} else {
|
||||
ShadeChannel.YOUR_REQUESTS
|
||||
}
|
||||
val lines = fresh.sortedByDescending { it.createdAt }.take(PILE_LINES).map { it.title }
|
||||
return Announcement.Pile(fresh.size, channel, lines)
|
||||
}
|
||||
|
||||
/** The phone channel per kind, as the user set it. No settings cached: on, the default for every kind. */
|
||||
@@ -92,3 +107,7 @@ internal fun deliveryWanted(signedIn: Boolean, enabled: Boolean): Boolean = sign
|
||||
|
||||
/** One line for a pile. */
|
||||
internal fun pileText(count: Int): String = "$count new notifications"
|
||||
|
||||
/** Under an expanded pile's titles: how many it holds beyond them, or null. */
|
||||
internal fun pileMore(p: Announcement.Pile): String? =
|
||||
(p.count - p.lines.size).takeIf { it > 0 }?.let { "and $it more" }
|
||||
|
||||
+11
-4
@@ -1,6 +1,7 @@
|
||||
package com.fabledsword.minstrel.notifications.delivery
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
@@ -50,14 +51,20 @@ class NotificationPoster @Inject constructor(
|
||||
.setContentIntent(openIntent(item.link, item.id.hashCode()))
|
||||
.build()
|
||||
|
||||
private fun pile(p: Announcement.Pile) =
|
||||
NotificationCompat.Builder(context, p.channel.id)
|
||||
/** Collapsed, the count; expanded, the newest titles and how many more. */
|
||||
private fun pile(p: Announcement.Pile): Notification {
|
||||
val style = NotificationCompat.InboxStyle().setBigContentTitle(pileText(p.count))
|
||||
p.lines.forEach { style.addLine(it) }
|
||||
pileMore(p)?.let { style.setSummaryText(it) }
|
||||
return NotificationCompat.Builder(context, p.channel.id)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle("Minstrel")
|
||||
.setContentText(pileText(p.count))
|
||||
.setContentTitle(pileText(p.count))
|
||||
.setContentText(p.lines.joinToString(" · "))
|
||||
.setStyle(style)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(openIntent(link = null, requestCode = PILE_ID))
|
||||
.build()
|
||||
}
|
||||
|
||||
/** Each notice gets its own request code, so their links stay apart. */
|
||||
private fun openIntent(link: String?, requestCode: Int): PendingIntent =
|
||||
|
||||
+10
@@ -99,6 +99,16 @@ class DeliveryPlanTest {
|
||||
assertEquals(5, pile.count)
|
||||
assertEquals(ShadeChannel.YOUR_REQUESTS, pile.channel)
|
||||
assertEquals("5 new notifications", pileText(pile.count))
|
||||
assertEquals(listOf("t-n5", "t-n4", "t-n3", "t-n2", "t-n1"), pile.lines)
|
||||
assertNull(pileMore(pile))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a pile names its newest notices and counts the rest`() {
|
||||
val many = (1..17).map { notice("n$it", it.toLong()) }
|
||||
val pile = assertIs<Announcement.Pile>(announcementFor(many))
|
||||
assertEquals(listOf("t-n17", "t-n16", "t-n15", "t-n14", "t-n13"), pile.lines)
|
||||
assertEquals("and 12 more", pileMore(pile))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user