WIP temp messages are replaced when same refId received from sever

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-12-04 15:46:54 +01:00
parent 8fbf70ee4e
commit aff7845e83
No known key found for this signature in database
GPG key ID: C793F8B59F43CE7B
10 changed files with 130 additions and 41 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 13, "version": 13,
"identityHash": "6986b68476bae871773348987eada812", "identityHash": "ec1e16b220080592a488165e493b4f89",
"entities": [ "entities": [
{ {
"tableName": "User", "tableName": "User",
@ -450,7 +450,7 @@
}, },
{ {
"tableName": "ChatMessages", "tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `sendingFailed` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [ "fields": [
{ {
"fieldPath": "internalId", "fieldPath": "internalId",
@ -601,6 +601,18 @@
"columnName": "timestamp", "columnName": "timestamp",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@ -725,7 +737,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6986b68476bae871773348987eada812')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ec1e16b220080592a488165e493b4f89')"
] ]
} }
} }

View file

@ -441,6 +441,7 @@ class ChatActivity :
chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java] messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
messageInputViewModel.setData(chatViewModel.getChatRepository())
this.lifecycleScope.launch { this.lifecycleScope.launch {
delay(DELAY_TO_SHOW_PROGRESS_BAR) delay(DELAY_TO_SHOW_PROGRESS_BAR)
@ -914,6 +915,15 @@ class ChatActivity :
.collect() .collect()
} }
this.lifecycleScope.launch {
chatViewModel.getRemoveMessageFlow
.onEach {
adapter!!.delete(it)
adapter!!.notifyDataSetChanged()
}
.collect()
}
this.lifecycleScope.launch { this.lifecycleScope.launch {
chatViewModel.getUpdateMessageFlow chatViewModel.getUpdateMessageFlow
.onEach { .onEach {

View file

@ -42,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
*/ */
val generalUIFlow: Flow<String> val generalUIFlow: Flow<String>
val removeMessageFlow: Flow<ChatMessage>
fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String) fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String)
fun loadInitialMessages(withNetworkParams: Bundle): Job fun loadInitialMessages(withNetworkParams: Bundle): Job

View file

@ -73,8 +73,7 @@ class OfflineFirstChatRepository @Inject constructor(
> >
> = MutableSharedFlow() > = MutableSharedFlow()
override val updateMessageFlow: override val updateMessageFlow: Flow<ChatMessage>
Flow<ChatMessage>
get() = _updateMessageFlow get() = _updateMessageFlow
private val _updateMessageFlow: private val _updateMessageFlow:
@ -87,8 +86,7 @@ class OfflineFirstChatRepository @Inject constructor(
private val _lastCommonReadFlow: private val _lastCommonReadFlow:
MutableSharedFlow<Int> = MutableSharedFlow() MutableSharedFlow<Int> = MutableSharedFlow()
override val lastReadMessageFlow: override val lastReadMessageFlow: Flow<Int>
Flow<Int>
get() = _lastReadMessageFlow get() = _lastReadMessageFlow
private val _lastReadMessageFlow: private val _lastReadMessageFlow:
@ -99,6 +97,12 @@ class OfflineFirstChatRepository @Inject constructor(
private val _generalUIFlow: MutableSharedFlow<String> = MutableSharedFlow() private val _generalUIFlow: MutableSharedFlow<String> = MutableSharedFlow()
override val removeMessageFlow: Flow<ChatMessage>
get() = _removeMessageFlow
private val _removeMessageFlow:
MutableSharedFlow<ChatMessage> = MutableSharedFlow()
private var newXChatLastCommonRead: Int? = null private var newXChatLastCommonRead: Int? = null
private var itIsPaused = false private var itIsPaused = false
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@ -174,6 +178,9 @@ class OfflineFirstChatRepository @Inject constructor(
if (newestMessageIdFromDb.toInt() != 0) { if (newestMessageIdFromDb.toInt() != 0) {
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb) val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
// TODO: somewhere here also handle temp messages. updateUiMessages(chatMessages, showUnreadMessagesMarker)
showMessagesBeforeAndEqual( showMessagesBeforeAndEqual(
internalConversationId, internalConversationId,
newestMessageIdFromDb, newestMessageIdFromDb,
@ -295,8 +302,7 @@ class OfflineFirstChatRepository @Inject constructor(
val weHaveMessagesFromOurself = chatMessages.any { it.actorId == currentUser.userId } val weHaveMessagesFromOurself = chatMessages.any { it.actorId == currentUser.userId }
showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself
val triple = Triple(true, showUnreadMessagesMarker, chatMessages) updateUiMessages(chatMessages, showUnreadMessagesMarker)
_messageFlow.emit(triple)
} else { } else {
Log.d(TAG, "resultsFromSync are null or empty") Log.d(TAG, "resultsFromSync are null or empty")
} }
@ -319,6 +325,33 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
private suspend fun updateUiMessages(chatMessages : List<ChatMessage>, showUnreadMessagesMarker: Boolean) {
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
.first()
.map(ChatMessageEntity::asModel)
oldTempMessages.forEach { _removeMessageFlow.emit(it) }
val tripleChatMessages = Triple(true, showUnreadMessagesMarker, chatMessages)
_messageFlow.emit(tripleChatMessages)
val chatMessagesReferenceIds = chatMessages.mapTo(HashSet(chatMessages.size)) { it.referenceId }
val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds }
chatDao.deleteTempChatMessages(
internalConversationId,
tempChatMessagesThatCanBeReplaced.map { it.referenceId!! }
)
val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
.first()
.map(ChatMessageEntity::asModel)
val triple = Triple(true, false, remainingTempMessages)
_messageFlow.emit(triple)
}
private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean { private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean {
val loadFromServer: Boolean val loadFromServer: Boolean
@ -797,7 +830,11 @@ class OfflineFirstChatRepository @Inject constructor(
): Flow<Result<ChatMessage?>> = ): Flow<Result<ChatMessage?>> =
flow { flow {
try { try {
val tempChatMessageEntity = createChatMessageEntity(internalConversationId, message.toString()) val tempChatMessageEntity = createChatMessageEntity(
internalConversationId,
message.toString(),
referenceId
)
// accessing internalConversationId creates UninitializedPropertyException because ChatViewModel and // accessing internalConversationId creates UninitializedPropertyException because ChatViewModel and
// MessageInputViewModel use different instances of ChatRepository for now // MessageInputViewModel use different instances of ChatRepository for now
@ -819,43 +856,35 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
private fun createChatMessageEntity(internalConversationId: String, message: String): ChatMessageEntity { private fun createChatMessageEntity(
// val id = chatMessageCounter++ internalConversationId: String,
message: String,
referenceId: String
): ChatMessageEntity {
val emoji1 = "\uD83D\uDE00" // 😀 val currentTimeMillies = System.currentTimeMillis()
val emoji2 = "\uD83D\uDE1C" // 😜
val reactions = LinkedHashMap<String, Int>()
reactions[emoji1] = 3
reactions[emoji2] = 4
val reactionsSelf = ArrayList<String>()
reactionsSelf.add(emoji1)
val entity = ChatMessageEntity( val entity = ChatMessageEntity(
internalId = internalConversationId + "_temp1", internalId = internalConversationId + "@_temp_" + currentTimeMillies,
internalConversationId = internalConversationId, internalConversationId = internalConversationId,
id = 111111111, id = currentTimeMillies,
message = message, message = message + " (temp)",
reactions = reactions,
reactionsSelf = reactionsSelf,
deleted = false, deleted = false,
token = "", token = conversationModel.token,
actorId = "", actorId = currentUser.userId!!,
actorType = "", actorType = "users",
accountId = 1, accountId = currentUser.id!!,
messageParameters = null, messageParameters = null,
messageType = "", messageType = "comment",
parentMessageId = null, parentMessageId = null,
systemMessageType = ChatMessage.SystemMessageType.DUMMY, systemMessageType = ChatMessage.SystemMessageType.DUMMY,
replyable = false, replyable = false,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis() / MILLIES,
expirationTimestamp = 0, expirationTimestamp = 0,
actorDisplayName = "test", actorDisplayName = currentUser.displayName!!,
lastEditActorType = null, referenceId = referenceId,
lastEditTimestamp = 0L, isTemporary = true,
renderMarkdown = true, sendingFailed = false
lastEditActorId = "",
lastEditActorDisplayName = ""
) )
return entity return entity
} }
@ -868,5 +897,6 @@ class OfflineFirstChatRepository @Inject constructor(
private const val HALF_SECOND = 500L private const val HALF_SECOND = 500L
private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100 private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100
private const val DEFAULT_MESSAGES_LIMIT = 100 private const val DEFAULT_MESSAGES_LIMIT = 100
private const val MILLIES = 1000
} }
} }

View file

@ -72,6 +72,10 @@ class ChatViewModel @Inject constructor(
lateinit var currentLifeCycleFlag: LifeCycleFlag lateinit var currentLifeCycleFlag: LifeCycleFlag
val disposableSet = mutableSetOf<Disposable>() val disposableSet = mutableSetOf<Disposable>()
fun getChatRepository(): ChatMessageRepository {
return chatRepository
}
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
currentLifeCycleFlag = LifeCycleFlag.RESUMED currentLifeCycleFlag = LifeCycleFlag.RESUMED
@ -125,6 +129,8 @@ class ChatViewModel @Inject constructor(
_chatMessageViewState.value = ChatMessageErrorState _chatMessageViewState.value = ChatMessageErrorState
} }
val getRemoveMessageFlow = chatRepository.removeMessageFlow
val getUpdateMessageFlow = chatRepository.updateMessageFlow val getUpdateMessageFlow = chatRepository.updateMessageFlow
val getLastCommonReadFlow = chatRepository.lastCommonReadFlow val getLastCommonReadFlow = chatRepository.lastCommonReadFlow

View file

@ -34,21 +34,27 @@ import java.lang.Thread.sleep
import javax.inject.Inject import javax.inject.Inject
class MessageInputViewModel @Inject constructor( class MessageInputViewModel @Inject constructor(
private val chatRepository: ChatMessageRepository,
private val audioRecorderManager: AudioRecorderManager, private val audioRecorderManager: AudioRecorderManager,
private val mediaPlayerManager: MediaPlayerManager, private val mediaPlayerManager: MediaPlayerManager,
private val audioFocusRequestManager: AudioFocusRequestManager, private val audioFocusRequestManager: AudioFocusRequestManager,
private val appPreferences: AppPreferences private val appPreferences: AppPreferences
) : ViewModel(), ) : ViewModel(),
DefaultLifecycleObserver { DefaultLifecycleObserver {
enum class LifeCycleFlag { enum class LifeCycleFlag {
PAUSED, PAUSED,
RESUMED, RESUMED,
STOPPED STOPPED
} }
lateinit var chatRepository: ChatMessageRepository
lateinit var currentLifeCycleFlag: LifeCycleFlag lateinit var currentLifeCycleFlag: LifeCycleFlag
val disposableSet = mutableSetOf<Disposable>() val disposableSet = mutableSetOf<Disposable>()
fun setData(chatMessageRepository: ChatMessageRepository){
chatRepository = chatMessageRepository
}
data class QueuedMessage( data class QueuedMessage(
val id: Int, val id: Int,
var message: CharSequence? = null, var message: CharSequence? = null,

View file

@ -152,7 +152,6 @@ class RepositoryModule {
@Provides @Provides
fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi) fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi)
@Singleton
@Provides @Provides
fun provideOfflineFirstChatRepository( fun provideOfflineFirstChatRepository(
chatMessagesDao: ChatMessagesDao, chatMessagesDao: ChatMessagesDao,

View file

@ -22,6 +22,7 @@ interface ChatMessagesDao {
SELECT MAX(id) as max_items SELECT MAX(id) as max_items
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 0
""" """
) )
fun getNewestMessageId(internalConversationId: String): Long fun getNewestMessageId(internalConversationId: String): Long
@ -36,6 +37,17 @@ interface ChatMessagesDao {
) )
fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *
FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND isTemporary = 1
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)
@ -59,6 +71,16 @@ interface ChatMessagesDao {
) )
fun deleteChatMessages(messageIds: List<Int>) fun deleteChatMessages(messageIds: List<Int>)
@Query(
value = """
DELETE FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND referenceId in (:referenceIds)
AND isTemporary = 1
"""
)
fun deleteTempChatMessages(internalConversationId: String, referenceIds: List<String>)
@Update @Update
fun updateChatMessage(message: ChatMessageEntity) fun updateChatMessage(message: ChatMessageEntity)

View file

@ -64,6 +64,8 @@ data class ChatMessageEntity(
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null, @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
@ColumnInfo(name = "referenceId") var referenceId: String? = null, @ColumnInfo(name = "referenceId") var referenceId: String? = null,
@ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, @ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType,
@ColumnInfo(name = "timestamp") var timestamp: Long = 0 @ColumnInfo(name = "timestamp") var timestamp: Long = 0,
@ColumnInfo(name = "isTemporary") var isTemporary: Boolean = false,
@ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false,
// missing/not needed: silent // missing/not needed: silent
) )

View file

@ -108,7 +108,7 @@ abstract class TalkDatabase : RoomDatabase() {
return Room return Room
.databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
// comment out openHelperFactory to view the database entries in Android Studio for debugging // comment out openHelperFactory to view the database entries in Android Studio for debugging
.openHelperFactory(factory) // .openHelperFactory(factory)
.addMigrations( .addMigrations(
Migrations.MIGRATION_6_8, Migrations.MIGRATION_6_8,
Migrations.MIGRATION_7_8, Migrations.MIGRATION_7_8,