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 new file mode 100644 index 0000000000..3b4330bb9a --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json @@ -0,0 +1,743 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "ec1e16b220080592a488165e493b4f89", + "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", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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, `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, 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", + "notNull": false + }, + { + "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": "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", + "notNull": false + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "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 + } + ], + "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, `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", + "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": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sendingFailed", + "columnName": "sendingFailed", + "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", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "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" + ] + } + ] + } + ], + "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, 'ec1e16b220080592a488165e493b4f89')" + ] + } +} \ 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 f1dc83286d..6c78b9d40c 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 @@ -14,6 +14,7 @@ import android.util.Log import android.util.TypedValue import android.view.View import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.google.android.flexbox.FlexboxLayout @@ -23,6 +24,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -58,6 +60,9 @@ class OutcomingTextMessageViewHolder(itemView: View) : @Inject lateinit var dateUtils: DateUtils + @Inject + lateinit var networkMonitor: NetworkMonitor + lateinit var commonMessageInterface: CommonMessageInterface override fun onBind(message: ChatMessage) { @@ -115,27 +120,27 @@ class OutcomingTextMessageViewHolder(itemView: View) : binding.messageQuote.quotedChatMessageView.visibility = View.GONE } - val readStatusDrawableInt = when (message.readStatus) { - ReadStatus.READ -> R.drawable.ic_check_all - ReadStatus.SENT -> R.drawable.ic_check - else -> null - } - - val readStatusContentDescriptionString = when (message.readStatus) { - ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) - ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) - else -> null - } - readStatusDrawableInt?.let { drawableInt -> - ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let { - binding.checkMark.setImageDrawable(it) - viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + CoroutineScope(Dispatchers.Main).launch { + if (message.sendingFailed) { + updateStatus( + R.drawable.baseline_report_problem_24, + "failed" + ) + } else if (message.isTempMessage && !networkMonitor.isOnline.first()) { + updateStatus( + R.drawable.ic_signal_wifi_off_white_24dp, + "offline" + ) + } else if (message.isTempMessage) { + updateSendingStatus() + } else if(message.readStatus == ReadStatus.READ){ + updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read)) + } else if(message.readStatus == ReadStatus.SENT) { + updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent)) } } - binding.checkMark.contentDescription = readStatusContentDescriptionString - itemView.setTag(R.string.replyable_message_view_tag, message.replyable) Reaction().showReactions( @@ -149,6 +154,27 @@ class OutcomingTextMessageViewHolder(itemView: View) : ) } + private fun updateStatus(readStatusDrawableInt: Int, description: String?) { + binding.sendingProgress.visibility = View.GONE + binding.checkMark.visibility = View.VISIBLE + readStatusDrawableInt.let { drawableInt -> + ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + binding.checkMark.contentDescription = description + } + + private fun updateSendingStatus() { + binding.sendingProgress.visibility = View.VISIBLE + binding.checkMark.visibility = View.GONE + + viewThemeUtils.material.colorProgressBar(binding.sendingProgress) + } + + + private fun longClickOnReaction(chatMessage: ChatMessage) { commonMessageInterface.onLongClickReactions(chatMessage) } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TemporaryMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/TemporaryMessageViewHolder.kt index 73552c8c7a..a8e6a8f4f6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/TemporaryMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TemporaryMessageViewHolder.kt @@ -13,6 +13,7 @@ import android.view.View import androidx.core.content.res.ResourcesCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole @@ -58,6 +59,14 @@ class TemporaryMessageViewHolder(outgoingView: View, payload: Any) : viewThemeUtils.platform.colorImageView(binding.tempMsgEdit, ColorRole.PRIMARY) viewThemeUtils.platform.colorImageView(binding.tempMsgDelete, ColorRole.PRIMARY) + binding.bubble.setOnClickListener { + if (binding.tempMsgActions.isVisible) { + binding.tempMsgActions.visibility = View.GONE + } else { + binding.tempMsgActions.visibility = View.VISIBLE + } + } + binding.tempMsgEdit.setOnClickListener { isEditing = !isEditing if (isEditing) { diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 78605a7fc4..679baeaff3 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -344,18 +344,14 @@ Observable> pullChatMessages(@Header("Authorization") Stri @FormUrlEncoded @POST - Observable sendChatMessage(@Header("Authorization") String authorization, + Observable sendChatMessage(@Header("Authorization") String authorization, @Url String url, @Field("message") CharSequence message, @Field("actorDisplayName") String actorDisplayName, @Field("replyTo") Integer replyTo, - @Field("silent") Boolean sendWithoutNotification); - - @FormUrlEncoded - @PUT - Observable editChatMessage(@Header("Authorization") String authorization, - @Url String url, - @Field("message") String message); + @Field("silent") Boolean sendWithoutNotification, + @Field("referenceId") String referenceId + ); @GET Observable> getSharedItems( diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 3bbdf0f901..c2cc6bd6e7 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -8,6 +8,7 @@ package com.nextcloud.talk.api import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall @@ -121,6 +122,26 @@ interface NcApiCoroutines { @DELETE suspend fun unarchiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + @FormUrlEncoded + @POST + suspend fun sendChatMessage( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("message") message: CharSequence, + @Field("actorDisplayName") actorDisplayName: String, + @Field("replyTo") replyTo: Int, + @Field("silent") sendWithoutNotification: Boolean, + @Field("referenceId") referenceId: String + ): ChatOverallSingleMessage + + @FormUrlEncoded + @PUT + suspend fun editChatMessage( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("message") message: String + ): ChatOverallSingleMessage + @FormUrlEncoded @POST suspend fun banActor( 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 4689d35e2c..c1cabac48b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -200,7 +200,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import retrofit2.HttpException import java.io.File import java.io.IOException import java.net.HttpURLConnection @@ -442,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) @@ -573,21 +573,21 @@ class ChatActivity : private fun initObservers() { Log.d(TAG, "initObservers Called") - messageInputViewModel.messageQueueFlow.observe(this) { list -> - list.forEachIndexed { _, qMsg -> - val temporaryChatMessage = ChatMessage() - temporaryChatMessage.jsonMessageId = TEMPORARY_MESSAGE_ID_INT - temporaryChatMessage.actorId = "-3" - temporaryChatMessage.timestamp = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS - temporaryChatMessage.message = qMsg.message.toString() - temporaryChatMessage.tempMessageId = qMsg.id - temporaryChatMessage.isTempMessage = true - temporaryChatMessage.parentMessageId = qMsg.replyTo!!.toLong() - val pos = adapter?.getMessagePositionById(qMsg.replyTo.toString()) - adapter?.addToStart(temporaryChatMessage, true) - adapter?.notifyDataSetChanged() - } - } + // messageInputViewModel.messageQueueFlow.observe(this) { list -> + // list.forEachIndexed { _, qMsg -> + // val temporaryChatMessage = ChatMessage() + // temporaryChatMessage.jsonMessageId = TEMPORARY_MESSAGE_ID_INT + // temporaryChatMessage.actorId = TEMPORARY_MESSAGE_ID_STRING + // temporaryChatMessage.timestamp = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + // temporaryChatMessage.message = qMsg.message.toString() + // temporaryChatMessage.tempMessageId = qMsg.id + // temporaryChatMessage.isTempMessage = true + // temporaryChatMessage.parentMessageId = qMsg.replyTo!!.toLong() + // val pos = adapter?.getMessagePositionById(qMsg.replyTo.toString()) + // adapter?.addToStart(temporaryChatMessage, true) + // adapter?.notifyDataSetChanged() + // } + // } messageInputViewModel.messageQueueSizeFlow.observe(this) { size -> if (size == 0) { @@ -697,7 +697,6 @@ class ChatActivity : withCredentials = credentials!!, withUrl = urlForChatting ) - messageInputViewModel.getTempMessagesFromMessageQueue(currentConversation!!.internalId) } } else { Log.w( @@ -791,18 +790,20 @@ class ChatActivity : } is MessageInputViewModel.SendChatMessageErrorState -> { - if (state.e is HttpException) { - val code = state.e.code() - if (code.toString().startsWith("2")) { - myFirstMessage = state.message - - if (binding.unreadMessagesPopup.isShown) { - binding.unreadMessagesPopup.visibility = View.GONE - } + binding.messagesListView.smoothScrollToPosition(0) - binding.messagesListView.smoothScrollToPosition(0) - } - } + // if (state.e is HttpException) { + // val code = state.e.code() + // if (code.toString().startsWith("2")) { + // myFirstMessage = state.message + // + // if (binding.unreadMessagesPopup.isShown) { + // binding.unreadMessagesPopup.visibility = View.GONE + // } + // + // binding.messagesListView.smoothScrollToPosition(0) + // } + // } } else -> {} @@ -915,6 +916,14 @@ class ChatActivity : .collect() } + this.lifecycleScope.launch { + chatViewModel.getRemoveMessageFlow + .onEach { + removeMessageById(it.id) + } + .collect() + } + this.lifecycleScope.launch { chatViewModel.getUpdateMessageFlow .onEach { @@ -1056,9 +1065,15 @@ class ChatActivity : } private fun removeUnreadMessagesMarker() { - val index = adapter?.getMessagePositionById(UNREAD_MESSAGES_MARKER_ID.toString()) + removeMessageById(UNREAD_MESSAGES_MARKER_ID.toString()) + } + + // do not use adapter.deleteById() as it seems to contain a bug! Use this method instead! + private fun removeMessageById(idToDelete: String) { + val index = adapter?.getMessagePositionById(idToDelete) if (index != null && index != -1) { adapter?.items?.removeAt(index) + adapter?.notifyItemRemoved(index) } } @@ -2708,7 +2723,6 @@ class ChatActivity : ) { if (message.item is ChatMessage) { val chatMessage = message.item as ChatMessage - if (chatMessage.jsonMessageId <= xChatLastCommonRead) { chatMessage.readStatus = ReadStatus.READ } else { @@ -3215,7 +3229,7 @@ class ChatActivity : val message = iMessage as ChatMessage if (hasVisibleItems(message) && !isSystemMessage(message) && - message.id != "-3" + message.id != TEMPORARY_MESSAGE_ID_STRING ) { MessageActionsDialog( this, @@ -3619,7 +3633,8 @@ class ChatActivity : CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == UNREAD_MESSAGES_MARKER_ID.toString() CONTENT_TYPE_CALL_STARTED -> message.id == "-2" - CONTENT_TYPE_TEMP -> message.id == "-3" + // CONTENT_TYPE_TEMP -> message.id == TEMPORARY_MESSAGE_ID_STRING + // CONTENT_TYPE_TEMP -> message.readStatus == ReadStatus.FAILED CONTENT_TYPE_DECK_CARD -> message.isDeckCard() else -> false @@ -3765,27 +3780,27 @@ class ChatActivity : } override fun editTemporaryMessage(id: Int, newMessage: String) { - messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage) - adapter?.notifyDataSetChanged() // TODO optimize this + // messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage) + // adapter?.notifyDataSetChanged() // TODO optimize this } override fun deleteTemporaryMessage(id: Int) { - messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id) - var i = 0 - val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1) - for (item in adapter?.items!!) { - if (i > max!! && max < 1) break - if (item.item is ChatMessage && - (item.item as ChatMessage).isTempMessage && - (item.item as ChatMessage).tempMessageId == id - ) { - val index = adapter?.items!!.indexOf(item) - adapter?.items!!.removeAt(index) - adapter?.notifyItemRemoved(index) - break - } - i++ - } + // messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id) + // var i = 0 + // val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1) + // for (item in adapter?.items!!) { + // if (i > max!! && max < 1) break + // if (item.item is ChatMessage && + // (item.item as ChatMessage).isTempMessage && + // (item.item as ChatMessage).tempMessageId == id + // ) { + // val index = adapter?.items!!.indexOf(item) + // adapter?.items!!.removeAt(index) + // adapter?.notifyItemRemoved(index) + // break + // } + // i++ + // } } private fun logConversationInfos(methodName: String) { 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 ae04f09026..02cc9aabe1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -158,7 +158,6 @@ class MessageInputFragment : Fragment() { override fun onResume() { super.onResume() - chatActivity.messageInputViewModel.restoreMessageQueue(conversationInternalId) } override fun onDestroyView() { @@ -199,7 +198,7 @@ class MessageInputFragment : Fragment() { wasOnline = !binding.fragmentConnectionLost.isShown val connectionGained = (!wasOnline && isOnline) Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") - handleMessageQueue(isOnline) + // handleMessageQueue(isOnline) handleUI(isOnline, connectionGained) }.collect() } @@ -296,22 +295,22 @@ class MessageInputFragment : Fragment() { } } - private fun handleMessageQueue(isOnline: Boolean) { - if (isOnline) { - chatActivity.messageInputViewModel.switchToMessageQueue(false) - chatActivity.messageInputViewModel.sendAndEmptyMessageQueue( - conversationInternalId, - chatActivity.conversationUser!!.getCredentials(), - ApiUtils.getUrlForChat( - chatActivity.chatApiVersion, - chatActivity.conversationUser!!.baseUrl!!, - chatActivity.roomToken - ) - ) - } else { - chatActivity.messageInputViewModel.switchToMessageQueue(true) - } - } + // private fun handleMessageQueue(isOnline: Boolean) { + // if (isOnline) { + // chatActivity.messageInputViewModel.switchToMessageQueue(false) + // chatActivity.messageInputViewModel.sendAndEmptyMessageQueue( + // conversationInternalId, + // chatActivity.conversationUser!!.getCredentials(), + // ApiUtils.getUrlForChat( + // chatActivity.chatApiVersion, + // chatActivity.conversationUser!!.baseUrl!!, + // chatActivity.roomToken + // ) + // ) + // } else { + // chatActivity.messageInputViewModel.switchToMessageQueue(true) + // } + // } private fun restoreState() { if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) { 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 5cbf39efe7..976f3db941 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 @@ -11,6 +11,7 @@ import android.os.Bundle import com.nextcloud.talk.chat.data.io.LifecycleAwareManager import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -41,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 @@ -75,4 +78,23 @@ interface ChatMessageRepository : LifecycleAwareManager { * Destroys unused resources. */ fun handleChatOnBackPress() + + suspend fun sendChatMessage( + credentials: String, + url: String, + message: CharSequence, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> + + suspend fun addTemporaryMessage( + message: CharSequence, + displayName: String, + replyTo: Int, + referenceId: String + ): Flow> + + suspend fun editChatMessage(credentials: String, url: String, text: String): Flow> } 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 7283cf8ecb..a899313fb4 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 @@ -113,11 +113,16 @@ data class ChatMessage( var openWhenDownloaded: Boolean = true, - var isTempMessage: Boolean = false, + var isTempMessage: Boolean = false, // TODO: replace logic from message drafts with logic from temp message sending - var tempMessageId: Int = -1 + var tempMessageId: Int = -1, // TODO: replace logic from message drafts with logic from temp message sending -) : MessageContentType, MessageContentType.Image { + var referenceId: String? = null, + + var sendingFailed: Boolean = true + +) : MessageContentType, + MessageContentType.Image { var extractedUrlToPreview: String? = null @@ -238,8 +243,8 @@ data class ChatMessage( } } - fun getCalculateMessageType(): MessageType { - return if (!TextUtils.isEmpty(systemMessage)) { + fun getCalculateMessageType(): MessageType = + if (!TextUtils.isEmpty(systemMessage)) { MessageType.SYSTEM_MESSAGE } else if (isVoiceMessage) { MessageType.VOICE_MESSAGE @@ -254,19 +259,15 @@ data class ChatMessage( } else { MessageType.REGULAR_TEXT_MESSAGE } - } - override fun getId(): String { - return jsonMessageId.toString() - } + override fun getId(): String = jsonMessageId.toString() - override fun getText(): String { - return if (message != null) { + override fun getText(): String = + if (message != null) { getParsedMessage(message, messageParameters)!! } else { "" } - } fun getNullsafeActorDisplayName() = if (!TextUtils.isEmpty(actorDisplayName)) { @@ -275,22 +276,19 @@ data class ChatMessage( sharedApplication!!.getString(R.string.nc_guest) } - override fun getUser(): IUser { - return object : IUser { - override fun getId(): String { - return "$actorType/$actorId" - } + override fun getUser(): IUser = + object : IUser { + override fun getId(): String = "$actorType/$actorId" - override fun getName(): String { - return if (!TextUtils.isEmpty(actorDisplayName)) { + override fun getName(): String = + if (!TextUtils.isEmpty(actorDisplayName)) { actorDisplayName!! } else { sharedApplication!!.getString(R.string.nc_guest) } - } - override fun getAvatar(): String? { - return when { + override fun getAvatar(): String? = + when { activeUser == null -> { null } @@ -312,21 +310,14 @@ data class ChatMessage( ApiUtils.getUrlForGuestAvatar(activeUser!!.baseUrl!!, apiId, true) } } - } } - } - override fun getCreatedAt(): Date { - return Date(timestamp * MILLIES) - } + override fun getCreatedAt(): Date = Date(timestamp * MILLIES) - override fun getSystemMessage(): String { - return EnumSystemMessageTypeConverter().convertToString(systemMessageType) - } + override fun getSystemMessage(): String = EnumSystemMessageTypeConverter().convertToString(systemMessageType) - private fun isHashMapEntryEqualTo(map: HashMap, key: String, searchTerm: String): Boolean { - return map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray()) - } + private fun isHashMapEntryEqualTo(map: HashMap, key: String, searchTerm: String): Boolean = + map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray()) // needed a equals and hashcode function to fix detekt errors override fun equals(other: Any?): Boolean { @@ -335,9 +326,7 @@ data class ChatMessage( return false } - override fun hashCode(): Int { - return 0 - } + override fun hashCode(): Int = 0 val isVoiceMessage: Boolean get() = "voice-message" == messageType diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index 3b52c0a41e..91375551ac 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -37,7 +37,7 @@ interface ChatNetworkDataSource { url: String, message: String, displayName: String - ): Observable // last two fields are false + ): Observable fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable fun shareLocationToNotes( @@ -49,18 +49,19 @@ interface ChatNetworkDataSource { ): Observable fun leaveRoom(credentials: String, url: String): Observable - fun sendChatMessage( + suspend fun sendChatMessage( credentials: String, url: String, message: CharSequence, displayName: String, replyTo: Int, - sendWithoutNotification: Boolean - ): Observable + sendWithoutNotification: Boolean, + referenceId: String + ): ChatOverallSingleMessage fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap): Observable> fun deleteChatMessage(credentials: String, url: String): Observable fun createRoom(credentials: String, url: String, map: Map): Observable fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable - fun editChatMessage(credentials: String, url: String, text: String): Observable + suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage } 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 bf3a9f7fab..6d123564c6 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 @@ -24,6 +24,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.chat.ChatOverall +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.preferences.AppPreferences @@ -37,6 +38,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -71,8 +73,7 @@ class OfflineFirstChatRepository @Inject constructor( > > = MutableSharedFlow() - override val updateMessageFlow: - Flow + override val updateMessageFlow: Flow get() = _updateMessageFlow private val _updateMessageFlow: @@ -85,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: @@ -97,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) @@ -172,11 +178,19 @@ class OfflineFirstChatRepository @Inject constructor( if (newestMessageIdFromDb.toInt() != 0) { val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb) - showMessagesBeforeAndEqual( - internalConversationId, + + val list = getMessagesBeforeAndEqual( newestMessageIdFromDb, + internalConversationId, limit ) + if (list.isNotEmpty()) { + updateUiMessages( + receivedChatMessages = list, + lookIntoFuture = false, + showUnreadMessagesMarker = false + ) + } // 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). @@ -293,8 +307,11 @@ 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( + receivedChatMessages = chatMessages, + lookIntoFuture = true, + showUnreadMessagesMarker = showUnreadMessagesMarker + ) } else { Log.d(TAG, "resultsFromSync are null or empty") } @@ -317,6 +334,38 @@ class OfflineFirstChatRepository @Inject constructor( } } + private suspend fun updateUiMessages( + receivedChatMessages : List, + lookIntoFuture: Boolean, + showUnreadMessagesMarker: Boolean + ) { + // remove all temp messages from UI + val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) + .first() + .map(ChatMessageEntity::asModel) + oldTempMessages.forEach { _removeMessageFlow.emit(it) } + + // add new messages to UI + val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages) + _messageFlow.emit(tripleChatMessages) + + // 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 } + chatDao.deleteTempChatMessages( + internalConversationId, + tempChatMessagesThatCanBeReplaced.map { it.referenceId!! } + ) + + // add the remaining temp messages to UI again + 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 @@ -671,31 +720,19 @@ class OfflineFirstChatRepository @Inject constructor( } } - private suspend fun showMessagesBeforeAndEqual(internalConversationId: String, messageId: Long, limit: Int) { - suspend fun getMessagesBeforeAndEqual( - messageId: Long, - internalConversationId: String, - messageLimit: Int - ): List = - chatDao.getMessagesForConversationBeforeAndEqual( - internalConversationId, - messageId, - messageLimit - ).map { - it.map(ChatMessageEntity::asModel) - }.first() - - val list = getMessagesBeforeAndEqual( - messageId, + suspend fun getMessagesBeforeAndEqual( + messageId: Long, + internalConversationId: String, + messageLimit: Int + ): List = + chatDao.getMessagesForConversationBeforeAndEqual( internalConversationId, - limit - ) + messageId, + messageLimit + ).map { + it.map(ChatMessageEntity::asModel) + }.first() - if (list.isNotEmpty()) { - val triple = Triple(false, false, list) - _messageFlow.emit(triple) - } - } private suspend fun showMessagesBefore(internalConversationId: String, messageId: Long, limit: Int) { suspend fun getMessagesBefore( @@ -739,6 +776,126 @@ class OfflineFirstChatRepository @Inject constructor( scope.cancel() } + override suspend fun sendChatMessage( + credentials: String, + url: String, + message: CharSequence, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> = + flow { + try { + val response = network.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId + ) + + val chatMessageModel = response.ocs?.data?.asModel() + + emit(Result.success(chatMessageModel)) + } catch (e: Exception) { + Log.e(TAG, "Error when sending message", e) + + val failedMessage = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first() + failedMessage.sendingFailed = true + chatDao.updateChatMessage(failedMessage) + + val failedMessageModel = failedMessage.asModel() + _removeMessageFlow.emit(failedMessageModel) + + val tripleChatMessages = Triple(true, false, listOf(failedMessageModel)) + _messageFlow.emit(tripleChatMessages) + + emit(Result.failure(e)) + } + } + + override suspend fun editChatMessage( + credentials: String, + url: String, + text: String + ): Flow> = + flow { + try { + val response = network.editChatMessage( + credentials, + url, + text + ) + emit(Result.success(response)) + } catch (e: Exception) { + emit(Result.failure(e)) + } + } + + override suspend fun addTemporaryMessage( + message: CharSequence, + displayName: String, + replyTo: Int, + referenceId: String + ): Flow> = + flow { + try { + val tempChatMessageEntity = createChatMessageEntity( + internalConversationId, + message.toString(), + referenceId + ) + + chatDao.upsertChatMessage(tempChatMessageEntity) + + val tempChatMessageModel = tempChatMessageEntity.asModel() + + emit(Result.success(tempChatMessageModel)) + + val triple = Triple(true, false, listOf(tempChatMessageModel)) + _messageFlow.emit(triple) + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when adding temporary message", e) + emit(Result.failure(e)) + } + } + + private fun createChatMessageEntity( + internalConversationId: String, + message: String, + referenceId: String + ): ChatMessageEntity { + + val currentTimeMillies = System.currentTimeMillis() + + val entity = ChatMessageEntity( + internalId = internalConversationId + "@_temp_" + currentTimeMillies, + internalConversationId = internalConversationId, + id = currentTimeMillies, + message = message, + deleted = false, + token = conversationModel.token, + actorId = currentUser.userId!!, + actorType = "users", + accountId = currentUser.id!!, + messageParameters = null, + messageType = "comment", + parentMessageId = null, + systemMessageType = ChatMessage.SystemMessageType.DUMMY, + replyable = false, + timestamp = System.currentTimeMillis() / MILLIES, + expirationTimestamp = 0, + actorDisplayName = currentUser.displayName!!, + referenceId = referenceId, + isTemporary = true, + sendingFailed = false + ) + return entity + } + companion object { val TAG = OfflineFirstChatRepository::class.simpleName private const val HTTP_CODE_OK: Int = 200 @@ -747,5 +904,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/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index 080b0706f6..1b8bb48d21 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.chat.data.network import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability @@ -16,10 +17,12 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.message.SendMessageUtils import io.reactivex.Observable import retrofit2.Response -class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { +class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines: NcApiCoroutines) : + ChatNetworkDataSource { override fun getRoom(user: User, roomToken: String): Observable { val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) @@ -103,26 +106,24 @@ class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { url: String, message: String, displayName: String - ): Observable { - return ncApi.sendChatMessage( + ): Observable = + ncApi.sendChatMessage( credentials, url, message, displayName, null, - false + false, + SendMessageUtils().generateReferenceId() // TODO add temp message before with ref id.. ).map { it } - } override fun checkForNoteToSelf( credentials: String, url: String, includeStatus: Boolean - ): Observable { - return ncApi.getRooms(credentials, url, includeStatus).map { it } - } + ): Observable = ncApi.getRooms(credentials, url, includeStatus).map { it } override fun shareLocationToNotes( credentials: String, @@ -130,52 +131,54 @@ class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { objectType: String, objectId: String, metadata: String - ): Observable { - return ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it } - } + ): Observable = ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it } - override fun leaveRoom(credentials: String, url: String): Observable { - return ncApi.leaveRoom(credentials, url).map { it } - } + override fun leaveRoom(credentials: String, url: String): Observable = + ncApi.leaveRoom(credentials, url).map { + it + } - override fun sendChatMessage( + override suspend fun sendChatMessage( credentials: String, url: String, message: CharSequence, displayName: String, replyTo: Int, - sendWithoutNotification: Boolean - ): Observable { - return ncApi.sendChatMessage(credentials, url, message, displayName, replyTo, sendWithoutNotification).map { - it - } - } + sendWithoutNotification: Boolean, + referenceId: String + ): ChatOverallSingleMessage = + ncApiCoroutines.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId + ) override fun pullChatMessages( credentials: String, url: String, fieldMap: HashMap - ): Observable> { - return ncApi.pullChatMessages(credentials, url, fieldMap).map { it } - } + ): Observable> = ncApi.pullChatMessages(credentials, url, fieldMap).map { it } - override fun deleteChatMessage(credentials: String, url: String): Observable { - return ncApi.deleteChatMessage(credentials, url).map { it } - } + override fun deleteChatMessage(credentials: String, url: String): Observable = + ncApi.deleteChatMessage(credentials, url).map { + it + } - override fun createRoom(credentials: String, url: String, map: Map): Observable { - return ncApi.createRoom(credentials, url, map).map { it } - } + override fun createRoom(credentials: String, url: String, map: Map): Observable = + ncApi.createRoom(credentials, url, map).map { + it + } override fun setChatReadMarker( credentials: String, url: String, previousMessageId: Int - ): Observable { - return ncApi.setChatReadMarker(credentials, url, previousMessageId).map { it } - } + ): Observable = ncApi.setChatReadMarker(credentials, url, previousMessageId).map { it } - override fun editChatMessage(credentials: String, url: String, text: String): Observable { - return ncApi.editChatMessage(credentials, url, text).map { it } - } + override suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage = + ncApiCoroutines.editChatMessage(credentials, url, text) } 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 1798d7403b..890a96b155 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 @@ -60,7 +60,8 @@ class ChatViewModel @Inject constructor( private val mediaRecorderManager: MediaRecorderManager, private val audioFocusRequestManager: AudioFocusRequestManager, private val userProvider: CurrentUserProviderNew -) : ViewModel(), DefaultLifecycleObserver { +) : ViewModel(), + DefaultLifecycleObserver { enum class LifeCycleFlag { PAUSED, @@ -71,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 @@ -124,6 +129,8 @@ class ChatViewModel @Inject constructor( _chatMessageViewState.value = ChatMessageErrorState } + val getRemoveMessageFlow = chatRepository.removeMessageFlow + val getUpdateMessageFlow = chatRepository.updateMessageFlow val getLastCommonReadFlow = chatRepository.lastCommonReadFlow @@ -466,12 +473,12 @@ class ChatViewModel @Inject constructor( chatNetworkDataSource.shareToNotes(credentials, url, message, displayName) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { + ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { disposableSet.add(d) } - override fun onNext(genericOverall: GenericOverall) { + override fun onNext(genericOverall: ChatOverallSingleMessage) { // unused atm } @@ -597,9 +604,7 @@ class ChatViewModel @Inject constructor( cachedFile.delete() } - fun getCurrentVoiceRecordFile(): String { - return mediaRecorderManager.currentVoiceRecordFile - } + fun getCurrentVoiceRecordFile(): String = mediaRecorderManager.currentVoiceRecordFile fun uploadFile(fileUri: String, room: String, displayName: String, metaData: String) { try { 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 54c73869d7..8bc2f9e88a 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 @@ -15,49 +15,56 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager import com.nextcloud.talk.chat.data.io.AudioRecorderManager import com.nextcloud.talk.chat.data.io.MediaPlayerManager import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage -import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.utils.message.SendMessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.commons.models.IMessage -import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import java.lang.Thread.sleep import javax.inject.Inject class MessageInputViewModel @Inject constructor( - private val chatNetworkDataSource: ChatNetworkDataSource, private val audioRecorderManager: AudioRecorderManager, private val mediaPlayerManager: MediaPlayerManager, private val audioFocusRequestManager: AudioFocusRequestManager, private val appPreferences: AppPreferences -) : ViewModel(), DefaultLifecycleObserver { +) : ViewModel(), + DefaultLifecycleObserver { + enum class LifeCycleFlag { PAUSED, RESUMED, STOPPED } + + lateinit var chatRepository: ChatMessageRepository lateinit var currentLifeCycleFlag: LifeCycleFlag val disposableSet = mutableSetOf() - data class QueuedMessage( - val id: Int, - var message: CharSequence? = null, - val displayName: String? = null, - val replyTo: Int? = null, - val sendWithoutNotification: Boolean? = null - ) + fun setData(chatMessageRepository: ChatMessageRepository){ + chatRepository = chatMessageRepository + } + + // data class QueuedMessage( + // val id: Int, + // var message: CharSequence? = null, + // val displayName: String? = null, + // val replyTo: Int? = null, + // val sendWithoutNotification: Boolean? = null + // ) - private var isQueueing: Boolean = false - private var messageQueue: MutableList = mutableListOf() + // private var isQueueing: Boolean = false + // private var messageQueue: MutableList = mutableListOf() override fun onResume(owner: LifecycleOwner) { super.onResume(owner) @@ -106,7 +113,7 @@ class MessageInputViewModel @Inject constructor( sealed interface ViewState object SendChatMessageStartState : ViewState class SendChatMessageSuccessState(val message: CharSequence) : ViewState - class SendChatMessageErrorState(val e: Throwable, val message: CharSequence) : ViewState + class SendChatMessageErrorState(val message: CharSequence) : ViewState private val _sendChatMessageViewState: MutableLiveData = MutableLiveData(SendChatMessageStartState) val sendChatMessageViewState: LiveData get() = _sendChatMessageViewState @@ -122,13 +129,13 @@ class MessageInputViewModel @Inject constructor( val isVoicePreviewPlaying: LiveData get() = _isVoicePreviewPlaying - private val _messageQueueSizeFlow = MutableStateFlow(messageQueue.size) + private val _messageQueueSizeFlow = MutableStateFlow(666) val messageQueueSizeFlow: LiveData get() = _messageQueueSizeFlow.asLiveData() - private val _messageQueueFlow: MutableLiveData> = MutableLiveData() - val messageQueueFlow: LiveData> - get() = _messageQueueFlow + // private val _messageQueueFlow: MutableLiveData> = MutableLiveData() + // val messageQueueFlow: LiveData> + // get() = _messageQueueFlow private val _callStartedFlow: MutableLiveData> = MutableLiveData() val callStartedFlow: LiveData> @@ -144,67 +151,72 @@ class MessageInputViewModel @Inject constructor( replyTo: Int, sendWithoutNotification: Boolean ) { - if (isQueueing) { - val tempID = System.currentTimeMillis().toInt() - val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification) - messageQueue = appPreferences.getMessageQueue(internalId) - messageQueue.add(qMsg) - appPreferences.saveMessageQueue(internalId, messageQueue) - _messageQueueSizeFlow.update { messageQueue.size } - _messageQueueFlow.postValue(listOf(qMsg)) - return - } - - chatNetworkDataSource.sendChatMessage( - credentials, - url, - message, - displayName, - replyTo, - sendWithoutNotification - ).subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableSet.add(d) - } + val referenceId = SendMessageUtils().generateReferenceId() + Log.d(TAG, "Random SHA-256 Hash: $referenceId") + + viewModelScope.launch { + chatRepository.addTemporaryMessage( + message, + displayName, + replyTo, + referenceId + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "temp message ref id: " + (result.getOrNull()?.referenceId ?: "none")) - override fun onError(e: Throwable) { - _sendChatMessageViewState.value = SendChatMessageErrorState(e, message) + _sendChatMessageViewState.value = SendChatMessageSuccessState(message) + } else { + _sendChatMessageViewState.value = SendChatMessageErrorState(message) } + } + } - override fun onComplete() { - // unused atm - } + // if (isQueueing) { + // val tempID = System.currentTimeMillis().toInt() + // val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification) + // messageQueue = appPreferences.getMessageQueue(internalId) + // messageQueue.add(qMsg) + // appPreferences.saveMessageQueue(internalId, messageQueue) + // _messageQueueSizeFlow.update { messageQueue.size } + // _messageQueueFlow.postValue(listOf(qMsg)) + // return + // } + + viewModelScope.launch { + chatRepository.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "received ref id: " + (result.getOrNull()?.referenceId ?: "none")) - override fun onNext(t: GenericOverall) { _sendChatMessageViewState.value = SendChatMessageSuccessState(message) + } else { + _sendChatMessageViewState.value = SendChatMessageErrorState(message) } - }) + } + } } fun editChatMessage(credentials: String, url: String, text: String) { - chatNetworkDataSource.editChatMessage(credentials, url, text) - .subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableSet.add(d) - } - - override fun onError(e: Throwable) { - Log.e(TAG, "failed to edit message", e) + viewModelScope.launch { + chatRepository.editChatMessage( + credentials, + url, + text + ).collect { result -> + if (result.isSuccess) { + _editMessageViewState.value = EditMessageSuccessState(result.getOrNull()!!) + } else { _editMessageViewState.value = EditMessageErrorState } - - override fun onComplete() { - // unused atm - } - - override fun onNext(messageEdited: ChatOverallSingleMessage) { - _editMessageViewState.value = EditMessageSuccessState(messageEdited) - } - }) + } + } } fun reply(message: IMessage?) { @@ -256,68 +268,68 @@ class MessageInputViewModel @Inject constructor( _getRecordingTime.postValue(time) } - fun sendAndEmptyMessageQueue(internalId: String, credentials: String, url: String) { - if (isQueueing) return - messageQueue.clear() - - val queue = appPreferences.getMessageQueue(internalId) - appPreferences.saveMessageQueue(internalId, null) // empties the queue - while (queue.size > 0) { - val msg = queue.removeAt(0) - sendChatMessage( - internalId, - credentials, - url, - msg.message!!, - msg.displayName!!, - msg.replyTo!!, - msg.sendWithoutNotification!! - ) - sleep(DELAY_BETWEEN_QUEUED_MESSAGES) - } - _messageQueueSizeFlow.tryEmit(0) - } - - fun getTempMessagesFromMessageQueue(internalId: String) { - val queue = appPreferences.getMessageQueue(internalId) - val list = mutableListOf() - for (msg in queue) { - list.add(msg) - } - _messageQueueFlow.postValue(list) - } - - fun switchToMessageQueue(shouldQueue: Boolean) { - isQueueing = shouldQueue - } - - fun restoreMessageQueue(internalId: String) { - messageQueue = appPreferences.getMessageQueue(internalId) - _messageQueueSizeFlow.tryEmit(messageQueue.size) - } - - fun removeFromQueue(internalId: String, id: Int) { - val queue = appPreferences.getMessageQueue(internalId) - for (qMsg in queue) { - if (qMsg.id == id) { - queue.remove(qMsg) - break - } - } - appPreferences.saveMessageQueue(internalId, queue) - _messageQueueSizeFlow.tryEmit(queue.size) - } - - fun editQueuedMessage(internalId: String, id: Int, newMessage: String) { - val queue = appPreferences.getMessageQueue(internalId) - for (qMsg in queue) { - if (qMsg.id == id) { - qMsg.message = newMessage - break - } - } - appPreferences.saveMessageQueue(internalId, queue) - } + // fun sendAndEmptyMessageQueue(internalId: String, credentials: String, url: String) { + // if (isQueueing) return + // messageQueue.clear() + // + // val queue = appPreferences.getMessageQueue(internalId) + // appPreferences.saveMessageQueue(internalId, null) // empties the queue + // while (queue.size > 0) { + // val msg = queue.removeAt(0) + // sendChatMessage( + // internalId, + // credentials, + // url, + // msg.message!!, + // msg.displayName!!, + // msg.replyTo!!, + // msg.sendWithoutNotification!! + // ) + // sleep(DELAY_BETWEEN_QUEUED_MESSAGES) + // } + // _messageQueueSizeFlow.tryEmit(0) + // } + // + // fun getTempMessagesFromMessageQueue(internalId: String) { + // val queue = appPreferences.getMessageQueue(internalId) + // val list = mutableListOf() + // for (msg in queue) { + // list.add(msg) + // } + // _messageQueueFlow.postValue(list) + // } + // + // fun switchToMessageQueue(shouldQueue: Boolean) { + // isQueueing = shouldQueue + // } + // + // fun restoreMessageQueue(internalId: String) { + // messageQueue = appPreferences.getMessageQueue(internalId) + // _messageQueueSizeFlow.tryEmit(messageQueue.size) + // } + // + // fun removeFromQueue(internalId: String, id: Int) { + // val queue = appPreferences.getMessageQueue(internalId) + // for (qMsg in queue) { + // if (qMsg.id == id) { + // queue.remove(qMsg) + // break + // } + // } + // appPreferences.saveMessageQueue(internalId, queue) + // _messageQueueSizeFlow.tryEmit(queue.size) + // } + // + // fun editQueuedMessage(internalId: String, id: Int, newMessage: String) { + // val queue = appPreferences.getMessageQueue(internalId) + // for (qMsg in queue) { + // if (qMsg.id == id) { + // qMsg.message = newMessage + // break + // } + // } + // appPreferences.saveMessageQueue(internalId, queue) + // } fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) { _callStartedFlow.postValue(Pair(recent, show)) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt index f2d30a83bb..8a68e366ad 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -10,7 +10,6 @@ package com.nextcloud.talk.conversationlist.data.network import android.util.Log import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource -import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.data.database.dao.ConversationsDao import com.nextcloud.talk.data.database.mappers.asEntity @@ -107,7 +106,7 @@ class OfflineFirstConversationsRepository @Inject constructor( var conversationsFromSync: List? = null if (!monitor.isOnline.first()) { - Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server") + Log.d(TAG, "Device is offline, can't load conversations from server") return 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 b33c9ffbbe..afa64efa56 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 @@ -65,6 +65,7 @@ import com.nextcloud.talk.utils.preferences.AppPreferences import dagger.Module import dagger.Provides import okhttp3.OkHttpClient +import javax.inject.Singleton @Module class RepositoryModule { @@ -74,87 +75,66 @@ class RepositoryModule { ncApi: NcApi, ncApiCoroutines: NcApiCoroutines, userProvider: CurrentUserProviderNew - ): ConversationsRepository { - return ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider) - } + ): ConversationsRepository = ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider) @Provides - fun provideSharedItemsRepository(ncApi: NcApi, dateUtils: DateUtils): SharedItemsRepository { - return SharedItemsRepositoryImpl(ncApi, dateUtils) - } + fun provideSharedItemsRepository(ncApi: NcApi, dateUtils: DateUtils): SharedItemsRepository = + SharedItemsRepositoryImpl(ncApi, dateUtils) @Provides - fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): UnifiedSearchRepository { - return UnifiedSearchRepositoryImpl(ncApi, userProvider) - } + fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): UnifiedSearchRepository = + UnifiedSearchRepositoryImpl(ncApi, userProvider) @Provides - fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository { - return PollRepositoryImpl(ncApi, userProvider) - } + fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository = + PollRepositoryImpl(ncApi, userProvider) @Provides fun provideRemoteFileBrowserItemsRepository( okHttpClient: OkHttpClient, userProvider: CurrentUserProviderNew - ): RemoteFileBrowserItemsRepository { - return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) - } + ): RemoteFileBrowserItemsRepository = RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) @Provides - fun provideUsersRepository(database: TalkDatabase): UsersRepository { - return UsersRepositoryImpl(database.usersDao()) - } + fun provideUsersRepository(database: TalkDatabase): UsersRepository = UsersRepositoryImpl(database.usersDao()) @Provides - fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository { - return ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao()) - } + fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository = + ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao()) @Provides fun provideReactionsRepository( ncApi: NcApi, userProvider: CurrentUserProviderNew, dao: ChatMessagesDao - ): ReactionsRepository { - return ReactionsRepositoryImpl(ncApi, userProvider, dao) - } + ): ReactionsRepository = ReactionsRepositoryImpl(ncApi, userProvider, dao) @Provides - fun provideCallRecordingRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): CallRecordingRepository { - return CallRecordingRepositoryImpl(ncApi, userProvider) - } + fun provideCallRecordingRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): CallRecordingRepository = + CallRecordingRepositoryImpl(ncApi, userProvider) @Provides fun provideRequestAssistanceRepository( ncApi: NcApi, userProvider: CurrentUserProviderNew - ): RequestAssistanceRepository { - return RequestAssistanceRepositoryImpl(ncApi, userProvider) - } + ): RequestAssistanceRepository = RequestAssistanceRepositoryImpl(ncApi, userProvider) @Provides fun provideOpenConversationsRepository( ncApi: NcApi, userProvider: CurrentUserProviderNew - ): OpenConversationsRepository { - return OpenConversationsRepositoryImpl(ncApi, userProvider) - } + ): OpenConversationsRepository = OpenConversationsRepositoryImpl(ncApi, userProvider) @Provides - fun translateRepository(ncApi: NcApi): TranslateRepository { - return TranslateRepositoryImpl(ncApi) - } + fun translateRepository(ncApi: NcApi): TranslateRepository = TranslateRepositoryImpl(ncApi) @Provides - fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource { - return RetrofitChatNetwork(ncApi) - } + fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource = + RetrofitChatNetwork(ncApi, ncApiCoroutines) @Provides - fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource { - return RetrofitConversationsNetwork(ncApi) - } + fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource = + RetrofitConversationsNetwork(ncApi) @Provides fun provideConversationInfoEditRepository( @@ -166,14 +146,11 @@ class RepositoryModule { } @Provides - fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository { - return ConversationRepositoryImpl(ncApi, userProvider) - } + fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository = + ConversationRepositoryImpl(ncApi, userProvider) @Provides - fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository { - return InvitationsRepositoryImpl(ncApi) - } + fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi) @Provides fun provideOfflineFirstChatRepository( @@ -183,8 +160,8 @@ class RepositoryModule { appPreferences: AppPreferences, networkMonitor: NetworkMonitor, userProvider: CurrentUserProviderNew - ): ChatMessageRepository { - return OfflineFirstChatRepository( + ): ChatMessageRepository = + OfflineFirstChatRepository( chatMessagesDao, chatBlocksDao, dataSource, @@ -192,7 +169,6 @@ class RepositoryModule { networkMonitor, userProvider ) - } @Provides fun provideOfflineFirstConversationsRepository( @@ -201,26 +177,22 @@ class RepositoryModule { chatNetworkDataSource: ChatNetworkDataSource, networkMonitor: NetworkMonitor, currentUserProviderNew: CurrentUserProviderNew - ): OfflineConversationsRepository { - return OfflineFirstConversationsRepository( + ): OfflineConversationsRepository = + OfflineFirstConversationsRepository( dao, dataSource, chatNetworkDataSource, networkMonitor, currentUserProviderNew ) - } @Provides - fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository { - return ContactsRepositoryImpl(ncApiCoroutines, userManager) - } + fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository = + ContactsRepositoryImpl(ncApiCoroutines, userManager) @Provides fun provideConversationCreationRepository( ncApiCoroutines: NcApiCoroutines, userManager: UserManager - ): ConversationCreationRepository { - return ConversationCreationRepositoryImpl(ncApiCoroutines, userManager) - } + ): ConversationCreationRepository = ConversationCreationRepositoryImpl(ncApiCoroutines, userManager) } 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 6fbf61ca1d..b3179c1497 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,29 @@ 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> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND referenceId = :referenceId + AND isTemporary = 1 + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertChatMessages(chatMessages: List) @@ -59,6 +83,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) @@ -77,6 +111,7 @@ interface ChatMessagesDao { SELECT * FROM ChatMessages WHERE internalConversationId = :internalConversationId AND id >= :messageId + AND isTemporary = 0 ORDER BY timestamp ASC, id ASC """ ) @@ -87,6 +122,7 @@ interface ChatMessagesDao { SELECT * FROM ChatMessages WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 AND id < :messageId ORDER BY timestamp DESC, id DESC LIMIT :limit @@ -103,6 +139,7 @@ interface ChatMessagesDao { SELECT * FROM ChatMessages WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 AND id <= :messageId ORDER BY timestamp DESC, id DESC LIMIT :limit @@ -119,6 +156,7 @@ interface ChatMessagesDao { SELECT COUNT(*) FROM ChatMessages WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 AND id BETWEEN :newestMessageId AND :oldestMessageId """ ) 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 30b856a6bf..1392a29201 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 @@ -10,6 +10,7 @@ package com.nextcloud.talk.data.database.mappers import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus fun ChatMessageJson.asEntity(accountId: Long) = ChatMessageEntity( @@ -37,7 +38,8 @@ fun ChatMessageJson.asEntity(accountId: Long) = lastEditActorId = lastEditActorId, lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, - deleted = deleted + deleted = deleted, + referenceId = referenceId ) fun ChatMessageEntity.asModel() = @@ -62,9 +64,23 @@ fun ChatMessageEntity.asModel() = lastEditActorId = lastEditActorId, lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, - isDeleted = deleted + isDeleted = deleted, + referenceId = referenceId, + isTempMessage = isTemporary, + sendingFailed = sendingFailed, + readStatus = setStatus(isTemporary, sendingFailed) ) +fun setStatus(isTemporary: Boolean, sendingFailed: Boolean): ReadStatus { + return if (sendingFailed) { + ReadStatus.FAILED + } else if (isTemporary) { + ReadStatus.SENDING + } else { + ReadStatus.NONE + } +} + fun ChatMessageJson.asModel() = ChatMessage( jsonMessageId = id.toInt(), @@ -87,5 +103,6 @@ fun ChatMessageJson.asModel() = lastEditActorId = lastEditActorId, lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, - isDeleted = deleted + isDeleted = deleted, + referenceId = referenceId ) 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 dbf1cce924..5349794c3a 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 @@ -52,6 +52,7 @@ data class ChatMessageEntity( @ColumnInfo(name = "deleted") var deleted: Boolean = false, @ColumnInfo(name = "expirationTimestamp") var expirationTimestamp: Int = 0, @ColumnInfo(name = "isReplyable") var replyable: Boolean = false, + @ColumnInfo(name = "isTemporary") var isTemporary: Boolean = false, @ColumnInfo(name = "lastEditActorDisplayName") var lastEditActorDisplayName: String? = null, @ColumnInfo(name = "lastEditActorId") var lastEditActorId: String? = null, @ColumnInfo(name = "lastEditActorType") var lastEditActorType: String? = null, @@ -62,8 +63,9 @@ data class ChatMessageEntity( @ColumnInfo(name = "parent") var parentMessageId: Long? = null, @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 = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, - @ColumnInfo(name = "timestamp") var timestamp: Long = 0 - // missing/not needed: referenceId + @ColumnInfo(name = "timestamp") var timestamp: Long = 0, // missing/not needed: silent ) 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 3e61f699d9..8d10b94b30 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 @@ -48,6 +48,13 @@ object Migrations { } } + val MIGRATION_12_13 = object : Migration(12, 13) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 12 to 13") + addReferenceIdToChatMessages(db) + } + } + fun migrateToRoom(db: SupportSQLiteDatabase) { db.execSQL( "CREATE TABLE User_new (" + @@ -257,4 +264,15 @@ object Migrations { Log.i("Migrations", "hasArchived already exists") } } + + fun addReferenceIdToChatMessages(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN referenceId TEXT;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column referenceId to table ChatMessages") + } + } } 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 053ad4766f..8fa20e9d36 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 @@ -49,7 +49,7 @@ import java.util.Locale ChatMessageEntity::class, ChatBlockEntity::class ], - version = 12, + version = 13, autoMigrations = [ AutoMigration(from = 9, to = 11) ], @@ -108,13 +108,14 @@ 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, Migrations.MIGRATION_8_9, Migrations.MIGRATION_10_11, - Migrations.MIGRATION_11_12 + Migrations.MIGRATION_11_12, + Migrations.MIGRATION_12_13 ) .allowMainThreadQueries() .addCallback( @@ -128,8 +129,8 @@ abstract class TalkDatabase : RoomDatabase() { .build() } - private fun getCipherMigrationHook(): SQLiteDatabaseHook { - return object : SQLiteDatabaseHook { + private fun getCipherMigrationHook(): SQLiteDatabaseHook = + object : SQLiteDatabaseHook { override fun preKey(database: SQLiteDatabase) { // unused atm } @@ -140,6 +141,5 @@ abstract class TalkDatabase : RoomDatabase() { Log.i(TAG, "DB cipher_migrate END") } } - } } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java index 6f273ab328..8679f65046 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java @@ -192,7 +192,6 @@ private void deleteUser(User user) { if (user.getId() != null) { String username = user.getUsername(); try { - appPreferences.deleteAllMessageQueuesFor(user.getUserId()); userManager.deleteUser(user.getId()); Log.d(TAG, "deleted user: " + username); } catch (Throwable e) { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt index 024e13fe65..60d039704a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt @@ -42,5 +42,6 @@ data class ChatMessageJson( @JsonField(name = ["lastEditActorId"]) var lastEditActorId: String? = null, @JsonField(name = ["lastEditActorType"]) var lastEditActorType: String? = null, @JsonField(name = ["lastEditTimestamp"]) var lastEditTimestamp: Long? = 0, - @JsonField(name = ["deleted"]) var deleted: Boolean = false + @JsonField(name = ["deleted"]) var deleted: Boolean = false, + @JsonField(name = ["referenceId"]) var referenceId: String? = null ) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt index 40a1e283cf..1441dd521a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt @@ -9,5 +9,7 @@ package com.nextcloud.talk.models.json.chat enum class ReadStatus { NONE, SENT, - READ + READ, + SENDING, + FAILED } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt index 09984c0c61..c70fe8b2f9 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt @@ -23,13 +23,14 @@ import com.nextcloud.talk.R import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.message.SendMessageUtils import io.reactivex.Observer import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -71,24 +72,31 @@ class DirectReplyReceiver : BroadcastReceiver() { sendDirectReply() } - private fun getMessageText(intent: Intent): CharSequence? { - return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY) - } + private fun getMessageText(intent: Intent): CharSequence? = + RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY) private fun sendDirectReply() { val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1)) val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl!!, roomToken!!) - ncApi.sendChatMessage(credentials, url, replyMessage, currentUser.displayName, null, false) + ncApi.sendChatMessage( + credentials, + url, + replyMessage, + currentUser.displayName, + null, + false, + SendMessageUtils().generateReferenceId() // TODO add temp chatMessage before with ref id... + ) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { + ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { // unused atm } - override fun onNext(genericOverall: GenericOverall) { + override fun onNext(message: ChatOverallSingleMessage) { confirmReplySent() } diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt new file mode 100644 index 0000000000..29b9700ea8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.message + +import java.security.MessageDigest +import java.util.UUID + +class SendMessageUtils { + fun generateReferenceId(): String { + val randomString = UUID.randomUUID().toString() + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(randomString.toByteArray(Charsets.UTF_8)) + return hashBytes.joinToString("") { "%02x".format(it) } + } + + companion object { + private val TAG = SendMessageUtils::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index a9265ae8ed..beb152a69c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -175,12 +175,6 @@ public interface AppPreferences { int getLastKnownId(String internalConversationId, int defaultValue); - void saveMessageQueue(String internalConversationId, List queue); - - List getMessageQueue(String internalConversationId); - - void deleteAllMessageQueuesFor(String userId); - void saveVoiceMessagePlaybackSpeedPreferences(Map speeds); Map readVoiceMessagePlaybackSpeedPreferences(); diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt index c1e611b9a4..742b27c265 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -501,75 +501,75 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return if (lastReadId.isNotEmpty()) lastReadId.toInt() else defaultValue } - override fun saveMessageQueue( - internalConversationId: String, - queue: MutableList? - ) { - runBlocking { - async { - var queueStr = "" - queue?.let { - for (msg in queue) { - val msgStr = "${msg.id},${msg.message},${msg.replyTo},${msg.displayName},${ - msg - .sendWithoutNotification - }^" - queueStr += msgStr - } - } - writeString(internalConversationId + MESSAGE_QUEUE, queueStr) - } - } - } - - @Suppress("Detekt.TooGenericExceptionCaught") - override fun getMessageQueue(internalConversationId: String): MutableList { - val queueStr = - runBlocking { async { readString(internalConversationId + MESSAGE_QUEUE).first() } }.getCompleted() - - val queue: MutableList = mutableListOf() - if (queueStr.isEmpty()) return queue - - for (msgStr in queueStr.split("^")) { - try { - if (msgStr.isNotEmpty()) { - val msgArray = msgStr.split(",") - val id = msgArray[ID].toInt() - val message = msgArray[MESSAGE_INDEX] - val replyTo = msgArray[REPLY_TO_INDEX].toInt() - val displayName = msgArray[DISPLAY_NAME_INDEX] - val silent = msgArray[SILENT_INDEX].toBoolean() - - val qMsg = MessageInputViewModel.QueuedMessage(id, message, displayName, replyTo, silent) - queue.add(qMsg) - } - } catch (e: IndexOutOfBoundsException) { - Log.e(TAG, "Message string: $msgStr\n Queue String: $queueStr \n$e") - } - } - - return queue - } - - override fun deleteAllMessageQueuesFor(userId: String) { - runBlocking { - async { - val keyList = mutableListOf>() - val preferencesMap = context.dataStore.data.first().asMap() - for (preference in preferencesMap) { - if (preference.key.name.contains("$userId@")) { - keyList.add(preference.key) - } - } - - for (key in keyList) { - context.dataStore.edit { - it.remove(key) - } - } - } - } - } + // override fun saveMessageQueue( + // internalConversationId: String, + // queue: MutableList? + // ) { + // runBlocking { + // async { + // var queueStr = "" + // queue?.let { + // for (msg in queue) { + // val msgStr = "${msg.id},${msg.message},${msg.replyTo},${msg.displayName},${ + // msg + // .sendWithoutNotification + // }^" + // queueStr += msgStr + // } + // } + // writeString(internalConversationId + MESSAGE_QUEUE, queueStr) + // } + // } + // } + // + // @Suppress("Detekt.TooGenericExceptionCaught") + // override fun getMessageQueue(internalConversationId: String): MutableList { + // val queueStr = + // runBlocking { async { readString(internalConversationId + MESSAGE_QUEUE).first() } }.getCompleted() + // + // val queue: MutableList = mutableListOf() + // if (queueStr.isEmpty()) return queue + // + // for (msgStr in queueStr.split("^")) { + // try { + // if (msgStr.isNotEmpty()) { + // val msgArray = msgStr.split(",") + // val id = msgArray[ID].toInt() + // val message = msgArray[MESSAGE_INDEX] + // val replyTo = msgArray[REPLY_TO_INDEX].toInt() + // val displayName = msgArray[DISPLAY_NAME_INDEX] + // val silent = msgArray[SILENT_INDEX].toBoolean() + // + // val qMsg = MessageInputViewModel.QueuedMessage(id, message, displayName, replyTo, silent) + // queue.add(qMsg) + // } + // } catch (e: IndexOutOfBoundsException) { + // Log.e(TAG, "Message string: $msgStr\n Queue String: $queueStr \n$e") + // } + // } + // + // return queue + // } + + // override fun deleteAllMessageQueuesFor(userId: String) { + // runBlocking { + // async { + // val keyList = mutableListOf>() + // val preferencesMap = context.dataStore.data.first().asMap() + // for (preference in preferencesMap) { + // if (preference.key.name.contains("$userId@")) { + // keyList.add(preference.key) + // } + // } + // + // for (key in keyList) { + // context.dataStore.edit { + // it.remove(key) + // } + // } + // } + // } + // } override fun saveVoiceMessagePlaybackSpeedPreferences(speeds: Map) { Json.encodeToString(speeds).let { diff --git a/app/src/main/res/drawable/baseline_replay_24.xml b/app/src/main/res/drawable/baseline_replay_24.xml new file mode 100644 index 0000000000..58390a9c35 --- /dev/null +++ b/app/src/main/res/drawable/baseline_replay_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_report_problem_24.xml b/app/src/main/res/drawable/baseline_report_problem_24.xml new file mode 100644 index 0000000000..a17ce9ad9a --- /dev/null +++ b/app/src/main/res/drawable/baseline_report_problem_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml index 4d0d2d4522..4ada0dcd85 100644 --- a/app/src/main/res/layout/item_custom_outcoming_text_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml @@ -16,6 +16,42 @@ android:layout_marginRight="16dp" android:layout_marginBottom="2dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:tint="@color/high_emphasis_text" + tools:src="@drawable/ic_warning_white"/> + + + android:layout_below="@id/bubble" + android:layout_alignParentEnd="true"> + + + tools:visibility="gone"/> + android:visibility="gone" + tools:visibility="visible"/> diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 46887447d6..31e4ce7fe5 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 72 errors and 158 warnings + Lint Report: 72 errors and 156 warnings