diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json index fdeb82d1e..3b4330bb9 100644 --- a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json @@ -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')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 47a959bb3..a5f482d30 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -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 { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index 218c3b658..976f3db94 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -42,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager { */ val generalUIFlow: Flow + val removeMessageFlow: Flow + fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String) fun loadInitialMessages(withNetworkParams: Bundle): Job diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index f00a39407..419178677 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -73,8 +73,7 @@ class OfflineFirstChatRepository @Inject constructor( > > = MutableSharedFlow() - override val updateMessageFlow: - Flow + override val updateMessageFlow: Flow get() = _updateMessageFlow private val _updateMessageFlow: @@ -87,8 +86,7 @@ class OfflineFirstChatRepository @Inject constructor( private val _lastCommonReadFlow: MutableSharedFlow = MutableSharedFlow() - override val lastReadMessageFlow: - Flow + override val lastReadMessageFlow: Flow get() = _lastReadMessageFlow private val _lastReadMessageFlow: @@ -99,6 +97,12 @@ class OfflineFirstChatRepository @Inject constructor( private val _generalUIFlow: MutableSharedFlow = MutableSharedFlow() + override val removeMessageFlow: Flow + get() = _removeMessageFlow + + private val _removeMessageFlow: + MutableSharedFlow = 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, 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> = 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() - reactions[emoji1] = 3 - reactions[emoji2] = 4 - - val reactionsSelf = ArrayList() - 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 } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index f5c12a9a4..890a96b15 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -72,6 +72,10 @@ class ChatViewModel @Inject constructor( lateinit var currentLifeCycleFlag: LifeCycleFlag val disposableSet = mutableSetOf() + 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 diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt index 750d6fca0..0ca79ffad 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -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() + fun setData(chatMessageRepository: ChatMessageRepository){ + chatRepository = chatMessageRepository + } + data class QueuedMessage( val id: Int, var message: CharSequence? = null, diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 32c4eff9e..afa64efa5 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -152,7 +152,6 @@ class RepositoryModule { @Provides fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi) - @Singleton @Provides fun provideOfflineFirstChatRepository( chatMessagesDao: ChatMessagesDao, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt index 6fbf61ca1..6357ce236 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt @@ -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> + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 1 + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempMessagesForConversation(internalConversationId: String): Flow> + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertChatMessages(chatMessages: List) @@ -59,6 +71,16 @@ interface ChatMessagesDao { ) fun deleteChatMessages(messageIds: List) + @Query( + value = """ + DELETE FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND referenceId in (:referenceIds) + AND isTemporary = 1 + """ + ) + fun deleteTempChatMessages(internalConversationId: String, referenceIds: List) + @Update fun updateChatMessage(message: ChatMessageEntity) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt index 19ed015bd..1b46d5e89 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt @@ -64,6 +64,8 @@ data class ChatMessageEntity( @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList? = 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 ) diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt index 5c3656c76..8fa20e9d3 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -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,