Merge branch 'mantra' into claude/sign-in-alpha-message-fd21ca
This commit is contained in:
@@ -8,14 +8,35 @@ import press.mantra.compose.database.model.ChatMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* The room's transcript, newest first -- the order the feed's `reverseLayout` draws from the
|
||||
* bottom up, so the first row is the line sitting above the composer.
|
||||
*
|
||||
* `id DESC` is not decoration. [press.mantra.compose.database.converters.MantraConverters]
|
||||
* stores an `Instant` as epoch seconds, so lines written inside one second tie: a ceremony
|
||||
* puts a dozen into a room faster than that, and a request with the answer it triggers
|
||||
* routinely lands inside one. `createdAt DESC` alone left those to whatever order the sorter
|
||||
* produced -- the rows as scanned, which under a descending query drew a same-second burst
|
||||
* backwards, and is free to change under a new index or a different plan, swapping two lines
|
||||
* in a transcript the user has already read.
|
||||
*
|
||||
* The key [ChatRoomDao] already breaks its last-message tie on, which is what makes the chat
|
||||
* list's preview and the bottom of the room it opens name the same line.
|
||||
*
|
||||
* One constant for both reads because they are the same list: the snapshot a screen loads
|
||||
* with and the flow it then follows must not order it differently.
|
||||
*/
|
||||
private const val TRANSCRIPT =
|
||||
"SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC, id DESC"
|
||||
|
||||
@Dao
|
||||
interface ChatMessageDao {
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC")
|
||||
@Query(TRANSCRIPT)
|
||||
fun observeChatMessagesByChatRoomId(chatRoomId: String): Flow<List<press.mantra.compose.database.model.intermdiate.LocalChatMessage>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC")
|
||||
@Query(TRANSCRIPT)
|
||||
suspend fun getChatMessagesByChatRoomId(chatRoomId: String): List<press.mantra.compose.database.model.intermdiate.LocalChatMessage>
|
||||
|
||||
@Query("SELECT * FROM ChatMessage WHERE giftWrapPayloadId = :giftWrapPayloadId ORDER BY createdAt DESC")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package press.mantra.compose.database.dao
|
||||
|
||||
import androidx.room3.Room
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import press.mantra.compose.database.MantraDatabase
|
||||
import press.mantra.compose.database.builder.getRoomDatabase
|
||||
@@ -226,6 +227,39 @@ class ChatMessageDaoJvmTest {
|
||||
assertEquals("newer", db.chatMessageDao().getChatMessagesByMarmotGroupEventId(eventId)?.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps are stored to the second, so a burst written in one second ties -- a ceremony
|
||||
* puts a dozen lines into a room faster than that, and a request with the answer it triggers
|
||||
* routinely lands inside one. With nothing to break the tie the transcript came back in
|
||||
* whatever order SQLite's sorter produced, which under a descending query drew a same-second
|
||||
* burst backwards and was free to change between two reads of the same room.
|
||||
*
|
||||
* `id DESC` is the key `ChatRoomDao` already breaks its last-message tie on, so the chat
|
||||
* list's preview and the bottom of the room it opens name the same line.
|
||||
*/
|
||||
@Test
|
||||
fun `lines written in the same second come back in write order`() = runBlocking {
|
||||
seedRooms()
|
||||
val burst = Instant.fromEpochSeconds(1_000)
|
||||
line(groupEventId = null, content = "first", createdAt = burst)
|
||||
line(groupEventId = null, content = "second", createdAt = burst)
|
||||
line(groupEventId = null, content = "third", createdAt = burst)
|
||||
|
||||
// Newest first, which is the order `reverseLayout` draws from the bottom up.
|
||||
assertEquals(
|
||||
listOf("third", "second", "first"),
|
||||
db.chatMessageDao().getChatMessagesByChatRoomId(roomOne).map { it.chatMessage.content }
|
||||
)
|
||||
|
||||
// The flow is what the open room actually reads, and it must not order differently
|
||||
// from the snapshot the same screen loads with.
|
||||
assertEquals(
|
||||
listOf("third", "second", "first"),
|
||||
db.chatMessageDao().observeChatMessagesByChatRoomId(roomOne).first()
|
||||
.map { it.chatMessage.content }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a senders lines are counted per room`() = runBlocking {
|
||||
seedRooms()
|
||||
|
||||
@@ -963,8 +963,12 @@ class ChronicleApplyJvmTest {
|
||||
)
|
||||
}
|
||||
|
||||
// Newest first, which is the transcript's order: the answer is the line above
|
||||
// the composer and the ask sits over it. Both land in the same second, so what
|
||||
// decides between them is `ChatMessageDao`'s `id DESC` rather than the sorter --
|
||||
// without it this read whichever way SQLite happened to hand the tie back.
|
||||
assertEquals(
|
||||
listOf(ChatMessage.TYPE_CHRONICLE_REQUESTED, ChatMessage.TYPE_CHRONICLE_RECEIVED),
|
||||
listOf(ChatMessage.TYPE_CHRONICLE_RECEIVED, ChatMessage.TYPE_CHRONICLE_REQUESTED),
|
||||
receiver.chatMessageDao().getChatMessagesByChatRoomId(chatRoomId)
|
||||
.map { it.chatMessage.messageType }
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user