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,
"database": {
"version": 13,
"identityHash": "6986b68476bae871773348987eada812",
"identityHash": "ec1e16b220080592a488165e493b4f89",
"entities": [
{
"tableName": "User",
@ -450,7 +450,7 @@
},
{
"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": [
{
"fieldPath": "internalId",
@ -601,6 +601,18 @@
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@ -725,7 +737,7 @@
"views": [],
"setupQueries": [
"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]
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
messageInputViewModel.setData(chatViewModel.getChatRepository())
this.lifecycleScope.launch {
delay(DELAY_TO_SHOW_PROGRESS_BAR)
@ -914,6 +915,15 @@ class ChatActivity :
.collect()
}
this.lifecycleScope.launch {
chatViewModel.getRemoveMessageFlow
.onEach {
adapter!!.delete(it)
adapter!!.notifyDataSetChanged()
}
.collect()
}
this.lifecycleScope.launch {
chatViewModel.getUpdateMessageFlow
.onEach {

View file

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

View file

@ -73,8 +73,7 @@ class OfflineFirstChatRepository @Inject constructor(
>
> = MutableSharedFlow()
override val updateMessageFlow:
Flow<ChatMessage>
override val updateMessageFlow: Flow<ChatMessage>
get() = _updateMessageFlow
private val _updateMessageFlow:
@ -87,8 +86,7 @@ class OfflineFirstChatRepository @Inject constructor(
private val _lastCommonReadFlow:
MutableSharedFlow<Int> = MutableSharedFlow()
override val lastReadMessageFlow:
Flow<Int>
override val lastReadMessageFlow: Flow<Int>
get() = _lastReadMessageFlow
private val _lastReadMessageFlow:
@ -99,6 +97,12 @@ class OfflineFirstChatRepository @Inject constructor(
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 itIsPaused = false
private val scope = CoroutineScope(Dispatchers.IO)
@ -174,6 +178,9 @@ class OfflineFirstChatRepository @Inject constructor(
if (newestMessageIdFromDb.toInt() != 0) {
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
// TODO: somewhere here also handle temp messages. updateUiMessages(chatMessages, showUnreadMessagesMarker)
showMessagesBeforeAndEqual(
internalConversationId,
newestMessageIdFromDb,
@ -295,8 +302,7 @@ class OfflineFirstChatRepository @Inject constructor(
val weHaveMessagesFromOurself = chatMessages.any { it.actorId == currentUser.userId }
showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself
val triple = Triple(true, showUnreadMessagesMarker, chatMessages)
_messageFlow.emit(triple)
updateUiMessages(chatMessages, showUnreadMessagesMarker)
} else {
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 {
val loadFromServer: Boolean
@ -797,7 +830,11 @@ class OfflineFirstChatRepository @Inject constructor(
): Flow<Result<ChatMessage?>> =
flow {
try {
val tempChatMessageEntity = createChatMessageEntity(internalConversationId, message.toString())
val tempChatMessageEntity = createChatMessageEntity(
internalConversationId,
message.toString(),
referenceId
)
// accessing internalConversationId creates UninitializedPropertyException because ChatViewModel and
// MessageInputViewModel use different instances of ChatRepository for now
@ -819,43 +856,35 @@ class OfflineFirstChatRepository @Inject constructor(
}
}
private fun createChatMessageEntity(internalConversationId: String, message: String): ChatMessageEntity {
// val id = chatMessageCounter++
private fun createChatMessageEntity(
internalConversationId: String,
message: String,
referenceId: String
): ChatMessageEntity {
val emoji1 = "\uD83D\uDE00" // 😀
val emoji2 = "\uD83D\uDE1C" // 😜
val reactions = LinkedHashMap<String, Int>()
reactions[emoji1] = 3
reactions[emoji2] = 4
val reactionsSelf = ArrayList<String>()
reactionsSelf.add(emoji1)
val currentTimeMillies = System.currentTimeMillis()
val entity = ChatMessageEntity(
internalId = internalConversationId + "_temp1",
internalId = internalConversationId + "@_temp_" + currentTimeMillies,
internalConversationId = internalConversationId,
id = 111111111,
message = message,
reactions = reactions,
reactionsSelf = reactionsSelf,
id = currentTimeMillies,
message = message + " (temp)",
deleted = false,
token = "",
actorId = "",
actorType = "",
accountId = 1,
token = conversationModel.token,
actorId = currentUser.userId!!,
actorType = "users",
accountId = currentUser.id!!,
messageParameters = null,
messageType = "",
messageType = "comment",
parentMessageId = null,
systemMessageType = ChatMessage.SystemMessageType.DUMMY,
replyable = false,
timestamp = System.currentTimeMillis(),
timestamp = System.currentTimeMillis() / MILLIES,
expirationTimestamp = 0,
actorDisplayName = "test",
lastEditActorType = null,
lastEditTimestamp = 0L,
renderMarkdown = true,
lastEditActorId = "",
lastEditActorDisplayName = ""
actorDisplayName = currentUser.displayName!!,
referenceId = referenceId,
isTemporary = true,
sendingFailed = false
)
return entity
}
@ -868,5 +897,6 @@ class OfflineFirstChatRepository @Inject constructor(
private const val HALF_SECOND = 500L
private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 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
val disposableSet = mutableSetOf<Disposable>()
fun getChatRepository(): ChatMessageRepository {
return chatRepository
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
currentLifeCycleFlag = LifeCycleFlag.RESUMED
@ -125,6 +129,8 @@ class ChatViewModel @Inject constructor(
_chatMessageViewState.value = ChatMessageErrorState
}
val getRemoveMessageFlow = chatRepository.removeMessageFlow
val getUpdateMessageFlow = chatRepository.updateMessageFlow
val getLastCommonReadFlow = chatRepository.lastCommonReadFlow

View file

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

View file

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

View file

@ -22,6 +22,7 @@ interface ChatMessagesDao {
SELECT MAX(id) as max_items
FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND isTemporary = 0
"""
)
fun getNewestMessageId(internalConversationId: String): Long
@ -36,6 +37,17 @@ interface ChatMessagesDao {
)
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)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)
@ -59,6 +71,16 @@ interface ChatMessagesDao {
)
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
fun updateChatMessage(message: ChatMessageEntity)

View file

@ -64,6 +64,8 @@ data class ChatMessageEntity(
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
@ColumnInfo(name = "referenceId") var referenceId: String? = null,
@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
)

View file

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