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 000000000..fdeb82d1e --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json @@ -0,0 +1,731 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "6986b68476bae871773348987eada812", + "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, 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 + } + ], + "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, '6986b68476bae871773348987eada812')" + ] + } +} \ No newline at end of file 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 78605a7fc..5ed659da7 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -344,12 +344,14 @@ public interface NcApi { @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); + @Field("silent") Boolean sendWithoutNotification, + @Field("referenceId") String referenceId + ); @FormUrlEncoded @PUT 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 7283cf8ec..ed06df75d 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 @@ -115,9 +115,12 @@ data class ChatMessage( var isTempMessage: Boolean = false, - var tempMessageId: Int = -1 + var tempMessageId: Int = -1, -) : MessageContentType, MessageContentType.Image { + var referenceId: String? = null + +) : MessageContentType, + MessageContentType.Image { var extractedUrlToPreview: String? = null @@ -238,8 +241,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 +257,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 +274,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 +308,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 +324,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 3b52c0a41..079523e69 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( @@ -55,8 +55,9 @@ interface ChatNetworkDataSource { message: CharSequence, displayName: String, replyTo: Int, - sendWithoutNotification: Boolean - ): Observable + sendWithoutNotification: Boolean, + referenceId: String + ): Observable fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap): Observable> fun deleteChatMessage(credentials: String, url: String): Observable 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 080b0706f..5a9f9fd50 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 @@ -16,6 +16,7 @@ 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 @@ -103,26 +104,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,13 +129,12 @@ 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( credentials: String, @@ -144,36 +142,42 @@ class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { message: CharSequence, displayName: String, replyTo: Int, - sendWithoutNotification: Boolean - ): Observable { - return ncApi.sendChatMessage(credentials, url, message, displayName, replyTo, sendWithoutNotification).map { + sendWithoutNotification: Boolean, + referenceId: String + ): Observable = + ncApi.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId + ).map { it } - } 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 } 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 1798d7403..f5c12a9a4 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, @@ -466,12 +467,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 +598,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 54c73869d..191006d86 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 @@ -21,7 +21,7 @@ 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 @@ -39,7 +39,8 @@ class MessageInputViewModel @Inject constructor( private val mediaPlayerManager: MediaPlayerManager, private val audioFocusRequestManager: AudioFocusRequestManager, private val appPreferences: AppPreferences -) : ViewModel(), DefaultLifecycleObserver { +) : ViewModel(), + DefaultLifecycleObserver { enum class LifeCycleFlag { PAUSED, RESUMED, @@ -144,6 +145,11 @@ class MessageInputViewModel @Inject constructor( replyTo: Int, sendWithoutNotification: Boolean ) { + // TODO: add temporary message with ref id + + val referenceId = SendMessageUtils().generateReferenceId() + Log.d(TAG, "Random SHA-256 Hash: $referenceId") + if (isQueueing) { val tempID = System.currentTimeMillis().toInt() val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification) @@ -161,10 +167,11 @@ class MessageInputViewModel @Inject constructor( message, displayName, replyTo, - sendWithoutNotification + sendWithoutNotification, + referenceId ).subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { + ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { disposableSet.add(d) } @@ -177,7 +184,9 @@ class MessageInputViewModel @Inject constructor( // unused atm } - override fun onNext(t: GenericOverall) { + override fun onNext(t: ChatOverallSingleMessage) { + Log.d(TAG, "received ref id: " + (t.ocs?.data?.referenceId ?: "none")) + // TODO check ref id and replace temp message _sendChatMessageViewState.value = SendChatMessageSuccessState(message) } }) 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 30b856a6b..4ab3c4955 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 @@ -37,7 +37,8 @@ fun ChatMessageJson.asEntity(accountId: Long) = lastEditActorId = lastEditActorId, lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, - deleted = deleted + deleted = deleted, + referenceId = referenceId ) fun ChatMessageEntity.asModel() = @@ -62,7 +63,8 @@ fun ChatMessageEntity.asModel() = lastEditActorId = lastEditActorId, lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, - isDeleted = deleted + isDeleted = deleted, + referenceId = referenceId ) fun ChatMessageJson.asModel() = @@ -87,5 +89,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 dbf1cce92..19ed015bd 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 @@ -62,8 +62,8 @@ 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 = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, @ColumnInfo(name = "timestamp") var timestamp: Long = 0 - // missing/not needed: referenceId // 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 3e61f699d..8d10b94b3 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 053ad4766..5c3656c76 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) ], @@ -114,7 +114,8 @@ abstract class TalkDatabase : RoomDatabase() { 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/models/json/chat/ChatMessageJson.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt index 024e13fe6..60d039704 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/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt index 09984c0c6..c70fe8b2f 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 000000000..29b9700ea --- /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 + } +}