diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json new file mode 100644 index 000000000..e1abb2b3f --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json @@ -0,0 +1,730 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "5bc4247e179307faa995552da5d34324", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "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, `isTemporary` 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, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "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, '5bc4247e179307faa995552da5d34324')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt index 6d40adb57..7510caf76 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt @@ -29,6 +29,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.model.SendStatus import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding @@ -184,7 +185,7 @@ class OutcomingTextMessageViewHolder(itemView: View) : binding.checkMark.visibility = View.INVISIBLE binding.sendingProgress.visibility = View.GONE - if (message.sendingFailed) { + if (message.sendStatus == SendStatus.FAILED) { updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed)) } else if (message.isTemporary) { updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending)) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index c6041ccd4..838a24bc7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -200,7 +200,7 @@ class MessageInputFragment : Fragment() { val connectionGained = (!wasOnline && isOnline) Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") if (connectionGained) { - chatActivity.messageInputViewModel.sendTempMessages( + chatActivity.messageInputViewModel.sendUnsentMessages( chatActivity.conversationUser!!.getCredentials(), ApiUtils.getUrlForChat( chatActivity.chatApiVersion, 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 8e2e3f3ed..02059853a 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 @@ -110,7 +110,7 @@ interface ChatMessageRepository : LifecycleAwareManager { suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow - suspend fun sendTempChatMessages(credentials: String, url: String) + suspend fun sendUnsentChatMessages(credentials: String, url: String) suspend fun deleteTempMessage(chatMessage: ChatMessage) } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index 3be97d912..f79ee5043 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -14,6 +14,7 @@ import android.util.Log import com.bluelinelabs.logansquare.annotation.JsonIgnore import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.database.model.SendStatus import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage import com.nextcloud.talk.models.json.chat.ReadStatus @@ -119,7 +120,7 @@ data class ChatMessage( var referenceId: String? = null, - var sendingFailed: Boolean = true, + var sendStatus: SendStatus? = null, var silent: Boolean = false 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 1fc1f9235..9049f262f 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 @@ -19,6 +19,7 @@ import com.nextcloud.talk.data.database.mappers.asEntity import com.nextcloud.talk.data.database.mappers.asModel import com.nextcloud.talk.data.database.model.ChatBlockEntity import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.SendStatus import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.extensions.toIntOrZero @@ -214,7 +215,8 @@ class OfflineFirstChatRepository @Inject constructor( ) } - sendTempChatMessages(credentials, urlForChatting) + // this call could be deleted when we have a worker to send messages.. + sendUnsentChatMessages(credentials, urlForChatting) // delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing // with them (otherwise there is a race condition). @@ -365,11 +367,18 @@ class OfflineFirstChatRepository @Inject constructor( lookIntoFuture: Boolean, showUnreadMessagesMarker: Boolean ) { + receivedChatMessages.forEach { + Log.d(TAG, "receivedChatMessage: " + it.message) + } + // remove all temp messages from UI val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) .first() .map(ChatMessageEntity::asModel) - oldTempMessages.forEach { _removeMessageFlow.emit(it) } + oldTempMessages.forEach { + Log.d(TAG, "oldTempMessage to be removed from UI: " + it.message) + _removeMessageFlow.emit(it) + } // add new messages to UI val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages) @@ -378,6 +387,9 @@ class OfflineFirstChatRepository @Inject constructor( // remove temp messages from DB that are now found in the new messages val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId } val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds } + tempChatMessagesThatCanBeReplaced.forEach { + Log.d(TAG, "oldTempMessage that was identified in newMessages: " + it.message) + } chatDao.deleteTempChatMessages( internalConversationId, tempChatMessagesThatCanBeReplaced.map { it.referenceId!! } @@ -389,6 +401,10 @@ class OfflineFirstChatRepository @Inject constructor( .sortedBy { it.internalId } .map(ChatMessageEntity::asModel) + remainingTempMessages.forEach { + Log.d(TAG, "remainingTempMessage: " + it.message) + } + val triple = Triple(true, false, remainingTempMessages) _messageFlow.emit(triple) } @@ -843,6 +859,17 @@ class OfflineFirstChatRepository @Inject constructor( val chatMessageModel = response.ocs?.data?.asModel() + val sentMessage = chatDao.getTempMessageForConversation( + internalConversationId, + referenceId + ).firstOrNull() + + sentMessage?.let { + it.sendStatus = SendStatus.SENT_PENDING_ACK + chatDao.updateChatMessage(it) + } + + Log.d(TAG, "sending chat message succeeded: " + message) emit(Result.success(chatMessageModel)) } .catch { e -> @@ -853,7 +880,7 @@ class OfflineFirstChatRepository @Inject constructor( referenceId ).firstOrNull() failedMessage?.let { - it.sendingFailed = true + it.sendStatus = SendStatus.FAILED chatDao.updateChatMessage(it) val failedMessageModel = it.asModel() @@ -874,7 +901,7 @@ class OfflineFirstChatRepository @Inject constructor( referenceId: String ): Flow> { val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first() - messageToResend.sendingFailed = false + messageToResend.sendStatus = SendStatus.PENDING chatDao.updateChatMessage(messageToResend) val messageToResendModel = messageToResend.asModel() @@ -930,8 +957,8 @@ class OfflineFirstChatRepository @Inject constructor( } } - override suspend fun sendTempChatMessages(credentials: String, url: String) { - val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first() + override suspend fun sendUnsentChatMessages(credentials: String, url: String) { + val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first() tempMessages.sortedBy { it.internalId }.onEach { sendChatMessage( credentials, @@ -1025,7 +1052,7 @@ class OfflineFirstChatRepository @Inject constructor( actorDisplayName = currentUser.displayName!!, referenceId = referenceId, isTemporary = true, - sendingFailed = false, + sendStatus = SendStatus.PENDING, silent = sendWithoutNotification ) return entity 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 12ced9f46..7c081b9d2 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 @@ -169,9 +169,9 @@ class MessageInputViewModel @Inject constructor( } } - fun sendTempMessages(credentials: String, url: String) { + fun sendUnsentMessages(credentials: String, url: String) { viewModelScope.launch { - chatRepository.sendTempChatMessages( + chatRepository.sendUnsentChatMessages( credentials, url ) 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 1008ce853..3373300de 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 @@ -50,6 +50,18 @@ interface ChatMessagesDao { ) fun getTempMessagesForConversation(internalConversationId: String): Flow> + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 1 + AND sendStatus != 'SENT_PENDING_ACK' + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow> + @Query( """ SELECT * diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt index 7aa236369..c2227dd75 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -68,7 +68,7 @@ fun ChatMessageEntity.asModel() = isDeleted = deleted, referenceId = referenceId, isTemporary = isTemporary, - sendingFailed = sendingFailed, + sendStatus = sendStatus, readStatus = ReadStatus.NONE, silent = silent ) 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 c00b2c2e7..36616858a 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,7 +64,7 @@ data class ChatMessageEntity( @ColumnInfo(name = "reactions") var reactions: LinkedHashMap? = null, @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList? = null, @ColumnInfo(name = "referenceId") var referenceId: String? = null, - @ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false, + @ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null, @ColumnInfo(name = "silent") var silent: Boolean = false, @ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, @ColumnInfo(name = "timestamp") var timestamp: Long = 0 diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt new file mode 100644 index 000000000..6d18227da --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +enum class SendStatus { + PENDING, + SENT_PENDING_ACK, + FAILED +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt index e9a5b2832..be0ccec8f 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt @@ -1,18 +1,31 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe * SPDX-FileCopyrightText: 2022 Andy Scherzinger * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.data.source.local import android.util.Log +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import java.sql.SQLException @Suppress("MagicNumber") object Migrations { + + //region Auto migrations + + @DeleteColumn(tableName = "ChatMessages", columnName = "sendingFailed") + class AutoMigration16To17 : AutoMigrationSpec + + //endregion + + //region Manual migrations + val MIGRATION_6_8 = object : Migration(6, 8) { override fun migrate(db: SupportSQLiteDatabase) { Log.i("Migrations", "Migrating 6 to 8") @@ -76,6 +89,8 @@ object Migrations { } } + //endregion + fun migrateToRoom(db: SupportSQLiteDatabase) { db.execSQL( "CREATE TABLE User_new (" + 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 1108fa394..6ba99dbea 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2023-2025 Marcel Hibbe * SPDX-FileCopyrightText: 2022 Andy Scherzinger * SPDX-FileCopyrightText: 2017-2020 Mario Danic * SPDX-License-Identifier: GPL-3.0-or-later @@ -23,12 +23,14 @@ import com.nextcloud.talk.data.database.dao.ConversationsDao import com.nextcloud.talk.data.database.model.ChatBlockEntity import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.Migrations.AutoMigration16To17 import com.nextcloud.talk.data.source.local.converters.ArrayListConverter import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter import com.nextcloud.talk.data.source.local.converters.LinkedHashMapConverter import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter +import com.nextcloud.talk.data.source.local.converters.SendStatusConverter import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter import com.nextcloud.talk.data.storage.ArbitraryStoragesDao @@ -49,9 +51,10 @@ import java.util.Locale ChatMessageEntity::class, ChatBlockEntity::class ], - version = 16, + version = 17, autoMigrations = [ - AutoMigration(from = 9, to = 10) + AutoMigration(from = 9, to = 10), + AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class) ], exportSchema = true ) @@ -63,7 +66,8 @@ import java.util.Locale SignalingSettingsConverter::class, HashMapHashMapConverter::class, LinkedHashMapConverter::class, - ArrayListConverter::class + ArrayListConverter::class, + SendStatusConverter::class ) abstract class TalkDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt new file mode 100644 index 000000000..f51e05c14 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.nextcloud.talk.data.database.model.SendStatus + +class SendStatusConverter { + @TypeConverter + fun fromStatus(value: SendStatus): String { + return value.name + } + + @TypeConverter + fun toStatus(value: String): SendStatus { + return SendStatus.valueOf(value) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt index 482c068d5..c7966f393 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt @@ -18,6 +18,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.model.SendStatus import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -58,9 +59,10 @@ class TempMessageActionsDialog( private fun initMenuItems() { this.lifecycleScope.launch { - initResendMessage(message.sendingFailed && networkMonitor.isOnline.value) - initMenuEditMessage(message.sendingFailed || !networkMonitor.isOnline.value) - initMenuDeleteMessage(message.sendingFailed || !networkMonitor.isOnline.value) + val sendingFailed = message.sendStatus == SendStatus.FAILED + initResendMessage(sendingFailed && networkMonitor.isOnline.value) + initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value) + initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value) initMenuItemCopy() } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt index f09e45873..5eaac9ef9 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt @@ -30,6 +30,11 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao { override fun getTempMessagesForConversation(internalConversationId: String): Flow> = flowOf() + override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow> { + // nothing to return here as long this class is only used for the Search window + return flowOf() + } + override fun getTempMessageForConversation( internalConversationId: String, referenceId: String