diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/UnreadDivider.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/UnreadDivider.kt index ba2f896b0..2c1340c91 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/UnreadDivider.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/UnreadDivider.kt @@ -24,6 +24,10 @@ internal val UNREAD_DIVIDER_LIFETIME = UnreadDividerLifetime.UntilClose * Compares ranges rather than matching an id, so a read-through message that was deleted or never * stored does not matter. The rows of one message share a `messageId`, so the divider never lands * inside a message. + * + * An [older] row without a server id is a pending or failed send, stored as `-(now)` and sorted + * among the newest rows, so its id says nothing about the boundary. The gap above it never takes + * the divider; otherwise a transcript would draw a second one under the same fixed key. */ internal fun unreadDividerBetween( newer: ChatListItem.ContentBubble, @@ -31,6 +35,7 @@ internal fun unreadDividerBetween( boundary: UnreadBoundary, ): Boolean { if (boundary !is UnreadBoundary.At || newer.isFromSelf) return false + if (older != null && older.messageId < 1) return false val olderId = older?.messageId ?: Long.MIN_VALUE return olderId <= boundary.readThrough && boundary.readThrough < newer.messageId } diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/UnreadDividerPlacementTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/UnreadDividerPlacementTest.kt index 81c72b821..559881f4c 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/UnreadDividerPlacementTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/UnreadDividerPlacementTest.kt @@ -63,6 +63,24 @@ class UnreadDividerPlacementTest { fun `own message first after pointer`() = assertEquals(3L, dividerAbove(UnreadBoundary.At(2, 1), row(1), row(2, self = true), row(3))) + /** + * A send that is pending or failed has no server id yet: its row carries `-(now)` and sorts + * among the newest. The gap above it is not the boundary, so the transcript keeps one divider. + */ + @Test + fun `an unsent own row between unread rows draws one divider`() { + val boundary = UnreadBoundary.At(1, 2) + val unsent = row(-noon.toEpochMilliseconds(), self = true) + val newestFirst = listOf(row(3), unsent, row(2), row(1)) + + val above = newestFirst.indices.mapNotNull { i -> + val newer = newestFirst[i] + newer.messageId.takeIf { unreadDividerBetween(newer, newestFirst.getOrNull(i + 1), boundary) } + } + + assertEquals(listOf(2L), above) + } + @Test fun `resolving draws nothing`() = assertEquals(null, dividerAbove(UnreadBoundary.Resolving, row(1), row(2)))